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'