i18n 1.0.0 → 1.14.7

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -32
  3. data/lib/i18n/backend/base.rb +93 -34
  4. data/lib/i18n/backend/cache.rb +10 -11
  5. data/lib/i18n/backend/cache_file.rb +36 -0
  6. data/lib/i18n/backend/cascade.rb +3 -1
  7. data/lib/i18n/backend/chain.rb +39 -6
  8. data/lib/i18n/backend/fallbacks.rb +48 -15
  9. data/lib/i18n/backend/flatten.rb +10 -5
  10. data/lib/i18n/backend/gettext.rb +4 -2
  11. data/lib/i18n/backend/interpolation_compiler.rb +8 -8
  12. data/lib/i18n/backend/key_value.rb +31 -4
  13. data/lib/i18n/backend/lazy_loadable.rb +184 -0
  14. data/lib/i18n/backend/memoize.rb +10 -2
  15. data/lib/i18n/backend/metadata.rb +5 -3
  16. data/lib/i18n/backend/pluralization.rb +61 -18
  17. data/lib/i18n/backend/simple.rb +44 -24
  18. data/lib/i18n/backend/transliterator.rb +26 -24
  19. data/lib/i18n/backend.rb +5 -1
  20. data/lib/i18n/config.rb +22 -4
  21. data/lib/i18n/exceptions.rb +71 -18
  22. data/lib/i18n/gettext/helpers.rb +4 -2
  23. data/lib/i18n/gettext/po_parser.rb +7 -7
  24. data/lib/i18n/gettext.rb +2 -0
  25. data/lib/i18n/interpolate/ruby.rb +22 -6
  26. data/lib/i18n/locale/fallbacks.rb +33 -22
  27. data/lib/i18n/locale/tag/parents.rb +8 -6
  28. data/lib/i18n/locale/tag/simple.rb +2 -2
  29. data/lib/i18n/locale.rb +2 -0
  30. data/lib/i18n/middleware.rb +2 -0
  31. data/lib/i18n/tests/basics.rb +5 -7
  32. data/lib/i18n/tests/defaults.rb +8 -1
  33. data/lib/i18n/tests/interpolation.rb +34 -7
  34. data/lib/i18n/tests/link.rb +11 -1
  35. data/lib/i18n/tests/localization/date.rb +37 -10
  36. data/lib/i18n/tests/localization/date_time.rb +28 -7
  37. data/lib/i18n/tests/localization/procs.rb +9 -7
  38. data/lib/i18n/tests/localization/time.rb +27 -5
  39. data/lib/i18n/tests/lookup.rb +11 -5
  40. data/lib/i18n/tests/pluralization.rb +1 -1
  41. data/lib/i18n/tests/procs.rb +23 -7
  42. data/lib/i18n/tests.rb +2 -0
  43. data/lib/i18n/utils.rb +55 -0
  44. data/lib/i18n/version.rb +3 -1
  45. data/lib/i18n.rb +179 -58
  46. metadata +16 -61
  47. data/gemfiles/Gemfile.rails-3.2.x +0 -10
  48. data/gemfiles/Gemfile.rails-4.0.x +0 -10
  49. data/gemfiles/Gemfile.rails-4.1.x +0 -10
  50. data/gemfiles/Gemfile.rails-4.2.x +0 -10
  51. data/gemfiles/Gemfile.rails-5.0.x +0 -10
  52. data/gemfiles/Gemfile.rails-5.1.x +0 -10
  53. data/gemfiles/Gemfile.rails-master +0 -10
  54. data/lib/i18n/core_ext/hash.rb +0 -29
  55. data/lib/i18n/core_ext/kernel/suppress_warnings.rb +0 -8
  56. data/lib/i18n/core_ext/string/interpolate.rb +0 -9
  57. data/test/api/all_features_test.rb +0 -58
  58. data/test/api/cascade_test.rb +0 -28
  59. data/test/api/chain_test.rb +0 -24
  60. data/test/api/fallbacks_test.rb +0 -30
  61. data/test/api/key_value_test.rb +0 -24
  62. data/test/api/memoize_test.rb +0 -56
  63. data/test/api/override_test.rb +0 -42
  64. data/test/api/pluralization_test.rb +0 -30
  65. data/test/api/simple_test.rb +0 -28
  66. data/test/backend/cache_test.rb +0 -109
  67. data/test/backend/cascade_test.rb +0 -86
  68. data/test/backend/chain_test.rb +0 -122
  69. data/test/backend/exceptions_test.rb +0 -36
  70. data/test/backend/fallbacks_test.rb +0 -219
  71. data/test/backend/interpolation_compiler_test.rb +0 -118
  72. data/test/backend/key_value_test.rb +0 -61
  73. data/test/backend/memoize_test.rb +0 -79
  74. data/test/backend/metadata_test.rb +0 -48
  75. data/test/backend/pluralization_test.rb +0 -45
  76. data/test/backend/simple_test.rb +0 -103
  77. data/test/backend/transliterator_test.rb +0 -84
  78. data/test/core_ext/hash_test.rb +0 -36
  79. data/test/gettext/api_test.rb +0 -214
  80. data/test/gettext/backend_test.rb +0 -92
  81. data/test/i18n/exceptions_test.rb +0 -117
  82. data/test/i18n/gettext_plural_keys_test.rb +0 -20
  83. data/test/i18n/interpolate_test.rb +0 -91
  84. data/test/i18n/load_path_test.rb +0 -34
  85. data/test/i18n/middleware_test.rb +0 -24
  86. data/test/i18n_test.rb +0 -462
  87. data/test/locale/fallbacks_test.rb +0 -133
  88. data/test/locale/tag/rfc4646_test.rb +0 -143
  89. data/test/locale/tag/simple_test.rb +0 -32
  90. data/test/run_all.rb +0 -20
  91. data/test/test_data/locales/de.po +0 -82
  92. data/test/test_data/locales/en.rb +0 -3
  93. data/test/test_data/locales/en.yml +0 -3
  94. data/test/test_data/locales/invalid/empty.yml +0 -0
  95. data/test/test_data/locales/invalid/syntax.yml +0 -4
  96. data/test/test_data/locales/plurals.rb +0 -113
  97. data/test/test_helper.rb +0 -61
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # I18n locale fallbacks are useful when you want your application to use
2
4
  # translations from other locales when translations for the current locale are
