i18n 1.14.4 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef99abbb2ed698cd9dd77ab484720a5ecf68be96a7064f2f8b432a69758ada07
4
- data.tar.gz: 0a4ade5577fe636c03d6ef9ca11f7a26b80a5c58078ee87c2f9dcf9fdd6882de
3
+ metadata.gz: cca63854d9dd69dfe1d36eb0388926fc5e7a7cd297ece21e2fdc431ef9c4c286
4
+ data.tar.gz: 1ab0f78752eabfd6a03b7c1f4a6f4e45c03689e9fbc0936cd65d6cd1f4103363
5
5
  SHA512:
6
- metadata.gz: 21b666c06e7c8ebdb78088ef2ecd8da866ebd414dff3f3a8535b3e950c12bf0f38695a22acac6da7775e8e245c7a99a0d1d9a6bc409e0ea3d552520a0be26f68
7
- data.tar.gz: c5f81a0bafbf70daa4a2c4374f3906ef31cea1d8f94d5d5e9b980224d1b6bc75f95cdd3e08709f6cae410c34f4285deb375d7e5b06e19f283743221606e23434
6
+ metadata.gz: e8dd4e1cf010b24d748b4e2713794296964301234b9dbed36c96ae591677fb9ca3d5df7f5aedd86a48a1f470ad81de07997f2103107af32786486892204222d4
7
+ data.tar.gz: 9e2008459e303dc8e87742231d2468faa3f2edcb61a30c3c26f5240cc5606a8132bccc70ec3e45a9cfcc38eaf04d84c74c6d643ef54f1df2534c66706677180d
@@ -54,8 +54,9 @@ 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 && !values.empty?
59
+ if !skip_interpolation && values && !values.empty?
59
60
  entry = if deep_interpolation
60
61
  deep_interpolate(locale, entry, values)
61
62
  else
@@ -151,7 +152,14 @@ module I18n
151
152
  result = catch(:exception) do
152
153
  case subject
153
154
  when Symbol
154
- 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
+ )
155
163
  when Proc
156
164
  date_or_time = options.delete(:object) || object
157
165
  resolve(locale, object, subject.call(date_or_time, **options))
@@ -188,8 +196,8 @@ module I18n
188
196
  #
189
197
  # if the given subject is an array then:
190
198
  # each element of the array is recursively interpolated (until it finds a string)
191
- # method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
192
- # # => "["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"]]
193
201
  def interpolate(locale, subject, values = EMPTY_HASH)
194
202
  return subject if values.empty?
195
203
 
@@ -244,7 +252,7 @@ module I18n
244
252
  # Loads a plain Ruby translations file. eval'ing the file must yield
245
253
  # a Hash containing translation data with locales as toplevel keys.
246
254
  def load_rb(filename)
247
- translations = eval(IO.read(filename), binding, filename)
255
+ translations = eval(IO.read(filename), binding, filename.to_s)
248
256
  [translations, false]
249
257
  end
250
258
 
@@ -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))
@@ -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
@@ -90,11 +90,13 @@ module I18n
90
90
  protected
91
91
 
92
92
  def compute(tags, include_defaults = true, exclude = [])
93
- result = Array(tags).flat_map do |tag|
93
+ result = []
94
+ Array(tags).each do |tag|
94
95
  tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude
95
- tags.each { |_tag| tags += compute(@map[_tag], false, exclude + tags) if @map[_tag] }
96
- tags
96
+ result += tags
97
+ tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] }
97
98
  end
99
+
98
100
  result.push(*defaults) if include_defaults
99
101
  result.uniq!
100
102
  result.compact!
@@ -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
@@ -118,6 +118,24 @@ module I18n
118
118
  assert_raises(I18n::ReservedInterpolationKey) { interpolate(:interpolate) }
119
119
  end
120
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
137
+ end
138
+
121
139
  test "interpolation: deep interpolation for default string" do
122
140
  assert_equal 'Hi %{name}!', interpolate(:default => 'Hi %{name}!', :deep_interpolation => true)
123
141
  end
@@ -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
@@ -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.14.4"
4
+ VERSION = "1.14.7"
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,7 +49,7 @@ 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
@@ -161,7 +162,7 @@ module I18n
161
162
  # or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
