From 9535c5647f7736b5e4e96c1c0188a578fd4746be Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Tue, 28 Jan 2020 16:24:21 +1100 Subject: [PATCH] Make pluralisation code an independent lib I considered moving the code to a service but I think that this code can be completely independent of the Open Food Network use case. It would be easy to move to a gem. The downcasing may need reconsidering for general use. --- .../services/option_value_namer.js.coffee | 4 +- config/locales/en.yml | 4 +- lib/open_food_network/i18n_inflections.rb | 65 +++++++++++++++++++ lib/open_food_network/option_value_namer.rb | 36 ++-------- .../i18n_inflections_spec.rb | 40 ++++++++++++ 5 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 lib/open_food_network/i18n_inflections.rb create mode 100644 spec/lib/open_food_network/i18n_inflections_spec.rb diff --git a/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee b/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee index 9d85b0f80a..49a2f470be 100644 --- a/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee +++ b/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee @@ -34,12 +34,12 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager return unit_name if count == undefined unit_key = @unit_key(unit_name) return unit_name unless unit_key - I18n.t(["unit_names", unit_key], {count: count, defaultValue: unit_name}) + I18n.t(["inflections", unit_key], {count: count, defaultValue: unit_name}) unit_key: (unit_name) -> unless I18n.unit_keys I18n.unit_keys = {} - for key, translations of I18n.t("unit_names") + for key, translations of I18n.t("inflections") for quantifier, translation of translations I18n.unit_keys[translation.toLowerCase()] = key diff --git a/config/locales/en.yml b/config/locales/en.yml index 146b3b5f36..c456285159 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2721,7 +2721,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using have_an_account: "Already have an account?" action_login: "Log in now." - # Most popular names used in variant_unit_name. + # Singular and plural forms of commonly used words. # We use these entries to pluralize unit names in every language. # # Extracted with the following query: @@ -2731,7 +2731,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using # puts " one: \"#{name}\"" # puts " other: \"#{name}s\""; # } - unit_names: + inflections: each: one: "each" other: "each" diff --git a/lib/open_food_network/i18n_inflections.rb b/lib/open_food_network/i18n_inflections.rb new file mode 100644 index 0000000000..5919da4000 --- /dev/null +++ b/lib/open_food_network/i18n_inflections.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module OpenFoodNetwork + # Pluralize or singularize words. + # + # We store some inflection data in locales and use a reverse lookup of a word + # to find the plural or singular of the same word. + # + # Here is one example with a French user: + # + # - We have a product with the variant unit name "bouquet". + # - The I18n.locale is set to :fr. + # - The French locale contains: + # bunch: + # one: "bouquet" + # other: "bouquets" + # - We create a table containing: + # "bouquet" => "bunch" + # "bouquets" => "bunch" + # - Looking up "bouquet" gives us the I18n key "bunch". + # - We find the right plural by calling I18n: + # + # I18n.t("inflections.bunch", count: 2, default: "bouquet") + # + # - This returns the correct plural "bouquets". + # - It returns the original "bouquet" if the word is missing from the locale. + module I18nInflections + # Make this a singleton to cache lookup tables. + extend self + + def pluralize(word, count) + return word if count.nil? + + key = i18n_key(word) + + return word unless key + + I18n.t(key, scope: "inflections", count: count, default: word) + end + + private + + def i18n_key(word) + @lookup ||= {} + + # The user may switch the locale. `I18n.t` is always using the current + # locale and we need a lookup table for each of them. + unless @lookup.key?(I18n.locale) + @lookup[I18n.locale] = build_i18n_key_lookup + end + + @lookup[I18n.locale][word.downcase] + end + + def build_i18n_key_lookup + lookup = {} + I18n.t("inflections")&.each do |key, translations| + translations.values.each do |translation| + lookup[translation.downcase] = key + end + end + lookup + end + end +end diff --git a/lib/open_food_network/option_value_namer.rb b/lib/open_food_network/option_value_namer.rb index 64a287308a..706a0b443d 100644 --- a/lib/open_food_network/option_value_namer.rb +++ b/lib/open_food_network/option_value_namer.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require "open_food_network/i18n_inflections" + module OpenFoodNetwork class OptionValueNamer def initialize(variant = nil) @@ -73,37 +77,7 @@ module OpenFoodNetwork end def pluralize(unit_name, count) - I18nUnitNames.instance.pluralize(unit_name, count) - end - - # Provides efficient access to unit name inflections. - # The singleton property ensures that the init code is run once only. - # The OptionValueNamer is instantiated in loops. - class I18nUnitNames - include Singleton - - def pluralize(unit_name, count) - return unit_name if count.nil? - - @unit_keys ||= unit_key_lookup - key = @unit_keys[unit_name.downcase] - - return unit_name unless key - - I18n.t(key, scope: "unit_names", count: count, default: unit_name) - end - - private - - def unit_key_lookup - lookup = {} - I18n.t("unit_names").each do |key, translations| - translations.values.each do |translation| - lookup[translation.downcase] = key - end - end - lookup - end + I18nInflections.pluralize(unit_name, count) end end end diff --git a/spec/lib/open_food_network/i18n_inflections_spec.rb b/spec/lib/open_food_network/i18n_inflections_spec.rb new file mode 100644 index 0000000000..b200ad5454 --- /dev/null +++ b/spec/lib/open_food_network/i18n_inflections_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'open_food_network/i18n_inflections' + +describe OpenFoodNetwork::I18nInflections do + let(:subject) { described_class } + + it "returns the same word if no plural is known" do + expect(subject.pluralize("foo", 2)).to eq "foo" + end + + it "finds the plural of a word" do + expect(subject.pluralize("bunch", 2)).to eq "bunches" + end + + it "finds the singular of a word" do + expect(subject.pluralize("bunch", 1)).to eq "bunch" + end + + it "ignores upper case" do + expect(subject.pluralize("Bunch", 2)).to eq "bunches" + end + + it "switches locales" do + skip "French plurals not available yet" + I18n.with_locale(:fr) do + expect(subject.pluralize("bouquet", 2)).to eq "bouquets" + end + end + + it "builds the lookup table once" do + # Cache the table: + subject.pluralize("bunch", 2) + + # Expect only one call for the plural: + expect(I18n).to receive(:t).once.and_call_original + subject.pluralize("bunch", 2) + end +end