i18n 0.4.0 → 1.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +0 -0
  3. data/README.md +127 -0
  4. data/lib/i18n/backend/base.rb +189 -111
  5. data/lib/i18n/backend/cache.rb +58 -22
  6. data/lib/i18n/backend/cache_file.rb +36 -0
  7. data/lib/i18n/backend/cascade.rb +9 -10
  8. data/lib/i18n/backend/chain.rb +95 -42
  9. data/lib/i18n/backend/fallbacks.rb +68 -22
  10. data/lib/i18n/backend/flatten.rb +13 -8
  11. data/lib/i18n/backend/gettext.rb +33 -25
  12. data/lib/i18n/backend/interpolation_compiler.rb +12 -14
  13. data/lib/i18n/backend/key_value.rb +112 -10
  14. data/lib/i18n/backend/lazy_loadable.rb +184 -0
  15. data/lib/i18n/backend/memoize.rb +13 -7
  16. data/lib/i18n/backend/metadata.rb +12 -6
  17. data/lib/i18n/backend/pluralization.rb +64 -25
  18. data/lib/i18n/backend/simple.rb +44 -18
  19. data/lib/i18n/backend/transliterator.rb +43 -33
  20. data/lib/i18n/backend.rb +5 -3
  21. data/lib/i18n/config.rb +91 -10
  22. data/lib/i18n/exceptions.rb +118 -22
  23. data/lib/i18n/gettext/helpers.rb +14 -4
  24. data/lib/i18n/gettext/po_parser.rb +7 -7
  25. data/lib/i18n/gettext.rb +6 -5
  26. data/lib/i18n/interpolate/ruby.rb +53 -0
  27. data/lib/i18n/locale/fallbacks.rb +33 -26
  28. data/lib/i18n/locale/tag/parents.rb +8 -8
  29. data/lib/i18n/locale/tag/rfc4646.rb +0 -2
  30. data/lib/i18n/locale/tag/simple.rb +2 -4
  31. data/lib/i18n/locale.rb +2 -0
  32. data/lib/i18n/middleware.rb +17 -0
  33. data/lib/i18n/tests/basics.rb +58 -0
  34. data/lib/i18n/tests/defaults.rb +52 -0
  35. data/lib/i18n/tests/interpolation.rb +167 -0
  36. data/lib/i18n/tests/link.rb +66 -0
  37. data/lib/i18n/tests/localization/date.rb +122 -0
  38. data/lib/i18n/tests/localization/date_time.rb +103 -0
  39. data/lib/i18n/tests/localization/procs.rb +118 -0
  40. data/lib/i18n/tests/localization/time.rb +103 -0
  41. data/lib/i18n/tests/localization.rb +19 -0
  42. data/lib/i18n/tests/lookup.rb +81 -0
  43. data/lib/i18n/tests/pluralization.rb +35 -0
  44. data/lib/i18n/tests/procs.rb +66 -0
  45. data/lib/i18n/tests.rb +14 -0
  46. data/lib/i18n/utils.rb +55 -0
  47. data/lib/i18n/version.rb +3 -1
  48. data/lib/i18n.rb +200 -87
  49. metadata +64 -56
  50. data/CHANGELOG.textile +0 -135
  51. data/README.textile +0 -93
  52. data/lib/i18n/backend/active_record/missing.rb +0 -65
  53. data/lib/i18n/backend/active_record/store_procs.rb +0 -38
  54. data/lib/i18n/backend/active_record/translation.rb +0 -93
  55. data/lib/i18n/backend/active_record.rb +0 -61
  56. data/lib/i18n/backend/cldr.rb +0 -100
  57. data/lib/i18n/core_ext/hash.rb +0 -29
  58. data/lib/i18n/core_ext/string/interpolate.rb +0 -98
@@ -1,9 +1,26 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'i18n/backend/base'
4
- require 'active_support/json'
5
4
 
6
5
  module I18n
6
+
7
+ begin
8
+ require 'oj'
9
+ class JSON
10
+ class << self
11
+ def encode(value)
12
+ Oj::Rails.encode(value)
13
+ end
14
+ def decode(value)
15
+ Oj.load(value)
16
+ end
17
+ end
18
+ end
19
+ rescue LoadError
20
+ require 'active_support/json'
21
+ JSON = ActiveSupport::JSON
22
+ end
23
+
7
24
  module Backend
8
25
  # This is a basic backend for key value stores. It receives on
9
26
  # initialization the store, which should respond to three methods:
@@ -59,7 +76,11 @@ module I18n
59
76
  @store, @subtrees = store, subtrees
60
77
  end
61
78
 
62
- def store_translations(locale, data, options = {})
79
+ def initialized?
80
+ !@store.nil?
81
+ end
82
+
83
+ def store_translations(locale, data, options = EMPTY_HASH)
63
84
  escape = options.fetch(:escape, true)
