Compare commits

...
This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.

1 Commits

Author SHA1 Message Date
Gerhard Schlager
7974fcba46 DEV: Lint locale files for correct usage of %{count}
Strings need to be pluralized when they contain the `%{count}` interpolation key. This also fixes all strings in core.
2022-12-07 01:22:37 +01:00
8 changed files with 100 additions and 38 deletions

View File

@ -544,7 +544,9 @@ en:
dominating_topic: Youve 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: Youve replied to @%{reply_username} %{count} times, did you know you could send them a personal message instead?
get_a_room:
one: "Youve replied to @%{reply_username} once, did you know you could send them a personal message instead?"
other: "Youve 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:

View File

@ -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
}}
</div>
</div>

View File

@ -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")

View File

@ -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 {

View File

@ -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 <a href=%{link} target="_blank">security settings</a>
other: Users in %{hint} and %{hint_2} will have access to this channel per the <a href=%{link} target="_blank">security settings</a>
hint_multiple_groups: Users in %{hint_1} and %{count} other groups will have access to this channel per the <a href=%{link} target="_blank">security settings</a>
hint_multiple_groups:
one: Users in %{hint_1} and %{count} other group will have access to this channel per the <a href=%{link} target="_blank">security settings</a>
other: Users in %{hint_1} and %{count} other groups will have access to this channel per the <a href=%{link} target="_blank">security settings</a>
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."

View File

@ -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 })
);
},
});

View File

@ -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.

View File

@ -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