From 5f9b7c67cd07ef78fbad5129285b1916da58a9ac Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 25 Sep 2015 00:43:23 +0200 Subject: [PATCH 1/2] Add tests to ensure correct pluralization --- spec/integrity/i18n_spec.rb | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/spec/integrity/i18n_spec.rb b/spec/integrity/i18n_spec.rb index e7b9c08fec9..5e95b706a72 100644 --- a/spec/integrity/i18n_spec.rb +++ b/spec/integrity/i18n_spec.rb @@ -58,7 +58,7 @@ describe "i18n integrity checks" do end end - describe 'keys in English locale files' do + describe 'English locale file' do locale_files = ['config/locales', 'plugins/**/locales'] .product(['server.en.yml', 'client.en.yml']) .collect { |dir, filename| Dir["#{Rails.root}/#{dir}/#{filename}"] } @@ -85,12 +85,42 @@ describe "i18n integrity checks" do end end + module Pluralizations + def self.load(path) + whitelist = Regexp.union([/messages.restrict_dependent_destroy/]) + + yaml = YAML.load_file("#{Rails.root}/#{path}") + pluralizations = find_pluralizations(yaml['en']) + pluralizations.reject! { |key| key.match(whitelist) } + pluralizations + end + + def self.find_pluralizations(hash, parent_key = '', pluralizations = Hash.new) + hash.each do |key, value| + if value.is_a? Hash + current_key = parent_key.blank? ? key : "#{parent_key}.#{key}" + find_pluralizations(value, current_key, pluralizations) + elsif key == 'one' || key == 'other' + pluralizations[parent_key] = hash + end + end + + pluralizations + end + end + locale_files.each do |path| context path do it 'has no duplicate keys' do duplicates = DuplicateKeyFinder.new.find_duplicates("#{Rails.root}/#{path}") expect(duplicates).to be_empty end + + Pluralizations.load(path).each do |key, values| + it "key '#{key}' has valid pluralizations" do + expect(values.keys).to contain_exactly('one', 'other') + end + end end end end From 6e33a21a7a59edc4237ee3003e31f8d33287f591 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 13 Nov 2015 21:19:46 +0100 Subject: [PATCH 2/2] FIX: Replace invalid pluralizations in locale files --- .../templates/modal/admin_badge_preview.hbs | 8 +++- .../discourse/components/post-menu.js.es6 | 4 +- .../controllers/feature-topic.js.es6 | 3 +- .../discourse/models/nav-item.js.es6 | 3 +- .../routes/build-category-route.js.es6 | 2 +- .../discourse/routes/build-topic-route.js.es6 | 2 +- .../templates/components/hamburger-menu.hbs | 14 ++++-- .../templates/modal/feature-topic.hbs | 18 +++++-- .../discourse/templates/user/preferences.hbs | 6 ++- config/locales/client.en.yml | 48 ++++++++----------- config/locales/server.en.yml | 11 +++-- lib/validators/post_validator.rb | 20 +++++--- 12 files changed, 85 insertions(+), 54 deletions(-) diff --git a/app/assets/javascripts/admin/templates/modal/admin_badge_preview.hbs b/app/assets/javascripts/admin/templates/modal/admin_badge_preview.hbs index 835a105cda9..e244d5b0ef9 100644 --- a/app/assets/javascripts/admin/templates/modal/admin_badge_preview.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin_badge_preview.hbs @@ -14,7 +14,13 @@ --> {{else}} -

{{{i18n 'admin.badges.preview.grant_count' count=count}}}

+

