diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index db8628c8a7..fe22af763f 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -544,7 +544,9 @@ en:
dominating_topic: You’ve posted a lot in this topic! Consider giving others an opportunity to reply here and discuss things with each other as well.
- get_a_room: You’ve replied to @%{reply_username} %{count} times, did you know you could send them a personal message instead?
+ get_a_room:
+ one: "You’ve replied to @%{reply_username} once, did you know you could send them a personal message instead?"
+ other: "You’ve replied to @%{reply_username} %{count} times, did you know you could send them a personal message instead?"
too_many_replies: |
### You have reached the reply limit for this topic
@@ -707,7 +709,9 @@ en:
topic_exists:
one: "Can't delete this category because it has %{count} topic. Oldest topic is %{topic_link}."
other: "Can't delete this category because it has %{count} topics. Oldest topic is %{topic_link}."
- topic_exists_no_oldest: "Can't delete this category because topic count is %{count}."
+ topic_exists_no_oldest:
+ one: "Can't delete this category because it has %{count} topic."
+ other: "Can't delete this category because it has %{count} topics."
uncategorized_description: "Topics that don't need a category, or don't fit into any other existing category."
trust_levels:
admin: "Admin"
@@ -2444,7 +2448,9 @@ en:
regex_invalid: "The regular expression is invalid: %{error}"
leading_trailing_slash: "The regular expression must not start and end with a slash."
unicode_usernames_avatars: "The internal system avatars do not support Unicode usernames."
- list_value_count: "The list must contain exactly %{count} values."
+ list_value_count:
+ one: "The list must contain exactly %{count} value."
+ other: "The list must contain exactly %{count} values."
markdown_linkify_tlds: "You cannot include a value of '*'."
google_oauth2_hd_groups: "You must configure all 'google oauth2 hd' settings before enabling this setting."
search_tokenize_chinese_enabled: "You must disable 'search_tokenize_chinese' before enabling this setting."
@@ -3275,10 +3281,16 @@ en:
email_reject_post_too_short:
title: "Email Reject Post Too Short"
subject_template: "[%{email_prefix}] Email issue -- Post too short"
- text_body_template: |
- We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
+ text_body_template:
+ one: |
+ We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
- To promote more in depth conversations, very short replies are not allowed. Can you please reply with at least %{count} characters? Alternatively, you can like a post via email by replying with "+1".
+ To promote more in depth conversations, very short replies are not allowed. Can you please reply with at least %{count} character? Alternatively, you can like a post via email by replying with "+1".
+
+ other: |
+ We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
+
+ To promote more in depth conversations, very short replies are not allowed. Can you please reply with at least %{count} characters? Alternatively, you can like a post via email by replying with "+1".
email_reject_invalid_post_action:
title: "Email Reject Invalid Post Action"
@@ -4613,7 +4625,9 @@ en:
mass_award:
errors:
invalid_csv: We encountered an error on line %{line_number}. Please confirm the CSV has one email per line.
- too_many_csv_entries: Too many entries in the CSV file. Please provide a CSV file with no more than %{count} entries.
+ too_many_csv_entries:
+ one: Too many entries in the CSV file. Please provide a CSV file with no more than %{count} entry.
+ other: Too many entries in the CSV file. Please provide a CSV file with no more than %{count} entries.
badge_disabled: Please enable the %{badge_name} badge first.
cant_grant_multiple_times: Can't grant the %{badge_name} badge multiple times to a single user.
editor:
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs
index 5597f74a5c..e6ff9eb74e 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs
@@ -62,7 +62,7 @@
{{d-icon "info-circle"}}
{{i18n
"chat.settings.retention_info"
- days=this.siteSettings.chat_channel_retention_days
+ count=this.siteSettings.chat_channel_retention_days
}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-retention-reminder.js b/plugins/chat/assets/javascripts/discourse/components/chat-retention-reminder.js
index ec66766760..e2f77faf6b 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-retention-reminder.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-retention-reminder.js
@@ -32,7 +32,7 @@ export default Component.extend({
days = this.siteSettings.chat_dm_retention_days;
translationKey = "chat.retention_reminders.dm";
}
- return I18n.t(translationKey, { days });
+ return I18n.t(translationKey, { count: days });
},
@discourseComputed("chatChannel.chatable_type")
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
index 00b9b8e0ae..299e0fc8bb 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
@@ -91,16 +91,29 @@ export default class CreateChannelController extends Controller.extend(
const allowedGroups = catPermissions.allowed_groups;
if (catPermissions.private) {
- const warningTranslationKey =
- allowedGroups.length < 3 ? "warning_groups" : "warning_multiple_groups";
+ let warningTranslationKey;
+
+ switch (allowedGroups.length) {
+ case 1:
+ warningTranslationKey =
+ "chat.create_channel.auto_join_users.warning_one_group";
+ break;
+ case 2:
+ warningTranslationKey =
+ "chat.create_channel.auto_join_users.warning_two_groups";
+ break;
+ default:
+ warningTranslationKey =
+ "chat.create_channel.auto_join_users.warning_multiple_groups";
+ break;
+ }
this.set(
"autoJoinWarning",
- I18n.t(`chat.create_channel.auto_join_users.${warningTranslationKey}`, {
- members_count: catPermissions.members_count,
+ I18n.t(warningTranslationKey, {
+ count: catPermissions.members_count,
group: escapeExpression(allowedGroups[0]),
group_2: escapeExpression(allowedGroups[1]),
- count: allowedGroups.length,
})
);
} else {
diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml
index d18182eea7..aa66e430db 100644
--- a/plugins/chat/config/locales/client.en.yml
+++ b/plugins/chat/config/locales/client.en.yml
@@ -116,10 +116,10 @@ en:
without_membership:
one: "%{username} has not joined this channel."
other: "%{username} and %{others} have not joined this channel."
- group_mentions_disabled:
+ group_mentions_disabled:
one: "%{group_name} doesn't allow mentions"
other: "%{group_name} and %{others} doesn't allow mentions"
- too_many_members:
+ too_many_members:
one: "%{group_name} has too many members. No one was notified"
other: "%{group_name} and %{others} have too many members. No one was notified"
warning_multiple:
@@ -132,12 +132,16 @@ en:
all: "Nobody will be notified"
unreachable:
one: "@%{group} doesn't allow mentions"
- other: "@%{group} and @%{group_2} doesn't allow mentions"
- unreachable_multiple: "@%{group} and %{count} others doesn't allow mentions"
+ other: "@%{group} and @%{group_2} don't allow mentions"
+ unreachable_multiple:
+ one: "@%{group} and %{count} other group don't allow mentions"
+ other: "@%{group} and %{count} others don't allow mentions"
too_many_members:
one: "Mentioning @%{group} exceeds the %{notification_limit} of %{limit}"
other: "Mentioning both @%{group} or @%{group_2} exceeds the %{notification_limit} of %{limit}"
- too_many_members_multiple: "These %{count} groups exceed the %{notification_limit} of %{limit}"
+ too_many_members_multiple:
+ one: "This %{count} group exceeds the %{notification_limit} of %{limit}"
+ other: "These %{count} groups exceed the %{notification_limit} of %{limit}"
users_limit:
one: "%{count} user"
other: "%{count} users"
@@ -272,10 +276,15 @@ en:
create_channel:
auto_join_users:
public_category_warning: "%{category} is a public category. Automatically add all recently active users to this channel?"
- warning_groups:
- one: Automatically add %{members_count} users from %{group}?
- other: Automatically add %{members_count} users from %{group} and %{group_2}?
- warning_multiple_groups: Automatically add %{members_count} users from %{group_1} and %{count} others?
+ warning_one_group:
+ one: Automatically add %{count} user from %{group}?
+ other: Automatically add %{count} users from %{group}?
+ warning_two_groups:
+ one: Automatically add %{count} user from %{group} and %{group_2}?
+ other: Automatically add %{count} users from %{group} and %{group_2}?
+ warning_multiple_groups:
+ one: Automatically add %{count} user from %{group_1} and multiple other groups?
+ other: Automatically add %{count} users from %{group_1} and multiple other groups?
choose_category:
label: "Choose a category"
none: "select one..."
@@ -283,7 +292,9 @@ en:
hint_groups:
one: Users in %{hint} will have access to this channel per the security settings
other: Users in %{hint} and %{hint_2} will have access to this channel per the security settings
- hint_multiple_groups: Users in %{hint_1} and %{count} other groups will have access to this channel per the security settings
+ hint_multiple_groups:
+ one: Users in %{hint_1} and %{count} other group will have access to this channel per the security settings
+ other: Users in %{hint_1} and %{count} other groups will have access to this channel per the security settings
create: "Create channel"
description: "Description (optional)"
name: "Channel name"
@@ -338,7 +349,9 @@ en:
saved: "Saved"
unfollow: "Leave"
admin_title: "Admin"
- retention_info: "Chat history will be saved for %{days} days."
+ retention_info:
+ one: "Chat history will be saved for %{count} day."
+ other: "Chat history will be saved for %{count} days."
admin:
title: "Chat"
@@ -410,8 +423,12 @@ en:
other: "%{commaSeparatedUsernames} and %{count} others are typing"
retention_reminders:
- public: "Channel history is retained for %{days} days."
- dm: "Personal chat history is retained for %{days} days."
+ public:
+ one: "Channel history is retained for %{count} day."
+ other: "Channel history is retained for %{count} days."
+ dm:
+ one: "Personal chat history is retained for %{count} day."
+ other: "Personal chat history is retained for %{count} days."
flags:
off_topic: "This message is not relevant to the current discussion as defined by the channel title, and should probably be moved elsewhere."
diff --git a/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js b/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js
index 89620f1c93..fe11e10cb9 100644
--- a/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js
+++ b/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js
@@ -28,7 +28,7 @@ module(
async test(assert) {
assert.equal(
query(".chat-retention-reminder-text").innerText.trim(),
- I18n.t("chat.retention_reminders.public", { days: 100 })
+ I18n.t("chat.retention_reminders.public", { count: 100 })
);
},
});
diff --git a/plugins/poll/config/locales/client.en.yml b/plugins/poll/config/locales/client.en.yml
index c7fa389041..80f11b3414 100644
--- a/plugins/poll/config/locales/client.en.yml
+++ b/plugins/poll/config/locales/client.en.yml
@@ -76,7 +76,9 @@ en:
breakdown:
title: "Poll results"
- votes: "%{count} votes"
+ votes:
+ one: "%{count} vote"
+ other: "%{count} votes"
breakdown: "Breakdown"
percentage: "Percentage"
count: "Count"
@@ -91,7 +93,9 @@ en:
insert: Insert Poll
help:
options_min_count: Enter at least 1 option.
- options_max_count: Enter at most %{count} options.
+ options_max_count:
+ one: Enter at most %{count} option.
+ other: Enter at most %{count} options.
invalid_min_value: Minimum value must be at least 1.
invalid_max_value: Maximum value must be at least 1, but less than or equal with the number of options.
invalid_values: Minimum value must be smaller than the maximum value.
diff --git a/script/i18n_lint.rb b/script/i18n_lint.rb
index b00de75191..fa3e2007fb 100755
--- a/script/i18n_lint.rb
+++ b/script/i18n_lint.rb
@@ -33,11 +33,18 @@ class LocaleFileValidator
wrong_pluralization_keys: "Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:",
invalid_one_keys: "The following keys contain the number 1 instead of the interpolation key %{count}:",
invalid_message_format_one_key: "The following keys use 'one {1 foo}' instead of the generic 'one {# foo}':",
+ missing_pluralization: "The following keys use %{count} without pluralization.\nSplit the key into `one` and `other` or add it to the allow list in `script/i18n_lint.rb`:",
}
PLURALIZATION_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other']
ENGLISH_KEYS = ['one', 'other']
+ COUNT_WITHOUT_PLURALIZATION_ALLOW_LIST = [
+ "errors.messages.",
+ "activemodel.errors.messages.",
+ "activerecord.errors.messages.",
+ ]
+
def initialize(filename)
@filename = filename
@errors = {}
@@ -73,7 +80,7 @@ class LocaleFileValidator
if Hash === value
each_translation(value, current_key, &block)
else
- yield(current_key, value.to_s)
+ yield(current_key, value.to_s, key)
end
end
end
@@ -83,22 +90,29 @@ class LocaleFileValidator
@errors[:invalid_relative_image_sources] = []
@errors[:invalid_interpolation_key_format] = []
@errors[:invalid_message_format_one_key] = []
+ @errors[:missing_pluralization] = []
- each_translation(yaml) do |key, value|
+ each_translation(yaml) do |full_key, value, last_key_part|
if value.match?(/href\s*=\s*["']\/[^\/]|\]\(\/[^\/]/i)
- @errors[:invalid_relative_links] << key
+ @errors[:invalid_relative_links] << full_key
end
if value.match?(/src\s*=\s*["']\/[^\/]/i)
- @errors[:invalid_relative_image_sources] << key
+ @errors[:invalid_relative_image_sources] << full_key
end
- if value.match?(/{{.+?}}/) && !key.end_with?("_MF")
- @errors[:invalid_interpolation_key_format] << key
+ if value.match?(/{{.+?}}/) && !full_key.end_with?("_MF")
+ @errors[:invalid_interpolation_key_format] << full_key
end
- if key.end_with?("_MF") && value.match?(/one {.*?1.*?}/)
- @errors[:invalid_message_format_one_key] << key
+ if full_key.end_with?("_MF") && value.match?(/one {.*?1.*?}/)
+ @errors[:invalid_message_format_one_key] << full_key
+ end
+
+ if value.include?("%{count}") && !ENGLISH_KEYS.include?(last_key_part) &&
+ COUNT_WITHOUT_PLURALIZATION_ALLOW_LIST.none? { |k| full_key.start_with?(k) }
+
+ @errors[:missing_pluralization] << full_key
end
end
end