From 826798b0c8d1a732ecd074b9bd50930f4e953ed5 Mon Sep 17 00:00:00 2001 From: Domizio Demichelis Date: Sun, 24 Jun 2018 22:14:08 +0200 Subject: [PATCH] refactoring of I18N constant: added support for multiple static languages and plural rules --- docs/api/frontend.md | 33 +++++++++++++++++++++++++++------ lib/locales/pagy.yml | 2 ++ lib/locales/plurals.rb | 25 +++++++++++++++++++++++++ lib/pagy/extras/i18n.rb | 2 +- lib/pagy/frontend.rb | 19 ++++++++++--------- test/pagy/extras/i18n_test.rb | 4 ++-- test/pagy/frontend_test.rb | 6 +++--- 7 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 lib/locales/plurals.rb diff --git a/docs/api/frontend.md b/docs/api/frontend.md index cc49d8ea9..0f499eb43 100644 --- a/docs/api/frontend.md +++ b/docs/api/frontend.md @@ -162,24 +162,45 @@ Pagy is I18n ready. That means that all the UI strings that Pagy uses are stored The YAML file is available at `Pagy.root.join('locales', 'pagy.yml')`. It contains a few entries used in the the UI by helpers and templates through the [pagy_t method](#pagy_tpath-vars) (eqivalent to the `I18n.t` or rails `t` helper). -By default, the `pagy_t` method uses the Pagy implementation of I18n, which does not depend on the `I18n` gem in any way. It's _5x faster_ and uses _3.5x less memory_, but it provides only pluralization/interpolation without translation, so it's only useful with single language apps (i.e. only `fr` or only `en` or only ...) +By default, the `pagy_t` method uses the Pagy implementation of I18n, which does not depend on the `I18n` gem in any way. It's _5x faster_ and uses _3.5x less memory_, but it provides only pluralization/interpolation without dynamic translation, so it's only useful with single language apps (i.e. only `fr` or only `en` or only ...) that don't need to switch between languages. If you need full blown I18n, you should require the `i18n` extra, which will override the `pagy_t` method to use directly `::I18n.t`. ### Pagy::Frontend::I18N Constant -The `Pagy::Frontend::I18N` constant is the core of the Pagy I18n implementation. It has no effect if you use the `i18n` extra (which uses the `I18n.t` method directly). This constant allows to control the dictionary file to load and the pluralization proc. +The `Pagy::Frontend::I18N` constant is the core of the Pagy I18n implementation. It has no effect if you use the `i18n` extra (which uses the `I18n.t` method directly). This constant allows to control the dictionary file, the language to load and the pluralization proc. -#### Pagy::Frontend::I18N.load_file(file) +#### Pagy::Frontend::I18N.load(file:..., language:'en') -This method allow to load a custom dictionary file, different from `Pagy.root.join('locales', 'pagy.yml')`. If the `i18n` extra is used it has no effect. It is tipically used in the Pagy initializer file _(see [Configuration](../how-to.md#global-configuration))_. For example: +This method allows to load a built-in language (different than the default 'en') and/or a custom dictionary file, different from `Pagy.root.join('locales', 'pagy.yml')`. If the `i18n` extra is used it has no effect. It is tipically used in the Pagy initializer file _(see [Configuration](../how-to.md#global-configuration))_. For example: ```ruby -Pagy::Frontend::I18N.load_file('path/to/dictionary.yml') +# this would load the Italian variant of the built-in dictionary +Pagy::Frontend::I18N.load(language:'it') + +# this would load the default English variant of 'path/to/dictionary.yml' +Pagy::Frontend::I18N.load(file:'path/to/dictionary.yml') + +# this would load the Italian variant of 'path/to/dictionary.yml' +Pagy::Frontend::I18N.load(file:'path/to/dictionary.yml', language:'it') ``` +**Notice**: the Pagy implementation of I18n is designed to speedup single language apps and does not provide dynamic translation, so the `language` is statically loaded at startup-time and cannot be changed. Use the `i18n` extra if you need dynamic translation. + #### Pagy::Frontend::I18N[:plurals] This variable controls the internal pluralization. If the `i18n` extra is used it has no effect. -By default the variable is set to a proc that receives the `count` as a single argument and returns the plural type string (e.g. something like `'zero'`, `'one'` or `'other'`, depending on the passed count). You should customize it only for pluralization types different than English. +Pagy tries to set the language plural rule proc when you use the `Pagy::Frontend::I18N.load` method, by loading the built-in plural rules. + +If there is no rule defined for the language loaded, the variable is set to the `:zero_one_other` plural rule (default for English language). + +You may want to define a custom rule for your custom language. For example: + +```ruby +# this would apply a custom pluralization rule to the current loaded dictionary +Pagy::Frontend::I18N[:plural] = -> (count) {|count| ...} +``` + +The custom proc should receive the `count` as a single argument and should return the plural type string (e.g. something like `'zero'`, `'one'` or `'other'`, depending on the passed count). You should customize it only for pluralization types not included in the built-in plural rules. In that case, please submit a PR with your dictionary file and plural rule. Thanks. + diff --git a/lib/locales/pagy.yml b/lib/locales/pagy.yml index 580018221..c94ea430e 100644 --- a/lib/locales/pagy.yml +++ b/lib/locales/pagy.yml @@ -22,3 +22,5 @@ en: items: show: "Show" items: "items per page" + +# PR for other languages are very welcome. Thanks! diff --git a/lib/locales/plurals.rb b/lib/locales/plurals.rb new file mode 100644 index 000000000..f88aeff0e --- /dev/null +++ b/lib/locales/plurals.rb @@ -0,0 +1,25 @@ +# This file adds support for multiple built-in plualiztion types. +# It defines the pluralization procs and gets eval(ed) at I18N.load time +# frozen_string_literal: true + +# utility variables +zero_one = ['zero', 'one'] + +# Rules +# A rule is a proc returning a plural type string based on the passed count +# Each plural rule may apply to many language/rule pairs below +rules = { + zero_one_other: -> (count) {zero_one[count] || 'other'} +} + +# Plurals (language/rule pairs) +# The default rule for missing languages is the :zero_one_other rule (used for English) +plurals = Hash.new(rules[:zero_one_other]) + +# Entries for all the languages defined in the pagy.yml dictionary +plurals['en'] = rules[:zero_one_other] + +# PR for other languages are very welcome. Thanks! + +# Return plurals +plurals diff --git a/lib/pagy/extras/i18n.rb b/lib/pagy/extras/i18n.rb index 1eb08d3fc..68b658f9c 100644 --- a/lib/pagy/extras/i18n.rb +++ b/lib/pagy/extras/i18n.rb @@ -8,7 +8,7 @@ module Frontend # Override the built-in pagy_t def pagy_t(*args) - I18n.t(*args) + ::I18n.t(*args) end end diff --git a/lib/pagy/frontend.rb b/lib/pagy/frontend.rb index 81d36ef51..fe0988a30 100644 --- a/lib/pagy/frontend.rb +++ b/lib/pagy/frontend.rb @@ -54,19 +54,20 @@ def pagy_link_proc(pagy, link_extra='') else '' end } #{extra}>#{text}" } end - # Pagy::Frontend::I18N constant - I18N_DATA = YAML.load_file(Pagy.root.join('locales', 'pagy.yml')).first[1] - zero_one = ['zero', 'one']; I18N = { plurals: -> (c) {zero_one[c] || 'other'}} - def I18N.load_file(file) I18N_DATA.replace(YAML.load_file(file).first[1]) end + # Pagy::Frontend::I18N + def (I18N = {data:{}}).load(file:Pagy.root.join('locales', 'pagy.yml'), language: 'en') + self[:data] = YAML.load_file(file)[language] + self[:plural] = eval(Pagy.root.join('locales', 'plurals.rb').read)[language] #rubocop:disable Security/Eval + end; I18N.load - # Similar to I18n.t for interpolation and pluralization but without translation - # Use only for single-language apps: it is specialized for Pagy and 5x faster than I18n.t - # See also https://ddnexus.github.io/pagy/extras/i18n to use the standard I18n gem instead + # Similar to I18n.t for streamlined interpolation and pluralization but without dynamic translation. + # It is specialized for Pagy and 5x faster than I18n.t (see https://ddnexus.github.io/pagy/api/frontend#i18n) + # See also https://ddnexus.github.io/pagy/extras/i18n if you need to use the standard I18n gem instead def pagy_t(path, vars={}) - value = I18N_DATA.dig(*path.to_s.split('.')) or return %(translation missing: "#{path}") + value = I18N[:data].dig(*path.split('.')) or return %(translation missing: "#{path}") if value.is_a?(Hash) vars.key?(:count) or return value - plural = I18N[:plurals].call(vars[:count]) + plural = I18N[:plural].call(vars[:count]) value.key?(plural) or return %(invalid pluralization data: "#{path}" cannot be used with count: #{vars[:count]}; key "#{plural}" is missing.) value = value[plural] or return %(translation missing: "#{path}") end diff --git a/test/pagy/extras/i18n_test.rb b/test/pagy/extras/i18n_test.rb index c1e26be4d..a1625099b 100644 --- a/test/pagy/extras/i18n_test.rb +++ b/test/pagy/extras/i18n_test.rb @@ -10,8 +10,8 @@ describe "#pagy_t" do def test_data - assert_equal "‹ Prev", Pagy::Frontend::I18N_DATA['pagy']['nav']['prev'] - assert_equal "…", Pagy::Frontend::I18N_DATA['pagy']['nav']['gap'] + assert_equal "‹ Prev", Pagy::Frontend::I18N[:data]['pagy']['nav']['prev'] + assert_equal "…", Pagy::Frontend::I18N[:data]['pagy']['nav']['gap'] end def test_translation diff --git a/test/pagy/frontend_test.rb b/test/pagy/frontend_test.rb index 14857cf93..da242b37b 100644 --- a/test/pagy/frontend_test.rb +++ b/test/pagy/frontend_test.rb @@ -107,8 +107,8 @@ def test_link_extras describe "#pagy_t" do def test_data - assert_equal "‹ Prev", Pagy::Frontend::I18N_DATA['pagy']['nav']['prev'] - assert_equal "…", Pagy::Frontend::I18N_DATA['pagy']['nav']['gap'] + assert_equal "‹ Prev", Pagy::Frontend::I18N[:data]['pagy']['nav']['prev'] + assert_equal "…", Pagy::Frontend::I18N[:data]['pagy']['nav']['gap'] end def test_translation @@ -147,7 +147,7 @@ def test_render_info_no_118n_key end def test_render_info_with_existing_118n_key - Pagy::Frontend::I18N_DATA['pagy']['info']['product'] = { 'zero' => 'Products', + Pagy::Frontend::I18N[:data]['pagy']['info']['product'] = { 'zero' => 'Products', 'one' => 'Product', 'other' => 'Products' } pagy = Pagy.new count: 0, item_path: 'pagy.info.product'