+ {{#if count}} + {{{i18n 'admin.badges.preview.grant_count' count=count}}} + {{else}} + {{{i18n 'admin.badges.preview.no_grant_count'}}} + {{/if}} +

{{#if count_warning}}
diff --git a/app/assets/javascripts/discourse/components/post-menu.js.es6 b/app/assets/javascripts/discourse/components/post-menu.js.es6 index 765539245a7..2b3e28e72a0 100644 --- a/app/assets/javascripts/discourse/components/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/post-menu.js.es6 @@ -248,7 +248,9 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, { if (likeCount > 0) { const likedPost = !!this.get('post.likeAction.acted'); - const label = likedPost ? 'post.has_likes_title_you' : 'post.has_likes_title'; + const label = likedPost + ? likeCount === 1 ? 'post.has_likes_title_only_you' : 'post.has_likes_title_you' + : 'post.has_likes_title'; return new Button('like-count', label, undefined, { className: 'like-count highlight-action', diff --git a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 index 4b189d168d3..34c1c9516f1 100644 --- a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 @@ -41,7 +41,8 @@ export default Ember.Controller.extend(ModalFunctionality, { @computed("categoryLink", "pinnedInCategoryCount") alreadyPinnedMessage(categoryLink, count) { - return I18n.t("topic.feature_topic.already_pinned", { categoryLink, count }); + const key = count === 0 ? "topic.feature_topic.not_pinned" : "topic.feature_topic.already_pinned"; + return I18n.t(key, { categoryLink, count }); }, @computed("parsedPinnedInCategoryUntil") diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index 64cb9fea433..1d614c0d1d0 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -12,12 +12,13 @@ const NavItem = Discourse.Model.extend({ } var extra = { count: count }; + var titleKey = count === 0 ? '.title' : '.title_with_count'; if (categoryName) { name = 'category'; extra.categoryName = toTitleCase(categoryName); } - return I18n.t("filters." + name.replace("/", ".") + ".title", extra); + return I18n.t("filters." + name.replace("/", ".") + titleKey, extra); }.property('categoryName', 'name', 'count'), categoryName: function() { diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index 8456eaf4faa..219ae7d3276 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -57,7 +57,7 @@ export default (filter, params) => { }, titleToken() { - const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', { count: 0 }), + const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title'), category = this.currentModel.category; return I18n.t('filters.with_category', { filter: filterText, category: category.get('name') }); diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 0da680a200f..890f0729f85 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -80,7 +80,7 @@ export default function(filter, extras) { titleToken() { if (filter === Discourse.Utilities.defaultHomepage()) { return; } - const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', {count: 0}); + const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title'); return I18n.t('filters.with_topics', {filter: filterText}); }, diff --git a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs index 3d875eb6062..6c3640a6c79 100644 --- a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs +++ b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs @@ -41,14 +41,22 @@ {{/if}} {{#menu-links}} -
  • {{d-link route="discovery.latest" class="latest-topics-link" label="filters.latest.title.zero"}}
  • +
  • {{d-link route="discovery.latest" class="latest-topics-link" label="filters.latest.title"}}
  • {{#if currentUser}}
  • - {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title" count=newCount}} + {{#if newCount}} + {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title_with_count" count=newCount}} + {{else}} + {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title"}} + {{/if}}
  • - {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title" count=unreadCount}} + {{#if unreadCount}} + {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title_with_count" count=unreadCount}} + {{else}} + {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title"}} + {{/if}}
  • {{/if}}
  • {{d-link route="discovery.top" class="top-topics-link" label="filters.top.title"}}
  • diff --git a/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs b/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs index 7aee95425e3..942fa261165 100644 --- a/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs +++ b/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs @@ -5,7 +5,11 @@ {{#if model.pinned_globally}}

    {{#conditional-loading-spinner size="small" condition=loading}} - {{{i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount}}} + {{#if pinnedGloballyCount}} + {{{i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount}}} + {{else}} + {{{i18n "topic.feature_topic.not_pinned_globally"}}} + {{/if}} {{/conditional-loading-spinner}}

    {{i18n "topic.feature_topic.global_pin_note"}}

    @@ -48,7 +52,11 @@

    {{#conditional-loading-spinner size="small" condition=loading}} - {{{i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount}}} + {{#if pinnedGloballyCount}} + {{{i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount}}} + {{else}} + {{{i18n "topic.feature_topic.not_pinned_globally"}}} + {{/if}} {{/conditional-loading-spinner}}

    @@ -71,7 +79,11 @@

    {{#conditional-loading-spinner size="small" condition=loading}} - {{{i18n "topic.feature_topic.already_banner" count=bannerCount}}} + {{#if bannerCount}} + {{{i18n "topic.feature_topic.banner_exists"}}} + {{else}} + {{{i18n "topic.feature_topic.no_banner_exists"}}} + {{/if}} {{/conditional-loading-spinner}}

    diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 3ebc10e5a5d..efe26f09d91 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -182,7 +182,11 @@ {{preference-checkbox labelKey="user.email_always" checked=model.email_always}}

    - {{i18n 'user.email.frequency' count=siteSettings.email_time_window_mins}} + {{#if siteSettings.email_time_window_mins}} + {{i18n 'user.email.frequency' count=siteSettings.email_time_window_mins}} + {{else}} + {{i18n 'user.email.frequency_immediately'}} + {{/if}}
    diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 162a03aaff0..552d104ed6d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -537,8 +537,8 @@ en: ok: "We will email you to confirm" invalid: "Please enter a valid email address" authenticated: "Your email has been authenticated by {{provider}}" + frequency_immediately: "We'll email you immediately if you haven't read the thing we're emailing you about." frequency: - zero: "We'll email you immediately if you haven't read the thing we're emailing you about." one: "We'll only email you if we haven't seen you in the last minute." other: "We'll only email you if we haven't seen you in the last {{count}} minutes." @@ -1225,25 +1225,24 @@ en: unpin_until: "Remove this topic from the top of the {{categoryLink}} category or wait until %{until}." pin_note: "Users can unpin the topic individually for themselves." pin_validation: "A date is required to pin this topic." + not_pinned: "There are no topics pinned in {{categoryLink}}." already_pinned: - zero: "There are no topics pinned in {{categoryLink}}." - one: "Topics currently pinned in {{categoryLink}}: 1." - other: "Topics currently pinned in {{categoryLink}}: {{count}}." + one: "Topics currently pinned in {{categoryLink}}: 1" + other: "Topics currently pinned in {{categoryLink}}: {{count}}" pin_globally: "Make this topic appear at the top of all topic lists until" confirm_pin_globally: "You already have {{count}} globally pinned topics. Too many pinned topics may be a burden for new and anonymous users. Are you sure you want to pin another topic globally?" unpin_globally: "Remove this topic from the top of all topic lists." unpin_globally_until: "Remove this topic from the top of all topic lists or wait until %{until}." global_pin_note: "Users can unpin the topic individually for themselves." + not_pinned_globally: "There are no topics pinned globally." already_pinned_globally: - zero: "There are no topics pinned globally." - one: "Topics currently pinned globally: 1." - other: "Topics currently pinned globally: {{count}}." + one: "Topics currently pinned globally: 1" + other: "Topics currently pinned globally: {{count}}" make_banner: "Make this topic into a banner that appears at the top of all pages." remove_banner: "Remove the banner that appears at the top of all pages." banner_note: "Users can dismiss the banner by closing it. Only one topic can be bannered at any given time." - already_banner: - zero: "There is no banner topic." - one: "There is currently a banner topic." + no_banner_exists: "There is no banner topic." + banner_exists: "There is currently a banner topic." inviting: "Inviting..." automatically_add_to_groups_optional: "This invite also includes access to these groups: (optional, admin only)" @@ -1364,8 +1363,8 @@ en: one: "1 person liked this post" other: "{{count}} people liked this post" + has_likes_title_only_you: "you liked this post" has_likes_title_you: - zero: "you liked this post" one: "you and 1 other person liked this post" other: "you and {{count}} other people liked this post" @@ -1515,11 +1514,6 @@ en: one: "1 person voted for this post" other: "{{count}} people voted for this post" - edits: - one: 1 edit - other: "{{count}} edits" - zero: no edits - delete: confirm: one: "Are you sure you want to delete that post?" @@ -1600,8 +1594,6 @@ en: position_disabled_click: 'enable the "fixed category positions" setting.' parent: "Parent Category" notifications: - title: '' - reasons: watching: title: "Watching" description: "You will automatically watch all new topics in these categories. You will be notified of every new post in every topic, and a count of new replies will be shown." @@ -1717,8 +1709,8 @@ en: with_topics: "%{filter} topics" with_category: "%{filter} %{category} topics" latest: - title: - zero: "Latest" + title: "Latest" + title_with_count: one: "Latest (1)" other: "Latest ({{count}})" help: "topics with recent posts" @@ -1736,23 +1728,21 @@ en: title_in: "Category - {{categoryName}}" help: "all topics grouped by category" unread: - title: - zero: "Unread" + title: "Unread" + title_with_count: one: "Unread (1)" other: "Unread ({{count}})" help: "topics you are currently watching or tracking with unread posts" lower_title_with_count: - zero: "" one: "1 unread" other: "{{count}} unread" new: lower_title_with_count: - zero: "" one: "1 new" other: "{{count}} new" lower_title: "new" - title: - zero: "New" + title: "New" + title_with_count: one: "New (1)" other: "New ({{count}})" help: "topics created in the last few days" @@ -1763,8 +1753,8 @@ en: title: "Bookmarks" help: "topics you have bookmarked" category: - title: - zero: "{{categoryName}}" + title: "{{categoryName}}" + title_with_count: one: "{{categoryName}} (1)" other: "{{categoryName}} ({{count}})" help: "latest topics in the {{categoryName}} category" @@ -2566,8 +2556,8 @@ en: bad_count_warning: header: "WARNING!" text: "There are missing grant samples. This happens when the badge query returns user IDs or post IDs that do not exist. This may cause unexpected results later on - please double-check your query." + no_grant_count: "No badges to be assigned." grant_count: - zero: "No badges to be assigned." one: "1 badge to be assigned." other: "%{count} badges to be assigned." sample: "Sample:" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index a96f5f25e09..c143a20b347 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -140,26 +140,27 @@ en: one: "1 reply" other: "%{count} replies" + no_mentions_allowed: "Sorry, you can't mention other users." too_many_mentions: - zero: "Sorry, you can't mention other users." one: "Sorry, you can only mention one other user in a post." other: "Sorry, you can only mention %{count} users in a post." + no_mentions_allowed_newuser: "Sorry, new users can't mention other users." too_many_mentions_newuser: - zero: "Sorry, new users can't mention other users." one: "Sorry, new users can only mention one other user in a post." other: "Sorry, new users can only mention %{count} users in a post." + no_images_allowed: "Sorry, new users can't put images in posts." too_many_images: - zero: "Sorry, new users can't put images in posts." one: "Sorry, new users can only put one image in a post." other: "Sorry, new users can only put %{count} images in a post." + no_attachments_allowed: "Sorry, new users can't put attachments in posts." too_many_attachments: - zero: "Sorry, new users can't put attachments in posts." one: "Sorry, new users can only put one attachment in a post." other: "Sorry, new users can only put %{count} attachments in a post." + no_links_allowed: "Sorry, new users can't put links in posts." too_many_links: - zero: "Sorry, new users can't put links in posts." one: "Sorry, new users can only put one link in a post." other: "Sorry, new users can only put %{count} links in a post." + spamming_host: "Sorry you cannot post a link to that host." user_is_suspended: "Suspended users are not allowed to post." topic_not_found: "Something has gone wrong. Perhaps this topic was closed or deleted while you were looking at it?" diff --git a/lib/validators/post_validator.rb b/lib/validators/post_validator.rb index fc16571476d..29dcb9a9e8a 100644 --- a/lib/validators/post_validator.rb +++ b/lib/validators/post_validator.rb @@ -51,9 +51,9 @@ class Validators::PostValidator < ActiveModel::Validator # Ensure maximum amount of mentions in a post def max_mention_validator(post) if acting_user_is_trusted?(post) - add_error_if_count_exceeded(post, :too_many_mentions, post.raw_mentions.size, SiteSetting.max_mentions_per_post) + add_error_if_count_exceeded(post, :no_mentions_allowed, :too_many_mentions, post.raw_mentions.size, SiteSetting.max_mentions_per_post) else - add_error_if_count_exceeded(post, :too_many_mentions_newuser, post.raw_mentions.size, SiteSetting.newuser_max_mentions_per_post) + add_error_if_count_exceeded(post, :no_mentions_allowed_newuser, :too_many_mentions_newuser, post.raw_mentions.size, SiteSetting.newuser_max_mentions_per_post) end end @@ -65,17 +65,17 @@ class Validators::PostValidator < ActiveModel::Validator # Ensure new users can not put too many images in a post def max_images_validator(post) - add_error_if_count_exceeded(post, :too_many_images, post.image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted?(post) + add_error_if_count_exceeded(post, :no_images_allowed, :too_many_images, post.image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted?(post) end # Ensure new users can not put too many attachments in a post def max_attachments_validator(post) - add_error_if_count_exceeded(post, :too_many_attachments, post.attachment_count, SiteSetting.newuser_max_attachments) unless acting_user_is_trusted?(post) + add_error_if_count_exceeded(post, :no_attachments_allowed, :too_many_attachments, post.attachment_count, SiteSetting.newuser_max_attachments) unless acting_user_is_trusted?(post) end # Ensure new users can not put too many links in a post def max_links_validator(post) - add_error_if_count_exceeded(post, :too_many_links, post.link_count, SiteSetting.newuser_max_links) unless acting_user_is_trusted?(post) + add_error_if_count_exceeded(post, :no_links_allowed, :too_many_links, post.link_count, SiteSetting.newuser_max_links) unless acting_user_is_trusted?(post) end # Stop us from posting the same thing too quickly @@ -98,7 +98,13 @@ class Validators::PostValidator < ActiveModel::Validator post.acting_user.present? && post.acting_user.has_trust_level?(TrustLevel[1]) end - def add_error_if_count_exceeded(post, key_for_translation, current_count, max_count) - post.errors.add(:base, I18n.t(key_for_translation, count: max_count)) if current_count > max_count + def add_error_if_count_exceeded(post, not_allowed_translation_key, limit_translation_key, current_count, max_count) + if current_count > max_count + if max_count == 0 + post.errors.add(:base, I18n.t(not_allowed_translation_key)) + else + post.errors.add(:base, I18n.t(limit_translation_key, count: max_count)) + end + end end end