3
5
  # missing. E.g. you might want to use :en translations when translations in
@@ -14,11 +16,13 @@ module I18n
14
16
  # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
15
17
  def fallbacks
16
18
  @@fallbacks ||= I18n::Locale::Fallbacks.new
19
+ Thread.current[:i18n_fallbacks] || @@fallbacks
17
20
  end
18
21
 
19
22
  # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
20
23
  def fallbacks=(fallbacks)
21
- @@fallbacks = fallbacks
24
+ @@fallbacks = fallbacks.is_a?(Array) ? I18n::Locale::Fallbacks.new(fallbacks) : fallbacks
25
+ Thread.current[:i18n_fallbacks] = @@fallbacks
22
26
  end
23
27
  end
24
28
 
@@ -34,25 +38,24 @@ module I18n
34
38
  # The default option takes precedence over fallback locales only when
35
39
  # it's a Symbol. When the default contains a String, Proc or Hash
36
40
  # it is evaluated last after all the fallback locales have been tried.
37
- def translate(locale, key, options = {})
41
+ def translate(locale, key, options = EMPTY_HASH)
38
42
  return super unless options.fetch(:fallback, true)
39
43
  return super if options[:fallback_in_progress]
40
44
  default = extract_non_symbol_default!(options) if options[:default]
41
45
 
