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