Translation support and initial spanish translation

Added translation support based on the Fractal i18n. To do this I've
added the gettext-rs crate dep. I'm using my own fork because the
official gettext-rs release includes the gettext source files and that
increase the distribution package a lot and for distribution with
flatkap we don't need to build gettext, the lib is in the gnome sdk. So
this gettext-rs fork is the same, but removing the not needed gettext
source files.

The i18n.rs file adds some useful functions to translate strings. These
functions wraps the original gettext and adds more functionality, to be
able to translate compound strings, something that's not supported by
the gettext function.

The 'i18n' function works like the gettext, receives a plain string
without params.

The 'i18n_f' function receives a string with "{}" and a ref to an array
of &str with substitutions for the "{}" in the original string. The
substitution is done by order.

The 'i18n_k' function receives a string with "{named}" and a ref to an
array of (&str, &str) with substitutions for the "{named}" in the
original string. The substitution is done by name, where the first &str
in the tuple is the name and the second the string to use for the
replace.

This mod also include ni18n variants of the three functions for plural
and singular translations.

I've also created the spanish translation.

See #61

https://gitlab.gnome.org/World/podcasts/issues/61
This commit is contained in:
Daniel García Moreno
2018-08-02 15:24:19 +02:00
parent f695ba4605
commit bea4915317
22 changed files with 756 additions and 21 deletions
+9
View File
@@ -5,6 +5,8 @@ use glib::{self, Variant};
use gtk;
use gtk::prelude::*;
use gettextrs::{bindtextdomain, setlocale, textdomain, LocaleCategory};
use crossbeam_channel::{unbounded, Receiver, Sender};
use fragile::Fragile;
use podcasts_data::Show;
@@ -25,6 +27,8 @@ use std::sync::Arc;
pub const APP_ID: &str = "org.gnome.Podcasts";
include!(concat!(env!("OUT_DIR"), "/build_globals.rs"));
/// Creates an action named `name` in the action map `T with the handler `F`
fn action<T, F>(thing: &T, name: &str, action: F)
where
@@ -304,6 +308,11 @@ impl App {
}
pub fn run() {
// Set up the textdomain for gettext
setlocale(LocaleCategory::LcAll, "");
bindtextdomain("gnome-podcasts", LOCALEDIR);
textdomain("gnome-podcasts");
let application = gtk::Application::new(APP_ID, gio::ApplicationFlags::empty())
.expect("Application Initialization failed...");
+4 -2
View File
@@ -15,6 +15,8 @@ use utils::{itunes_to_rss, refresh};
use std::rc::Rc;
use i18n::i18n;
#[derive(Debug, Clone)]
// TODO: Factor out the hamburger menu
// TODO: Make a proper state machine for the headerbar states
@@ -118,7 +120,7 @@ impl AddPopover {
} else {
self.add.set_sensitive(false);
self.result
.set_label("You are already subscribed to this Show");
.set_label(i18n("You are already subscribed to this Show").as_str());
self.result.show();
}
Ok(())
@@ -126,7 +128,7 @@ impl AddPopover {
Err(err) => {
self.add.set_sensitive(false);
if !url.is_empty() {
self.result.set_label("Invalid url");
self.result.set_label(i18n("Invalid url").as_str());
self.result.show();
error!("Error: {}", err);
} else {
+135
View File
@@ -0,0 +1,135 @@
extern crate gettextrs;
extern crate regex;
use self::gettextrs::gettext;
use self::gettextrs::ngettext;
use self::regex::Captures;
use self::regex::Regex;
#[allow(dead_code)]
fn freplace(input: String, args: &[&str]) -> String {
let mut parts = input.split("{}");
let mut output = parts.next().unwrap_or("").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.clone();
for (k, v) in kwargs {
if let Ok(re) = Regex::new(&format!("\\{{{}\\}}", k)) {
s = re
.replace_all(&s, |_: &Captures| v.to_string().clone())
.to_string();
}
}
s
}
#[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)
}
#[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)
}
#[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");
}
}
+4
View File
@@ -73,6 +73,8 @@ extern crate reqwest;
extern crate serde_json;
extern crate url;
extern crate gettextrs;
use log::Level;
use gtk::prelude::*;
@@ -108,6 +110,8 @@ mod settings;
mod static_resource;
mod utils;
mod i18n;
use app::App;
fn main() {
+9 -1
View File
@@ -3,6 +3,8 @@ use gio::{Settings, SettingsExt};
use gtk;
use gtk::prelude::*;
use i18n::i18n;
#[derive(Debug, Clone)]
pub struct Prefs {
dialog: gtk::Window,
@@ -53,7 +55,13 @@ impl Prefs {
let cleanup_p = settings.get_string("cleanup-age-period").unwrap();
let mut cleanup_pos = 0;
let store = gtk::ListStore::new(&[gtk::Type::String]);
for (i, item) in ["Seconds", "Minutes", "Hours", "Days", "Weeks"]
for (i, item) in [
i18n("Seconds"),
i18n("Minutes"),
i18n("Hours"),
i18n("Days"),
i18n("Weeks"),
]
.iter()
.enumerate()
{
+4 -2
View File
@@ -10,6 +10,8 @@ use stacks::{HomeStack, ShowStack};
use std::cell::RefCell;
use std::rc::Rc;
use i18n::i18n;
#[derive(Debug, Clone)]
pub struct Content {
stack: gtk::Stack,
@@ -24,8 +26,8 @@ impl Content {
let home = Rc::new(RefCell::new(HomeStack::new(sender.clone())?));
let shows = Rc::new(RefCell::new(ShowStack::new(sender.clone())));
stack.add_titled(&home.borrow().get_stack(), "home", "New");
stack.add_titled(&shows.borrow().get_stack(), "shows", "Shows");
stack.add_titled(&home.borrow().get_stack(), "home", &i18n("New"));
stack.add_titled(&shows.borrow().get_stack(), "shows", &i18n("Shows"));
let con = Content {
stack,
+7 -5
View File
@@ -29,6 +29,8 @@ use std::sync::{Arc, Mutex, RwLock};
use app::Action;
use i18n::i18n;
/// Lazy evaluates and loads widgets to the parent `container` widget.
///
/// Accepts an `IntoIterator`, `data`, as the source from which each widget
@@ -325,10 +327,10 @@ pub fn on_import_clicked(window: &gtk::ApplicationWindow, sender: &Sender<Action
// TODO: It might be better to use a FileChooserNative widget.
// Create the FileChooser Dialog
let dialog = FileChooserNative::new(
Some("Select the file from which to you want to Import Shows."),
Some(i18n("Select the file from which to you want to Import Shows.").as_str()),
Some(window),
FileChooserAction::Open,
Some("_Import"),
Some(i18n("_Import").as_str()),
None,
);
@@ -337,7 +339,7 @@ pub fn on_import_clicked(window: &gtk::ApplicationWindow, sender: &Sender<Action
// Set a filter to show only xml files
let filter = FileFilter::new();
FileFilterExt::set_name(&filter, Some("OPML file"));
FileFilterExt::set_name(&filter, Some(i18n("OPML file").as_str()));
filter.add_mime_type("application/xml");
filter.add_mime_type("text/xml");
dialog.add_filter(&filter);
@@ -355,12 +357,12 @@ pub fn on_import_clicked(window: &gtk::ApplicationWindow, sender: &Sender<Action
// Refresh the succesfully parsed feeds to index them
refresh(Some(sources), sender)
} else {
let text = String::from("Failed to parse the Imported file");
let text = i18n("Failed to parse the Imported file");
sender.send(Action::ErrorNotification(text));
}
}))
} else {
let text = String::from("Selected File could not be accessed.");
let text = i18n("Selected File could not be accessed.");
sender.send(Action::ErrorNotification(text));
}
}
+5 -2
View File
@@ -2,6 +2,8 @@ use app::APP_ID;
use gtk;
use gtk::prelude::*;
use i18n::i18n;
// Totally copied it from fractal.
// https://gitlab.gnome.org/danigm/fractal/blob/503e311e22b9d7540089d735b92af8e8f93560c5/fractal-gtk/src/app.rs#L1883-1912
/// Given a `window` create and attach an `gtk::AboutDialog` to it.
@@ -21,8 +23,8 @@ pub fn about_dialog(window: &gtk::ApplicationWindow) {
let dialog = gtk::AboutDialog::new();
dialog.set_logo_icon_name(APP_ID);
dialog.set_comments("Podcast Client for the GNOME Desktop.");
dialog.set_copyright("© 2017, 2018 Jordan Petridis");
dialog.set_comments(i18n("Podcast Client for the GNOME Desktop.").as_str());
dialog.set_copyright(i18n("© 2017, 2018 Jordan Petridis").as_str());
dialog.set_license_type(gtk::License::Gpl30);
dialog.set_modal(true);
// TODO: make it show it fetches the commit hash from which it was built
@@ -36,6 +38,7 @@ pub fn about_dialog(window: &gtk::ApplicationWindow) {
dialog.set_artists(&["Tobias Bernard", "Sam Hewitt"]);
dialog.set_authors(authors);
dialog.set_translator_credits(i18n("translator-credits").as_str());
dialog.connect_response(|dlg, _| dlg.destroy());
+4 -1
View File
@@ -21,6 +21,8 @@ use std::cell::RefCell;
use std::rc::{Rc, Weak};
use std::sync::{Arc, Mutex, TryLockError};
use i18n::i18n_f;
lazy_static! {
static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
// Declare a custom humansize option struct
@@ -123,7 +125,8 @@ impl InfoLabels {
// If the lenght is 1 or more minutes
if minutes != 0 {
// Set the label and show them.
self.duration.set_text(&format!("{} min", minutes));
self.duration
.set_text(&i18n_f("{} min", &[&minutes.to_string()]));
self.duration.show();
self.separator1.show();
return;
+3 -1
View File
@@ -23,6 +23,8 @@ use std::ops::Deref;
use std::path::Path;
use std::rc::Rc;
use i18n::i18n;
#[derive(Debug, Clone, Copy)]
enum SeekDirection {
Backwards,
@@ -295,7 +297,7 @@ impl PlayerWidget {
// Log gst errors.
s.player.connect_error(clone!(sender => move |_, _error| {
// sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
let s = "The media player was unable to execute an action.".into();
let s = i18n("The media player was unable to execute an action.");
sender.send(Action::ErrorNotification(s));
}));
+5 -3
View File
@@ -17,6 +17,8 @@ use widgets::appnotif::{InAppNotification, UndoState};
use std::sync::Arc;
use i18n::{i18n, i18n_f};
#[derive(Debug, Clone)]
pub struct ShowMenu {
pub container: gtk::PopoverMenu,
@@ -136,12 +138,12 @@ pub fn mark_all_notif(pd: Arc<Show>, sender: &Sender<Action>) -> InAppNotificati
});
let undo_callback = clone!(sender => move || sender.send(Action::RefreshWidgetIfSame(id)));
let text = "Marked all episodes as listened";
InAppNotification::new(text, callback, undo_callback, UndoState::Shown)
let text = i18n("Marked all episodes as listened");
InAppNotification::new(&text, callback, undo_callback, UndoState::Shown)
}
pub fn remove_show_notif(pd: Arc<Show>, sender: Sender<Action>) -> InAppNotification {
let text = format!("Unsubscribed from {}", pd.title());
let text = i18n_f("Unsubscribed from {}", &[pd.title()]);
let res = utils::ignore_show(pd.id());
debug_assert!(res.is_ok());