42
- begin
43
- options[:fallback_in_progress] = true
44
- I18n.fallbacks[locale].each do |fallback|
45
- begin
46
- catch(:exception) do
47
- result = super(fallback, key, options)
48
- return result unless result.nil?
46
+ fallback_options = options.merge(:fallback_in_progress => true, fallback_original_locale: locale)
47
+ I18n.fallbacks[locale].each do |fallback|
48
+ begin
49
+ catch(:exception) do
50
+ result = super(fallback, key, fallback_options)
51
+ unless result.nil?
52
+ on_fallback(locale, fallback, key, options) if locale.to_s != fallback.to_s
53
+ return result
49
54
  end
50
- rescue I18n::InvalidLocale
51
- # we do nothing when the locale is invalid, as this is a fallback anyways.
52
55
  end
56
+ rescue I18n::InvalidLocale
57
+ # we do nothing when the locale is invalid, as this is a fallback anyways.
53
58
  end
54
- ensure
55
- options.delete(:fallback_in_progress)
56
59
  end
57
60
 
58
61
  return if options.key?(:default) && options[:default].nil?
@@ -61,6 +64,28 @@ module I18n
61
64
  throw(:exception, I18n::MissingTranslation.new(locale, key, options))
62
65
  end
63
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(
75
+ :locale => options[:fallback_original_locale],
76
+ :throw => true,
77
+ :skip_interpolation => true
78
+ ))
79
+ when Proc
80
+ date_or_time = options.delete(:object) || object
81
+ resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
82
+ else
83
+ subject
84
+ end
85
+ end
86
+ result unless result.is_a?(MissingTranslation)
87
+ end
88
+
64
89
  def extract_non_symbol_default!(options)
65
90
  defaults = [options[:default]].flatten
66
91
  first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
@@ -70,10 +95,11 @@ module I18n
70
95
  return first_non_symbol_default
71
96
  end
72
97
 
73
- def exists?(locale, key)
98
+ def exists?(locale, key, options = EMPTY_HASH)
99
+ return super unless options.fetch(:fallback, true)
74
100
  I18n.fallbacks[locale].each do |fallback|
75
101
  begin
76
- return true if super(fallback, key)
102
+ return true if super(fallback, key, options)
77
103
  rescue I18n::InvalidLocale
78
104
  # we do nothing when the locale is invalid, as this is a fallback anyways.
79
105
  end
@@ -81,6 +107,13 @@ module I18n
81
107
 
82
108
  false
83
109
  end
110
+
111
+ private
112
+
113
+ # Overwrite on_fallback to add specified logic when the fallback succeeds.
114
+ def on_fallback(_original_locale, _fallback_locale, _key, _options)
115
+ nil
116
+ end
84
117
  end
85
118
  end
86
119
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module I18n
2
4
  module Backend
3
5
  # This module contains several helpers to assist flattening translations.
@@ -16,14 +18,17 @@ module I18n
16
18
  # and creates way less objects than the one at I18n.normalize_keys.
17
19
  # It also handles escaping the translation keys.
18
20
  def self.normalize_flat_keys(locale, key, scope, separator)
19
- keys = [scope, key].flatten.compact
21
+ keys = [scope, key]
22
+ keys.flatten!
23
+ keys.compact!
24
+
20
25
  separator ||= I18n.default_separator
21
26
 
22
27
  if separator != FLATTEN_SEPARATOR
23
- keys.map! do |k|
24
- k.to_s.tr("#{FLATTEN_SEPARATOR}#{separator}",
25
- "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}")
26
- end
28
+ from_str = "#{FLATTEN_SEPARATOR}#{separator}"
29
+ to_str = "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}"
30
+
31
+ keys.map! { |k| k.to_s.tr from_str, to_str }
27
32
  end
28
33
 
29
34
  keys.join(".")
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'i18n/gettext'
2
4
  require 'i18n/gettext/po_parser'
3
5
 
@@ -39,7 +41,7 @@ module I18n
39
41
  def load_po(filename)
40
42
  locale = ::File.basename(filename, '.po').to_sym
41
43
  data = normalize(locale, parse(filename))
42
- { locale => data }
44
+ [{ locale => data }, false]
43
45
  end
