i18n 1.13.0 → 1.14.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e85066f2cf6bbf170ff946391c6037d0335e5314d0c1e0723e980d5ee7df7af0
4
- data.tar.gz: 2a4629aa0644c2a6e8cc5d175d4912b5f5cd4abc066dd5ee196270b7a3eadd08
3
+ metadata.gz: edf1a48f0dd110066260f4d93cd2af16e73e9f91845a16126794d4c9af5b664c
4
+ data.tar.gz: 36a488fcbbaefdabb42e1a6f42af534fa36f640de5674d31a92407d6dfd1835d
5
5
  SHA512:
6
- metadata.gz: '01326189eb7b675594bd63c802ae9470135e9aab222de4df1429c99c585b9b59b172ed62386c03b82fd45c862bbb42484dc89e0289c83970315f9a5fa03ba91c'
7
- data.tar.gz: 290489ae277759fe65197e65d6486b7cca67605c2c98197a472c91bb2496d9db168dfd2082706b87f6f806051827b8df12cc0037258f3bf10d5f74a9ddb9f575
6
+ metadata.gz: 82897d5266dfb62379930d6f94346dc0e62c14e619995e3933dc6339d0dd6d06d4b491449d1f4eb43128de995e3d9837624e4132b7b506455e0169dece108b7f
7
+ data.tar.gz: 8938b0f8d862e989f7427ccb2299722914bd485af9afff24bf0bda6cd8f438dc750cacd8c85d45292787f2e78438ead7c7533d6c696f5d5b9fc7109535a48ee4
data/README.md CHANGED
@@ -13,10 +13,14 @@ Currently maintained by @radar.
13
13
 
14
14
  You will most commonly use this library within a Rails app.
15
15
 
