summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--default.nix2
-rw-r--r--po/LINGUAS1
-rw-r--r--po/bowl.pot132
-rw-r--r--po/ru.po137
-rw-r--r--src/components/post_editor.rs21
-rw-r--r--src/components/signin.rs19
-rw-r--r--src/components/smart_summary.rs4
-rw-r--r--src/lib.rs21
-rw-r--r--src/main.rs7
9 files changed, 313 insertions, 31 deletions
diff --git a/default.nix b/default.nix
index c79ba7c..3ea5c00 100644
--- a/default.nix
+++ b/default.nix
@@ -6,7 +6,7 @@
 
 let
   src = let
-    suffixes = [ "meson.options" "meson.build" ".sh" ".po" ".pot" ".in" ];
+    suffixes = [ "meson.options" "meson.build" ".sh" ".po" ".pot" ".in" "LINGUAS" ];
     suffixFilter = name: type: let
       base = baseNameOf (toString name);
     in type == "directory" || lib.any (ext: lib.hasSuffix ext base) suffixes;
diff --git a/po/LINGUAS b/po/LINGUAS
index e69de29..adc719b 100644
--- a/po/LINGUAS
+++ b/po/LINGUAS
@@ -0,0 +1 @@
+ru
\ No newline at end of file
diff --git a/po/bowl.pot b/po/bowl.pot
new file mode 100644
index 0000000..24e87b5
--- /dev/null
+++ b/po/bowl.pot
@@ -0,0 +1,132 @@
+# Bowl for Kittybox.
+# Copyright (C) 2024 Vika Shleina
+# This file is distributed under the same license as the bowl package.
+# Vika <vika@fireburn.ru>, 2024.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: bowl\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-01 18:02+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: data/xyz.vikanezrimaya.kittybox.Bowl.desktop.in.in:3 src/lib.rs:187
+msgid "Bowl"
+msgstr ""
+
+#: data/xyz.vikanezrimaya.kittybox.Bowl.desktop.in.in:4
+msgid "Minimalist Micropub post creator"
+msgstr ""
+
+#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
+#: data/xyz.vikanezrimaya.kittybox.Bowl.desktop.in.in:10
+msgid "Micropub;IndieWeb;Kittybox;"
+msgstr ""
+
+#. TRANSLATORS: please keep the newline and `<b>` tags
+#: src/components/smart_summary.rs:47
+msgid ""
+"<b>Smart Summary</b>\n"
+"Ask a language model for a single-sentence summary."
+msgstr ""
+
+#: src/components/post_editor.rs:142
+msgid "Name"
+msgstr ""
+
+#: src/components/post_editor.rs:157
+msgid "Summary"
+msgstr ""
+
+#: src/components/post_editor.rs:179
+msgid "Tags"
+msgstr ""
+
+#: src/components/post_editor.rs:227
+msgid "Content"
+msgstr ""
+
+#: src/components/post_editor.rs:280
+msgid "Visibility"
+msgstr ""
+
+#: src/components/post_editor.rs:493
+msgid "Smart Summary error: {}"
+msgstr ""
+
+#: src/components/post_editor.rs:540
+msgid "Post submitted"
+msgstr ""
+
+#: src/components/post_editor.rs:541
+msgid "Open"
+msgstr ""
+
+#: src/components/post_editor.rs:559
+msgid "Error sending post: {}"
+msgstr ""
+
+#: src/components/signin.rs:91
+msgid "Thank you! This window can now be closed."
+msgstr ""
+
+#: src/components/signin.rs:210 src/components/signin.rs:249
+msgid "Sign in"
+msgstr ""
+
+#: src/components/signin.rs:215
+msgid ""
+"Please sign in with your website to use Bowl.\n"
+"Your website needs to support IndieAuth and Micropub for this app to work."
+msgstr ""
+
+#: src/components/signin.rs:245
+msgid "Talking to your website..."
+msgstr ""
+
+#: src/components/signin.rs:247
+msgid "Waiting for authorization..."
+msgstr ""
+
+#: src/components/signin.rs:455
+msgid "state doesn't match what we remember, ceremony aborted"
+msgstr ""
+
+#: src/components/signin.rs:463
+msgid "issuer doesn't match what we remember, ceremony aborted"
+msgstr ""
+
+#: src/lib.rs:131
+msgid "Bowl for Kittybox"
+msgstr ""
+
+#: src/lib.rs:169
+msgid "Sign out"
+msgstr ""
+
+#: src/lib.rs:170
+msgid "Preferences"
+msgstr ""
+
+#: src/lib.rs:171
+msgid "About"
+msgstr ""
+
+#: src/lib.rs:185
+msgid "Bowl - Sign in with your website"
+msgstr ""
+
+#: src/lib.rs:201
+msgid "Publish"
+msgstr ""
+
+#: src/lib.rs:331
+msgid "Micropub access token for {}"
+msgstr ""
diff --git a/po/ru.po b/po/ru.po
new file mode 100644
index 0000000..29af65e
--- /dev/null
+++ b/po/ru.po
@@ -0,0 +1,137 @@
+# Russian translations for PACKAGE package.
+# Copyright (C) 2024 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Vika <vika@fireburn.ru>, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-01 18:35+0300\n"
+"PO-Revision-Date: 2024-09-01 18:36+0300\n"
+"Last-Translator: Vika <vika@fireburn.ru>\n"
+"Language-Team: Russian <gnu@d07.ru>\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: data/xyz.vikanezrimaya.kittybox.Bowl.desktop.in.in:3 src/lib.rs:187
+msgid "Bowl"
+msgstr "Bowl"
+
+#: data/xyz.vikanezrimaya.kittybox.Bowl.desktop.in.in:4
+msgid "Minimalist Micropub post creator"
+msgstr "Минималистичная Micropub-утилита для написания постов в блог"
+
+#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
+#: data/xyz.vikanezrimaya.kittybox.Bowl.desktop.in.in:10
+msgid "Micropub;IndieWeb;Kittybox;"
+msgstr "Miropub;IndieWeb;Kittybox"
+
+#. TRANSLATORS: please keep the newline and `<b>` tags
+#: src/components/smart_summary.rs:47
+msgid ""
+"<b>Smart Summary</b>\n"
+"Ask a language model for a single-sentence summary."
+msgstr ""
+"<b>Умная Выжимка</b>\n"
+"Попросит языковую модель описать содержимое статьи в одном предложении."
+
+#: src/components/post_editor.rs:142
+msgid "Name"
+msgstr "Название"
+
+#: src/components/post_editor.rs:157
+msgid "Summary"
+msgstr "Содержание"
+
+#: src/components/post_editor.rs:179
+msgid "Tags"
+msgstr "Тэги"
+
+#: src/components/post_editor.rs:227
+msgid "Content"
+msgstr "Текст"
+
+#: src/components/post_editor.rs:280
+msgid "Visibility"
+msgstr ""
+
+#: src/components/post_editor.rs:493
+msgid "Smart Summary error: {}"
+msgstr "Ошибка Умной Выжимки: {}"
+
+#: src/components/post_editor.rs:540
+msgid "Post submitted"
+msgstr "Статья отправлена"
+
+#: src/components/post_editor.rs:541
+msgid "Open"
+msgstr "Открыть"
+
+#: src/components/post_editor.rs:559
+msgid "Error sending post: {}"
+msgstr "Ошибка отправки статьи: {}"
+
+#: src/components/signin.rs:91
+msgid "Thank you! This window can now be closed."
+msgstr "Благодарим Вас! Это окно можно закрыть."
+
+#: src/components/signin.rs:210 src/components/signin.rs:249
+msgid "Sign in"
+msgstr "Войти"
+
+#: src/components/signin.rs:215
+msgid ""
+"Please sign in with your website to use Bowl.\n"
+"Your website needs to support IndieAuth and Micropub for this app to work."
+msgstr ""
+"Пожалуйста, войдите со своим веб-сайтом, чтобы использовать Боул.\n"
+"Ваш веб-сайт должен поддерживать протоколы IndieAuth и Micropub для корректной работы приложения."
+
+#: src/components/signin.rs:245
+msgid "Talking to your website..."
+msgstr "Общаемся с Вашим веб-сайтом..."
+
+#: src/components/signin.rs:247
+msgid "Waiting for authorization..."
+msgstr "Ждём авторизации..."
+
+#: src/components/signin.rs:455
+msgid "state doesn't match what we remember, ceremony aborted"
+msgstr "поле state не совпадает с тем, что мы помним, церемония отменена"
+
+#: src/components/signin.rs:463
+msgid "issuer doesn't match what we remember, ceremony aborted"
+msgstr "поле issuer не совпадает с тем, что мы помним, церемония отменена"
+
+#: src/lib.rs:131
+msgid "Bowl for Kittybox"
+msgstr "Bowl для Kittybox"
+
+#: src/lib.rs:169
+msgid "Sign out"
+msgstr "Выйти"
+
+#: src/lib.rs:170
+msgid "Preferences"
+msgstr "Настройки"
+
+#: src/lib.rs:171
+msgid "About"
+msgstr "О приложении"
+
+#: src/lib.rs:185
+msgid "Bowl - Sign in with your website"
+msgstr "Bowl - Войдите со своим веб-сайтом"
+
+#: src/lib.rs:201
+msgid "Publish"
+msgstr "Опубликовать"
+
+#: src/lib.rs:331
+msgid "Micropub access token for {}"
+msgstr "Токен доступа Micropub для {}"
diff --git a/src/components/post_editor.rs b/src/components/post_editor.rs
index 534b0dd..d1a6cf0 100644
--- a/src/components/post_editor.rs
+++ b/src/components/post_editor.rs
@@ -1,3 +1,4 @@
+use gettextrs::*;
 use crate::components;
 use crate::components::tag_pill::*;
 use adw::prelude::*;