44
46
 
45
47
  def parse(filename)
@@ -57,7 +59,7 @@ module I18n
57
59
  { part => _normalized.empty? ? value : _normalized }
58
60
  end
59
61
 
60
- result.deep_merge!(normalized)
62
+ Utils.deep_merge!(result, normalized)
61
63
  end
62
64
  result
63
65
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # The InterpolationCompiler module contains optimizations that can tremendously
2
4
  # speed up the interpolation process on the Simple backend.
3
5
  #
@@ -19,8 +21,7 @@ module I18n
19
21
  module Compiler
20
22
  extend self
21
23
 
22
- TOKENIZER = /(%%\{[^\}]+\}|%\{[^\}]+\})/
23
- INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/
24
+ TOKENIZER = /(%%?\{[^}]+\})/
24
25
 
25
26
  def compile_if_an_interpolation(string)
26
27
  if interpolated_str?(string)
@@ -35,7 +36,7 @@ module I18n
35
36
  end
36
37
 
37
38
  def interpolated_str?(str)
38
- str.kind_of?(::String) && str =~ INTERPOLATION_SYNTAX_PATTERN
39
+ str.kind_of?(::String) && str =~ TOKENIZER
39
40
  end
40
41
 
41
42
  protected
@@ -46,13 +47,12 @@ module I18n
46
47
 
47
48
  def compiled_interpolation_body(str)
48
49
  tokenize(str).map do |token|
49
- (matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
50
+ token.match(TOKENIZER) ? handle_interpolation_token(token) : escape_plain_str(token)
50
51
  end.join
51
52
  end
52
53
 
53
- def handle_interpolation_token(interpolation, matchdata)
54
- escaped, pattern, key = matchdata.values_at(1, 2, 3)
55
- escaped ? pattern : compile_interpolation_token(key.to_sym)
54
+ def handle_interpolation_token(token)
55
+ token.start_with?('%%') ? token[1..] : compile_interpolation_token(token[2..-2])
56
56
  end
57
57
 
58
58
  def compile_interpolation_token(key)
@@ -104,7 +104,7 @@ module I18n
104
104
  end
105
105
  end
106
106
 
107
- def store_translations(locale, data, options = {})
107
+ def store_translations(locale, data, options = EMPTY_HASH)
108
108
  compile_all_strings_in(data)
109
109
  super
110
110
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'i18n/backend/base'
2
4
 
3
5
  module I18n
@@ -74,7 +76,11 @@ module I18n
74
76
  @store, @subtrees = store, subtrees
75
77
  end
76
78
 
77
- def store_translations(locale, data, options = {})
79
+ def initialized?
80
+ !@store.nil?
81
+ end
82
+
83
+ def store_translations(locale, data, options = EMPTY_HASH)
78
84
  escape = options.fetch(:escape, true)
79
85
  flatten_translations(locale, data, escape, @subtrees).each do |key, value|
80
86
  key = "#{locale}.#{key}"
@@ -83,7 +89,7 @@ module I18n
83
89
  when Hash
84
90
  if @subtrees && (old_value = @store[key])
85
91
  old_value = JSON.decode(old_value)
86
- 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)
87
93
  end
88
94
  when Proc
89
95
  raise "Key-value stores cannot handle procs"
@@ -103,17 +109,37 @@ module I18n
103
109
 
104
110
  protected
105
111
 
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
+
106
132
  def subtrees?
107
133
  @subtrees
108
134
  end
109
135
 
110
- def lookup(locale, key, scope = [], options = {})
136
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
111
137
  key = normalize_flat_keys(locale, key, scope, options[:separator])
112
138
  value = @store["#{locale}.#{key}"]
113
139
  value = JSON.decode(value) if value
114
140
 
115
141
  if value.is_a?(Hash)
116
- value.deep_symbolize_keys
142
+ Utils.deep_symbolize_keys(value)
117
143
  elsif !value.nil?
118
144
  value
