From 1db46013d9389a82855baf93b6fe51e8c1f3a506 Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Fri, 5 Aug 2022 18:54:04 -0400 Subject: [PATCH] general: Add translation infrastructure --- Cargo.lock | 9 +- Cargo.toml | 1 + po/POTFILES | 10 +- po/meson.build | 14 ++- src/application.rs | 5 +- src/i18n.rs | 221 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/widgets/image_view.rs | 5 +- src/window.rs | 7 +- 9 files changed, 260 insertions(+), 13 deletions(-) create mode 100644 src/i18n.rs diff --git a/Cargo.lock b/Cargo.lock index 883e891..57dc11e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -879,6 +879,7 @@ dependencies = [ "log", "once_cell", "pretty_env_logger", + "regex", "svg_metadata", ] @@ -1151,9 +1152,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -1162,9 +1163,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "roxmltree" diff --git a/Cargo.toml b/Cargo.toml index 60facf8..416277d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ pretty_env_logger = "0.4.0" futures-channel = "0.3.19" futures = "0.3.21" svg_metadata = "0.4.2" +regex = "1.6.0" [dependencies.ashpd] version = "0.2.2" diff --git a/po/POTFILES b/po/POTFILES index 165de45..f5ee6c9 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,4 +1,12 @@ +data/gtk/help_overlay.ui +data/gtk/image_page.ui +data/gtk/image_view.ui +data/gtk/properties_view.ui +data/gtk/window.ui data/org.gnome.Loupe.desktop.in data/org.gnome.Loupe.appdata.xml.in data/org.gnome.Loupe.gschema.xml -src/window.ui +src/widgets/image_view.rs +src/widgets/properties_view.rs +src/widgets/application.rs +src/widgets/window.rs \ No newline at end of file diff --git a/po/meson.build b/po/meson.build index 33d3b05..c0b74c9 100644 --- a/po/meson.build +++ b/po/meson.build @@ -1 +1,13 @@ -i18n.gettext('loupe', preset: 'glib') +i18n.gettext( + meson.project_name(), + args: [ + '--keyword=i18n', + '--keyword=i18n_f', + '--keyword=i18n_k', + '--keyword=ni18n:1,2', + '--keyword=ni18n_f:1,2', + '--keyword=ni18n_k:1,2' + ], + preset: 'glib' +) + diff --git a/src/application.rs b/src/application.rs index 10269e6..ced0226 100644 --- a/src/application.rs +++ b/src/application.rs @@ -18,6 +18,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::deps::*; +use crate::i18n::*; use adw::prelude::*; use adw::subclass::prelude::*; @@ -173,9 +174,9 @@ impl LpApplication { String::from("Jakub Steiner"), String::from("Tobias Bernard"), ]) - .copyright("© 2021 The GNOME Project") + .copyright(&i18n("© 2021 The GNOME Project")) .license_type(gtk::License::Gpl30) - .program_name("Image Viewer") + .program_name(&i18n("Image Viewer")) .logo_icon_name(config::APP_ID) .version(config::VERSION) .build(); diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..4c1ce44 --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,221 @@ +// i18n.rs +// +// Copyright 2018 Daniel García Moreno +// +// Copied from https://gitlab.gnome.org/GNOME/fractal/-/blob/376248fd4a3bff05897ef39111a4e6d1a558f333/fractal-gtk/src/util/i18n.rs +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use gettextrs::gettext; +use gettextrs::ngettext; +use gettextrs::npgettext; +use gettextrs::pgettext; +use regex::Captures; +use regex::Regex; + +#[allow(dead_code)] +fn freplace(input: String, args: &[&str]) -> String { + let mut parts = input.split("{}"); + let mut output = parts.next().unwrap_or_default().to_string(); + for (p, a) in parts.zip(args.iter()) { + output += &(a.to_string() + &p.to_string()); + } + output +} + +#[allow(dead_code)] +fn kreplace(input: String, kwargs: &[(&str, &str)]) -> String { + let mut s = input; + for (k, v) in kwargs { + if let Ok(re) = Regex::new(&format!("\\{{{}\\}}", k)) { + s = re + .replace_all(&s, |_: &Captures<'_>| v.to_string()) + .to_string(); + } + } + + s +} + +// Simple translations functions + +#[allow(dead_code)] +pub fn i18n(format: &str) -> String { + gettext(format) +} + +#[allow(dead_code)] +pub fn i18n_f(format: &str, args: &[&str]) -> String { + let s = gettext(format); + freplace(s, args) +} + +#[allow(dead_code)] +pub fn i18n_k(format: &str, kwargs: &[(&str, &str)]) -> String { + let s = gettext(format); + kreplace(s, kwargs) +} + +// Singular and plural translations functions + +#[allow(dead_code)] +pub fn ni18n(single: &str, multiple: &str, number: u32) -> String { + ngettext(single, multiple, number) +} + +#[allow(dead_code)] +pub fn ni18n_f(single: &str, multiple: &str, number: u32, args: &[&str]) -> String { + let s = ngettext(single, multiple, number); + freplace(s, args) +} + +#[allow(dead_code)] +pub fn ni18n_k(single: &str, multiple: &str, number: u32, kwargs: &[(&str, &str)]) -> String { + let s = ngettext(single, multiple, number); + kreplace(s, kwargs) +} + +// Translations with context functions + +#[allow(dead_code)] +pub fn pi18n(ctx: &str, format: &str) -> String { + pgettext(ctx, format) +} + +#[allow(dead_code)] +pub fn pi18n_f(ctx: &str, format: &str, args: &[&str]) -> String { + let s = pgettext(ctx, format); + freplace(s, args) +} + +#[allow(dead_code)] +pub fn pi18n_k(ctx: &str, format: &str, kwargs: &[(&str, &str)]) -> String { + let s = pgettext(ctx, format); + kreplace(s, kwargs) +} + +// Singular and plural with context + +#[allow(dead_code)] +pub fn pni18n(ctx: &str, single: &str, multiple: &str, number: u32) -> String { + npgettext(ctx, single, multiple, number) +} + +#[allow(dead_code)] +pub fn pni18n_f(ctx: &str, single: &str, multiple: &str, number: u32, args: &[&str]) -> String { + let s = npgettext(ctx, single, multiple, number); + freplace(s, args) +} + +#[allow(dead_code)] +pub fn pni18n_k( + ctx: &str, + single: &str, + multiple: &str, + number: u32, + kwargs: &[(&str, &str)], +) -> String { + let s = npgettext(ctx, single, multiple, number); + kreplace(s, kwargs) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_i18n() { + let out = i18n("translate1"); + assert_eq!(out, "translate1"); + + let out = ni18n("translate1", "translate multi", 1); + assert_eq!(out, "translate1"); + let out = ni18n("translate1", "translate multi", 2); + assert_eq!(out, "translate multi"); + } + + #[test] + fn test_i18n_f() { + let out = i18n_f("{} param", &["one"]); + assert_eq!(out, "one param"); + + let out = i18n_f("middle {} param", &["one"]); + assert_eq!(out, "middle one param"); + + let out = i18n_f("end {}", &["one"]); + assert_eq!(out, "end one"); + + let out = i18n_f("multiple {} and {}", &["one", "two"]); + assert_eq!(out, "multiple one and two"); + + let out = ni18n_f("singular {} and {}", "plural {} and {}", 2, &["one", "two"]); + assert_eq!(out, "plural one and two"); + let out = ni18n_f("singular {} and {}", "plural {} and {}", 1, &["one", "two"]); + assert_eq!(out, "singular one and two"); + } + + #[test] + fn test_i18n_k() { + let out = i18n_k("{one} param", &[("one", "one")]); + assert_eq!(out, "one param"); + + let out = i18n_k("middle {one} param", &[("one", "one")]); + assert_eq!(out, "middle one param"); + + let out = i18n_k("end {one}", &[("one", "one")]); + assert_eq!(out, "end one"); + + let out = i18n_k("multiple {one} and {two}", &[("one", "1"), ("two", "two")]); + assert_eq!(out, "multiple 1 and two"); + + let out = i18n_k("multiple {two} and {one}", &[("one", "1"), ("two", "two")]); + assert_eq!(out, "multiple two and 1"); + + let out = i18n_k("multiple {one} and {one}", &[("one", "1"), ("two", "two")]); + assert_eq!(out, "multiple 1 and 1"); + + let out = ni18n_k( + "singular {one} and {two}", + "plural {one} and {two}", + 1, + &[("one", "1"), ("two", "two")], + ); + assert_eq!(out, "singular 1 and two"); + let out = ni18n_k( + "singular {one} and {two}", + "plural {one} and {two}", + 2, + &[("one", "1"), ("two", "two")], + ); + assert_eq!(out, "plural 1 and two"); + } + + #[test] + fn test_pi18n() { + let out = pi18n("This is the context", "translate1"); + assert_eq!(out, "translate1"); + + let out = pni18n("context", "translate1", "translate multi", 1); + assert_eq!(out, "translate1"); + let out = pni18n("The context string", "translate1", "translate multi", 2); + assert_eq!(out, "translate multi"); + + let out = pi18n_f("Context for translation", "{} param", &["one"]); + assert_eq!(out, "one param"); + + let out = pi18n_k("context", "{one} param", &[("one", "one")]); + assert_eq!(out, "one param"); + } +} diff --git a/src/main.rs b/src/main.rs index 99f3bdd..9ebb39b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ use gtk::gio::{self, prelude::*}; mod application; mod config; mod file_model; +mod i18n; mod thumbnail; mod util; mod widgets; diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index 8e4c3aa..4e2cca2 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -18,6 +18,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::deps::*; +use crate::i18n::*; use adw::subclass::prelude::*; use glib::clone; @@ -375,8 +376,8 @@ impl LpImageView { wallpaper::SetOn::Background, ) .await { - Ok(_) => "Set as wallpaper.", - Err(_) => "Could not set wallpaper.", + Ok(_) => i18n("Set as wallpaper."), + Err(_) => i18n("Could not set wallpaper."), }; view.activate_action( diff --git a/src/window.rs b/src/window.rs index a752f37..6606e4b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -18,6 +18,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::deps::*; +use crate::i18n::*; use adw::subclass::prelude::*; use glib::clone; @@ -180,7 +181,7 @@ mod imp { obj.set_image_from_file(&file, false); } else { obj.show_toast( - format!("\"{}\" is not a valid image.", info.display_name()), + i18n_f("\"{}\" is not a valid image.", &[&info.display_name()]), adw::ToastPriority::High, ); } @@ -296,7 +297,7 @@ impl LpWindow { if let Err(e) = imp.image_view.copy() { log::error!("Failed to copy to clipboard: {}", e); } else { - self.show_toast("Image copied to clipboard", adw::ToastPriority::High); + self.show_toast(i18n("Image copied to clipboard"), adw::ToastPriority::High); } } @@ -380,7 +381,7 @@ impl LpWindow { #[template_callback] fn window_title(&self, file: Option) -> String { file.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name - .unwrap_or_else(|| String::from("Image Viewer")) // Return that or the default if there's nothing + .unwrap_or_else(|| i18n("Image Viewer")) // Return that or the default if there's nothing } // We also have a closure that returns `adw::FlapFoldPolicy`.