i18n 1.8.9 → 1.10.0

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: fd6aad7b79f41eae2970ab9570b3e141b479fcf748273925515c67d31efc1ef9
4
- data.tar.gz: 1ddd5c7df6a618e95514139007a6dca81237cf71226b81b2a1b6890746542e29
3
+ metadata.gz: bcce939890b82f2f78ef93aa98b2be5995f172f6c37b95c53800c155f2036eec
4
+ data.tar.gz: ae4d72446e698fc7c0666f0e9dad6f11fa669d768fe9272d0c89e14aba8d2274
5
5
  SHA512:
6
- metadata.gz: 4adc990bc7c22716e61e0185dccfbe789e50d88c05d5c21894ee6fbaec7b3f6bbf32ea3db29a977c0a586c858c2a6692248da8ca456a94ed5045727498220617
7
- data.tar.gz: b19ddacc5aaa10bc55ea15bfedf4d1964035045cedb117cfa87a1472a9c60205d9d23b721cc93f1b5f5de7b117440853bf7c7ea5d7b94b6352fb0c5b8afb83a2
6
+ metadata.gz: bf36e9389526d5a48bbfc4043bc2c4587ddbfbfab41a2ccaafa801570c3a838530a001c6c3df1669d5e0beb14b63b394253cef6d6fdd12aa936ae8c07ecdbf72
7
+ data.tar.gz: 82b972e13f2342ca864e4ead724008aa75ad24bbf6d4f14603330e2999501cfb5c05ee6a9e5cd2cffbf48f07f6da868fccc33b4771f7b4b085989c4def36a7b9
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.
@@ -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)
57
58
  if values
58
59
  entry = if deep_interpolation
59
60
  deep_interpolate(locale, entry, values)
@@ -153,6 +154,7 @@ module I18n
153
154
  end
154
155
  result unless result.is_a?(MissingTranslation)
155
156
  end
157
+ alias_method :resolve_entry, :resolve
156
158
 
157
159
  # Picks a translation from a pluralized mnemonic subkey according to English
158
160
  # pluralization rules :
@@ -223,24 +225,31 @@ module I18n
223
225
  def load_file(filename)
224
226
  type = File.extname(filename).tr('.', '').downcase
225
227
  raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
226
- data = send(:"load_#{type}", filename)
228
+ data, keys_symbolized = send(:"load_#{type}", filename)
227
229
  unless data.is_a?(Hash)
228
230
  raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
229
231
  end
230
- data.each { |locale, d| store_translations(locale, d || {}) }
232
+ data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
233
+
234
+ data
231
235
  end
232
236
 
233
237
  # Loads a plain Ruby translations file. eval'ing the file must yield
234
238
  # a Hash containing translation data with locales as toplevel keys.
235
239
  def load_rb(filename)
236
- eval(IO.read(filename), binding, filename)
240
+ translations = eval(IO.read(filename), binding, filename)
241
+ [translations, false]
237
242
  end
238
243
 
239
244
  # Loads a YAML translations file. The data must have locales as
240
245
  # toplevel keys.
241
246
  def load_yml(filename)
242
247
  begin
243
- YAML.load_file(filename)
248
+ if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
249
+ [YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
250
+ else
251
+ [YAML.load_file(filename), false]
252
+ end
244
253
  rescue TypeError, ScriptError, StandardError => e
245
254
  raise InvalidLocaleData.new(filename, e.inspect)
246
255
  end
@@ -251,7 +260,12 @@ module I18n
251
260
  # toplevel keys.
252
261
  def load_json(filename)
253
262
  begin
254
- ::JSON.parse(File.read(filename))
263
+ # Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
264
+ if ::JSON.respond_to?(:load_file)
265
+ [::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
266
+ else
267
+ [::JSON.parse(File.read(filename)), false]
268
+ end
255
269
  rescue TypeError, StandardError => e
256
270
  raise InvalidLocaleData.new(filename, e.inspect)
257
271
  end
@@ -17,8 +17,6 @@ module I18n
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
19
  class Chain
20
- using I18n::HashRefinements
21
-
22
20
  module Implementation
23
21
  include Base
24
22
 
@@ -55,7 +53,7 @@ module I18n
55
53
 
56
54
  def translate(locale, key, default_options = EMPTY_HASH)
57
55
  namespace = nil
58
- options = default_options.except(:default)
56
+ options = Utils.except(default_options, :default)
59
57
 
60
58
  backends.each do |backend|
61
59
  catch(:exception) do
@@ -101,7 +99,7 @@ module I18n
101
99
  init_translations unless initialized?
102
100
  translations
103
101
  end
104
- memo.deep_merge!(partial_translations) { |_, a, b| b || a }
102
+ Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a }
105
103
  end
106
104
  end
107
105
 
@@ -43,13 +43,13 @@ 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
50
50
  result = super(fallback, key, fallback_options)
51
51
  unless result.nil?
52
- on_fallback(locale, fallback, key, options) if locale != fallback
52
+ on_fallback(locale, fallback, key, options) if locale.to_s != fallback.to_s
53
53
  return result
54
54
  end
55
55
  end
@@ -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)}
@@ -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) }
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
@@ -19,8 +19,6 @@ 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
26
24
 