64
85
  flatten_translations(locale, data, escape, @subtrees).each do |key, value|
65
86
  key = "#{locale}.#{key}"
@@ -67,14 +88,14 @@ module I18n
67
88
  case value
68
89
  when Hash
69
90
  if @subtrees && (old_value = @store[key])
70
- old_value = ActiveSupport::JSON.decode(old_value)
71
- value = old_value.deep_symbolize_keys.deep_merge!(value) if old_value.is_a?(Hash)
91
+ old_value = JSON.decode(old_value)
92
+ value = Utils.deep_merge!(Utils.deep_symbolize_keys(old_value), value) if old_value.is_a?(Hash)
72
93
  end
73
94
  when Proc
74
95
  raise "Key-value stores cannot handle procs"
75
96
  end
76
97
 
77
- @store[key] = ActiveSupport::JSON.encode(value) unless value.is_a?(Symbol)
98
+ @store[key] = JSON.encode(value) unless value.is_a?(Symbol)
78
99
  end
79
100
  end
80
101
 
@@ -88,15 +109,96 @@ module I18n
88
109
 
89
110
  protected
90
111
 
91
- def lookup(locale, key, scope = [], options = {})
112
+ # Queries the translations from the key-value store and converts
113
+ # them into a hash such as the one returned from loading the
114
+ # haml files
115
+ def translations
116
+ @translations = Utils.deep_symbolize_keys(@store.keys.clone.map do |main_key|
117
+ main_value = JSON.decode(@store[main_key])
118
+ main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
119
+ {key.to_sym => value}
120
+ end
121
+ end.inject{|hash, elem| Utils.deep_merge!(hash, elem)})
122
+ end
123
+
124
+ def init_translations
125
+ # NO OP
126
+ # This call made also inside Simple Backend and accessed by
127
+ # other plugins like I18n-js and babilu and
128
+ # to use it along with the Chain backend we need to
129
+ # provide a uniform API even for protected methods :S
130
+ end
131
+
132
+ def subtrees?
133
+ @subtrees
134
+ end
135
+
136
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
92
137
  key = normalize_flat_keys(locale, key, scope, options[:separator])
93
138
  value = @store["#{locale}.#{key}"]
94
- value = ActiveSupport::JSON.decode(value) if value
95
- value.is_a?(Hash) ? value.deep_symbolize_keys : value
139
+ value = JSON.decode(value) if value
140
+
141
+ if value.is_a?(Hash)
142
+ Utils.deep_symbolize_keys(value)
143
+ elsif !value.nil?
144
+ value
145
+ elsif !@subtrees
146
+ SubtreeProxy.new("#{locale}.#{key}", @store)
147
+ end
148
+ end
149
+
150
+ def pluralize(locale, entry, count)
151
+ if subtrees?
152
+ super
153
+ else
154
+ return entry unless entry.is_a?(Hash)
155
+ key = pluralization_key(entry, count)
156
+ entry[key]
157
+ end
158
+ end
159
+ end
160
+
161
+ class SubtreeProxy
162
+ def initialize(master_key, store)
163
+ @master_key = master_key
164
+ @store = store
165
+ @subtree = nil
166
+ end
167
+
168
+ def has_key?(key)
169
+ @subtree && @subtree.has_key?(key) || self[key]
170
+ end
171
+
172
+ def [](key)
173
+ unless @subtree && value = @subtree[key]
174
+ value = @store["#{@master_key}.#{key}"]
175
+ if value
176
+ value = JSON.decode(value)
177
+ (@subtree ||= {})[key] = value
178
+ end
179
+ end
180
+ value
181
+ end
182
+
183
+ def is_a?(klass)
184
+ Hash == klass || super
185
+ end
186
+ alias :kind_of? :is_a?
187
+
188
+ def instance_of?(klass)
189
+ Hash == klass || super
190
+ end
191
+
192
+ def nil?
193
+ @subtree.nil?
194
+ end
195
+
196
+ def inspect
197
+ @subtree.inspect
96
198
  end
97
199
  end
98
200
 
99
201
  include Implementation
100
202
  end
101
203
  end
102
- end
204
+ end
@@ -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
@@ -1,11 +1,11 @@
1
- # encoding: utf-8
2
- #
1
+ # frozen_string_literal: true
2
+
3
3
  # Memoize module simply memoizes the values returned by lookup using
4
4
  # a flat hash and can tremendously speed up the lookup process in a backend.
5
5
  #
6
6
  # To enable it you can simply include the Memoize module to your backend:
7
7
  #
8
- # I18n::Backend::Simple.send(:include, I18n::Backend::Memoize)
8
+ # I18n::Backend::Simple.include(I18n::Backend::Memoize)
9
9
  #
10
10
  # Notice that it's the responsibility of the backend to define whenever the