@@ -138,7 +139,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
                         #[name = "name_label"]
                         gtk::Label {
-                            set_markup: "Name",
+                            set_markup: &gettext("Name"),
                             set_margin_horizontal: 10,
                             set_halign: gtk::Align::Start,
                             set_valign: gtk::Align::Center,
@@ -153,7 +154,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
                         #[name = "summary_label"]
                         gtk::Label {
-                            set_markup: "Summary",
+                            set_markup: &gettext("Summary"),
                             set_margin_horizontal: 10,
                             set_halign: gtk::Align::Start,
                             set_valign: gtk::Align::Center,
@@ -175,7 +176,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
                         #[name = "tag_label"]
                         gtk::Label {
-                            set_markup: "Tags",
+                            set_markup: &gettext("Tags"),
                             set_margin_horizontal: 10,
                             set_halign: gtk::Align::Start,
                             set_valign: gtk::Align::Center,
@@ -223,7 +224,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
                         #[name = "content_label"]
                         gtk::Label {
-                            set_markup: "Content",
+                            set_markup: &gettext("Content"),
                             set_halign: gtk::Align::Start,
                             set_valign: gtk::Align::Start,
                             set_margin_vertical: 10,
@@ -276,7 +277,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
                                         #[name = "visibility_label"]
                                         gtk::Label {
-                                            set_markup: "Visibility",
+                                            set_markup: &gettext("Visibility"),
                                             set_halign: gtk::Align::Start,
                                             set_valign: gtk::Align::Start,
                                             set_margin_vertical: 10,
@@ -373,7 +374,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                 [] as [gtk::Expression; 0],
                 glib::closure::RustClosure::new(|v| {
                     let list_item = v[0].get::<adw::EnumListItem>().unwrap();
-                    Some(list_item.name().into())
+                    Some(gettext(list_item.name().as_str()).into())
                 })
             )
         ));
@@ -489,7 +490,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
             Input::SmartSummary(components::SmartSummaryOutput::Error(err)) => {
                 self.set_smart_summary_busy_guard(None);
 
-                let toast = adw::Toast::new(&format!("Smart Summary error: {}", err));
+                let toast = adw::Toast::new(&gettext!("Smart Summary error: {}", err));
                 toast.set_timeout(0);
                 toast.set_priority(adw::ToastPriority::High);
                 root.add_toast(toast);
@@ -536,8 +537,8 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                 self.summary_buffer.set_text("");
                 self.tags.guard().clear();
                 self.content_buffer.set_text("");
-                let toast = adw::Toast::new("Post submitted");
-                toast.set_button_label(Some("Open"));
+                let toast = adw::Toast::new(&gettext("Post submitted"));
+                toast.set_button_label(Some(&gettext("Open")));
                 toast.connect_button_clicked(move |toast| {
                     gtk::UriLauncher::new(&location.to_string()).launch(
                         None::<&adw::ApplicationWindow>,
@@ -555,7 +556,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                 root.add_toast(toast);
             },
             Input::SubmitError(err) => {
-                let toast = adw::Toast::new(&format!("Error sending post: {}", err));
+                let toast = adw::Toast::new(&gettext!("Error sending post: {}", err));
                 toast.set_timeout(0);
                 toast.set_priority(adw::ToastPriority::High);
 
diff --git a/src/components/signin.rs b/src/components/signin.rs
index 08e850a..156686e 100644
--- a/src/components/signin.rs
+++ b/src/components/signin.rs
@@ -1,3 +1,4 @@
+use gettextrs::*;
 use std::cell::RefCell;
 
 use adw::prelude::*;
@@ -87,7 +88,7 @@ fn callback_handler(sender: AsyncComponentSender<SignIn>) -> impl Fn(&soup::Serv
                 msg.set_response(
                     Some("text/plain; charset=\"utf-8\""),
                     soup::MemoryUse::Static,
-                    "Thank you! This window can now be closed.".as_bytes()
+                    gettext("Thank you! This window can now be closed.").as_bytes()
                 );
                 msg.connect_finished(move |_| {
                     sender.input(Input::Callback(Ok(response.take().unwrap())));
@@ -206,12 +207,12 @@ impl AsyncComponent for SignIn {
 
                     gtk::Label {
                         add_css_class: "title-1",
-                        set_text: "Sign in",
+                        set_text: &gettext("Sign in"),
                         set_justify: gtk::Justification::Center,
                     },
 
                     gtk::Label {
-                        set_text: "Please sign in with your website to use Bowl.\nYour website needs to support IndieAuth and Micropub for this app to work.",
+                        set_text: &gettext("Please sign in with your website to use Bowl.\nYour website needs to support IndieAuth and Micropub for this app to work."),
                         set_wrap: true,
                         set_halign: gtk::Align::BaselineCenter,
                         set_valign: gtk::Align::BaselineCenter,
@@ -240,12 +241,12 @@ impl AsyncComponent for SignIn {
                             },
                             gtk::Label {
                                 #[watch]
-                                set_text: if model.busy_guard.is_some() {
-                                    "Talking to your website..."
+                                set_text: &if model.busy_guard.is_some() {
+                                    gettext("Talking to your website...")
                                 } else if model.callback_server.is_some() {
-                                    "Waiting for authorization..."
+                                    gettext("Waiting for authorization...")
                                 } else {
-                                    "Sign in"
+                                    gettext("Sign in")
                                 },
                             }
                         },
@@ -451,7 +452,7 @@ impl AsyncComponent for SignIn {
                 if res.state != self.state {
                     return self.bail_out(widgets, sender, IndieauthError {
                         kind: kittybox_indieauth::ErrorKind::InvalidRequest,
-                        msg: Some("state doesn't match what we remember, ceremony aborted".to_owned()),
+                        msg: Some(gettext("state doesn't match what we remember, ceremony aborted")),
                         error_uri: None,
                     }.into())
                 }
@@ -459,7 +460,7 @@ impl AsyncComponent for SignIn {
                 if res.iss != metadata.issuer {
                     return self.bail_out(widgets, sender, IndieauthError {
                         kind: kittybox_indieauth::ErrorKind::InvalidRequest,
-                        msg: Some("issuer doesn't match what we remember, ceremony aborted".to_owned()),
+                        msg: Some(gettext("issuer doesn't match what we remember, ceremony aborted")),
                         error_uri: None,
                     }.into())
                 }
diff --git a/src/components/smart_summary.rs b/src/components/smart_summary.rs
index fbfc80b..9da67af 100644
--- a/src/components/smart_summary.rs
+++ b/src/components/smart_summary.rs
@@ -1,4 +1,5 @@
 use adw::prelude::*;
+use gettextrs::*;
 use relm4::{gtk, prelude::{Component, ComponentParts}, ComponentSender};
 
 #[derive(Debug, Default)]
@@ -42,7 +43,8 @@ impl Component for SmartSummaryButton {
             connect_clicked => Input::ButtonPressed,
             #[watch]
             set_sensitive: !model.busy,
-            set_tooltip_markup: Some("<b>Smart Summary</b>\nAsk a language model for a single-sentence summary."),
+            // TRANSLATORS: please keep the newline and `<b>` tags
+            set_tooltip_markup: Some(gettext("<b>Smart Summary</b>\nAsk a language model for a single-sentence summary.")).as_deref(),
 
             if model.busy {
                 gtk::Spinner { set_spinning: true }
diff --git a/src/lib.rs b/src/lib.rs
index 530eab6..ee92350 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+use gettextrs::*;
 use adw::prelude::*;
 use libsecret::prelude::{RetrievableExtManual, RetrievableExt};
 use relm4::{actions::{RelmAction, RelmActionGroup}, gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
@@ -14,7 +15,7 @@ pub mod components {
     };
 
     pub(crate) mod tag_pill;
-    // pub(crate) use tag_pill::{TagPill, TagPillDelete};
+    // pub(crate) use tag_pill::{TagPill, TagPillDelete}
 
     pub mod signin;
     pub use signin::{SignIn, Output as SignInOutput};
@@ -127,7 +128,7 @@ impl App {
 
     fn about() -> adw::AboutDialog {
         adw::AboutDialog::builder()
-            .application_name("Bowl for Kittybox")
+            .application_name(gettext("Bowl for Kittybox"))
             .developer_name("Vika Shleina")
             .version(env!("CARGO_PKG_VERSION"))
             .website("https://kittybox.fireburn.ru/bowl/")
@@ -165,9 +166,9 @@ impl AsyncComponent for App {
 
     menu! {
         main_menu: {
-            "Sign out" => SignOutAction,
-            "Preferences" => PreferencesAction,
-            "About" => AboutAction,
+            &gettext("Sign out") => SignOutAction,
+            &gettext("Preferences") => PreferencesAction,
+            &gettext("About") => AboutAction,
         }
     }
 
@@ -181,10 +182,10 @@ impl AsyncComponent for App {
 
             #[watch]
             set_title: if model.micropub.is_none() {
-                Some("Bowl – Sign in with your website")
+                Some(gettext("Bowl - Sign in with your website"))
             } else {
-                Some("Bowl")
-            },
+                Some(gettext("Bowl"))
+            }.as_deref(),
 
             adw::ToolbarView {
                 add_top_bar = &adw::HeaderBar {
@@ -197,7 +198,7 @@ impl AsyncComponent for App {
                     },
                     pack_end = &gtk::Button {
                         set_icon_name: "document-send-symbolic",
-                        set_tooltip: "Send post",
+                        set_tooltip: &gettext("Publish"),
                         #[watch]
                         set_visible: model.micropub.is_some(),
                         #[watch]
@@ -327,7 +328,7 @@ impl AsyncComponent for App {
                     Some(&self.secret_schema),
                     attributes.clone(),
                     Some(libsecret::COLLECTION_DEFAULT),
-                    &format!("Micropub access token for {}", &data.me),
+                    &gettext!("Micropub access token for {}", &data.me),
                     &data.access_token
                 ).await {
                     Ok(()) => {},
diff --git a/src/main.rs b/src/main.rs
index d906a66..d7dd0a1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,6 +4,13 @@ static GLIB_LOGGER: glib::GlibLogger = glib::GlibLogger::new(
 );
 
 fn main() {
+    gettextrs::bindtextdomain(
+        env!("CARGO_PKG_NAME"),
+        env!("LOCALEDIR")
+    ).expect("failed to bind text domain");
+    gettextrs::bind_textdomain_codeset(env!("CARGO_PKG_NAME"), "UTF-8").unwrap();
+    gettextrs::textdomain(env!("CARGO_PKG_NAME")).unwrap();
+
     log::set_logger(&GLIB_LOGGER).unwrap();
     log::set_max_level(log::LevelFilter::Debug);