From bea491531709c25ba614b7a204fa37126a160e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Moreno?= Date: Thu, 2 Aug 2018 11:50:09 +0200 Subject: [PATCH] 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 --- .gitignore | 1 + Cargo.lock | 32 ++ README.md | 19 +- meson.build | 8 +- podcasts-gtk/Cargo.toml | 1 + podcasts-gtk/build.rs | 20 ++ podcasts-gtk/po/LINGUAS | 3 + podcasts-gtk/po/POTFILES.in | 48 +++ podcasts-gtk/po/es.po | 432 ++++++++++++++++++++++++ podcasts-gtk/po/meson.build | 4 + podcasts-gtk/src/app.rs | 9 + podcasts-gtk/src/headerbar.rs | 6 +- podcasts-gtk/src/i18n.rs | 135 ++++++++ podcasts-gtk/src/main.rs | 4 + podcasts-gtk/src/prefs.rs | 10 +- podcasts-gtk/src/stacks/content.rs | 6 +- podcasts-gtk/src/utils.rs | 12 +- podcasts-gtk/src/widgets/aboutdialog.rs | 7 +- podcasts-gtk/src/widgets/episode.rs | 5 +- podcasts-gtk/src/widgets/player.rs | 4 +- podcasts-gtk/src/widgets/show_menu.rs | 8 +- scripts/cargo.sh | 3 +- 22 files changed, 756 insertions(+), 21 deletions(-) create mode 100644 podcasts-gtk/po/LINGUAS create mode 100644 podcasts-gtk/po/POTFILES.in create mode 100644 podcasts-gtk/po/es.po create mode 100644 podcasts-gtk/po/meson.build create mode 100644 podcasts-gtk/src/i18n.rs diff --git a/.gitignore b/.gitignore index b7772ed..ba23686 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ vendor/ .criterion/ org.gnome.*.json~ +podcasts-gtk/po/gnome-podcasts.pot diff --git a/Cargo.lock b/Cargo.lock index 27c165e..73fd068 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -657,6 +657,23 @@ dependencies = [ "pkg-config 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "gettext-rs" +version = "0.3.0" +source = "git+https://github.com/danigm/gettext-rs?branch=no-gettext#db8f12e140f0db5aabafe8dd210c7fc1520a55ff" +dependencies = [ + "gettext-sys 0.19.8 (git+https://github.com/danigm/gettext-rs?branch=no-gettext)", + "locale_config 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gettext-sys" +version = "0.19.8" +source = "git+https://github.com/danigm/gettext-rs?branch=no-gettext#db8f12e140f0db5aabafe8dd210c7fc1520a55ff" +dependencies = [ + "cc 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gio" version = "0.4.1" @@ -1086,6 +1103,17 @@ dependencies = [ "vcpkg 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "locale_config" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "log" version = "0.3.9" @@ -1522,6 +1550,7 @@ dependencies = [ "fragile 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "gdk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "gdk-pixbuf 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gettext-rs 0.3.0 (git+https://github.com/danigm/gettext-rs?branch=no-gettext)", "gio 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "gstreamer 0.11.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2559,6 +2588,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum gdk-pixbuf 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c2d2199eba47ebcb9977ce28179649bdd59305ef465c4e6f9b65aaa41c24e6b5" "checksum gdk-pixbuf-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df6a3b73e04fafc07f5ebc083f1096a773412e627828e1103a55e921f81187d8" "checksum gdk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3162ff940526ddff71bf1f630facee6b5e05d282d125ba0c4c803842819b80c3" +"checksum gettext-rs 0.3.0 (git+https://github.com/danigm/gettext-rs?branch=no-gettext)" = "" +"checksum gettext-sys 0.19.8 (git+https://github.com/danigm/gettext-rs?branch=no-gettext)" = "" "checksum gio 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2db9fad8f1b0d4c7338a210a6cbdf081dcc1a3c223718c698c4f313f6c288acb" "checksum gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2a57872499171d279f8577ce83837da4cae62b08dd32892236ed67ab7ea61030" "checksum glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5e0be1b1432e227bcd1a9b28db9dc1474a7e7fd4227e08e16f35304f32d09b61" @@ -2597,6 +2628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" "checksum libflate 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7d4b4c7aff5bac19b956f693d0ea0eade8066deb092186ae954fa6ba14daab98" "checksum libsqlite3-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e9eb7b8e152b6a01be6a4a2917248381875758250dc3df5d46caf9250341dda" +"checksum locale_config 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "14fbee0e39bc2dd6a2427c4fdea66e9826cc1fd09b0a0b7550359f5f6efe1dab" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "61bd98ae7f7b754bc53dca7d44b604f733c6bba044ea6f41bc8d89272d8161d2" "checksum loggerv 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ba6b0664956d197c6e0223870c1cd1ec4117aea282b4c0bd5ab01119d31d708d" diff --git a/README.md b/README.md index ec85950..dcfc502 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,23 @@ There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source c [contribution-guidelines]: https://gitlab.gnome.org/World/podcasts/blob/master/CONTRIBUTING.md +### Translations + +If you want to add a new language you should update the file +`podcasts-gtk/po/LINUGAS` and add the new lang to the list. + +To generate .pot files you should run: + +``` +ninja -C _build gnome-podcasts-pot +``` + +To generate .po files you should run: + +``` +ninja -C _build gnome-podcasts-update-po +``` + ## Overview @@ -174,4 +191,4 @@ And almost the entirety of the build system is copied from the [Fractal][fractal [issues]: https://gitlab.gnome.org/World/podcasts/issues [new_issue]: https://gitlab.gnome.org/World/podcasts/issues/new [builder]: https://wiki.gnome.org/Apps/Builder -[get_builder]: https://wiki.gnome.org/Apps/Builder/Downloads \ No newline at end of file +[get_builder]: https://wiki.gnome.org/Apps/Builder/Downloads diff --git a/meson.build b/meson.build index c52193a..1e668fa 100644 --- a/meson.build +++ b/meson.build @@ -15,6 +15,7 @@ podcasts_version_micro = version_array[2].to_int() podcasts_prefix = get_option('prefix') podcasts_bindir = join_paths(podcasts_prefix, get_option('bindir')) +podcasts_localedir = join_paths(podcasts_prefix, get_option('localedir')) podcasts_conf = configuration_data() podcasts_conf.set('BINDIR', podcasts_bindir) @@ -23,6 +24,9 @@ datadir = get_option('datadir') icondir = join_paths(datadir, 'icons') subdir('podcasts-gtk/resources') +i18n = import('i18n') +subdir('podcasts-gtk/po') + cargo = find_program('cargo', required: false) gresource = find_program('glib-compile-resources', required: false) cargo_vendor = find_program('cargo-vendor', required: false) @@ -34,8 +38,8 @@ cargo_release = custom_target('cargo-build', output: ['gnome-podcasts'], install: true, install_dir: podcasts_bindir, - command: [cargo_script, '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) + command: [cargo_script, '@CURRENT_SOURCE_DIR@', '@OUTPUT@', podcasts_localedir]) run_target('release', command: ['scripts/release.sh', meson.project_name() + '-' + podcasts_version - ]) \ No newline at end of file + ]) diff --git a/podcasts-gtk/Cargo.toml b/podcasts-gtk/Cargo.toml index 6951287..4abd3cd 100644 --- a/podcasts-gtk/Cargo.toml +++ b/podcasts-gtk/Cargo.toml @@ -28,6 +28,7 @@ reqwest = "0.8.6" serde_json = "1.0.24" # html2text = "0.1.8" html2text = { git = "https://github.com/alatiera/rust-html2text" } +gettext-rs = { git = "https://github.com/danigm/gettext-rs", branch = "no-gettext", features = ["gettext-system"] } [dependencies.gtk] features = ["v3_22"] diff --git a/podcasts-gtk/build.rs b/podcasts-gtk/build.rs index 71f34e1..80b6c81 100644 --- a/podcasts-gtk/build.rs +++ b/podcasts-gtk/build.rs @@ -1,3 +1,7 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; use std::process::Command; fn main() { @@ -10,4 +14,20 @@ fn main() { .current_dir("resources") .status() .unwrap(); + + // Generating build globals + let default_locales = "./podcasts-gtk/po".to_string(); + let out_dir = env::var("OUT_DIR").unwrap(); + let localedir = env::var("PODCASTS_LOCALEDIR").unwrap_or(default_locales); + let dest_path = Path::new(&out_dir).join("build_globals.rs"); + let mut f = File::create(&dest_path).unwrap(); + + let globals = format!( + " +pub static LOCALEDIR: &'static str = \"{}\"; +", + localedir + ); + + f.write_all(&globals.into_bytes()[..]).unwrap(); } diff --git a/podcasts-gtk/po/LINGUAS b/podcasts-gtk/po/LINGUAS new file mode 100644 index 0000000..792c566 --- /dev/null +++ b/podcasts-gtk/po/LINGUAS @@ -0,0 +1,3 @@ +# please keep this list sorted alphabetically +# +es diff --git a/podcasts-gtk/po/POTFILES.in b/podcasts-gtk/po/POTFILES.in new file mode 100644 index 0000000..5c67ac0 --- /dev/null +++ b/podcasts-gtk/po/POTFILES.in @@ -0,0 +1,48 @@ +# List of source files containing translatable strings. +# Please keep this file sorted alphabetically. +podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml +podcasts-gtk/resources/org.gnome.Podcasts.desktop +podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml + +# ui files +podcasts-gtk/resources/gtk/shows_child.ui +podcasts-gtk/resources/gtk/hamburger.ui +podcasts-gtk/resources/gtk/show_widget.ui +podcasts-gtk/resources/gtk/help-overlay.ui +podcasts-gtk/resources/gtk/shows_view.ui +podcasts-gtk/resources/gtk/episode_widget.ui +podcasts-gtk/resources/gtk/prefs.ui +podcasts-gtk/resources/gtk/home_view.ui +podcasts-gtk/resources/gtk/secondary_menu.ui +podcasts-gtk/resources/gtk/empty_view.ui +podcasts-gtk/resources/gtk/show_menu.ui +podcasts-gtk/resources/gtk/player_toolbar.ui +podcasts-gtk/resources/gtk/empty_show.ui +podcasts-gtk/resources/gtk/headerbar.ui +podcasts-gtk/resources/gtk/home_episode.ui +podcasts-gtk/resources/gtk/inapp_notif.ui + +# rust files +podcasts-gtk/src/manager.rs +podcasts-gtk/src/app.rs +podcasts-gtk/src/widgets/show_menu.rs +podcasts-gtk/src/widgets/appnotif.rs +podcasts-gtk/src/widgets/empty.rs +podcasts-gtk/src/widgets/mod.rs +podcasts-gtk/src/widgets/shows_view.rs +podcasts-gtk/src/widgets/home_view.rs +podcasts-gtk/src/widgets/show.rs +podcasts-gtk/src/widgets/player.rs +podcasts-gtk/src/widgets/aboutdialog.rs +podcasts-gtk/src/widgets/episode.rs +podcasts-gtk/src/headerbar.rs +podcasts-gtk/src/static_resource.rs +podcasts-gtk/src/utils.rs +podcasts-gtk/src/settings.rs +podcasts-gtk/src/main.rs +podcasts-gtk/src/stacks/content.rs +podcasts-gtk/src/stacks/mod.rs +podcasts-gtk/src/stacks/home.rs +podcasts-gtk/src/stacks/populated.rs +podcasts-gtk/src/stacks/show.rs +podcasts-gtk/src/prefs.rs diff --git a/podcasts-gtk/po/es.po b/podcasts-gtk/po/es.po new file mode 100644 index 0000000..acd0e0d --- /dev/null +++ b/podcasts-gtk/po/es.po @@ -0,0 +1,432 @@ +# Spanish translations for gnome-podcasts package. +# Copyright (C) 2018 THE gnome-podcasts'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome-podcasts package. +# Automatically generated, 2018. +# Daniel Garcia Moreno , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome-podcasts\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-08-02 11:42+0200\n" +"PO-Revision-Date: 2018-08-02 11:46+0200\n" +"Last-Translator: Daniel Garcia Moreno \n" +"Language-Team: Español; Castellano \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 2.91.7\n" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15 +msgid "Top position of the last open main window" +msgstr "Posición superior de la última ventana abierta" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19 +msgid "Left position of the last open main window" +msgstr "Posición izquierda de la última ventana abierta" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23 +msgid "Height of the last open main window" +msgstr "Alto de la última ventana abierta" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27 +msgid "Width of the last open main window" +msgstr "Ancho de la última ventana abierta" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31 +msgid "Maximized state of the last open main window" +msgstr "Estado de maximizado de la última ventana abierta" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36 +msgid "Enable or disable dark theme" +msgstr "Activar o desactivar el tema oscuro" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41 +msgid "Whether to periodically refresh content" +msgstr "Si refrescar el contenido periódicamente" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46 +msgid "How many periods of time to wait between automatic refreshes" +msgstr "Cuántos periodos de tiempo a esperar entre refrescos automáticos" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50 +msgid "What period of time to wait between automatic refreshes" +msgstr "Qué periodo de tiempo hay que esperar entre refrescos automáticos" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54 +msgid "Whether to refresh content after startup" +msgstr "Si refrescar el contenido al iniciar" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60 +msgid "How many periods of time to wait between automatic cleanups" +msgstr "Cuántos periodos de tiempo hay que esperar entre limpiados automáticos" + +#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64 +msgid "What period of time to wait between automatic cleanups" +msgstr "Cuántos periodos de tiempo hay que esperar entre limpiados automáticos" + +#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:3 +#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:4 +msgid "Podcasts" +msgstr "Podcasts" + +#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:4 +#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:11 +msgid "Listen to your favorite podcasts, right from your desktop." +msgstr "Escucha tus podcasts favoritos, directamente desde tu escritorio." + +#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:5 +msgid "org.gnome.Podcasts" +msgstr "org.gnome.Podcasts" + +#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:11 +msgid "Podcast;RSS;" +msgstr "Podcast;RSS;" + +#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:9 +msgid "Podcast app for GNOME" +msgstr "Aplicación de Podcast para GNOME" + +#: podcasts-gtk/resources/gtk/hamburger.ui:7 +msgid "_Check for new episodes" +msgstr "_Comprobar si hay nuevos episodios" + +#: podcasts-gtk/resources/gtk/hamburger.ui:12 +msgid "_Import Shows" +msgstr "_Importar programas" + +#: podcasts-gtk/resources/gtk/hamburger.ui:22 +msgid "_Preferences" +msgstr "_Preferencias" + +#: podcasts-gtk/resources/gtk/hamburger.ui:29 +msgid "_Keyboard Shortcuts" +msgstr "Atajos de _teclado" + +#: podcasts-gtk/resources/gtk/hamburger.ui:37 +msgid "_About" +msgstr "_Acerca de" + +#: podcasts-gtk/resources/gtk/show_widget.ui:220 +msgid "Mark all episodes as listened" +msgstr "Marcar todos los episodios como escuchados" + +#: podcasts-gtk/resources/gtk/help-overlay.ui:12 +msgid "General" +msgstr "General" + +#: podcasts-gtk/resources/gtk/help-overlay.ui:18 +msgctxt "shortcut window" +msgid "Check for new episodes" +msgstr "Comprobar si hay nuevos episodios" + +#: podcasts-gtk/resources/gtk/help-overlay.ui:25 +msgctxt "shortcut window" +msgid "Preferences" +msgstr "Preferencias" + +#: podcasts-gtk/resources/gtk/help-overlay.ui:32 +msgctxt "shortcut window" +msgid "Quit the application" +msgstr "Salir de la aplicación" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:55 +#: podcasts-gtk/resources/gtk/player_toolbar.ui:160 +msgid "Episode Title" +msgstr "Título del episodio" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:78 +msgid "3 Jan" +msgstr "3 Jan" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:95 +#: podcasts-gtk/resources/gtk/episode_widget.ui:128 +msgid "·" +msgstr "·" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:111 +msgid "42 min" +msgstr "42 min" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:144 +msgid "0 MB" +msgstr "0 MB" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:161 +msgid "/" +msgstr "/" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:178 +msgid "Calculating episode size..." +msgstr "Calculando tamaño del episodio..." + +#: podcasts-gtk/resources/gtk/episode_widget.ui:213 +msgid "Cancel" +msgstr "Cancelar" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:231 +msgid "Download this episode" +msgstr "Descargar este episodio" + +#: podcasts-gtk/resources/gtk/episode_widget.ui:255 +msgid "Play this episode" +msgstr "Reproducir este episodio" + +#: podcasts-gtk/resources/gtk/prefs.ui:14 +msgid "Preferences" +msgstr "Preferencias" + +#: podcasts-gtk/resources/gtk/prefs.ui:48 +msgid "Appearance" +msgstr "Aspecto" + +#: podcasts-gtk/resources/gtk/prefs.ui:92 +msgid "Dark Theme" +msgstr "Tema oscuro" + +#: podcasts-gtk/resources/gtk/prefs.ui:138 +msgid "Delete played episodes" +msgstr "Eliminar episodios reproducidos" + +#: podcasts-gtk/resources/gtk/prefs.ui:183 +msgid "After" +msgstr "Después" + +#: podcasts-gtk/resources/gtk/home_view.ui:94 +msgid "Today" +msgstr "Hoy" + +#: podcasts-gtk/resources/gtk/home_view.ui:150 +msgid "Yesterday" +msgstr "Ayer" + +#: podcasts-gtk/resources/gtk/home_view.ui:206 +msgid "This Week" +msgstr "Esta semana" + +#: podcasts-gtk/resources/gtk/home_view.ui:262 +msgid "This Month" +msgstr "Este mes" + +#: podcasts-gtk/resources/gtk/home_view.ui:319 +msgid "Older" +msgstr "Antiguo" + +#: podcasts-gtk/resources/gtk/secondary_menu.ui:7 +msgid "_Mark all episodes as played" +msgstr "_Marcar todos los episodios como reproducidos" + +#: podcasts-gtk/resources/gtk/secondary_menu.ui:11 +msgid "_Website" +msgstr "Página _Web" + +#: podcasts-gtk/resources/gtk/secondary_menu.ui:15 +msgid "_Unsubscribe" +msgstr "_Cancelar suscripción" + +#: podcasts-gtk/resources/gtk/empty_view.ui:66 +msgid "Get some shows" +msgstr "Consigue algún programa" + +#: podcasts-gtk/resources/gtk/empty_view.ui:103 +msgid "Add new shows via feed URL" +msgstr "Añadir nuevo programa a través de canal URL" + +#: podcasts-gtk/resources/gtk/empty_view.ui:132 +msgid "Import shows from another device" +msgstr "Importar programas de otro dispositivo" + +#: podcasts-gtk/resources/gtk/show_menu.ui:23 +msgid "Open Website" +msgstr "Abrir Web" + +#: podcasts-gtk/resources/gtk/show_menu.ui:36 +msgid "Mark all as played" +msgstr "Marcar como reproducido" + +#: podcasts-gtk/resources/gtk/show_menu.ui:61 +msgid "Unsubscribe" +msgstr "Cancelar suscripción" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:44 +msgid "Rewind 10 seconds" +msgstr "Atrás 10 segundos" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:59 +msgid "Play" +msgstr "Reproducir" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:75 +msgid "Pause" +msgstr "Pausa" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:91 +msgid "Fast Forward 10 seconds" +msgstr "Adelante 10 segundos" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:141 +#: podcasts-gtk/resources/gtk/headerbar.ui:247 +msgid "Show Title" +msgstr "Título de Programa" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:257 +msgid "Change the Playback speed" +msgstr "Cambiar velocidad de reproducción" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:272 +#: podcasts-gtk/resources/gtk/player_toolbar.ui:352 +msgid "1.00x" +msgstr "1.00x" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:316 +msgid "1.50x" +msgstr "1.50x" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:320 +msgid "1.5 speed rate" +msgstr "velocidad 1.5" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:334 +msgid "1.25x" +msgstr "1.25x" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:338 +msgid "1.25 speed rate" +msgstr "velocidad 1.25" + +#: podcasts-gtk/resources/gtk/player_toolbar.ui:356 +msgid "Normal speed" +msgstr "Velocidad normal" + +#: podcasts-gtk/resources/gtk/empty_show.ui:35 +msgid "This show does not have any episodes" +msgstr "Este programa no tiene episodios todavía" + +#: podcasts-gtk/resources/gtk/empty_show.ui:51 +msgid "If you think this is an Error, Plese consider opening a bug report." +msgstr "" +"Si crees que esto es un error, por favor, considera abrir un bug report." + +#: podcasts-gtk/resources/gtk/headerbar.ui:35 +#: podcasts-gtk/resources/gtk/headerbar.ui:155 +msgid "Add a new feed" +msgstr "Añadir un nuevo canal" + +#: podcasts-gtk/resources/gtk/headerbar.ui:56 +msgid "Enter feed address to add" +msgstr "Introduce la dirección del canal a añadir" + +#: podcasts-gtk/resources/gtk/headerbar.ui:92 +msgid "Add" +msgstr "Añadir" + +#: podcasts-gtk/resources/gtk/headerbar.ui:136 +msgid "You are already subscribed to that feed!" +msgstr "¡Ya estás suscrito a este canal!" + +#: podcasts-gtk/resources/gtk/headerbar.ui:176 +msgid "Back" +msgstr "Atrás" + +#: podcasts-gtk/resources/gtk/headerbar.ui:212 +msgid "Fetching new episodes" +msgstr "Obteniendo nuevos episodios" + +#: podcasts-gtk/resources/gtk/inapp_notif.ui:35 +msgid "An in-app action notification" +msgstr "Una notificación de acción in-app" + +#: podcasts-gtk/resources/gtk/inapp_notif.ui:72 +msgid "Undo" +msgstr "Deshacer" + +#: podcasts-gtk/src/widgets/show_menu.rs:141 +msgid "Marked all episodes as listened" +msgstr "Marcar todos los episodios como escuchados" + +#: podcasts-gtk/src/widgets/show_menu.rs:146 +msgid "Unsubscribed from {}" +msgstr "Suscripción cancelada para {}" + +#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error))); +#: podcasts-gtk/src/widgets/player.rs:300 +msgid "The media player was unable to execute an action." +msgstr "El reproductor no ha podido reproducir una acción." + +#: podcasts-gtk/src/widgets/aboutdialog.rs:26 +msgid "Podcast Client for the GNOME Desktop." +msgstr "Aplicación de Podcast para el escritorio GNOME." + +#: podcasts-gtk/src/widgets/aboutdialog.rs:27 +msgid "© 2017, 2018 Jordan Petridis" +msgstr "© 2017, 2018 Jordan Petridis" + +#: podcasts-gtk/src/widgets/aboutdialog.rs:41 +msgid "translator-credits" +msgstr "\"Daniel García Moreno \"" + +#. Set the label and show them. +#: podcasts-gtk/src/widgets/episode.rs:128 +msgid "{} min" +msgstr "{} min" + +#: podcasts-gtk/src/headerbar.rs:123 +msgid "You are already subscribed to this Show" +msgstr "Ya estás suscrito a este programa" + +#: podcasts-gtk/src/headerbar.rs:131 +msgid "Invalid url" +msgstr "Url no válida" + +#: podcasts-gtk/src/utils.rs:330 +msgid "Select the file from which to you want to Import Shows." +msgstr "Selecciona el fichero desde el cual quieres importar programas." + +#: podcasts-gtk/src/utils.rs:333 +msgid "_Import" +msgstr "_Importar" + +#: podcasts-gtk/src/utils.rs:342 +msgid "OPML file" +msgstr "fichero OPML" + +#: podcasts-gtk/src/utils.rs:360 +msgid "Failed to parse the Imported file" +msgstr "Fallo al leer el fichero importado" + +#: podcasts-gtk/src/utils.rs:365 +msgid "Selected File could not be accessed." +msgstr "El fichero seleccionado no es accesible." + +#: podcasts-gtk/src/stacks/content.rs:29 +msgid "New" +msgstr "Nuevo" + +#: podcasts-gtk/src/stacks/content.rs:30 +msgid "Shows" +msgstr "Programas" + +#: podcasts-gtk/src/prefs.rs:58 +msgid "Seconds" +msgstr "Segundos" + +#: podcasts-gtk/src/prefs.rs:58 +msgid "Minutes" +msgstr "Minutos" + +#: podcasts-gtk/src/prefs.rs:58 +msgid "Hours" +msgstr "Horas" + +#: podcasts-gtk/src/prefs.rs:59 +msgid "Days" +msgstr "Días" + +#: podcasts-gtk/src/prefs.rs:59 +msgid "Weeks" +msgstr "Semanas" + +#~ msgid "Jordan Petridis and others" +#~ msgstr "Jordan Petridis y otros" diff --git a/podcasts-gtk/po/meson.build b/podcasts-gtk/po/meson.build new file mode 100644 index 0000000..a6be46d --- /dev/null +++ b/podcasts-gtk/po/meson.build @@ -0,0 +1,4 @@ +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/podcasts-gtk/src/app.rs b/podcasts-gtk/src/app.rs index 598065f..f3510ca 100644 --- a/podcasts-gtk/src/app.rs +++ b/podcasts-gtk/src/app.rs @@ -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(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..."); diff --git a/podcasts-gtk/src/headerbar.rs b/podcasts-gtk/src/headerbar.rs index 5d7ffa0..6c9bd09 100644 --- a/podcasts-gtk/src/headerbar.rs +++ b/podcasts-gtk/src/headerbar.rs @@ -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 { diff --git a/podcasts-gtk/src/i18n.rs b/podcasts-gtk/src/i18n.rs new file mode 100644 index 0000000..d4fc98d --- /dev/null +++ b/podcasts-gtk/src/i18n.rs @@ -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"); + } +} diff --git a/podcasts-gtk/src/main.rs b/podcasts-gtk/src/main.rs index eaf8816..ef5364d 100644 --- a/podcasts-gtk/src/main.rs +++ b/podcasts-gtk/src/main.rs @@ -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() { diff --git a/podcasts-gtk/src/prefs.rs b/podcasts-gtk/src/prefs.rs index de5e989..cc2b5db 100644 --- a/podcasts-gtk/src/prefs.rs +++ b/podcasts-gtk/src/prefs.rs @@ -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() { diff --git a/podcasts-gtk/src/stacks/content.rs b/podcasts-gtk/src/stacks/content.rs index c4d6265..9fdc383 100644 --- a/podcasts-gtk/src/stacks/content.rs +++ b/podcasts-gtk/src/stacks/content.rs @@ -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, diff --git a/podcasts-gtk/src/utils.rs b/podcasts-gtk/src/utils.rs index 20155c1..a0eac16 100644 --- a/podcasts-gtk/src/utils.rs +++ b/podcasts-gtk/src/utils.rs @@ -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: >k::ApplicationWindow, sender: &Sender = { // 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; diff --git a/podcasts-gtk/src/widgets/player.rs b/podcasts-gtk/src/widgets/player.rs index fe0a76f..8e4b42e 100644 --- a/podcasts-gtk/src/widgets/player.rs +++ b/podcasts-gtk/src/widgets/player.rs @@ -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)); })); diff --git a/podcasts-gtk/src/widgets/show_menu.rs b/podcasts-gtk/src/widgets/show_menu.rs index 87eec4d..2be4fd5 100644 --- a/podcasts-gtk/src/widgets/show_menu.rs +++ b/podcasts-gtk/src/widgets/show_menu.rs @@ -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, sender: &Sender) -> 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, sender: Sender) -> 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()); diff --git a/scripts/cargo.sh b/scripts/cargo.sh index 73c7923..47f5d82 100755 --- a/scripts/cargo.sh +++ b/scripts/cargo.sh @@ -2,6 +2,7 @@ export CARGO_HOME=$1/target/cargo-home export RUSTFLAGS="--cfg rayon_unstable" +export PODCASTS_LOCALEDIR="$3" if [[ $DEBUG = true ]] then @@ -10,4 +11,4 @@ then else echo "RELEASE MODE" cargo build --release -p podcasts-gtk && cp $1/target/release/podcasts-gtk $2 -fi \ No newline at end of file +fi