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