Browse Source

Fix plural strings without a cardinal number

A new lint in gettext disallows `ngettext` strings where a variable is
present in the singular but not the plural. So the solution is to use
`gettext` for the singular and `gettext` or `ngettext` for the plural,
depending on whether we need to use the count.

This is justified in the gettext docs that say that `ngettext`
shouldn't be used if the count isn't used in the strings.
merge-requests/1714/merge
Kévin Commaille 2 months ago
parent
commit
9c2eceb737
No known key found for this signature in database
GPG Key ID: F26F4BE20A08255B
  1. 94
      src/components/dialogs/message_dialogs.rs
  2. 19
      src/session_view/room_details/general_page.rs
  3. 21
      src/session_view/room_details/invite_subpage/mod.rs
  4. 31
      src/session_view/room_details/members_page/members_list_view/membership_subpage_row.rs
  5. 32
      src/session_view/room_details/members_page/members_list_view/mod.rs
  6. 26
      src/session_view/room_details/upgrade_dialog/mod.rs

94
src/components/dialogs/message_dialogs.rs

@ -369,24 +369,35 @@ pub(crate) async fn confirm_mute_room_member_dialog(
.first()
.expect("there should be at least one member")
.upcast_ref();
let count = members.len() as u32;
let heading = ngettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name. The count cannot be zero.
"Mute {user}?",
"Mute Members?",
count,
&[("user", &first_member.display_name())],
);
let body = ngettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name. The count cannot be zero.
"Are you sure you want to mute {user_id}? They will not be able to send new messages to this room.",
"Are you sure you want to mute these members? They will not be able to send new messages to this room.",
count,
&[("user_id", first_member.user_id().as_str())],
);
let is_single_member = members.len() == 1;
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let heading = if is_single_member {
gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Mute {user}?",
&[("user", &first_member.display_name())],
)
} else {
gettext("Mute Members?")
};
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let body = if is_single_member {
gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Are you sure you want to mute {user_id}? They will not be able to send new messages to this room.",
&[("user_id", first_member.user_id().as_str())],
)
} else {
gettext(
"Are you sure you want to mute these members? They will not be able to send new messages to this room.",
)
};
// Ask for confirmation.
let confirm_dialog = adw::AlertDialog::builder()
@ -418,24 +429,35 @@ pub(crate) async fn confirm_set_room_member_power_level_same_as_own_dialog(
.first()
.expect("there should be at least one member")
.upcast_ref();
let count = members.len() as u32;
let heading = ngettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name. The count cannot be zero.
"Promote {user}?",
"Promote Members?",
count,
&[("user", &first_member.display_name())],
);
let body = ngettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name. The count cannot be zero.
"If you promote {user_id} to the same level as yours, you will not be able to demote them in the future.",
"If you promote these members to the same level as yours, you will not be able to demote them in the future.",
count,
&[("user_id", first_member.user_id().as_str())],
);
let is_single_member = members.len() == 1;
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let heading = if is_single_member {
gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Promote {user}?",
&[("user", &first_member.display_name())],
)
} else {
gettext("Promote Members?")
};
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let body = if is_single_member {
gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name. The count cannot be zero.
"If you promote {user_id} to the same level as yours, you will not be able to demote them in the future.",
&[("user_id", first_member.user_id().as_str())],
)
} else {
gettext(
"If you promote these members to the same level as yours, you will not be able to demote them in the future.",
)
};
// Ask for confirmation.
let confirm_dialog = adw::AlertDialog::builder()

19
src/session_view/room_details/general_page.rs

