[fractal] i18n: Add formatting i18n methods compatible with xgettext
- From: Kévin Commaille <kcommaille src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] i18n: Add formatting i18n methods compatible with xgettext
- Date: Wed, 6 Apr 2022 17:54:25 +0000 (UTC)
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"><widget> 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]