11
11
  # cache should be cleaned.
@@ -16,7 +16,7 @@ module I18n
16
16
  @memoized_locales ||= super
17
17
  end
18
18
 
19
- def store_translations(locale, data, options = {})
19
+ def store_translations(locale, data, options = EMPTY_HASH)
20
20
  reset_memoizations!(locale)
21
21
  super
22
22
  end
@@ -26,9 +26,15 @@ module I18n
26
26
  super
27
27
  end
28
28
 
29
+ def eager_load!
30
+ memoized_lookup
31
+ available_locales
32
+ super
33
+ end
34
+
29
35
  protected
30
36
 
31
- def lookup(locale, key, scope = nil, options = {})
37
+ def lookup(locale, key, scope = nil, options = EMPTY_HASH)
32
38
  flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
33
39
  key, scope, options[:separator]).to_sym
34
40
  flat_hash = memoized_lookup[locale.to_sym]
@@ -36,7 +42,7 @@ module I18n
36
42
  end
37
43
 
38
44
  def memoized_lookup
39
- @memoized_lookup ||= Hash.new { |h, k| h[k] = {} }
45
+ @memoized_lookup ||= I18n.new_double_nested_cache
40
46
  end
41
47
 
42
48
  def reset_memoizations!(locale=nil)
@@ -45,4 +51,4 @@ module I18n
45
51
  end
46
52
  end
47
53
  end
48
- end
54
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # I18n translation metadata is useful when you want to access information
2
4
  # about how a translation was looked up, pluralized or interpolated in
3
5
  # your application.
@@ -12,7 +14,7 @@
12
14
  # To enable translation metadata you can simply include the Metadata module
13
15
  # into the Simple backend class - or whatever other backend you are using:
14
16
  #
15
- # I18n::Backend::Simple.send(:include, I18n::Backend::Metadata)
17
+ # I18n::Backend::Simple.include(I18n::Backend::Metadata)
16
18
  #
17
19
  module I18n
18
20
  module Backend
@@ -21,29 +23,33 @@ module I18n
21
23
  def included(base)
22
24
  Object.class_eval do
23
25
  def translation_metadata
24
- @translation_metadata ||= {}
26
+ unless self.frozen?
27
+ @translation_metadata ||= {}
28
+ else
29
+ {}
30
+ end
25
31
  end
26
32
 
27
33
  def translation_metadata=(translation_metadata)
28
- @translation_metadata = translation_metadata
34
+ @translation_metadata = translation_metadata unless self.frozen?
29
35
  end
30
36
  end unless Object.method_defined?(:translation_metadata)
31
37
  end
32
38
  end
33
39
 
34
- def translate(locale, key, options = {})
40
+ def translate(locale, key, options = EMPTY_HASH)
35
41
  metadata = {
36
42
  :locale => locale,
37
43
  :key => key,
38
44
  :scope => options[:scope],
39
45
  :default => options[:default],
40
46
  :separator => options[:separator],
41
- :values => options.reject { |name, value| Base::RESERVED_KEYS.include?(name) }
47
+ :values => options.reject { |name, _value| RESERVED_KEYS.include?(name) }
42
48
  }
43
49
  with_metadata(metadata) { super }
44
50
  end
45
51
 
46
- def interpolate(locale, entry, values = {})
52
+ def interpolate(locale, entry, values = EMPTY_HASH)
47
53
  metadata = entry.translation_metadata.merge(:original => entry)
48
54
  with_metadata(metadata) { super }
49
55
  end
@@ -1,15 +1,13 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- # I18n locale fallbacks are useful when you want your application to use
4
- # translations from other locales when translations for the current locale are
5
- # missing. E.g. you might want to use :en translations when translations in
6
- # your applications main locale :de are missing.
3
+ # I18n Pluralization are useful when you want your application to
4
+ # customize pluralization rules.
7
5
  #
8
6
  # To enable locale specific pluralizations you can simply include the
9
7
  # Pluralization module to the Simple backend - or whatever other backend you
10
8
  # are using.
11
9
  #
12
- # I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
10
+ # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
13
11
  #
14
12
  # You also need to make sure to provide pluralization algorithms to the
15
13
  # backend, i.e. include them to your I18n.load_path accordingly.
@@ -18,26 +16,57 @@ module I18n
18
16
  module Pluralization
19
17
  # Overwrites the Base backend translate method so that it will check the
20
18
  # translation meta data space (:i18n) for a locale specific pluralization
21
- # 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
22
20
  # pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
23
21
  #
24
- # Pluralization rules are expected to respond to #call(entry, count) and
25
- # return a pluralization key. Valid keys depend on the translation data
26
- # hash (entry) but it is generally recommended to follow CLDR's style,
27
- # i.e., return one of the keys :zero, :one, :few, :many, :other.
22
+ # Pluralization rules are expected to respond to #call(count) and
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
28
27
  #
