Skip to content

Commit 9540cf1

Browse files
authored
Merge pull request #630 from movermeyer/movermeyer/pluralization_cldr_support
Add support for CLDR data in `I18n::Backend::Pluralization`
2 parents 829f95e + a7b92a1 commit 9540cf1

File tree

2 files changed

+72
-24
lines changed

2 files changed

+72
-24
lines changed

lib/i18n/backend/pluralization.rb

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,75 @@ module Backend
1616
module Pluralization
1717
# Overwrites the Base backend translate method so that it will check the
1818
# translation meta data space (:i18n) for a locale specific pluralization
19-
# rule and use it to pluralize the given entry. I.e. the library expects
19+
# rule and use it to pluralize the given entry. I.e., the library expects
2020
# pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
2121
#
2222
# Pluralization rules are expected to respond to #call(count) and
23-
# return a pluralization key. Valid keys depend on the translation data
24-
# hash (entry) but it is generally recommended to follow CLDR's style,
25-
# i.e., return one of the keys :zero, :one, :few, :many, :other.
23+
# return a pluralization key. Valid keys depend on the pluralization
24+
# rules for the locale, as defined in the CLDR.
25+
# As of v41, 6 locale-specific plural categories are defined:
26+
# :few, :many, :one, :other, :two, :zero
2627
#
27-
# The :zero key is always picked directly when count equals 0 AND the
28-
# translation data has the key :zero. This way translators are free to
29-
# either pick a special :zero translation even for languages where the
30-
# pluralizer does not return a :zero key.
28+
# n.b., The :one plural category does not imply the number 1.
29+
# Instead, :one is a category for any number that behaves like 1 in
30+
# that locale. For example, in some locales, :one is used for numbers
31+
# that end in "1" (like 1, 21, 151) but that don't end in
32+
# 11 (like 11, 111, 10311).
33+
# Similar notes apply to the :two, and :zero plural categories.
34+
#
35+
# If you want to have different strings for the categories of count == 0
36+
# (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car")
37+
# use the explicit `"0"` and `"1"` keys.
38+
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
3139
def pluralize(locale, entry, count)
3240
return entry unless entry.is_a?(Hash) && count
3341

3442
pluralizer = pluralizer(locale)
3543
if pluralizer.respond_to?(:call)
36-
key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
37-
raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
38-
entry[key]
44+
# "0" and "1" are special cases
45+
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
46+
if count == 0 || count == 1
47+
value = entry[symbolic_count(count)]
48+
return value if value
49+
end
50+
51+
# Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
52+
# > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
53+
# > 1. If "x" is numeric, the path falls back to the path with [@count=«the plural rules category for x for that locale»], within that the same locale.
54+
# > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
55+
# > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
56+
# Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
57+
plural_rule_category = pluralizer.call(count)
58+
59+
value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
60+
entry[plural_rule_category] || entry[:other]
61+
else
62+
raise InvalidPluralizationData.new(entry, count, plural_rule_category)
63+
end
3964
else
4065
super
4166
end
4267
end
4368

4469
protected
4570

46-
def pluralizers
47-
@pluralizers ||= {}
48-
end
71+
def pluralizers
72+
@pluralizers ||= {}
73+
end
4974

50-
def pluralizer(locale)
51-
pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
52-
end
75+
def pluralizer(locale)
76+
pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
77+
end
78+
79+
private
80+
81+
# Normalizes categories of 0.0 and 1.0
82+
# and returns the symbolic version
83+
def symbolic_count(count)
84+
count = 0 if count == 0
85+
count = 1 if count == 1
86+
count.to_s.to_sym
87+
end
5388
end
5489
end
5590
end

test/backend/pluralization_test.rb

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,22 @@ class Backend < I18n::Backend::Simple
99
def setup
1010
super
1111
I18n.backend = Backend.new
12-
@rule = lambda { |n| n == 1 ? :one : n == 0 || (2..10).include?(n % 100) ? :few : (11..19).include?(n % 100) ? :many : :other }
12+
@rule = lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : n == 0 || (2..10).include?(n % 100) ? :few : (11..19).include?(n % 100) ? :many : :other }
1313
store_translations(:xx, :i18n => { :plural => { :rule => @rule } })
14-
@entry = { :zero => 'zero', :one => 'one', :few => 'few', :many => 'many', :other => 'other' }
14+
@entry = { :"0" => 'none', :"1" => 'single', :one => 'one', :few => 'few', :many => 'many', :other => 'other' }
1515
end
1616

1717
test "pluralization picks a pluralizer from :'i18n.pluralize'" do
1818
assert_equal @rule, I18n.backend.send(:pluralizer, :xx)
1919
end
2020

21-
test "pluralization picks :one for 1" do
21+
test "pluralization picks the explicit 1 rule for count == 1, the explicit rule takes priority over the matching :one rule" do
22+
assert_equal 'single', I18n.t(:count => 1, :default => @entry, :locale => :xx)
23+
assert_equal 'single', I18n.t(:count => 1.0, :default => @entry, :locale => :xx)
24+
end
25+
26+
test "pluralization picks :one for 1, since in this case that is the matching rule for 1 (when there is no explicit 1 rule)" do
27+
@entry.delete(:"1")
2228
assert_equal 'one', I18n.t(:count => 1, :default => @entry, :locale => :xx)
2329
end
2430

@@ -30,15 +36,22 @@ def setup
3036
assert_equal 'many', I18n.t(:count => 11, :default => @entry, :locale => :xx)
3137
end
3238

33-
test "pluralization picks zero for 0 if the key is contained in the data" do
34-
assert_equal 'zero', I18n.t(:count => 0, :default => @entry, :locale => :xx)
39+
test "pluralization picks explicit 0 rule for count == 0, since the explicit rule takes priority over the matching :few rule" do
40+
assert_equal 'none', I18n.t(:count => 0, :default => @entry, :locale => :xx)
41+
assert_equal 'none', I18n.t(:count => 0.0, :default => @entry, :locale => :xx)
42+
assert_equal 'none', I18n.t(:count => -0, :default => @entry, :locale => :xx)
3543
end
3644

37-
test "pluralization picks few for 0 if the key is not contained in the data" do
38-
@entry.delete(:zero)
45+
test "pluralization picks :few for 0 (when there is no explicit 0 rule)" do
46+
@entry.delete(:"0")
3947
assert_equal 'few', I18n.t(:count => 0, :default => @entry, :locale => :xx)
4048
end
4149

50+
test "pluralization does Lateral Inheritance to :other to cover missing data" do
51+
@entry.delete(:many)
52+
assert_equal 'other', I18n.t(:count => 11, :default => @entry, :locale => :xx)
53+
end
54+
4255
test "pluralization picks one for 1 if the entry has attributes hash on unknown locale" do
4356
@entry[:attributes] = { :field => 'field', :second => 'second' }
4457
assert_equal 'one', I18n.t(:count => 1, :default => @entry, :locale => :pirate)

0 commit comments

Comments
 (0)