[fractal] i18n: Add formatting i18n methods compatible with xgettext



commit 5de88e83ff591f75803b12b3fe6adb1637293b47
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Mon Apr 4 14:43:11 2022 +0200

    i18n: Add formatting i18n methods compatible with xgettext

 data/resources/ui/content-invite.ui                |   3 +-
 po/POTFILES.in                                     |   1 +
 po/POTFILES.skip                                   |   3 +
 po/meson.build                                     |   4 +-
 scripts/checks.sh                                  |  23 +++-
 src/i18n.rs                                        |  70 ++++++++++++
 src/login/mod.rs                                   |  45 ++++----
 src/main.rs                                        |   2 +
 src/secret.rs                                      |  10 +-
 .../account_settings/devices_page/device_row.rs    |   5 +-
 src/session/content/invite.rs                      |   8 ++
 .../content/room_details/member_page/mod.rs        |  20 +++-
 src/session/content/room_history/state_row/mod.rs  | 124 ++++++++++++++++-----
 .../content/room_history/verification_info_bar.rs  |  23 ++--
 .../verification/identity_verification_widget.rs   |  59 +++++++---
 src/session/mod.rs                                 |   2 +-
 src/session/room/mod.rs                            |  52 ++++++---
 src/session/room_list.rs                           |   5 +-
 src/user_facing_error.rs                           |  13 ++-
 src/window.rs                                      |   6 +-
 20 files changed, 366 insertions(+), 112 deletions(-)
---
diff --git a/data/resources/ui/content-invite.ui b/data/resources/ui/content-invite.ui
index 2ebcf6d0f..2587b7de7 100644
--- a/data/resources/ui/content-invite.ui
+++ b/data/resources/ui/content-invite.ui
@@ -86,8 +86,7 @@
                       </object>
                     </child>
                     <child>
-                      <object class="LabelWithWidgets">
-                        <property name="label" translatable="yes">&lt;widget&gt; invited you</property>
+                      <object class="LabelWithWidgets" id="inviter">
                         <child>
                           <object class="Pill">
                             <binding name="user">
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 7719f7fa9..da8651382 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -52,6 +52,7 @@ src/session/account_settings/user_page/change_password_subpage.rs
 src/session/account_settings/user_page/deactivate_account_subpage.rs
 src/session/account_settings/user_page/mod.rs
 src/session/content/explore/public_room_row.rs
