i18n 1.8.10 → 1.14.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63d76facf7fc1bb9f1f7fca47cec61259726d0879a7e187646f54a7e94f0f73c
4
- data.tar.gz: ca27ecf10f4cf612a57fa0747aeb2431f42fa08210ce1df79675c17d52b5c203
3
+ metadata.gz: 692bec57675f1d97cdd0dff4a7b444e1777ba2f360dc3cf7162d5a98e45ab0a5
4
+ data.tar.gz: 954df8c7788ce253f730e4bf6911373198b26c24c2274113b2e12b6d6527b486
5
5
  SHA512:
6
- metadata.gz: 3007d330c4b3c1d88feb69d3d4f877cfd19d15461929b7af70c902090ec8872e7dfe2c6a2ed0bec56111b3146fe7f2c1718c9e70aaa424369f6d5f8fa4a4ae42
7
- data.tar.gz: 4c2119abc3a28a9d2a18e429befb3ade43de7b5cf65c48f2c67896905d30358759ada31c8442414fd6d859a6a45f52441b7ca05ccac18790ed3f6ea0edc3b97e
6
+ metadata.gz: 26ebf6f4edea8d2a6cf511496bcad2c0e50323c44263c4d27e44f12860f9e4e35e093514bb1fb3cd9925591b5e3db62917af79ee2710b4cc759735dd1ff4d538
7
+ data.tar.gz: 1764731c2157cddb200a8dec9286248f7fac4e8a23548305aa09e70b5591d924748179c9534bb46cd6d7d892987963566d95e20854d5928b51a0fbad76dded3b
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # Ruby I18n
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/i18n.svg)](https://badge.fury.io/rb/i18n)
3
4
  [![Build Status](https://github.com/ruby-i18n/i18n/workflows/Ruby/badge.svg)](https://github.com/ruby-i18n/i18n/actions?query=workflow%3ARuby)
4
5
 
5
6
  Ruby internationalization and localization (i18n) solution.
@@ -25,7 +26,7 @@ gem 'i18n'
25
26
  Then configure I18n with some translations, and a default locale:
26
27
 
27
28
  ```ruby
28
- I18n.load_path << Dir[File.expand_path("config/locales") + "/*.yml"]
29
+ I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
29
30
  I18n.default_locale = :en # (note that `en` is already the default!)
30
31
  ```
31
32
 
@@ -2,12 +2,10 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'json'
5
- require 'i18n/core_ext/hash'
6
5
 
7
6
  module I18n
8
7
  module Backend
9
8
  module Base
10
- using I18n::HashRefinements
11
9
  include I18n::Backend::Transliterator
12
10
 
13
11
  # Accepts a list of paths to translation files. Loads translations from
@@ -15,7 +13,10 @@ module I18n
15
13
  # for details.
16
14
  def load_translations(*filenames)
17
15
  filenames = I18n.load_path if filenames.empty?
18
- filenames.flatten.each { |filename| load_file(filename) }
16
+ filenames.flatten.each do |filename|
17
+ loaded_translations = load_file(filename)
18
+ yield filename, loaded_translations if block_given?
19
+ end
19
20
  end
20
21
 
21
22
  # This method receives a locale, a data hash and options for storing translations.
@@ -34,7 +35,7 @@ module I18n
34
35
  if entry.nil? && options.key?(:default)
35
36
  entry = default(locale, key, options[:default], options)
36
37
  else
37
- entry = resolve(locale, key, entry, options)
38
+ entry = resolve_entry(locale, key, entry, options)
38
39
  end
39
40
 
40
41
  count = options[:count]
@@ -53,7 +54,7 @@ module I18n
53
54
  end
54
55
 
55
56
  deep_interpolation = options[:deep_interpolation]
56
- values = options.except(*RESERVED_KEYS)
57
+ values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
57
58
  if values
58
59
  entry = if deep_interpolation
59
60
  deep_interpolate(locale, entry, values)
@@ -65,7 +66,7 @@ module I18n
65
66
  end
66
67
 
67
68
  def exists?(locale, key, options = EMPTY_HASH)
68
- lookup(locale, key) != nil
69
+ lookup(locale, key, options[:scope]) != nil
69
70
  end
70
71
 
71
72
  # Acts the same as +strftime+, but uses a localized version of the
@@ -122,7 +123,12 @@ module I18n
122
123
  # first translation that can be resolved. Otherwise it tries to resolve
123
124
  # the translation directly.
124
125
  def default(locale, object, subject, options = EMPTY_HASH)
125
- options = options.reject { |key, value| key == :default }
126
+ if options.size == 1 && options.has_key?(:default)
127
+ options = {}
128
+ else
129
+ options = Utils.except(options, :default)
130
+ end
131
+
126
132
  case subject
127
133
  when Array
128
134
  subject.each do |item|
@@ -153,6 +159,7 @@ module I18n
153
159
  end
154
160
  result unless result.is_a?(MissingTranslation)
155
161
  end
162
+ alias_method :resolve_entry, :resolve
156
163
 
157
164
  # Picks a translation from a pluralized mnemonic subkey according to English
158
165
  # pluralization rules :
@@ -164,7 +171,7 @@ module I18n
164
171
  # Other backends can implement more flexible or complex pluralization rules.
165
172
  def pluralize(locale, entry, count)
166
173
  entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
167
- return entry unless entry.is_a?(Hash) && count && entry.values.none? { |v| v.is_a?(Hash) }
174
+ return entry unless entry.is_a?(Hash) && count
168
175
 
169
176
  key = pluralization_key(entry, count)
170
177
  raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
@@ -223,24 +230,31 @@ module I18n
223
230
  def load_file(filename)
224
231
  type = File.extname(filename).tr('.', '').downcase
225
232
  raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
226
- data = send(:"load_#{type}", filename)
233
+ data, keys_symbolized = send(:"load_#{type}", filename)
227
234
  unless data.is_a?(Hash)
228
235
  raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
229
236
  end
230
- data.each { |locale, d| store_translations(locale, d || {}) }
237
+ data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
238
+
239
+ data
231
240
  end
232
241
 
233
242
  # Loads a plain Ruby translations file. eval'ing the file must yield
234
243
  # a Hash containing translation data with locales as toplevel keys.
235
244
  def load_rb(filename)
236
- eval(IO.read(filename), binding, filename)
245
+ translations = eval(IO.read(filename), binding, filename)
246
+ [translations, false]
237
247
  end
238
248
 
239
249
  # Loads a YAML translations file. The data must have locales as
240
250
  # toplevel keys.
241
251
  def load_yml(filename)
242
252
  begin
243
- YAML.load_file(filename)
253
+ if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
254
+ [YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
255
+ else
256
+ [YAML.load_file(filename), false]
257
+ end
244
258
  rescue TypeError, ScriptError, StandardError => e
245
259
  raise InvalidLocaleData.new(filename, e.inspect)
246
260
  end
@@ -251,7 +265,12 @@ module I18n
251
265
  # toplevel keys.
252
266
  def load_json(filename)
253
267
  begin
254
- ::JSON.parse(File.read(filename))
268
+ # Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
269
+ if ::JSON.respond_to?(:load_file)
270
+ [::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
271
+ else
272
+ [::JSON.parse(File.read(filename)), false]
273
+ end
255
274
  rescue TypeError, StandardError => e
256
275
  raise InvalidLocaleData.new(filename, e.inspect)
257
276
  end
@@ -268,8 +287,8 @@ module I18n
268
287
  when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
269
288
  when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
270
289
  when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
271
- when '%p' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase if object.respond_to? :hour
272
- when '%P' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase if object.respond_to? :hour
290
+ when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
291
+ when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
273
292
  end
274
293
  end
275
294
  rescue MissingTranslationData => e
@@ -16,9 +16,9 @@ module I18n
16
16
  #
17
17
  # The implementation assumes that all backends added to the Chain implement
18
18
  # a lookup method with the same API as Simple backend does.
19
+ #
20
+ # Fallback translations using the :default option are only used by the last backend of a chain.
19
21
  class Chain
20
- using I18n::HashRefinements
21
-
22
22
  module Implementation
23
23
  include Base
24
24
 
@@ -55,7 +55,7 @@ module I18n
55
55
 
56
56
  def translate(locale, key, default_options = EMPTY_HASH)
57
57
  namespace = nil
58
- options = default_options.except(:default)
58
+ options = Utils.except(default_options, :default)
59
59
 
60
60
  backends.each do |backend|
61
61
  catch(:exception) do
@@ -101,7 +101,7 @@ module I18n
101
101
  init_translations unless initialized?
102
102
  translations
103
103
  end
104
- memo.deep_merge!(partial_translations) { |_, a, b| b || a }
104
+ Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a }
105
105
  end
106
106
  end
107
107
 
@@ -43,7 +43,7 @@ module I18n
43
43
  return super if options[:fallback_in_progress]
44
44
  default = extract_non_symbol_default!(options) if options[:default]
45
45
 
46
- fallback_options = options.merge(:fallback_in_progress => true)
46
+ fallback_options = options.merge(:fallback_in_progress => true, fallback_original_locale: locale)
47
47
  I18n.fallbacks[locale].each do |fallback|
48
48
  begin
49
49
  catch(:exception) do
@@ -64,6 +64,24 @@ module I18n
64
64
  throw(:exception, I18n::MissingTranslation.new(locale, key, options))
65
65
  end
66
66
 
67
+ def resolve_entry(locale, object, subject, options = EMPTY_HASH)
68
+ return subject if options[:resolve] == false
69
+ result = catch(:exception) do
70
+ options.delete(:fallback_in_progress) if options.key?(:fallback_in_progress)
71
+
72
+ case subject
73
+ when Symbol
74
+ I18n.translate(subject, **options.merge(:locale => options[:fallback_original_locale], :throw => true))
75
+ when Proc
76
+ date_or_time = options.delete(:object) || object
77
+ resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
78
+ else
79
+ subject
80
+ end
81
+ end
82
+ result unless result.is_a?(MissingTranslation)
83
+ end
84
+
67
85
  def extract_non_symbol_default!(options)
68
86
  defaults = [options[:default]].flatten
69
87
  first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
@@ -89,7 +107,7 @@ module I18n
89
107
  private
90
108
 
91
109
  # Overwrite on_fallback to add specified logic when the fallback succeeds.
92
- def on_fallback(_original_locale, _fallback_locale, _key, _optoins)
110
+ def on_fallback(_original_locale, _fallback_locale, _key, _options)
93
111
  nil
94
112
  end
95
113
  end
@@ -31,8 +31,6 @@ module I18n
31
31
  # Without it strings containing periods (".") will not be translated.
32
32
 
33
33
  module Gettext
34
- using I18n::HashRefinements
35
-
36
34
  class PoData < Hash
37
35
  def set_comment(msgid_or_sym, comment)
38
36
  # ignore
@@ -43,7 +41,7 @@ module I18n
43
41
  def load_po(filename)
44
42
  locale = ::File.basename(filename, '.po').to_sym
45
43
  data = normalize(locale, parse(filename))
46
- { locale => data }
44
+ [{ locale => data }, false]
47
45
  end
48
46
 
49
47
  def parse(filename)
@@ -61,7 +59,7 @@ module I18n
61
59
  { part => _normalized.empty? ? value : _normalized }
62
60
  end
63
61
 
64
- result.deep_merge!(normalized)
62
+ Utils.deep_merge!(result, normalized)
65
63
  end
66
64
  result
67
65
  end
@@ -67,8 +67,6 @@ module I18n
67
67
  #
68
68
  # This is useful if you are using a KeyValue backend chained to a Simple backend.
69
69
  class KeyValue
70
- using I18n::HashRefinements
71
-
72
70
  module Implementation
73
71
  attr_accessor :store
74
72
 
@@ -91,7 +89,7 @@ module I18n
91
89
  when Hash
92
90
  if @subtrees && (old_value = @store[key])
93
91
  old_value = JSON.decode(old_value)
94
- value = old_value.deep_symbolize_keys.deep_merge!(value) if old_value.is_a?(Hash)
92
+ value = Utils.deep_merge!(Utils.deep_symbolize_keys(old_value), value) if old_value.is_a?(Hash)
95
93
  end
96
94
  when Proc
97
95
  raise "Key-value stores cannot handle procs"
@@ -115,12 +113,12 @@ module I18n
115
113
  # them into a hash such as the one returned from loading the
116
114
  # haml files
117
115
  def translations
118
- @translations = @store.keys.clone.map do |main_key|
116
+ @translations = Utils.deep_symbolize_keys(@store.keys.clone.map do |main_key|
119
117
  main_value = JSON.decode(@store[main_key])
120
118
  main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
121
119
  {key.to_sym => value}
122
120
  end
123
- end.inject{|hash, elem| hash.deep_merge!(elem)}.deep_symbolize_keys
121
+ end.inject{|hash, elem| Utils.deep_merge!(hash, elem)})
124
122
  end
125
123
 
126
124
  def init_translations
@@ -141,7 +139,7 @@ module I18n
141
139
  value = JSON.decode(value) if value
142
140
 
143
141
  if value.is_a?(Hash)
144
- value.deep_symbolize_keys
142
+ Utils.deep_symbolize_keys(value)
145
143
  elsif !value.nil?
146
144
  value
147
145
  elsif !@subtrees
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module Backend
5
+ # Backend that lazy loads translations based on the current locale. This
6
+ # implementation avoids loading all translations up front. Instead, it only
7
+ # loads the translations that belong to the current locale. This offers a
8
+ # performance incentive in local development and test environments for
9
+ # applications with many translations for many different locales. It's
10
+ # particularly useful when the application only refers to a single locales'
11
+ # translations at a time (ex. A Rails workload). The implementation
12
+ # identifies which translation files from the load path belong to the
13
+ # current locale by pattern matching against their path name.
14
+ #
15
+ # Specifically, a translation file is considered to belong to a locale if:
16
+ # a) the filename is in the I18n load path
17
+ # b) the filename ends in a supported extension (ie. .yml, .json, .po, .rb)
18
+ # c) the filename starts with the locale identifier
19
+ # d) the locale identifier and optional proceeding text is separated by an underscore, ie. "_".
20
+ #
21
+ # Examples:
22
+ # Valid files that will be selected by this backend:
23
+ #
24
+ # "files/locales/en_translation.yml" (Selected for locale "en")
25
+ # "files/locales/fr.po" (Selected for locale "fr")
26
+ #
27
+ # Invalid files that won't be selected by this backend:
28
+ #
29
+ # "files/locales/translation-file"
30
+ # "files/locales/en-translation.unsupported"
31
+ # "files/locales/french/translation.yml"
32
+ # "files/locales/fr/translation.yml"
33
+ #
34
+ # The implementation uses this assumption to defer the loading of
35
+ # translation files until the current locale actually requires them.
36
+ #
37
+ # The backend has two working modes: lazy_load and eager_load.
38
+ #
39
+ # Note: This backend should only be enabled in test environments!
40
+ # When the mode is set to false, the backend behaves exactly like the
41
+ # Simple backend, with an additional check that the paths being loaded
42
+ # abide by the format. If paths can't be matched to the format, an error is raised.
43
+ #
44
+ # You can configure lazy loaded backends through the initializer or backends
45
+ # accessor:
46
+ #
47
+ # # In test environments
48
+ #
49
+ # I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: true)
50
+ #
51
+ # # In other environments, such as production and CI
52
+ #
53
+ # I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: false) # default
54
+ #
55
+ class LocaleExtractor
56
+ class << self
57
+ def locale_from_path(path)
58
+ name = File.basename(path, ".*")
59
+ locale = name.split("_").first
60
+ locale.to_sym unless locale.nil?
61
+ end
62
+ end
63
+ end
64
+
65
+ class LazyLoadable < Simple
66
+ def initialize(lazy_load: false)
67
+ @lazy_load = lazy_load
68
+ end
69
+
70
+ # Returns whether the current locale is initialized.
71
+ def initialized?
72
+ if lazy_load?
73
+ initialized_locales[I18n.locale]
74
+ else
75
+ super
76
+ end
77
+ end
78
+
79
+ # Clean up translations and uninitialize all locales.
80
+ def reload!
81
+ if lazy_load?
82
+ @initialized_locales = nil
83
+ @translations = nil
84
+ else
85
+ super
86
+ end
87
+ end
88
+
89
+ # Eager loading is not supported in the lazy context.
90
+ def eager_load!
91
+ if lazy_load?
92
+ raise UnsupportedMethod.new(__method__, self.class, "Cannot eager load translations because backend was configured with lazy_load: true.")
93
+ else
94
+ super
95
+ end
96
+ end
97
+
98
+ # Parse the load path and extract all locales.
99
+ def available_locales
100
+ if lazy_load?
101
+ I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }.uniq
102
+ else
103
+ super
104
+ end
105
+ end
106
+
107
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
108
+ if lazy_load?
109
+ I18n.with_locale(locale) do
110
+ super
111
+ end
112
+ else
113
+ super
114
+ end
115
+ end
116
+
117
+ protected
118
+
119
+
120
+ # Load translations from files that belong to the current locale.
121
+ def init_translations
122
+ file_errors = if lazy_load?
123
+ initialized_locales[I18n.locale] = true
124
+ load_translations_and_collect_file_errors(filenames_for_current_locale)
125
+ else
126
+ @initialized = true
127
+ load_translations_and_collect_file_errors(I18n.load_path)
128
+ end
129
+
130
+ raise InvalidFilenames.new(file_errors) unless file_errors.empty?
131
+ end
132
+
133
+ def initialized_locales
134
+ @initialized_locales ||= Hash.new(false)
135
+ end
136
+
137
+ private
138
+
139
+ def lazy_load?
140
+ @lazy_load
141
+ end
142
+
143
+ class FilenameIncorrect < StandardError
144
+ def initialize(file, expected_locale, unexpected_locales)
145
+ super "#{file} can only load translations for \"#{expected_locale}\". Found translations for: #{unexpected_locales}."
146
+ end
147
+ end
148
+
149
+ # Loads each file supplied and asserts that the file only loads
150
+ # translations as expected by the name. The method returns a list of
151
+ # errors corresponding to offending files.
152
+ def load_translations_and_collect_file_errors(files)
153
+ errors = []
154
+
155
+ load_translations(files) do |file, loaded_translations|
156
+ assert_file_named_correctly!(file, loaded_translations)
157
+ rescue FilenameIncorrect => e
158
+ errors << e
159
+ end
160
+
161
+ errors
162
+ end
163
+
164
+ # Select all files from I18n load path that belong to current locale.
165
+ # These files must start with the locale identifier (ie. "en", "pt-BR"),
166
+ # followed by an "_" demarcation to separate proceeding text.
167
+ def filenames_for_current_locale
168
+ I18n.load_path.flatten.select do |path|
169
+ LocaleExtractor.locale_from_path(path) == I18n.locale
170
+ end
171
+ end
172
+
173
+ # Checks if a filename is named in correspondence to the translations it loaded.
174
+ # The locale extracted from the path must be the single locale loaded in the translations.
175
+ def assert_file_named_correctly!(file, translations)
176
+ loaded_locales = translations.keys.map(&:to_sym)
177
+ expected_locale = LocaleExtractor.locale_from_path(file)
178
+ unexpected_locales = loaded_locales.reject { |locale| locale == expected_locale }
179
+
180
+ raise FilenameIncorrect.new(file, expected_locale, unexpected_locales) unless unexpected_locales.empty?
181
+ end
182
+ end
183
+ end
184
+ end
@@ -16,26 +16,57 @@ module I18n
16
16
  module Pluralization
17
17
  # Overwrites the Base backend translate method so that it will check the
18
18
  # 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
20
20
  # pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
21
21
  #
22
22
  # 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
26
27
  #
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
31
39
  def pluralize(locale, entry, count)
32
40
  return entry unless entry.is_a?(Hash) && count
33
41
 
34
42
  pluralizer = pluralizer(locale)
35
43
  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
+ # Deprecation: The use of the `zero` key in this way is incorrect.
45
+ # Users that want a different string for the case of `count == 0` should use the explicit "0" key instead.
46
+ # We keep this incorrect behaviour for now for backwards compatibility until we can remove it.
47
+ # Ref: https://github.com/ruby-i18n/i18n/issues/629
48
+ return entry[:zero] if count == 0 && entry.has_key?(:zero)
49
+
50
+ # "0" and "1" are special cases
51
+ # https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
52
+ if count == 0 || count == 1
53
+ value = entry[symbolic_count(count)]
54
+ return value if value
55
+ end
56
+
57
+ # Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
58
+ # > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
59
+ # > 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.
60
+ # > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
61
+ # > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
62
+ # Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
63
+ plural_rule_category = pluralizer.call(count)
64
+
65
+ value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
66
+ entry[plural_rule_category] || entry[:other]
67
+ else
68
+ raise InvalidPluralizationData.new(entry, count, plural_rule_category)
69
+ end
39
70
  else
40
71
  super
41
72
  end
@@ -43,13 +74,23 @@ module I18n
43
74
 
44
75
  protected
45
76
 
46
- def pluralizers
47
- @pluralizers ||= {}
48
- end
77
+ def pluralizers
78
+ @pluralizers ||= {}
79
+ end
49
80
 
50
- def pluralizer(locale)
51
- pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
52
- end
81
+ def pluralizer(locale)
82
+ pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
83
+ end
84
+
85
+ private
86
+
87
+ # Normalizes categories of 0.0 and 1.0
88
+ # and returns the symbolic version
89
+ def symbolic_count(count)
90
+ count = 0 if count == 0
91
+ count = 1 if count == 1
92
+ count.to_s.to_sym
93
+ end
53
94
  end
54
95
  end
55
96
  end
@@ -19,10 +19,11 @@ module I18n
19
19
  #
20
20
  # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
21
21
  class Simple
22
- using I18n::HashRefinements
23
-
24
22
  module Implementation
25
23
  include Base
24
+
25
+ # Mutex to ensure that concurrent translations loading will be thread-safe
26
+ MUTEX = Mutex.new
26
27
 
27
28
  def initialized?
28
29
  @initialized ||= false
@@ -35,14 +36,13 @@ module I18n
35
36
  def store_translations(locale, data, options = EMPTY_HASH)
36
37
  if I18n.enforce_available_locales &&
37
38
  I18n.available_locales_initialized? &&
38
- !I18n.available_locales.include?(locale.to_sym) &&
39
- !I18n.available_locales.include?(locale.to_s)
39
+ !I18n.locale_available?(locale)
40
40
  return data
41
41
  end
42
42
  locale = locale.to_sym
43
43
  translations[locale] ||= Concurrent::Hash.new
44
- data = data.deep_symbolize_keys
45
- translations[locale].deep_merge!(data)
44
+ data = Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false)
45
+ Utils.deep_merge!(translations[locale], data)
46
46
  end
47
47
 
48
48
  # Get available locales from the translations hash
@@ -71,7 +71,11 @@ module I18n
71
71
  # call `init_translations`
72
72
  init_translations if do_init && !initialized?
73
73
 
74
- @translations ||= Concurrent::Hash.new { |h, k| h[k] = Concurrent::Hash.new }
74
+ @translations ||= Concurrent::Hash.new do |h, k|
75
+ MUTEX.synchronize do
76
+ h[k] = Concurrent::Hash.new
77
+ end
78
+ end
75
79
  end
76
80
 
77
81
  protected
@@ -97,7 +101,7 @@ module I18n
97
101
  return nil unless result.has_key?(_key)
98
102
  end
99
103
  result = result[_key]
100
- result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
104
+ result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
101
105
  result
102
106
  end
103
107
  end