@@ -35,14 +33,13 @@ module I18n
35
33
  def store_translations(locale, data, options = EMPTY_HASH)
36
34
  if I18n.enforce_available_locales &&
37
35
  I18n.available_locales_initialized? &&
38
- !I18n.available_locales.include?(locale.to_sym) &&
39
- !I18n.available_locales.include?(locale.to_s)
36
+ !I18n.locale_available?(locale)
40
37
  return data
41
38
  end
42
39
  locale = locale.to_sym
43
40
  translations[locale] ||= Concurrent::Hash.new
44
- data = data.deep_symbolize_keys
45
- translations[locale].deep_merge!(data)
41
+ data = Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false)
42
+ Utils.deep_merge!(translations[locale], data)
46
43
  end
47
44
 
48
45
  # Get available locales from the translations hash
@@ -97,7 +94,7 @@ module I18n
97
94
  return nil unless result.has_key?(_key)
98
95
  end
99
96
  result = result[_key]
100
- result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
97
+ result = resolve_entry(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
101
98
  result
102
99
  end
103
100
  end
data/lib/i18n/backend.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  module I18n
4
4
  module Backend
5
5
  autoload :Base, 'i18n/backend/base'
6
- autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
7
6
  autoload :Cache, 'i18n/backend/cache'
8
7
  autoload :CacheFile, 'i18n/backend/cache_file'
9
8
  autoload :Cascade, 'i18n/backend/cascade'
@@ -11,7 +10,9 @@ module I18n
11
10
  autoload :Fallbacks, 'i18n/backend/fallbacks'
12
11
  autoload :Flatten, 'i18n/backend/flatten'
13
12
  autoload :Gettext, 'i18n/backend/gettext'
13
+ autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
14
14
  autoload :KeyValue, 'i18n/backend/key_value'
15
+ autoload :LazyLoadable, 'i18n/backend/lazy_loadable'
15
16
  autoload :Memoize, 'i18n/backend/memoize'
16
17
  autoload :Metadata, 'i18n/backend/metadata'
17
18
  autoload :Pluralization, 'i18n/backend/pluralization'
@@ -47,10 +47,12 @@ module I18n
47
47
 
48
48
  class MissingTranslation < ArgumentError
49
49
  module Base
50
+ PERMITTED_KEYS = [:scope].freeze
51
+
50
52
  attr_reader :locale, :key, :options
51
53
 
52
54
  def initialize(locale, key, options = EMPTY_HASH)
53
- @key, @locale, @options = key, locale, options.dup
55
+ @key, @locale, @options = key, locale, options.slice(*PERMITTED_KEYS)
54
56
  options.each { |k, v| self.options[k] = v.inspect if v.is_a?(Proc) }
55
57
  end
56
58
 
@@ -108,4 +110,38 @@ module I18n
108
110
  super "can not load translations from #{filename}, the file type #{type} is not known"
109
111
  end
110
112
  end
113
+
114
+ class UnsupportedMethod < ArgumentError
115
+ attr_reader :method, :backend_klass, :msg
116
+ def initialize(method, backend_klass, msg)
117
+ @method = method
118
+ @backend_klass = backend_klass
119
+ @msg = msg
120
+ super "#{backend_klass} does not support the ##{method} method. #{msg}"
121
+ end
122
+ end
123
+
124
+ class InvalidFilenames < ArgumentError
125
+ NUMBER_OF_ERRORS_SHOWN = 20
126
+ def initialize(file_errors)
127
+ super <<~MSG
128
+ Found #{file_errors.count} error(s).
129
+ The first #{[file_errors.count, NUMBER_OF_ERRORS_SHOWN].min} error(s):
130
+ #{file_errors.map(&:message).first(NUMBER_OF_ERRORS_SHOWN).join("\n")}
131
+
132
+ To use the LazyLoadable backend:
133
+ 1. Filenames must start with the locale.
134
+ 2. An underscore must separate the locale with any optional text that follows.
135
+ 3. The file must only contain translation data for the single locale.
136
+
137
+ Example:
138
+ "/config/locales/fr.yml" which contains:
139
+ ```yml
140
+ fr:
141
+ dog:
142
+ chien
143
+ ```
144
+ MSG
145
+ end
146
+ end
111
147
  end
@@ -5,7 +5,7 @@ module I18n
5
5
  DEFAULT_INTERPOLATION_PATTERNS = [
6
6
  /%%/,
7
7
  /%\{([\w|]+)\}/, # matches placeholders like "%{foo} or %{foo|word}"
8
- /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
8
+ /%<(\w+)>([^\d]*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
9
9
  ].freeze
10
10
  INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)
11
11
  deprecate_constant :INTERPOLATION_PATTERN
@@ -14,7 +14,7 @@ module I18n
14
14
  # Return String or raises MissingInterpolationArgument exception.
15
15
  # Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler.
16
16
  def interpolate(string, values)
17
- raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ RESERVED_KEYS_PATTERN
17
+ raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ I18n.reserved_keys_pattern
18
18
  raise ArgumentError.new('Interpolation values must be a Hash.') unless values.kind_of?(Hash)
19
19
  interpolate_hash(string, values)
20
20
  end
@@ -15,19 +15,12 @@
15
15
  # * all parent locales of a given locale (e.g. :es for :"es-MX") first,
16
16
  # * the current default locales and all of their parents second
17
17
  #
18
- # The default locales are set to [I18n.default_locale] by default but can be
19
- # set to something else.
18
+ # The default locales are set to [] by default but can be set to something else.
20
19
  #
21
20
  # One can additionally add any number of additional fallback locales manually.
22
21
  # These will be added before the default locales to the fallback chain. For
23
22
  # example:
24
23
  #
25
- # # using the default locale as default fallback locale
26
- #
27
- # I18n.default_locale = :"en-US"
28
- # I18n.fallbacks = I18n::Locale::Fallbacks.new(:"de-AT" => :"de-DE")
29
- # I18n.fallbacks[:"de-AT"] # => [:"de-AT", :de, :"de-DE"]
30
- #
31
24
  # # using a custom locale as default fallback locale
32
25
  #
33
26
  # I18n.fallbacks = I18n::Locale::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de)
@@ -46,7 +39,7 @@
46
39
  # fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en]
47
40
  # fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en]
48
41
  #
49
- # # people speaking Sami as spoken in Finnland also speak Swedish and Finnish as spoken in Finnland
42
+ # # people speaking Sami as spoken in Finland also speak Swedish and Finnish as spoken in Finland
50
43
  # fallbacks.map(:sms => [:"se-FI", :"fi-FI"])
51
44
  # fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en]
52
45
 
@@ -71,13 +64,18 @@ module I18n
71
64
  super || store(locale, compute(locale))
72
65
  end
73
66
 
74
- def map(mappings)
75
- mappings.each do |from, to|
76
- from, to = from.to_sym, Array(to)
77
- to.each do |_to|
78
- @map[from] ||= []
79
- @map[from] << _to.to_sym
67
+ def map(*args, &block)
68
+ if args.count == 1 && !block_given?
69
+ mappings = args.first
70
+ mappings.each do |from, to|
71
+ from, to = from.to_sym, Array(to)
72
+ to.each do |_to|
73
+ @map[from] ||= []
74
+ @map[from] << _to.to_sym
75
+ end
80
76
  end
77
+ else
78
+ @map.map(*args, &block)
81
79
  end
82
80
  end
83
81
 
@@ -5,12 +5,11 @@ module I18n
5
5
  I18n.available_locales = nil
6
6
  end
7
7
 
8
- test "available_locales returns the locales stored to the backend by default" do
8
+ test "available_locales returns the available_locales produced by the backend, by default" do
9
9
  I18n.backend.store_translations('de', :foo => 'bar')
10
10
  I18n.backend.store_translations('en', :foo => 'foo')
11
11
 
12
- assert I18n.available_locales.include?(:de)
13
- assert I18n.available_locales.include?(:en)
12
+ assert_equal I18n.available_locales, I18n.backend.available_locales
14
13
  end
15
14
 
16
15
  test "available_locales can be set to something else independently from the actual locale data" do
@@ -24,8 +23,7 @@ module I18n
24
23
  assert_equal [:foo, :bar], I18n.available_locales
25
24
 
26
25
  I18n.available_locales = nil
27
- assert I18n.available_locales.include?(:de)
28
- assert I18n.available_locales.include?(:en)
26
+ assert_equal I18n.available_locales, I18n.backend.available_locales
29
27
  end
30
28
 
31
29
  test "available_locales memoizes when set explicitely" do
@@ -37,7 +37,7 @@ module I18n
37
37
  end
38
38
 
39
39
  test "defaults: given an array of missing keys it raises a MissingTranslationData exception" do
40
- assert_raise I18n::MissingTranslationData do
40
+ assert_raises I18n::MissingTranslationData do
41
41
  I18n.t(:does_not_exist, :default => [:does_not_exist_2, :does_not_exist_3], :raise => true)
42
42
  end
43
43
  end
@@ -41,7 +41,7 @@ module I18n
41
41
  end
42
42
 
43
43
  test "interpolation: given values but missing a key it raises I18n::MissingInterpolationArgument" do
44
- assert_raise(I18n::MissingInterpolationArgument) do
44
+ assert_raises(I18n::MissingInterpolationArgument) do
45
45
  interpolate(:default => '%{foo}', :bar => 'bar')
46
46
  end
47
47
  end
@@ -77,13 +77,13 @@ module I18n
77
77
 
78
78
  if Object.const_defined?(:Encoding)
79
79
  test "interpolation: given a euc-jp translation and a utf-8 value it raises Encoding::CompatibilityError" do
80
- assert_raise(Encoding::CompatibilityError) do
80
+ assert_raises(Encoding::CompatibilityError) do
81
81
  interpolate(:default => euc_jp('こんにちは、%{name}さん!'), :name => 'ゆきひろ')
82
82
  end
83
83
  end
84
84
 
85
85
  test "interpolation: given a utf-8 translation and a euc-jp value it raises Encoding::CompatibilityError" do
86
- assert_raise(Encoding::CompatibilityError) do
86
+ assert_raises(Encoding::CompatibilityError) do
87
87
  interpolate(:default => 'こんにちは、%{name}さん!', :name => euc_jp('ゆきひろ'))
88
88
  end
89
89
  end
@@ -108,10 +108,10 @@ module I18n
108
108
  end
109
109
 
110
110
  test "interpolation: given a translations containing a reserved key it raises I18n::ReservedInterpolationKey" do
111
- assert_raise(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{exception_handler}') }
112
- assert_raise(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{default}') }
113
- assert_raise(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{separator}') }
114
- assert_raise(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{scope}') }
111
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{exception_handler}') }
112
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{default}') }
113
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{separator}') }
114
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{scope}') }
115
115
  end
116
116
 
117
117
  test "interpolation: deep interpolation for default string" do
@@ -78,15 +78,15 @@ module I18n
78
78
  end
79
79
 
80
80
  test "localize Date: given nil it raises I18n::ArgumentError" do
81
- assert_raise(I18n::ArgumentError) { I18n.l(nil) }
81
+ assert_raises(I18n::ArgumentError) { I18n.l(nil) }
82
82
  end
83
83
 
84
84
  test "localize Date: given a plain Object it raises I18n::ArgumentError" do
85
- assert_raise(I18n::ArgumentError) { I18n.l(Object.new) }
85
+ assert_raises(I18n::ArgumentError) { I18n.l(Object.new) }
86
86
  end
87
87
 
88
88
  test "localize Date: given a format is missing it raises I18n::MissingTranslationData" do
89
- assert_raise(I18n::MissingTranslationData) { I18n.l(@date, :format => :missing) }
89
+ assert_raises(I18n::MissingTranslationData) { I18n.l(@date, :format => :missing) }
90
90
  end
91
91
 
92
92
  test "localize Date: it does not alter the format string" do
@@ -78,7 +78,7 @@ module I18n
78
78
  end
79
79
 
80
80
  test "localize DateTime: given a format is missing it raises I18n::MissingTranslationData" do
81
- assert_raise(I18n::MissingTranslationData) { I18n.l(@datetime, :format => :missing) }
81
+ assert_raises(I18n::MissingTranslationData) { I18n.l(@datetime, :format => :missing) }
82
82
  end
83
83
 
84
84
  protected
@@ -74,6 +74,7 @@ module I18n
74
74
  arg.strftime('%a, %d %b %Y')
75
75
  when Hash
76
76
  arg.delete(:fallback_in_progress)
77
+ arg.delete(:fallback_original_locale)
77
78
  arg.inspect
78
79
  else
79
80
  arg.inspect
@@ -79,7 +79,7 @@ module I18n
79
79
  end
80
80
 
81
81
  test "localize Time: given a format is missing it raises I18n::MissingTranslationData" do
82
- assert_raise(I18n::MissingTranslationData) { I18n.l(@time, :format => :missing) }
82
+ assert_raises(I18n::MissingTranslationData) { I18n.l(@time, :format => :missing) }
83
83
  end
84
84
 
85
85
  protected
@@ -34,7 +34,7 @@ module I18n
34
34
  end
35
35
 
36
36
  test "lookup: given a missing key, no default and the raise option it raises MissingTranslationData" do
37
- assert_raise(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) }
37
+ assert_raises(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) }
38
38
  end
39
39
 
40
40
  test "lookup: does not raise an exception if no translation data is present for the given locale" do
@@ -61,7 +61,7 @@ module I18n
61
61
  # In fact it probably *should* fail but Rails currently relies on using the default locale instead.
62
62
  # So we'll stick to this for now until we get it fixed in Rails.
63
63
  test "lookup: given nil as a locale it does not raise but use the default locale" do
64
- # assert_raise(I18n::InvalidLocale) { I18n.t(:bar, :locale => nil) }
64
+ # assert_raises(I18n::InvalidLocale) { I18n.t(:bar, :locale => nil) }
65
65
  assert_nothing_raised { I18n.t(:bar, :locale => nil) }
66
66
  end
67
67
 
@@ -28,7 +28,7 @@ module I18n
28
28
  end
29
29
 
30
30
  test "pluralization: given incomplete pluralization data it raises I18n::InvalidPluralizationData" do
31
- assert_raise(I18n::InvalidPluralizationData) { I18n.t(:default => { :one => 'bar' }, :count => 2) }
31
+ assert_raises(I18n::InvalidPluralizationData) { I18n.t(:default => { :one => 'bar' }, :count => 2) }
32
32
  end
33
33
  end
34
34
  end
@@ -53,7 +53,13 @@ module I18n
53
53
 
54
54
 
55
55
  def self.filter_args(*args)
56
- args.map {|arg| arg.delete(:fallback_in_progress) if arg.is_a?(Hash) ; arg }.inspect
56
+ args.map do |arg|
57
+ if arg.is_a?(Hash)
58
+ arg.delete(:fallback_in_progress)
59
+ arg.delete(:fallback_original_locale)
60
+ end
61
+ arg
62
+ end.inspect
57
63
  end
58
64
  end
59
65
  end
data/lib/i18n/utils.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module Utils
5
+ class << self
6
+ if Hash.method_defined?(:except)
7
+ def except(hash, *keys)
8
+ hash.except(*keys)
9
+ end
10
+ else
11
+ def except(hash, *keys)
12
+ hash = hash.dup
13
+ keys.each { |k| hash.delete(k) }
14
+ hash
15
+ end
16
+ end
17
+
18
+ def deep_merge(hash, other_hash, &block)
19
+ deep_merge!(hash.dup, other_hash, &block)
20
+ end
21
+
22
+ def deep_merge!(hash, other_hash, &block)
23
+ hash.merge!(other_hash) do |key, this_val, other_val|
24
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
25
+ deep_merge(this_val, other_val, &block)
26
+ elsif block_given?
27
+ yield key, this_val, other_val
28
+ else
29
+ other_val
30
+ end
31
+ end
32
+ end
33
+
34
+ def deep_symbolize_keys(hash)
35
+ hash.each_with_object({}) do |(key, value), result|
36
+ result[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize_keys_in_object(value)
37
+ result
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def deep_symbolize_keys_in_object(value)
44
+ case value
45
+ when Hash
46
+ deep_symbolize_keys(value)
47
+ when Array
48
+ value.map { |e| deep_symbolize_keys_in_object(e) }
49
+ else
50
+ value
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/i18n/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18n
4
- VERSION = "1.8.9"
4
+ VERSION = "1.10.0"
5
5
  end
data/lib/i18n.rb CHANGED
@@ -4,6 +4,7 @@ require 'concurrent/map'
4
4
  require 'concurrent/hash'
5
5
 
6
6
  require 'i18n/version'
7
+ require 'i18n/utils'
7
8
  require 'i18n/exceptions'
8
9
  require 'i18n/interpolate/ruby'
9
10
 
@@ -22,6 +23,7 @@ module I18n
22
23
  exception_handler
23
24
  fallback
24
25
  fallback_in_progress
26
+ fallback_original_locale
25
27
  format
26
28
  object
27
29
  raise
@@ -29,14 +31,26 @@ module I18n
29
31
  scope
30
32
  separator
31
33
  throw
32
- ].freeze
33
- RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/
34
+ ]
34
35
  EMPTY_HASH = {}.freeze
35
36
 
36
37
  def self.new_double_nested_cache # :nodoc:
37
38
  Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
38
39
  end
39
40
 
41
+ # Marks a key as reserved. Reserved keys are used internally,
42
+ # and can't also be used for interpolation. If you are using any
43
+ # extra keys as I18n options, you should call I18n.reserve_key
44
+ # before any I18n.translate (etc) calls are made.
45
+ def self.reserve_key(key)
46
+ RESERVED_KEYS << key.to_sym
47
+ @reserved_keys_pattern = nil
48
+ end
49
+
50
+ def self.reserved_keys_pattern # :nodoc:
51
+ @reserved_keys_pattern ||= /%\{(#{RESERVED_KEYS.join("|")})\}/
52
+ end
53
+
40
54
  module Base
41
55
  # Gets I18n configuration object.
42
56
  def config
@@ -260,14 +274,14 @@ module I18n
260
274
  #
261
275
  # Setting a Hash using Ruby:
262
276
  #
263
- # store_translations(:de, :i18n => {
264
- # :transliterate => {
265
- # :rule => {
266
- # "ü" => "ue",
267
- # "ö" => "oe"
268
- # }
269
- # }
270
- # )
277
+ # store_translations(:de, i18n: {
278
+ # transliterate: {
279
+ # rule: {
280
+ # 'ü' => 'ue',
281
+ # 'ö' => 'oe'
282
+ # }
283
+ # }
284
+ # })
271
285
  #
272
286
  # Setting a Proc:
273
287
  #
@@ -324,11 +338,11 @@ module I18n
324
338
  def normalize_keys(locale, key, scope, separator = nil)
325
339
  separator ||= I18n.default_separator
326
340
 
327
- keys = []
328
- keys.concat normalize_key(locale, separator)
329
- keys.concat normalize_key(scope, separator)
330
- keys.concat normalize_key(key, separator)
331
- keys
341
+ [
342
+ *normalize_key(locale, separator),
343
+ *normalize_key(scope, separator),
344
+ *normalize_key(key, separator)
345
+ ]
332
346
  end
333
347
 
334
348
  # Returns true when the passed locale, which can be either a String or a
@@ -396,7 +410,7 @@ module I18n
396
410
  keys.delete('')
397
411
  keys.map! do |k|
398
412
  case k
399
- when /\A[-+]?[1-9]\d*\z/ # integer
413
+ when /\A[-+]?([1-9]\d*|0)\z/ # integer
400
414
  k.to_i
401
415
  when 'true'
402
416
  true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.9
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Fuchs
@@ -13,15 +13,15 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2021-02-12 00:00:00.000000000 Z
16
+ date: 2022-02-14 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
+ name: concurrent-ruby
19
20
  requirement: !ruby/object:Gem::Requirement
20
21
  requirements:
21
22
  - - "~>"
22
23
  - !ruby/object:Gem::Version
23
24
  version: '1.0'
24
- name: concurrent-ruby
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,13 +49,13 @@ files:
49
49
  - lib/i18n/backend/gettext.rb
50
50
  - lib/i18n/backend/interpolation_compiler.rb
51
51
  - lib/i18n/backend/key_value.rb
52
+ - lib/i18n/backend/lazy_loadable.rb
52
53
  - lib/i18n/backend/memoize.rb
53
54
  - lib/i18n/backend/metadata.rb
54
55
  - lib/i18n/backend/pluralization.rb
55
56
  - lib/i18n/backend/simple.rb
56
57
  - lib/i18n/backend/transliterator.rb
57
58
  - lib/i18n/config.rb
58
- - lib/i18n/core_ext/hash.rb
59
59
  - lib/i18n/exceptions.rb
60
60
  - lib/i18n/gettext.rb
61
61
  - lib/i18n/gettext/helpers.rb
@@ -81,6 +81,7 @@ files:
81
81
  - lib/i18n/tests/lookup.rb
82
82
  - lib/i18n/tests/pluralization.rb
83
83
  - lib/i18n/tests/procs.rb
84
+ - lib/i18n/utils.rb
84
85
  - lib/i18n/version.rb
85
86
  homepage: https://github.com/ruby-i18n/i18n
86
87
  licenses:
@@ -105,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
106
  - !ruby/object:Gem::Version
106
107
  version: 1.3.5
107
108
  requirements: []
108
- rubygems_version: 3.0.6
109
+ rubygems_version: 3.1.6
109
110
  signing_key:
110
111
  specification_version: 4
111
112
  summary: New wave Internationalization support for Ruby
@@ -1,59 +0,0 @@
1
- module I18n
2
- module HashRefinements
3
- refine Hash do
4
- using I18n::HashRefinements
5
- def except(*keys)
6
- dup.except!(*keys)
7
- end unless method_defined?(:except)
8
-
9
- def except!(*keys)
10
- keys.each { |key| delete(key) }
11
- self
12
- end
13
-
14
- def deep_symbolize_keys
15
- each_with_object({}) do |(key, value), result|
16
- result[symbolize_key(key)] = deep_symbolize_keys_in_object(value)
17
- result
18
- end
19
- end
20
-
21
- # deep_merge from activesupport 5
22
- # Copyright (c) 2005-2019 David Heinemeier Hansson
23
- def deep_merge(other_hash, &block)
24
- dup.deep_merge!(other_hash, &block)
25
- end
26
-
27
- # deep_merge! from activesupport 5
28
- # Copyright (c) 2005-2019 David Heinemeier Hansson
29
- def deep_merge!(other_hash, &block)
30
- merge!(other_hash) do |key, this_val, other_val|
31
- if this_val.is_a?(Hash) && other_val.is_a?(Hash)
32
- this_val.deep_merge(other_val, &block)
33
- elsif block_given?
34
- block.call(key, this_val, other_val)
35
- else
36
- other_val
37
- end
38
- end
39
- end
40
-
41
- def symbolize_key(key)
42
- key.respond_to?(:to_sym) ? key.to_sym : key
43
- end
44
-
45
- private
46
-
47
- def deep_symbolize_keys_in_object(value)
48
- case value
49
- when Hash
50
- value.deep_symbolize_keys
51
- when Array
52
- value.map { |e| deep_symbolize_keys_in_object(e) }
53
- else
54
- value
55
- end
56
- end
57
- end
58
- end
59
- end