+src/session/content/invite.rs
 src/session/content/room_details/member_page/mod.rs
 src/session/content/room_details/mod.rs
 src/session/content/room_history/item_row.rs
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
new file mode 100644
index 000000000..395707b39
--- /dev/null
+++ b/po/POTFILES.skip
@@ -0,0 +1,3 @@
+# These are files that we don't want to translate
+# Please keep this file sorted alphabetically.
+src/i18n.rs
diff --git a/po/meson.build b/po/meson.build
index 57d1266b3..4a226d453 100644
--- a/po/meson.build
+++ b/po/meson.build
@@ -1 +1,3 @@
-i18n.gettext(gettext_package, preset: 'glib')
+i18n.gettext(gettext_package,
+             args: ['--keyword=gettext_f', '--keyword=ngettext_f:1,2',],
+             preset: 'glib')
diff --git a/scripts/checks.sh b/scripts/checks.sh
index 1936ba503..0e3b0c9da 100755
--- a/scripts/checks.sh
+++ b/scripts/checks.sh
@@ -312,8 +312,11 @@ check_potfiles() {
     # Get UI files with 'translatable="yes"'.
     ui_files=(`grep -lIr 'translatable="yes"' data/resources/ui/*`)
 
-    # Get Rust files with regex 'gettext[!]?\('.
-    rs_files=(`grep -lIrE 'gettext[!]?\(' src/*`)
+    # Get Rust files with regex 'gettext(_f)?\(', except `src/i18n.rs`.
+    rs_files=(`grep -lIrE 'gettext(_f)?\(' --exclude=i18n.rs src/*`)
+    
+    # Get Rust files with macros, regex 'gettext!\('.
+    rs_macro_files=(`grep -lIrE 'gettext!\(' src/*`)
 
     # Remove common files
     to_diff1=("${ui_potfiles[@]}")
@@ -352,7 +355,7 @@ check_potfiles() {
         ret=1
     elif [[ $files_count -ne 0 ]]; then
         echo ""
-        echo -e "$error Found $files_count with translatable strings not present in POTFILES.in:"
+        echo -e "$error Found $files_count files with translatable strings not present in POTFILES.in:"
         ret=1
     fi
     for file in ${ui_files[@]}; do
@@ -362,6 +365,20 @@ check_potfiles() {
         echo $file
     done
 
+    let rs_macro_count=$((${#rs_macro_files[@]}))
+    if [[ $rs_macro_count -eq 1 ]]; then
+        echo ""
+        echo -e "$error Found 1 Rust file that uses a gettext-rs macro, use the corresponding i18n method 
instead:"
+        ret=1
+    elif [[ $rs_macro_count -ne 0 ]]; then
+        echo ""
+        echo -e "$error Found $rs_macro_count Rust files that use a gettext-rs macro, use the corresponding 
i18n method instead:"
+        ret=1
+    fi
+    for file in ${rs_macro_files[@]}; do
+        echo $file
+    done
+
     if [[ ret -eq 1 ]]; then
         echo ""
         echo -e "  Checking po/POTFILES.in result: $fail"
diff --git a/src/i18n.rs b/src/i18n.rs
new file mode 100644
index 000000000..ea704f949
--- /dev/null
+++ b/src/i18n.rs
@@ -0,0 +1,70 @@
+use gettextrs::{gettext, ngettext};
+
+fn freplace(s: String, args: &[(&str, &str)]) -> String {
+    let mut s = s;
+
+    for (k, v) in args {
+        s = s.replace(&format!("{{{}}}", k), v);
+    }
+
+    s
+}
+
+/// Like `gettext`, but replaces named variables with the given dictionary.
+///
+/// The expected format to replace is `{name}`, where `name` is the first string
+/// in the dictionary entry tuple.
+pub fn gettext_f(msgid: &str, args: &[(&str, &str)]) -> String {
+    let s = gettext(msgid);
+    freplace(s, args)
+}
+
+/// Like `ngettext`, but replaces named variables with the given dictionary.
+///
+/// The expected format to replace is `{name}`, where `name` is the first string
+/// in the dictionary entry tuple.
+pub fn ngettext_f(msgid: &str, msgid_plural: &str, n: u32, args: &[(&str, &str)]) -> String {
+    let s = ngettext(msgid, msgid_plural, n);
+    freplace(s, args)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_gettext_f() {
+        let out = gettext_f("{one} param", &[("one", "one")]);
+        assert_eq!(out, "one param");
+
+        let out = gettext_f("middle {one} param", &[("one", "one")]);
+        assert_eq!(out, "middle one param");
+
+        let out = gettext_f("end {one}", &[("one", "one")]);
+        assert_eq!(out, "end one");
+
+        let out = gettext_f("multiple {one} and {two}", &[("one", "1"), ("two", "two")]);
+        assert_eq!(out, "multiple 1 and two");
+
+        let out = gettext_f("multiple {two} and {one}", &[("one", "1"), ("two", "two")]);
+        assert_eq!(out, "multiple two and 1");
+
+        let out = gettext_f("multiple {one} and {one}", &[("one", "1"), ("two", "two")]);
+        assert_eq!(out, "multiple 1 and 1");
+
+        let out = ngettext_f(
+            "singular {one} and {two}",
+            "plural {one} and {two}",
+            1,
+            &[("one", "1"), ("two", "two")],
+        );
+        assert_eq!(out, "singular 1 and two");
+        let out = ngettext_f(
+            "singular {one} and {two}",
+            "plural {one} and {two}",
+            2,
+            &[("one", "1"), ("two", "two")],
+        );
+        assert_eq!(out, "plural 1 and two");
+    }
+}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 5500bb450..1c179969e 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -23,7 +23,7 @@ use login_advanced_dialog::LoginAdvancedDialog;
 
 use crate::{
     components::{EntryRow, PasswordEntryRow, SpinnerButton, Toast},
-    spawn, spawn_tokio,
+    gettext_f, spawn, spawn_tokio,
     user_facing_error::UserFacingError,
     Session,
 };
@@ -339,7 +339,15 @@ impl Login {
             ));
         } else {
             priv_.homeserver_entry.set_title(&gettext("Homeserver URL"));
-            priv_.homeserver_help.set_markup(&gettext("The URL of your Matrix homeserver, for example <span 
segment=\"word\">https://gnome.modular.im</span>"));
+            priv_.homeserver_help.set_markup(&gettext_f(
+                // Translators: Do NOT translate the content between '{' and '}', this is a
+                // variable name.
+                "The URL of your Matrix homeserver, for example {address}",
+                &[(
+                    "address",
+                    "<span segment=\"word\">https://gnome.modular.im</span>",
+                )],
+            ));
         }
         self.update_next_action();
     }
@@ -450,24 +458,23 @@ impl Login {
 
     fn show_password_page(&self) {
         let priv_ = self.imp();
-        if self.autodiscovery() {
-            // Translators: the variable is a domain name, eg. gnome.org.
-            priv_.password_title.set_markup(&gettext!(
-                "Connecting to {}",
-                format!(
-                    "<span segment=\"word\">{}</span>",
-                    priv_.homeserver_entry.text()
-                )
-            ));
+
+        let domain_name = if self.autodiscovery() {
+            priv_.homeserver_entry.text().to_string()
         } else {
-            priv_.password_title.set_markup(&gettext!(
-                "Connecting to {}",
-                format!(
-                    "<span segment=\"word\">{}</span>",
-                    self.homeserver_pretty().unwrap()
-                )
-            ));
-        }
+            self.homeserver_pretty().unwrap()
+        };
+
+        priv_.password_title.set_markup(&gettext_f(
+            // Translators: Do NOT translate the content between '{' and '}', this is a variable
+            // name.
+            "Connecting to {domain_name}",
+            &[(
+                "domain_name",
+                &format!("<span segment=\"word\">{}</span>", domain_name),
+            )],
+        ));
+
         self.set_visible_child("password");
     }
 
diff --git a/src/main.rs b/src/main.rs
index 6118c8abd..3efd8ca87 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,6 +12,7 @@ mod components;
 mod contrib;
 mod error_page;
 mod greeter;
+mod i18n;
 mod login;
 mod secret;
 mod session;
@@ -28,6 +29,7 @@ use self::{
     application::Application,
     error_page::{ErrorPage, ErrorSubpage},
     greeter::Greeter,
+    i18n::*,
     login::Login,
     session::Session,
     user_facing_error::UserFacingError,
diff --git a/src/secret.rs b/src/secret.rs
index 1cb1be694..97db546f4 100644
--- a/src/secret.rs
+++ b/src/secret.rs
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
 use serde_json::error::Error as JsonError;
 use url::Url;
 
-use crate::{config::APP_ID, ErrorSubpage};
+use crate::{config::APP_ID, gettext_f, ErrorSubpage};
 
 /// Any error that can happen when interacting with the secret service.
 #[derive(Debug, Clone)]
@@ -266,8 +266,12 @@ pub async fn store_session(session: &StoredSession) -> Result<(), SecretError> {
         Some(&schema()),
         attributes,
         Some(&COLLECTION_DEFAULT),
-        // Translators: The parameter is a Matrix User ID
-        &gettext!("Fractal: Matrix credentials for {}", session.user_id),
+        &gettext_f(
+            // Translators: Do NOT translate the content between '{' and '}', this is a variable
+            // name.
+            "Fractal: Matrix credentials for {user_id}",
+            &[("user_id", session.user_id.as_str())],
+        ),
         &secret,
     )
     .await?;
diff --git a/src/session/account_settings/devices_page/device_row.rs 
b/src/session/account_settings/devices_page/device_row.rs
index 76ef9cda8..ba11ced14 100644
--- a/src/session/account_settings/devices_page/device_row.rs
+++ b/src/session/account_settings/devices_page/device_row.rs
@@ -6,7 +6,7 @@ use log::error;
 use super::Device;
 use crate::{
     components::{AuthError, SpinnerButton, Toast},
-    spawn,
+    gettext_f, spawn,
 };
 
 mod imp {
@@ -212,7 +212,8 @@ impl DeviceRow {
                         error!("Failed to disconnect device {}: {err:?}", device.device_id());
                         if let Some(adw_window) = window.and_then(|w| 
w.downcast::<adw::PreferencesWindow>().ok()) {
                             let device_name = device.display_name();
-                            let error_message = gettext!("Failed to disconnect device “{}”", device_name);
+                            // Translators: Do NOT translate the content between '{' and '}', this is a 
variable name.
+                            let error_message = gettext_f("Failed to disconnect device “{device_name}”", 
&[("device_name", device_name)]);
                             adw_window.add_toast(&Toast::new(&error_message).into());
                         }
                     },
diff --git a/src/session/content/invite.rs b/src/session/content/invite.rs
index 60e1a3c34..47a4eb59c 100644
--- a/src/session/content/invite.rs
+++ b/src/session/content/invite.rs
@@ -3,6 +3,7 @@ use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate
 
 use crate::{
     components::{Avatar, LabelWithWidgets, Pill, SpinnerButton},
+    gettext_f,
     session::room::{Room, RoomType},
     spawn,
 };
@@ -30,6 +31,8 @@ mod imp {
         #[template_child]
         pub room_topic: TemplateChild<gtk::Label>,
         #[template_child]
+        pub inviter: TemplateChild<LabelWithWidgets>,
+        #[template_child]
         pub accept_button: TemplateChild<SpinnerButton>,
         #[template_child]
         pub reject_button: TemplateChild<SpinnerButton>,
@@ -126,6 +129,11 @@ mod imp {
 
             self.room_topic
                 .set_visible(!self.room_topic.label().is_empty());
+
+            // Translators: Do NOT translate the content between '{' and '}', this is a
+            // variable name.
+            self.inviter
+                .set_label(Some(gettext_f("{user} invited you", &[("user", "widget")])));
         }
     }
 
diff --git a/src/session/content/room_details/member_page/mod.rs 
b/src/session/content/room_details/member_page/mod.rs
index 2ed11ce17..b305149d9 100644
--- a/src/session/content/room_details/member_page/mod.rs
+++ b/src/session/content/room_details/member_page/mod.rs
@@ -1,5 +1,4 @@
 use adw::{prelude::*, subclass::prelude::*};
-use gettextrs::ngettext;
 use gtk::{
     glib::{self, clone, closure},
     subclass::prelude::*,
@@ -13,6 +12,7 @@ mod member_row;
 use self::{member_menu::MemberMenu, member_row::MemberRow};
 use crate::{
     components::{Avatar, Badge},
+    ngettext_f,
     prelude::*,
     session::{
         content::RoomDetails,
@@ -231,7 +231,14 @@ impl MemberPage {
         let priv_ = self.imp();
         priv_
             .member_count
-            .set_text(&ngettext!("{} Member", "{} Members", n, n));
+            // Translators: Do NOT translate the content between '{' and '}', this is a variable
+            // name.
+            .set_text(&ngettext_f(
+                "1 Member",
+                "{n} Members",
+                n,
+                &[("n", &n.to_string())],
+            ));
         // FIXME: This won't be needed when we can request the natural height
         // on AdwPreferencesPage
         // See: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/77
@@ -283,7 +290,14 @@ impl MemberPage {
         priv_.invited_section.set_visible(n > 0);
         priv_
             .invited_section
-            .set_title(&ngettext!("{} Invited", "{} Invited", n, n));
+            // Translators: Do NOT translate the content between '{' and '}', this is a variable
+            // name.
+            .set_title(&ngettext_f(
+                "1 Invited",
+                "{} Invited",
+                n,
+                &[("n", &n.to_string())],
+            ));
         // FIXME: This won't be needed when we can request the natural height
         // on AdwPreferencesPage
         // See: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/77
diff --git a/src/session/content/room_history/state_row/mod.rs 
b/src/session/content/room_history/state_row/mod.rs
index 37a9ef676..8e051efec 100644
--- a/src/session/content/room_history/state_row/mod.rs
+++ b/src/session/content/room_history/state_row/mod.rs
@@ -10,6 +10,7 @@ use matrix_sdk::ruma::events::{
 };
 
 use self::{creation::StateCreation, tombstone::StateTombstone};
+use crate::gettext_f;
 
 mod imp {
     use glib::subclass::InitializingObject;
@@ -87,19 +88,30 @@ impl StateRow {
                             {
                                 if let Some(prev_name) = prev.displayname {
                                     if event.displayname == None {
-                                        Some(gettext!("{} removed their display name.", prev_name))
+                                        Some(gettext_f(
+                                            // Translators: Do NOT translate the content between
+                                            // '{' and '}', this is a variable name.
+                                            "{previous_user_name} removed their display name.",
+                                            &[("previous_user_name", &prev_name)],
+                                        ))
                                     } else {
-                                        Some(gettext!(
-                                            "{} changed their display name to {}.",
-                                            prev_name,
-                                            display_name
+                                        Some(gettext_f(
+                                            // Translators: Do NOT translate the content between
+                                            // '{' and '}', this is a variable name.
+                                            "{previous_user_name} changed their display name to 
{new_user_name}.",
+                                            &[("previous_user_name", &prev_name),
+                                            ("new_user_name", &display_name)]
                                         ))
                                     }
                                 } else {
-                                    Some(gettext!(
-                                        "{} set their display name to {}.",
-                                        state.state_key(),
-                                        display_name
+                                    Some(gettext_f(
+                                        // Translators: Do NOT translate the content between
+                                        // '{' and '}', this is a variable name.
+                                        "{user_id} set their display name to {new_user_name}.",
+                                        &[
+                                            ("user_id", state.state_key()),
+                                            ("new_user_name", &display_name),
+                                        ],
                                     ))
                                 }
                             }
@@ -107,28 +119,50 @@ impl StateRow {
                                 if event.avatar_url != prev.avatar_url =>
                             {
                                 if prev.avatar_url == None {
-                                    Some(gettext!("{} set their avatar.", display_name))
+                                    Some(gettext_f(
+                                        // Translators: Do NOT translate the content between
+                                        // '{' and '}', this is a variable name.
+                                        "{user} set their avatar.",
+                                        &[("user", &display_name)],
+                                    ))
                                 } else if event.avatar_url == None {
-                                    Some(gettext!("{} removed their avatar.", display_name))
+                                    Some(gettext_f(
+                                        // Translators: Do NOT translate the content between
+                                        // '{' and '}', this is a variable name.
+                                        "{user} removed their avatar.",
+                                        &[("user", &display_name)],
+                                    ))
                                 } else {
-                                    Some(gettext!("{} changed their avatar.", display_name))
+                                    Some(gettext_f(
+                                        // Translators: Do NOT translate the content between
+                                        // '{' and '}', this is a variable name.
+                                        "{user} changed their avatar.",
+                                        &[("user", &display_name)],
+                                    ))
                                 }
                             }
                             _ => None,
                         };
 
-                        WidgetType::Text(
-                            message.unwrap_or(gettext!("{} joined this room.", display_name)),
-                        )
-                    }
-                    MembershipState::Invite => {
-                        WidgetType::Text(gettext!("{} was invited to this room.", display_name))
+                        WidgetType::Text(message.unwrap_or_else(|| {
+                            // Translators: Do NOT translate the content between '{' and '}', this
+                            // is a variable name.
+                            gettext_f("{user} joined this room.", &[("user", &display_name)])
+                        }))
                     }
+                    MembershipState::Invite => WidgetType::Text(gettext_f(
+                        // Translators: Do NOT translate the content between '{' and '}', this is
+                        // a variable name.
+                        "{user} was invited to this room.",
+                        &[("user", &display_name)],
+                    )),
                     MembershipState::Knock => {
                         // TODO: Add button to invite the user.
-                        WidgetType::Text(gettext!(
-                            "{} requested to be invited to this room.",
-                            display_name
+                        WidgetType::Text(gettext_f(
+                            // Translators: Do NOT translate the content between '{' and '}', this
+                            // is a variable name.
+                            "{user} requested to be invited to this room.",
+                            &[("user", &display_name)],
                         ))
                     }
                     MembershipState::Leave => {
@@ -137,30 +171,55 @@ impl StateRow {
                                 if prev.membership == MembershipState::Invite =>
                             {
                                 if state.state_key() == state.sender() {
-                                    Some(gettext!("{} rejected the invite.", display_name))
+                                    Some(gettext_f(
+                                        // Translators: Do NOT translate the content between
+                                        // '{' and '}', this is a variable name.
+                                        "{user} rejected the invite.",
+                                        &[("user", &display_name)],
+                                    ))
                                 } else {
-                                    Some(gettext!("{}’s invite was revoked'.", display_name))
+                                    Some(gettext_f(
+                                        // Translators: Do NOT translate the content between
+                                        // '{' and '}', this is a variable name.
+                                        "{user}’s invite was revoked'.",
+                                        &[("user", &display_name)],
+                                    ))
                                 }
                             }
                             Some(AnyStateEventContent::RoomMember(prev))
                                 if prev.membership == MembershipState::Ban =>
                             {
-                                Some(gettext!("{} was unbanned.", display_name))
+                                Some(gettext_f(
+                                    // Translators: Do NOT translate the content between
+                                    // '{' and '}', this is a variable name.
+                                    "{user} was unbanned.",
+                                    &[("user", &display_name)],
+                                ))
                             }
                             _ => None,
                         };
 
                         WidgetType::Text(message.unwrap_or_else(|| {
                             if state.state_key() == state.sender() {
-                                gettext!("{} left the room.", display_name)
+                                // Translators: Do NOT translate the content between '{' and '}',
+                                // this is a variable name.
+                                gettext_f("{user} left the room.", &[("user", &display_name)])
                             } else {
-                                gettext!("{} was kicked out of the room.", display_name)
+                                gettext_f(
+                                    // Translators: Do NOT translate the content between '{' and
+                                    // '}', this is a variable name.
+                                    "{user} was kicked out of the room.",
+                                    &[("user", &display_name)],
+                                )
                             }
                         }))
                     }
-                    MembershipState::Ban => {
-                        WidgetType::Text(gettext!("{} was banned.", display_name))
-                    }
+                    MembershipState::Ban => WidgetType::Text(gettext_f(
+                        // Translators: Do NOT translate the content between '{' and '}', this is
+                        // a variable name.
+                        "{user} was banned.",
+                        &[("user", &display_name)],
+                    )),
                     _ => {
                         warn!("Unsupported room member event: {:?}", state);
                         WidgetType::Text(gettext("An unsupported room member event was received."))
@@ -172,7 +231,12 @@ impl StateRow {
                     s if s.is_empty() => state.state_key().into(),
                     s => s,
                 };
-                WidgetType::Text(gettext!("{} was invited to this room.", display_name))
+                WidgetType::Text(gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "{user} was invited to this room.",
+                    &[("user", &display_name)],
+                ))
             }
             AnyStateEventContent::RoomTombstone(event) => {
                 WidgetType::Tombstone(StateTombstone::new(&event))
diff --git a/src/session/content/room_history/verification_info_bar.rs 
b/src/session/content/room_history/verification_info_bar.rs
index ac9d47b68..43b0b2e5d 100644
--- a/src/session/content/room_history/verification_info_bar.rs
+++ b/src/session/content/room_history/verification_info_bar.rs
@@ -2,10 +2,14 @@ use adw::subclass::prelude::*;
 use gettextrs::gettext;
 use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
 
-use crate::session::{
-    user::UserExt,
-    verification::{IdentityVerification, VerificationState},
+use crate::{
+    gettext_f,
+    session::{
+        user::UserExt,
+        verification::{IdentityVerification, VerificationState},
+    },
 };
+
 mod imp {
     use std::cell::RefCell;
 
@@ -160,11 +164,14 @@ impl VerificationInfoBar {
             if request.is_finished() {
                 false
             } else if matches!(request.state(), VerificationState::Requested) {
-                // Translators: The value is the display name of the user who wants to be
-                // verified
-                priv_.label.set_markup(&gettext!(
-                    "<b>{}</b> wants to be verified",
-                    request.user().display_name()
+                priv_.label.set_markup(&gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "{user_name} wants to be verified",
+                    &[(
+                        "user_name",
+                        &format!("<b>{}</b>", request.user().display_name()),
+                    )],
                 ));
                 priv_.accept_btn.set_label(&gettext("Verify"));
                 priv_.cancel_btn.set_label(&gettext("Decline"));
diff --git a/src/session/content/verification/identity_verification_widget.rs 
b/src/session/content/verification/identity_verification_widget.rs
index 840d6c502..d9e95546c 100644
--- a/src/session/content/verification/identity_verification_widget.rs
+++ b/src/session/content/verification/identity_verification_widget.rs
@@ -8,6 +8,7 @@ use super::Emoji;
 use crate::{
     components::SpinnerButton,
     contrib::{QRCode, QRCodeExt, QrCodeScanner},
+    gettext_f,
     session::{
         user::UserExt,
         verification::{
@@ -547,32 +548,54 @@ impl IdentityVerificationWidget {
                 priv_.label1.set_markup(&gettext("Verification Request"));
                 priv_
                     .label2
-                    .set_markup(&gettext!("<b>{}</b> asked do be verified. Verifying an user increases the 
security of the conversation.", name));
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    .set_markup(&gettext_f("{user} asked to be verified. Verifying a user increases the 
security of the conversation.", &[("user", &format!("<b>{}</b>", name))]));
                 priv_.label3.set_markup(&gettext("Verification Request"));
-                priv_.label4.set_markup(&gettext!(
-                    "Scan the QR code shown on the device of <b>{}</b>.",
-                    name
+                priv_.label4.set_markup(&gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "Scan the QR code shown on the device of {user}.",
+                    &[("user", &format!("<b>{}</b>", name))],
                 ));
-                priv_.label5.set_markup(&gettext!("You scanned the QR code successfully. <b>{}</b> may need 
to confirm the verification.", name));
+                // Translators: Do NOT translate the content between '{' and '}', this is a
+                // variable name.
+                priv_.label5.set_markup(&gettext_f("You scanned the QR code successfully. {user} may need to 
confirm the verification.", &[("user", &format!("<b>{}</b>", name))]));
                 priv_.label8.set_markup(&gettext("Verification Request"));
-                priv_.label9.set_markup(&gettext(
-                    "Ask <b>{}</b> to scan this QR code from their session.",
+                priv_.label9.set_markup(&gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "Ask {user} to scan this QR code from their session.",
+                    &[("user", &format!("<b>{}</b>", name))],
                 ));
                 priv_.label10.set_markup(&gettext("Verification Request"));
-                priv_.label11.set_markup(&gettext!(
-                    "Ask <b>{}</b> if they see the following emoji appear in the same order on their 
screen.",
-                    name
+                priv_.label11.set_markup(&gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "Ask {user} if they see the following emoji appear in the same order on their screen.",
+                    &[("user", &format!("<b>{}</b>", name))]
                 ));
                 priv_.label12.set_markup(&gettext("Verification Complete"));
-                priv_.label13.set_markup(&gettext!("<b>{}</b> is verified and you can now be sure that your 
communication will be private.", name));
-                priv_.label14.set_markup(&gettext!("Waiting for {}", name));
-                priv_.label15.set_markup(&gettext!(
-                    "Ask <b>{}</b> to accept the verification request.",
-                    name
+                // Translators: Do NOT translate the content between '{' and '}', this is a
+                // variable name.
+                priv_.label13.set_markup(&gettext_f("{user} is verified and you can now be sure that your 
communication will be private.", &[("user", &format!("<b>{}</b>", name))]));
+                priv_.label14.set_markup(&gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "Waiting for {user}",
+                    &[("user", &format!("<b>{}</b>", name))],
                 ));
-                priv_.label16.set_markup(&gettext!(
-                    "Does <b>{}</b> see a confirmation shield on their session?",
-                    name
+                priv_.label15.set_markup(&gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "Ask {user} to accept the verification request.",
+                    &[("user", &format!("<b>{}</b>", name))],
+                ));
+                priv_.label16.set_markup(&gettext_f(
+                    // Translators: Do NOT translate the content between '{' and '}', this is a
+                    // variable name.
+                    "Does {user} see a confirmation shield on their session?",
+                    &[("user", &format!("<b>{}</b>", name))],
                 ));
             }
         }
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 108884960..cae721659 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -460,7 +460,7 @@ impl Session {
                         warn!("Couldn't store session: {:?}", error);
                         if let Some(window) = self.parent_window() {
                             window.switch_to_error_page(
-                                &gettext!("Unable to store session: {}", error),
+                                &format!("{}\n\n{}", gettext("Unable to store session"), error),
                                 error,
                             );
                         }
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index 63c1a9977..a05076a8b 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -58,6 +58,7 @@ pub use self::{
 };
 use crate::{
     components::{Pill, Toast},
+    gettext_f, ngettext_f,
     prelude::*,
     session::{
         avatar::update_room_avatar_from_file, room::member_list::MemberList, Avatar, Session, User,
@@ -415,7 +416,8 @@ impl Room {
 
                             let room_pill = Pill::for_room(&obj);
                             let error = Toast::builder()
-                                .title(&gettext("Failed to forget <widget>."))
+                                // Translators: Do NOT translate the content between '{' and '}', this is a 
variable name.
+                                .title(&gettext_f("Failed to forget {room}.", &[("room", "<widget>")]))
                                 .widgets(&[&room_pill])
                                 .build();
 
@@ -560,10 +562,10 @@ impl Room {
 
                                 let room_pill = Pill::for_room(&obj);
                                 let error = Toast::builder()
-                                    .title(&gettext!(
-                                        "Failed to move <widget> from {} to {}.",
-                                        previous_category.to_string(),
-                                        category.to_string()
+                                    .title(&gettext_f(
+                                        // Translators: Do NOT translate the content between '{' and '}', 
this is a variable name.
+                                        "Failed to move {room} from {previous_category} to {new_category}.",
+                                        &[("room", "<widget>"),("previous_category", 
&previous_category.to_string()), ("new_category", &category.to_string())],
                                     ))
                                     .widgets(&[&room_pill])
                                     .build();
@@ -1269,8 +1271,11 @@ impl Room {
 
                     let room_pill = Pill::for_room(self);
                     let error = Toast::builder()
-                        .title(&gettext(
-                            "Failed to accept invitation for <widget>. Try again later.",
+                        .title(&gettext_f(
+                            // Translators: Do NOT translate the content between '{' and '}', this
+                            // is a variable name.
+                            "Failed to accept invitation for {room}. Try again later.",
+                            &[("room", "<widget>")],
                         ))
                         .widgets(&[&room_pill])
                         .build();
@@ -1300,8 +1305,11 @@ impl Room {
 
                     let room_pill = Pill::for_room(self);
                     let error = Toast::builder()
-                        .title(&gettext(
-                            "Failed to reject invitation for <widget>. Try again later.",
+                        .title(&gettext_f(
+                            // Translators: Do NOT translate the content between '{' and '}', this
+                            // is a variable name.
+                            "Failed to reject invitation for {room}. Try again later.",
+                            &[("room", "<widget>")],
                         ))
                         .widgets(&[&room_pill])
                         .build();
@@ -1465,13 +1473,25 @@ impl Room {
                 let first_failed = failed_invites.first().unwrap();
 
                 // TODO: should we show all the failed users?
-                let error_message = if no_failed == 1 {
-                    gettext("Failed to invite <widget> to <widget>. Try again later.")
-                } else if no_failed == 2 {
-                    gettext("Failed to invite <widget> and some other user to <widget>. Try again later.")
-                } else {
-                    gettext("Failed to invite <widget> and some other users to <widget>. Try again later.")
-                };
+                let error_message =
+                    if no_failed == 1 {
+                        gettext_f(
+                            // Translators: Do NOT translate the content between '{' and '}', this
+                            // is a variable name.
+                            "Failed to invite {user} to {room}. Try again later.",
+                            &[("user", "<widget>"), ("room", "<widget>")],
+                        )
+                    } else {
+                        let n = (no_failed - 1) as u32;
+                        ngettext_f(
+                        // Translators: Do NOT translate the content between '{' and '}', this
+                        // is a variable name.
+                        "Failed to invite {user} and 1 other user to {room}. Try again later.",
+                        "Failed to invite {user} and {n} other users to {room}. Try again later.",
+                        n,
+                        &[("user", "<widget>"), ("room", "<widget>"), ("n", &n.to_string())],
+                    )
+                    };
                 let user_pill = Pill::for_user(first_failed);
                 let room_pill = Pill::for_room(self);
                 let error = Toast::builder()
diff --git a/src/session/room_list.rs b/src/session/room_list.rs
index d222e9c92..d0a6a20bd 100644
--- a/src/session/room_list.rs
+++ b/src/session/room_list.rs
@@ -1,6 +1,5 @@
 use std::{cell::Cell, collections::HashSet};
 
-use gettextrs::gettext;
 use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
 use indexmap::map::IndexMap;
 use log::error;
@@ -11,6 +10,7 @@ use matrix_sdk::{
 
 use crate::{
     components::Toast,
+    gettext_f,
     session::{room::Room, Session},
     spawn, spawn_tokio,
 };
@@ -322,7 +322,8 @@ impl RoomList {
                         obj.pending_rooms_remove(&identifier);
                         error!("Joining room {} failed: {}", identifier, error);
                         let error = Toast::new(
-                            &gettext!("Failed to join room {}. Try again later.", identifier)
+                            // Translators: Do NOT translate the content between '{' and '}', this is a 
variable name.
+                            &gettext_f("Failed to join room {room_name}. Try again later.", &[("room_name", 
identifier.as_str())])
                         );
 
                         if let Some(window) = obj.session().parent_window() {
diff --git a/src/user_facing_error.rs b/src/user_facing_error.rs
index defcc9899..902669363 100644
--- a/src/user_facing_error.rs
+++ b/src/user_facing_error.rs
@@ -8,6 +8,8 @@ use matrix_sdk::{
     ClientBuildError, Error, HttpError,
 };
 
+use crate::ngettext_f;
+
 pub trait UserFacingError {
     fn to_user_facing(self) -> String;
 }
@@ -29,9 +31,14 @@ impl UserFacingError for HttpError {
                     UserDeactivated => gettext("The account is deactivated."),
                     LimitExceeded { retry_after_ms } => {
                         if let Some(ms) = retry_after_ms {
-                            gettext!(
-                                "You exceeded the homeserver’s rate limit, retry in {} seconds.",
-                                ms.as_secs()
+                            let secs = ms.as_secs() as u32;
+                            ngettext_f(
+                                // Translators: Do NOT translate the content between '{' and '}',
+                                // this is a variable name.
+                                "You exceeded the homeserver’s rate limit, retry in 1 second.",
+                                "You exceeded the homeserver’s rate limit, retry in {n} seconds.",
+                                secs,
+                                &[("n", &secs.to_string())],
                             )
                         } else {
                             gettext("You exceeded the homeserver’s rate limit, try again later.")
diff --git a/src/window.rs b/src/window.rs
index 94f0e04ee..63cde6b73 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -199,7 +199,11 @@ impl Window {
             Err(error) => {
                 warn!("Failed to restore previous sessions: {:?}", error);
                 self.switch_to_error_page(
-                    &gettext!("Failed to restore previous sessions: {}", error),
+                    &format!(
+                        "{}\n\n{}",
+                        gettext("Failed to restore previous sessions"),
+                        error,
+                    ),
                     error,
                 );
             }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]