@ -1,5 +1,5 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::{gettext, ngettext};
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, clone},
@ -458,7 +458,13 @@ mod imp {
// the members count to make sure we do not show a list that is too long.
let is_direct_with_few_members = room.is_direct() && joined_members_count < 5;
if is_direct_with_few_members {
let title = ngettext("Member", "Members", joined_members_count);
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let title = if joined_members_count == 1 {
gettext("Member")
} else {
gettext("Members")
};
self.direct_members_group.set_title(&title);
// Set model of direct members list dynamically to avoid creating unnecessary
@ -503,8 +509,13 @@ mod imp {
server_joined_members_count.max(joined_members_count.into());
self.members_row.set_count(joined_members_count.to_string());
let n = joined_members_count.try_into().unwrap_or(u32::MAX);
let title = ngettext("Member", "Members", n);
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let title = if joined_members_count == 1 {
gettext("Member")
} else {
gettext("Members")
};
self.members_row.set_title(&title);
if self.direct_members_list_has_bound_model.get() {

21
src/session_view/room_details/invite_subpage/mod.rs

@ -1,5 +1,5 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::ngettext;
use gettextrs::{gettext, ngettext};
use gtk::{gdk, glib, glib::clone};
use tracing::error;
@ -219,22 +219,35 @@ mod imp {
);
}
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
if n == 0 {
self.close();
} else {
} else if n == 1 {
let first_failed =
invite_list.first_invitee().map(|item| item.user()).unwrap();
toast!(
self.obj(),
ngettext(
gettext(
// Translators: Do NOT translate the content between '{' and '}', these
// are variable names.
"Could not invite {user} to {room}",
),
@user = first_failed,
@room,
n,
);
} else {
toast!(
self.obj(),
ngettext(
// Translators: Do NOT translate the content between '{' and '}', these
// are variable names. The count is always greater than 1.
"Could not invite 1 user to {room}",
"Could not invite {n} users to {room}",
n as u32,
),
@user = first_failed,
@room,
n,
);

31
src/session_view/room_details/members_page/members_list_view/membership_subpage_row.rs

@ -1,4 +1,4 @@
use gettextrs::npgettext;
use gettextrs::{gettext, pgettext};
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
use crate::{
@ -110,14 +110,35 @@ mod imp {
let item = self.item.borrow().clone()?;
let count = item.model().n_items();
// We don't use the count in the strings so we use separate pgettext calls for
// singular and plural rather than using npgettext.
let label = match item.kind() {
MembershipListKind::Join => return None,
// Translators: As in 'Invited Room Member(s)'.
MembershipListKind::Invite => npgettext("members", "Invited", "Invited", count),
// Translators: As in 'Banned Room Member(s)'.
MembershipListKind::Ban => npgettext("members", "Banned", "Banned", count),
MembershipListKind::Invite => {
if count == 1 {
// Translators: This is singular, as in 'Invited Room Member'.
pgettext("members", "Invited")
} else {
// Translators: This is plural, as in 'Invited Room Members'.
pgettext("members", "Invited")
}
}
MembershipListKind::Ban => {
if count == 1 {
// Translators: This is singular, as in 'Banned Room Member'.
pgettext("members", "Banned")
} else {
// Translators: This is plural, as in 'Banned Room Members'.
pgettext("members", "Banned")
}
}
MembershipListKind::Knock => {
npgettext("members", "Invite Request", "Invite Requests", count)
if count == 1 {
gettext("Invite Request")
} else {
gettext("Invite Requests")
}
}
};

32
src/session_view/room_details/members_page/members_list_view/mod.rs

@ -1,5 +1,5 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::{gettext, ngettext};
use gettextrs::gettext;
use gtk::{
gio, glib,
glib::{clone, closure},
@ -354,15 +354,37 @@ mod imp {
let count = membership_list.n_items();
let is_empty = count == 0;
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let title = match kind {
MembershipListKind::Join => ngettext("Room Member", "Room Members", count),
MembershipListKind::Join => {
if count == 1 {
gettext("Room Member")
} else {
gettext("Room Members")
}
}
MembershipListKind::Invite => {
ngettext("Invited Room Member", "Invited Room Members", count)
if count == 1 {
gettext("Invited Room Member")
} else {
gettext("Invited Room Members")
}
}
MembershipListKind::Ban => {
ngettext("Banned Room Member", "Banned Room Members", count)
if count == 1 {
gettext("Banned Room Member")
} else {
gettext("Banned Room Members")
}
}
MembershipListKind::Knock => {
if count == 1 {
gettext("Invite Request")
} else {
gettext("Invite Requests")
}
}
MembershipListKind::Knock => ngettext("Invite Request", "Invite Requests", count),
};
self.obj().set_title(&title);

26
src/session_view/room_details/upgrade_dialog/mod.rs

@ -1,7 +1,7 @@
use std::cmp::Ordering;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::{gettext, ngettext};
use gettextrs::gettext;
use gtk::{gio, glib, pango};
use ruma::{
OwnedUserId, RoomVersionId,
@ -196,19 +196,25 @@ mod imp {
return;
}
let other_creators_count = u32::try_from(info.other_creators_count).unwrap_or(u32::MAX);
// We don't use the count in the strings so we use separate gettext calls for
// singular and plural rather than using ngettext.
let text = if info.own_user_is_creator {
ngettext(
"After the upgrade, you will be the only creator in the room. The other creator will be demoted to the default power level.",
"After the upgrade, you will be the only creator in the room. The other creators will be demoted to the default power level.",
other_creators_count,
if info.other_creators_count == 1 {
gettext(
"After the upgrade, you will be the only creator in the room. The other creator will be demoted to the default power level.",
)
} else {
gettext(
"After the upgrade, you will be the only creator in the room. The other creators will be demoted to the default power level.",
)
}
} else if info.other_creators_count == 1 {
gettext(
"After the upgrade, you will be the only creator in the room. The current creator will be demoted to the default power level.",
)
} else {
ngettext(
"After the upgrade, you will be the only creator in the room. The current creator will be demoted to the default power level.",
gettext(
"After the upgrade, you will be the only creator in the room. The current creators will be demoted to the default power level.",
other_creators_count,
)
};

Loading…
Cancel
Save