16
+ We support Rails versions from 6.0 and up.
17
+
16
18
  [See the Rails Guide](https://guides.rubyonrails.org/i18n.html) for an example of its usage.
17
19
 
18
20
  ### Ruby (without Rails)
19
21
 
22
+ We support Ruby versions from 3.0 and up.
23
+
20
24
  If you want to use this library without Rails, you can simply add `i18n` to your `Gemfile`:
21
25
 
22
26
  ```ruby
@@ -54,13 +54,16 @@ module I18n
54
54
  end
55
55
 
56
56
  deep_interpolation = options[:deep_interpolation]
57
+ skip_interpolation = options[:skip_interpolation]
57
58
  values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
58
- if values
59
+ if !skip_interpolation && values && !values.empty?
59
60
  entry = if deep_interpolation
60
61
  deep_interpolate(locale, entry, values)
61
62
  else
62
63
  interpolate(locale, entry, values)
63
64
  end
65
+ elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
66
+ raise ReservedInterpolationKey.new($1.to_sym, entry)
64
67
  end
65
68
  entry
66
69
  end
@@ -149,7 +152,14 @@ module I18n
149
152
  result = catch(:exception) do
150
153
  case subject
151
154
  when Symbol
152
- I18n.translate(subject, **options.merge(:locale => locale, :throw => true))
155
+ I18n.translate(
156
+ subject,
157
+ **options.merge(
158
+ :locale => locale,
159
+ :throw => true,
160
+ :skip_interpolation => true
161
+ )
162
+ )
153
163
  when Proc
154
164
  date_or_time = options.delete(:object) || object
155
165
  resolve(locale, object, subject.call(date_or_time, **options))
@@ -186,8 +196,8 @@ module I18n
186
196
  #
187
197
  # if the given subject is an array then:
188
198
  # each element of the array is recursively interpolated (until it finds a string)
189
- # method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
190
- # # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
199
+ # method interpolates ["yes, %{user}", ["maybe no, %{user}", "no, %{user}"]], :user => "bartuz"
200
+ # # => ["yes, bartuz", ["maybe no, bartuz", "no, bartuz"]]
191
201
  def interpolate(locale, subject, values = EMPTY_HASH)
192
202
  return subject if values.empty?
193
203
 
@@ -242,7 +252,7 @@ module I18n
242
252
  # Loads a plain Ruby translations file. eval'ing the file must yield
243
253
  # a Hash containing translation data with locales as toplevel keys.
244
254
  def load_rb(filename)
245
- translations = eval(IO.read(filename), binding, filename)
255
+ translations = eval(IO.read(filename), binding, filename.to_s)
246
256
  [translations, false]
247
257
  end
248
258
 
@@ -16,6 +16,8 @@ module I18n
16
16
  #
17
17
  # The implementation assumes that all backends added to the Chain implement
18
18
  # a lookup method with the same API as Simple backend does.
19
+ #
20
+ # Fallback translations using the :default option are only used by the last backend of a chain.
19
21
  class Chain
20
22
  module Implementation
21
23
  include Base
@@ -71,7 +71,11 @@ module I18n
71
71
 
72
72
  case subject
73
73
  when Symbol
74
- I18n.translate(subject, **options.merge(:locale => options[:fallback_original_locale], :throw => true))
74
+ I18n.translate(subject, **options.merge(
75
+ :locale => options[:fallback_original_locale],
76
+ :throw => true,
77
+ :skip_interpolation => true
78
+ ))
75
79
  when Proc
76
80
  date_or_time = options.delete(:object) || object
77
81
  resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
@@ -95,7 +99,7 @@ module I18n
95
99
  return super unless options.fetch(:fallback, true)
96
100
  I18n.fallbacks[locale].each do |fallback|
97
101
  begin
98
- return true if super(fallback, key)
102
+ return true if super(fallback, key, options)
99
103
  rescue I18n::InvalidLocale
100
104
  # we do nothing when the locale is invalid, as this is a fallback anyways.
101
105
  end
@@ -21,11 +21,11 @@ module I18n
21
21
  module Compiler
22
22
  extend self
23
23
 
24
- TOKENIZER = /(%%\{[^\}]+\}|%\{[^\}]+\})/
25
- INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/
24
+ TOKENIZER = /(%%?\{[^}]+\})/
26
25
 
27
26
  def compile_if_an_interpolation(string)
28
27
  if interpolated_str?(string)
28
+ string = +string
29
29
  string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
30
30
  def i18n_interpolate(v = {})
31
31
  "#{compiled_interpolation_body(string)}"
@@ -37,7 +37,7 @@ module I18n
37
37
  end
38
38
 
39
39
  def interpolated_str?(str)
40
- str.kind_of?(::String) && str =~ INTERPOLATION_SYNTAX_PATTERN
40
+ str.kind_of?(::String) && str =~ TOKENIZER
41
41
  end
42
42
 
43
43
  protected
@@ -48,13 +48,12 @@ module I18n
48
48
 
49
49
  def compiled_interpolation_body(str)
50
50
  tokenize(str).map do |token|
51
- (matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
51
+ token.match(TOKENIZER) ? handle_interpolation_token(token) : escape_plain_str(token)
52
52
  end.join
53
53
  end
54
54
 
55
- def handle_interpolation_token(interpolation, matchdata)
56
- escaped, pattern, key = matchdata.values_at(1, 2, 3)
57
- escaped ? pattern : compile_interpolation_token(key.to_sym)
55
+ def handle_interpolation_token(token)
56
+ token.start_with?('%%') ? token[1..] : compile_interpolation_token(token[2..-2])
58
57
  end
59
58
 
60
59
  def compile_interpolation_token(key)
@@ -98,7 +98,7 @@ module I18n
98
98
  # Parse the load path and extract all locales.
99
99
  def available_locales
100
100
  if lazy_load?
101
- I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }
101
+ I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }.uniq
102
102
  else
103
103
  super
104
104
  end
@@ -10,14 +10,14 @@ module I18n
10
10
  # The implementation is provided by a Implementation module allowing to easily
11
11
  # extend Simple backend's behavior by including modules. E.g.:
12
12
  #
13
- # module I18n::Backend::Pluralization
14
- # def pluralize(*args)
15
- # # extended pluralization logic
16
- # super
17
- # end
18
- # end
19
- #
20
- # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
13
+ # module I18n::Backend::Pluralization
14
+ # def pluralize(*args)
15
+ # # extended pluralization logic
16
+ # super
17
+ # end
18
+ # end
19
+ #
20
+ # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
21
21
  class Simple
22
22
  module Implementation
23
23
  include Base
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi'
4
-
5
3
  module I18n
6
4
  class ExceptionHandler
7
5
  def call(exception, _locale, _key, _options)
@@ -47,7 +45,7 @@ module I18n
47
45
 
48
46
  class MissingTranslation < ArgumentError
49
47
  module Base
50
- PERMITTED_KEYS = [:scope].freeze
48
+ PERMITTED_KEYS = [:scope, :default].freeze
51
49
 
52
50
  attr_reader :locale, :key, :options
53
51
 
@@ -63,8 +61,18 @@ module I18n
63
61
  end
64
62
 
65
63
  def message
66
- "translation missing: #{keys.join('.')}"
64
+ if (default = options[:default]).is_a?(Array) && default.any?
65
+ other_options = ([key, *default]).map { |k| normalized_option(k).prepend('- ') }.join("\n")
66
+ "Translation missing. Options considered were:\n#{other_options}"
67
+ else
68
+ "Translation missing: #{keys.join('.')}"
69
+ end
67
70
  end
71
+
72
+ def normalized_option(key)
73
+ I18n.normalize_keys(locale, key, options[:scope]).join('.')
74
+ end
75
+
68
76
  alias :to_s :message
69
77
 
70
78
  def to_exception
@@ -79,14 +79,24 @@ module I18n
79
79
  end
80
80
  end
81
81
 
82
+ def empty?
83
+ @map.empty? && @defaults.empty?
84
+ end
85
+
86
+ def inspect
87
+ "#<#{self.class.name} @map=#{@map.inspect} @defaults=#{@defaults.inspect}>"
88
+ end
89
+
82
90
  protected
83
91
 
84
92
  def compute(tags, include_defaults = true, exclude = [])
85
- result = Array(tags).flat_map do |tag|
93
+ result = []
94
+ Array(tags).each do |tag|
86
95
  tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude
87
- tags.each { |_tag| tags += compute(@map[_tag], false, exclude + tags) if @map[_tag] }
88
- tags
96
+ result += tags
97
+ tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] }
89
98
  end
99
+
90
100
  result.push(*defaults) if include_defaults
91
101
  result.uniq!
92
102
  result.compact!
@@ -10,7 +10,7 @@ module I18n
10
10
  def call(env)
11
11
  @app.call(env)
12
12
  ensure
13
- Thread.current[:i18n_config] = I18n::Config.new
13
+ Thread.current.thread_variable_set(:i18n_config, I18n::Config.new)
14
14
  end
15
15
 
16
16
  end
@@ -47,6 +47,13 @@ module I18n
47
47
  I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' })
48
48
  assert_equal 'bar', I18n.t(nil, :default => :'foo|bar', :separator => '|')
49
49
  end
50
+
51
+ # Addresses issue: #599
52
+ test "defaults: only interpolates once when resolving defaults" do
53
+ I18n.backend.store_translations(:en, :greeting => 'hey %{name}')
54
+ assert_equal 'hey %{dont_interpolate_me}',
55
+ I18n.t(:does_not_exist, :name => '%{dont_interpolate_me}', default: [:greeting])
56
+ end
50
57
  end
51
58
  end
52
59
  end
@@ -89,14 +89,14 @@ module I18n
89
89
  end
90
90
 
91
91
  test "interpolation: ASCII strings in the backend should be encoded to UTF8 if interpolation options are in UTF8" do
92
- I18n.backend.store_translations 'en', 'encoding' => ('%{who} let me go'.force_encoding("ASCII"))
92
+ I18n.backend.store_translations 'en', 'encoding' => ('%{who} let me go'.dup.force_encoding(Encoding::US_ASCII))
93
93
  result = I18n.t 'encoding', :who => "måmmå miå"
94
94
  assert_equal Encoding::UTF_8, result.encoding
95
95
  end
96
96
 
97
97
  test "interpolation: UTF8 strings in the backend are still returned as UTF8 with ASCII interpolation" do
98
98
  I18n.backend.store_translations 'en', 'encoding' => 'måmmå miå %{what}'
99
- result = I18n.t 'encoding', :what => 'let me go'.force_encoding("ASCII")
99
+ result = I18n.t 'encoding', :what => 'let me go'.dup.force_encoding(Encoding::US_ASCII)
100
100
  assert_equal Encoding::UTF_8, result.encoding
101
101
  end
102
102
 
@@ -112,6 +112,28 @@ module I18n
112
112
  assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{default}') }
113
113
  assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{separator}') }
114
114
  assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{scope}') }
115
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:default => '%{scope}') }
116
+
117
+ I18n.backend.store_translations(:en, :interpolate => 'Hi %{scope}!')
118
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:interpolate) }
119
+ end
120
+
121
+ test "interpolation: it does not raise I18n::ReservedInterpolationKey for escaped variables" do
122
+ assert_nothing_raised do
123
+ assert_equal '%{separator}', interpolate(:foo => :bar, :default => '%%{separator}')
124
+ end
125
+
126
+ # Note: The two interpolations below do not remove the escape character (%) because
127
+ # I18n should not alter the strings when no interpolation parameters are given,
128
+ # see the comment at the top of this file.
129
+ assert_nothing_raised do
130
+ assert_equal '%%{scope}', interpolate(:default => '%%{scope}')
131
+ end
132
+
133
+ I18n.backend.store_translations(:en, :interpolate => 'Hi %%{scope}!')
134
+ assert_nothing_raised do
135
+ assert_equal 'Hi %%{scope}!', interpolate(:interpolate)
136
+ end
115
137
  end
116
138
 
117
139
  test "interpolation: deep interpolation for default string" do
@@ -150,7 +172,7 @@ module I18n
150
172
  end
151
173
 
152
174
  def euc_jp(string)
153
- string.encode!(Encoding::EUC_JP)
175
+ string.encode(Encoding::EUC_JP)
154
176
  end
155
177
 
156
178
  def interpolate(*args)
@@ -64,7 +64,7 @@ module I18n
64
64
  end
65
65
 
66
66
  test "localize Date: given missing translations it returns the correct error message" do
67
- assert_equal 'translation missing: fr.date.abbr_month_names', I18n.l(@date, :format => '%b', :locale => :fr)
67
+ assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@date, :format => '%b', :locale => :fr)
68
68
  end
69
69
 
70
70
  test "localize Date: given an unknown format it does not fail" do
@@ -60,7 +60,7 @@ module I18n
60
60
  end
61
61
 
62
62
  test "localize DateTime: given missing translations it returns the correct error message" do
63
- assert_equal 'translation missing: fr.date.abbr_month_names', I18n.l(@datetime, :format => '%b', :locale => :fr)
63
+ assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@datetime, :format => '%b', :locale => :fr)
64
64
  end
65
65
 
66
66
  test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator" do
@@ -34,7 +34,7 @@ module I18n
34
34
  test "localize Date: given a format that resolves to a Proc it calls the Proc with the object and extra options" do
35
35
  setup_time_proc_translations
36
36
  date = ::Date.new(2008, 3, 1)
37
- assert_equal '[Sat, 01 Mar 2008, {:foo=>"foo"}]', I18n.l(date, :format => :proc, :foo => 'foo', :locale => :ru)
37
+ assert_equal %|[Sat, 01 Mar 2008, #{{:foo=>"foo"}}]|, I18n.l(date, :format => :proc, :foo => 'foo', :locale => :ru)
38
38
  end
39
39
 
40
40
  test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object" do
@@ -46,7 +46,7 @@ module I18n
46
46
  test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object and extra options" do
47
47
  setup_time_proc_translations
48
48
  datetime = ::DateTime.new(2008, 3, 1, 6)
49
- assert_equal '[Sat, 01 Mar 2008 06:00:00 +00:00, {:foo=>"foo"}]', I18n.l(datetime, :format => :proc, :foo => 'foo', :locale => :ru)
49
+ assert_equal %|[Sat, 01 Mar 2008 06:00:00 +00:00, #{{:foo=>"foo"}}]|, I18n.l(datetime, :format => :proc, :foo => 'foo', :locale => :ru)
50
50
  end
51
51
 
52
52
  test "localize Time: given a format that resolves to a Proc it calls the Proc with the object" do
@@ -61,7 +61,7 @@ module I18n
61
61
  end
62
62
 
63
63
  test "localize Time: given missing translations it returns the correct error message" do
64
- assert_equal 'translation missing: fr.date.abbr_month_names', I18n.l(@time, :format => '%b', :locale => :fr)
64
+ assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@time, :format => '%b', :locale => :fr)
65
65
  end
66
66
 
67
67
  test "localize Time: given a meridian indicator format it returns the correct meridian indicator" do
@@ -30,7 +30,7 @@ module I18n
30
30
  end
31
31
 
32
32
  test "lookup: given a missing key, no default and no raise option it returns an error message" do
33
- assert_equal "translation missing: en.missing", I18n.t(:missing)
33
+ assert_equal "Translation missing: en.missing", I18n.t(:missing)
34
34
  end
35
35
 
36
36
  test "lookup: given a missing key, no default and the raise option it raises MissingTranslationData" do
@@ -76,6 +76,12 @@ module I18n
76
76
  test "lookup: a resulting Hash is not frozen" do
77
77
  assert !I18n.t(:hash).frozen?
78
78
  end
79
+
80
+ # Addresses issue: #599
81
+ test "lookup: only interpolates once when resolving symbols" do
82
+ I18n.backend.store_translations(:en, foo: :bar, bar: '%{value}')
83
+ assert_equal '%{dont_interpolate_me}', I18n.t(:foo, value: '%{dont_interpolate_me}')
84
+ end
79
85
  end
80
86
  end
81
87
  end
@@ -5,34 +5,38 @@ module I18n
5
5
  module Procs
6
6
  test "lookup: given a translation is a proc it calls the proc with the key and interpolation values" do
7
7
  I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| I18n::Tests::Procs.filter_args(*args) })
8
- assert_equal '[:a_lambda, {:foo=>"foo"}]', I18n.t(:a_lambda, :foo => 'foo')
8
+ assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(:a_lambda, :foo => 'foo')
9
9
  end
10
10
 
11
11
  test "lookup: given a translation is a proc it passes the interpolation values as keyword arguments" do
12
12
  I18n.backend.store_translations(:en, :a_lambda => lambda { |key, foo:, **| I18n::Tests::Procs.filter_args(key, foo: foo) })
13
- assert_equal '[:a_lambda, {:foo=>"foo"}]', I18n.t(:a_lambda, :foo => 'foo')
13
+ assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(:a_lambda, :foo => 'foo')
14
14
  end
15
15
 
16
16
  test "defaults: given a default is a Proc it calls it with the key and interpolation values" do
17
17
  proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
18
- assert_equal '[nil, {:foo=>"foo"}]', I18n.t(nil, :default => proc, :foo => 'foo')
18
+ assert_equal %|[nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo')
19
19
  end
20
20
 
21
21
  test "defaults: given a default is a key that resolves to a Proc it calls it with the key and interpolation values" do
22
22
  the_lambda = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
23
23
  I18n.backend.store_translations(:en, :a_lambda => the_lambda)
24
- assert_equal '[:a_lambda, {:foo=>"foo"}]', I18n.t(nil, :default => :a_lambda, :foo => 'foo')
25
- assert_equal '[:a_lambda, {:foo=>"foo"}]', I18n.t(nil, :default => [nil, :a_lambda], :foo => 'foo')
24
+ assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => :a_lambda, :foo => 'foo')
25
+ assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => [nil, :a_lambda], :foo => 'foo')
26
26
  end
27
27
 
28
28
  test "interpolation: given an interpolation value is a lambda it calls it with key and values before interpolating it" do
29
29
  proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
30
- assert_match %r(\[\{:foo=>#<Proc.*>\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc)
30
+ if RUBY_VERSION < "3.4"
31
+ assert_match %r(\[\{:foo=>#<Proc.*>\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc)
32
+ else
33
+ assert_match %r(\[\{foo: #<Proc.*>\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc)
34
+ end
31
35
  end
32
36
 
33
37
  test "interpolation: given a key resolves to a Proc that returns a string then interpolation still works" do
34
38
  proc = lambda { |*args| "%{foo}: " + I18n::Tests::Procs.filter_args(*args) }
35
- assert_equal 'foo: [nil, {:foo=>"foo"}]', I18n.t(nil, :default => proc, :foo => 'foo')
39
+ assert_equal %|foo: [nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo')
36
40
  end
37
41
 
38
42
  test "pluralization: given a key resolves to a Proc that returns valid data then pluralization still works" do
@@ -57,6 +61,7 @@ module I18n
57
61
  if arg.is_a?(Hash)
58
62
  arg.delete(:fallback_in_progress)
59
63
  arg.delete(:fallback_original_locale)
64
+ arg.delete(:skip_interpolation)
60
65
  end
61
66
  arg
62
67
  end.inspect
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.13.0"
4
+ VERSION = "1.14.8"
5
5
  end
data/lib/i18n.rb CHANGED
@@ -19,6 +19,7 @@ module I18n
19
19
  RESERVED_KEYS = %i[
20
20
  cascade
21
21
  deep_interpolation
22
+ skip_interpolation
22
23
  default
23
24
  exception_handler
24
25
  fallback
@@ -48,18 +49,19 @@ module I18n
48
49
  end
49
50
 
50
51
  def self.reserved_keys_pattern # :nodoc:
51
- @reserved_keys_pattern ||= /%\{(#{RESERVED_KEYS.join("|")})\}/
52
+ @reserved_keys_pattern ||= /(?<!%)%\{(#{RESERVED_KEYS.join("|")})\}/
52
53
  end
53
54
 
54
55
  module Base
55
56
  # Gets I18n configuration object.
56
57
  def config
57
- Thread.current[:i18n_config] ||= I18n::Config.new
58
+ Thread.current.thread_variable_get(:i18n_config) ||
59
+ Thread.current.thread_variable_set(:i18n_config, I18n::Config.new)
58
60
  end
59
61
 
60
62
  # Sets I18n configuration object.
61
63
  def config=(value)
62
- Thread.current[:i18n_config] = value
64
+ Thread.current.thread_variable_set(:i18n_config, value)
63
65
  end
64
66
 
65
67
  # Write methods which delegates to the configuration object
@@ -161,7 +163,7 @@ module I18n
161
163
  # or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
162
164
  # I18n.t :foo, :default => [:bar, 'default']
163
165
  #
164
- # *BULK LOOKUP*
166
+ # <b>BULK LOOKUP</b>
165
167
  #
166
168
  # This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
167
169
  # I18n.t [:foo, :bar]
@@ -180,7 +182,7 @@ module I18n
180
182
  # E.g. assuming the key <tt>:salutation</tt> resolves to:
181
183
  # lambda { |key, options| options[:gender] == 'm' ? "Mr. #{options[:name]}" : "Mrs. #{options[:name]}" }
182
184
  #
183
- # Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
185
+ # Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith')</tt> will result in "Mrs. Smith".
184
186
  #
185
187
  # Note that the string returned by lambda will go through string interpolation too,
186
188
  # so the following lambda would give the same result:
@@ -192,7 +194,7 @@ module I18n
192
194
  # always return the same translations/values per unique combination of argument
193
195
  # values.
194
196
  #
195
- # *Ruby 2.7+ keyword arguments warning*
197
+ # <b>Ruby 2.7+ keyword arguments warning</b>
196
198
  #
197
199
  # This method uses keyword arguments.
198
200
  # There is a breaking change in ruby that produces warning with ruby 2.7 and won't work as expected with ruby 3.0
@@ -231,11 +233,41 @@ module I18n
231
233
  end
232
234
  alias :t! :translate!
233
235
 
236
+ # Returns an array of interpolation keys for the given translation key
237
+ #
238
+ # *Examples*
239
+ #
240
+ # Suppose we have the following:
241
+ # I18n.t 'example.zero' == 'Zero interpolations'
242
+ # I18n.t 'example.one' == 'One interpolation %{foo}'
243
+ # I18n.t 'example.two' == 'Two interpolations %{foo} %{bar}'
244
+ # I18n.t 'example.three' == ['One %{foo}', 'Two %{bar}', 'Three %{baz}']
245
+ # I18n.t 'example.one', locale: :other == 'One interpolation %{baz}'
246
+ #
247
+ # Then we can expect the following results:
248
+ # I18n.interpolation_keys('example.zero') #=> []
249
+ # I18n.interpolation_keys('example.one') #=> ['foo']
250
+ # I18n.interpolation_keys('example.two') #=> ['foo', 'bar']
251
+ # I18n.interpolation_keys('example.three') #=> ['foo', 'bar', 'baz']
252
+ # I18n.interpolation_keys('one', scope: 'example', locale: :other) #=> ['baz']
253
+ # I18n.interpolation_keys('does-not-exist') #=> []
254
+ # I18n.interpolation_keys('example') #=> []
255
+ def interpolation_keys(key, **options)
256
+ raise I18n::ArgumentError if !key.is_a?(String) || key.empty?
257
+
258
+ return [] unless exists?(key, **options.slice(:locale, :scope))
259
+
260
+ translation = translate(key, **options.slice(:locale, :scope))
261
+ interpolation_keys_from_translation(translation)
262
+ .flatten.compact
263
+ end
264
+
234
265
  # Returns true if a translation exists for a given key, otherwise returns false.
235
266
  def exists?(key, _locale = nil, locale: _locale, **options)
236
267
  locale ||= config.locale
237
268
  raise Disabled.new('exists?') if locale == false
238
- raise I18n::ArgumentError if key.is_a?(String) && key.empty?
269
+ raise I18n::ArgumentError if (key.is_a?(String) && key.empty?) || key.nil?
270
+
239
271
  config.backend.exists?(locale, key, options)
240
272
  end
241
273
 
@@ -331,11 +363,12 @@ module I18n
331
363
  # keys are Symbols.
332
364
  def normalize_keys(locale, key, scope, separator = nil)
333
365
  separator ||= I18n.default_separator
334
- locale = locale.to_sym if locale
335
366
 
336
- result = [locale]
337
- result.concat(normalize_key(scope, separator)) if scope
338
- result.concat(normalize_key(key, separator))
367
+ [
368
+ *normalize_key(locale, separator),
369
+ *normalize_key(scope, separator),
370
+ *normalize_key(key, separator)
371
+ ]
339
372
  end
340
373
 
341
374
  # Returns true when the passed locale, which can be either a String or a
@@ -428,6 +461,17 @@ module I18n
428
461
  keys
429
462
  end
430
463
  end
464
+
465
+ def interpolation_keys_from_translation(translation)
466
+ case translation
467
+ when ::String
468
+ translation.scan(Regexp.union(I18n.config.interpolation_patterns))
469
+ when ::Array
470
+ translation.map { |element| interpolation_keys_from_translation(element) }
471
+ else
472
+ []
473
+ end
474
+ end
431
475
  end
432
476
 
433
477
  extend Base
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.13.0
4
+ version: 1.14.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Fuchs
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2023-04-26 00:00:00.000000000 Z
16
+ date: 2025-12-21 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: concurrent-ruby
@@ -106,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
106
  - !ruby/object:Gem::Version
107
107
  version: 1.3.5
108
108
  requirements: []
109
- rubygems_version: 3.3.16
109
+ rubygems_version: 3.5.23
110
110
  signing_key:
111
111
  specification_version: 4
112
112
  summary: New wave Internationalization support for Ruby