162
163
  # I18n.t :foo, :default => [:bar, 'default']
163
164
  #
164
- # *BULK LOOKUP*
165
+ # <b>BULK LOOKUP</b>
165
166
  #
166
167
  # This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
167
168
  # I18n.t [:foo, :bar]
@@ -180,7 +181,7 @@ module I18n
180
181
  # E.g. assuming the key <tt>:salutation</tt> resolves to:
181
182
  # lambda { |key, options| options[:gender] == 'm' ? "Mr. #{options[:name]}" : "Mrs. #{options[:name]}" }
182
183
  #
183
- # Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
184
+ # Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith')</tt> will result in "Mrs. Smith".
184
185
  #
185
186
  # Note that the string returned by lambda will go through string interpolation too,
186
187
  # so the following lambda would give the same result:
@@ -192,7 +193,7 @@ module I18n
192
193
  # always return the same translations/values per unique combination of argument
193
194
  # values.
194
195
  #
195
- # *Ruby 2.7+ keyword arguments warning*
196
+ # <b>Ruby 2.7+ keyword arguments warning</b>
196
197
  #
197
198
  # This method uses keyword arguments.
198
199
  # 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 +232,41 @@ module I18n
231
232
  end
232
233
  alias :t! :translate!
233
234
 
235
+ # Returns an array of interpolation keys for the given translation key
236
+ #
237
+ # *Examples*
238
+ #
239
+ # Suppose we have the following:
240
+ # I18n.t 'example.zero' == 'Zero interpolations'
241
+ # I18n.t 'example.one' == 'One interpolation %{foo}'
242
+ # I18n.t 'example.two' == 'Two interpolations %{foo} %{bar}'
243
+ # I18n.t 'example.three' == ['One %{foo}', 'Two %{bar}', 'Three %{baz}']
244
+ # I18n.t 'example.one', locale: :other == 'One interpolation %{baz}'
245
+ #
246
+ # Then we can expect the following results:
247
+ # I18n.interpolation_keys('example.zero') #=> []
248
+ # I18n.interpolation_keys('example.one') #=> ['foo']
249
+ # I18n.interpolation_keys('example.two') #=> ['foo', 'bar']
250
+ # I18n.interpolation_keys('example.three') #=> ['foo', 'bar', 'baz']
251
+ # I18n.interpolation_keys('one', scope: 'example', locale: :other) #=> ['baz']
252
+ # I18n.interpolation_keys('does-not-exist') #=> []
253
+ # I18n.interpolation_keys('example') #=> []
254
+ def interpolation_keys(key, **options)
255
+ raise I18n::ArgumentError if !key.is_a?(String) || key.empty?
256
+
257
+ return [] unless exists?(key, **options.slice(:locale, :scope))
258
+
259
+ translation = translate(key, **options.slice(:locale, :scope))
260
+ interpolation_keys_from_translation(translation)
261
+ .flatten.compact
262
+ end
263
+
234
264
  # Returns true if a translation exists for a given key, otherwise returns false.
235
265
  def exists?(key, _locale = nil, locale: _locale, **options)
236
266
  locale ||= config.locale
237
267
  raise Disabled.new('exists?') if locale == false
238
- raise I18n::ArgumentError if key.is_a?(String) && key.empty?
268
+ raise I18n::ArgumentError if (key.is_a?(String) && key.empty?) || key.nil?
269
+
239
270
  config.backend.exists?(locale, key, options)
240
271
  end
241
272
 
@@ -429,6 +460,17 @@ module I18n
429
460
  keys
430
461
  end
431
462
  end
463
+
464
+ def interpolation_keys_from_translation(translation)
465
+ case translation
466
+ when ::String
467
+ translation.scan(Regexp.union(I18n.config.interpolation_patterns))
468
+ when ::Array
469
+ translation.map { |element| interpolation_keys_from_translation(element) }
470
+ else
471
+ []
472
+ end
473
+ end
432
474
  end
433
475
 
434
476
  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.14.4
4
+ version: 1.14.7
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: 2024-03-06 00:00:00.000000000 Z
16
+ date: 2025-01-19 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.5.1
109
+ rubygems_version: 3.5.23
110
110
  signing_key:
111
111
  specification_version: 4
112
112
  summary: New wave Internationalization support for Ruby