119
145
  elsif !@subtrees
@@ -125,6 +151,7 @@ module I18n
125
151
  if subtrees?
126
152
  super
127
153
  else
154
+ return entry unless entry.is_a?(Hash)
128
155
  key = pluralization_key(entry, count)
129
156
  entry[key]
130
157
  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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Memoize module simply memoizes the values returned by lookup using
2
4
  # a flat hash and can tremendously speed up the lookup process in a backend.
3
5
  #
@@ -14,7 +16,7 @@ module I18n
14
16
  @memoized_locales ||= super
15
17
  end
16
18
 
17
- def store_translations(locale, data, options = {})
19
+ def store_translations(locale, data, options = EMPTY_HASH)
18
20
  reset_memoizations!(locale)
19
21
  super
20
22
  end
@@ -24,9 +26,15 @@ module I18n
24
26
  super
25
27
  end
26
28
 
29
+ def eager_load!
30
+ memoized_lookup
31
+ available_locales
32
+ super
33
+ end
34
+
27
35
  protected
28
36
 
29
- def lookup(locale, key, scope = nil, options = {})
37
+ def lookup(locale, key, scope = nil, options = EMPTY_HASH)
30
38
  flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
31
39
  key, scope, options[:separator]).to_sym
32
40
  flat_hash = memoized_lookup[locale.to_sym]
@@ -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.
@@ -35,19 +37,19 @@ module I18n
35
37
  end
36
38
  end
37
39
 
38
- def translate(locale, key, options = {})
40
+ def translate(locale, key, options = EMPTY_HASH)
39
41
  metadata = {
40
42
  :locale => locale,
41
43
  :key => key,
42
44
  :scope => options[:scope],
43
45
  :default => options[:default],
44
46
  :separator => options[:separator],
45
- :values => options.reject { |name, value| RESERVED_KEYS.include?(name) }
47
+ :values => options.reject { |name, _value| RESERVED_KEYS.include?(name) }
46
48
  }
47
49
  with_metadata(metadata) { super }
48
50
  end
49
51
 
50
- def interpolate(locale, entry, values = {})
52
+ def interpolate(locale, entry, values = EMPTY_HASH)
51
53
  metadata = entry.translation_metadata.merge(:original => entry)
52
54
  with_metadata(metadata) { super }
53
55
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # I18n Pluralization are useful when you want your application to
2
4
  # customize pluralization rules.
3
5
  #
@@ -14,26 +16,57 @@ module I18n
14
16
  module Pluralization
15
17
  # Overwrites the Base backend translate method so that it will check the
16
18
  # translation meta data space (:i18n) for a locale specific pluralization
17
- # 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
18
20
  # pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
19
21
  #
20
22
  # Pluralization rules are expected to respond to #call(count) and
21
- # return a pluralization key. Valid keys depend on the translation data
22
- # hash (entry) but it is generally recommended to follow CLDR's style,
23
- # 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
27
+ #
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.
24
34
  #
25
- # The :zero key is always picked directly when count equals 0 AND the
26
- # translation data has the key :zero. This way translators are free to
27
- # either pick a special :zero translation even for languages where the
28
- # pluralizer does not return a :zero key.
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
29
39
  def pluralize(locale, entry, count)
30
- return entry unless entry.is_a?(Hash) and count
40
+ return entry unless entry.is_a?(Hash) && count
31
41
 
32
42
  pluralizer = pluralizer(locale)
33
43
  if pluralizer.respond_to?(:call)
34
- key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
35
- raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
36
- 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
37
70
  else
38
71
  super
39
72
  end
@@ -41,13 +74,23 @@ module I18n
41
74
 
42
75
  protected
43
76
 
44
- def pluralizers
45
- @pluralizers ||= {}
46
- end
77
+ def pluralizers
78
+ @pluralizers ||= {}
79
+ end
47
80
 
48
- def pluralizer(locale)
49
- pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
50
- 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
51
94
  end
52
95
  end
53
96
  end