29
- # The :zero key is always picked directly when count equals 0 AND the
30
- # translation data has the key :zero. This way translators are free to
31
- # either pick a special :zero translation even for languages where the
32
- # 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
33
39
  def pluralize(locale, entry, count)
34
- return entry unless entry.is_a?(Hash) and count
40
+ return entry unless entry.is_a?(Hash) && count
35
41
 
36
42
  pluralizer = pluralizer(locale)
37
43
  if pluralizer.respond_to?(:call)
38
- key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
39
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
40
- 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
41
70
  else
42
71
  super
43
72
  end
@@ -45,13 +74,23 @@ module I18n
45
74
 
46
75
  protected
47
76
 
48
- def pluralizers
49
- @pluralizers ||= {}
50
- end
77
+ def pluralizers
78
+ @pluralizers ||= {}
79
+ end
51
80
 
52
- def pluralizer(locale)
53
- pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
54
- 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
55
94
  end
56
95
  end
57
96
  end
@@ -1,4 +1,6 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/backend/base'
2
4
 
3
5
  module I18n
4
6
  module Backend
@@ -15,10 +17,13 @@ module I18n
15
17
  # end
16
18
  # end
17
19
  #
18
- # I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
20
+ # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
19
21
  class Simple
20
22
  module Implementation
21
23
  include Base
24
+
25
+ # Mutex to ensure that concurrent translations loading will be thread-safe
26
+ MUTEX = Mutex.new
22
27
 
23
28
  def initialized?
24
29
  @initialized ||= false
@@ -28,18 +33,23 @@ module I18n
28
33
  # This uses a deep merge for the translations hash, so existing
29
34
  # translations will be overwritten by new ones only at the deepest
30
35
  # level of the hash.
31
- def store_translations(locale, data, options = {})
36
+ def store_translations(locale, data, options = EMPTY_HASH)
37
+ if I18n.enforce_available_locales &&
38
+ I18n.available_locales_initialized? &&
39
+ !I18n.locale_available?(locale)
40
+ return data
41
+ end
32
42
  locale = locale.to_sym
33
- translations[locale] ||= {}
34
- data = data.deep_symbolize_keys
35
- translations[locale].deep_merge!(data)
43
+ translations[locale] ||= Concurrent::Hash.new
44
+ data = Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false)
45
+ Utils.deep_merge!(translations[locale], data)
36
46
  end
37
47
 
38
48
  # Get available locales from the translations hash
39
49
  def available_locales
40
50
  init_translations unless initialized?
41
51
  translations.inject([]) do |locales, (locale, data)|
42
- locales << locale unless (data.keys - [:i18n]).empty?
52
+ locales << locale unless data.size <= 1 && (data.empty? || data.has_key?(:i18n))
43
53
  locales
44
54
  end
45
55
  end
@@ -51,6 +61,23 @@ module I18n
51
61
  super
52
62
  end
53
63
 
64
+ def eager_load!
65
+ init_translations unless initialized?
66
+ super
67
+ end
68
+
69
+ def translations(do_init: false)
70
+ # To avoid returning empty translations,
71
+ # call `init_translations`
72
+ init_translations if do_init && !initialized?
73
+
74
+ @translations ||= Concurrent::Hash.new do |h, k|
75
+ MUTEX.synchronize do
76
+ h[k] = Concurrent::Hash.new
77
+ end
78
+ end
79
+ end
80
+
54
81
  protected
55
82
 
56
83
  def init_translations
@@ -58,24 +85,23 @@ module I18n
58
85
  @initialized = true
59
86
  end
60
87
 
61
- def translations
62
- @translations ||= {}
63
- end
64
-
65
88
  # Looks up a translation from the translations hash. Returns nil if
66
- # eiher key is nil, or locale, scope or key do not exist as a key in the
89
+ # either key is nil, or locale, scope or key do not exist as a key in the
67
90
  # nested translations hash. Splits keys or scopes containing dots
68
91
  # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
69
92
  # <tt>%w(currency format)</tt>.
70
- def lookup(locale, key, scope = [], options = {})
93
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
71
94
  init_translations unless initialized?
72
95
  keys = I18n.normalize_keys(locale, key, scope, options[:separator])
73
96
 
74
- keys.inject(translations) do |result, key|
75
- key = key.to_sym
76
- return nil unless result.is_a?(Hash) && result.has_key?(key)
77
- result = result[key]
78
- result = resolve(locale, key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
97
+ keys.inject(translations) do |result, _key|
98
+ return nil unless result.is_a?(Hash)
99
+ unless result.has_key?(_key)
100
+ _key = _key.to_s.to_sym
101
+ return nil unless result.has_key?(_key)
102
+ end
103
+ result = result[_key]
104
+ result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
79
105
  result
80
106
  end
81
107
  end