diff --git a/i18n.gemspec b/i18n.gemspec
index a6b55ea5..8dcf493f 100644
--- a/i18n.gemspec
+++ b/i18n.gemspec
@@ -25,6 +25,9 @@ Gem::Specification.new do |s|
s.require_path = 'lib'
s.required_rubygems_version = '>= 1.3.5'
s.required_ruby_version = '>= 2.3.0'
+ s.post_install_message = if RUBY_VERSION < '3.2'
+ "PSA: I18n will be dropping support for Ruby < 3.2 in the next major release (April 2025), due to Ruby's end of life for 3.1 and below (https://endoflife.date/ruby). Please upgrade to Ruby 3.2 or newer by April 2025 to continue using future versions of this gem."
+ end
s.add_dependency 'concurrent-ruby', '~> 1.0'
end
diff --git a/lib/i18n.rb b/lib/i18n.rb
index 2980f025..9a6a535d 100644
--- a/lib/i18n.rb
+++ b/lib/i18n.rb
@@ -19,6 +19,7 @@ module I18n
RESERVED_KEYS = %i[
cascade
deep_interpolation
+ skip_interpolation
default
exception_handler
fallback
@@ -161,7 +162,7 @@ def eager_load!
# or default if no translations for :foo and :bar were found.
# I18n.t :foo, :default => [:bar, 'default']
#
- # *BULK LOOKUP*
+ # BULK LOOKUP
#
# This returns an array with the translations for :foo and :bar.
# I18n.t [:foo, :bar]
@@ -180,7 +181,7 @@ def eager_load!
# E.g. assuming the key :salutation resolves to:
# lambda { |key, options| options[:gender] == 'm' ? "Mr. #{options[:name]}" : "Mrs. #{options[:name]}" }
#
- # Then I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
+ # Then I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
#
# Note that the string returned by lambda will go through string interpolation too,
# so the following lambda would give the same result:
@@ -192,7 +193,7 @@ def eager_load!
# always return the same translations/values per unique combination of argument
# values.
#
- # *Ruby 2.7+ keyword arguments warning*
+ # Ruby 2.7+ keyword arguments warning
#
# This method uses keyword arguments.
# There is a breaking change in ruby that produces warning with ruby 2.7 and won't work as expected with ruby 3.0
@@ -264,7 +265,8 @@ def interpolation_keys(key, **options)
def exists?(key, _locale = nil, locale: _locale, **options)
locale ||= config.locale
raise Disabled.new('exists?') if locale == false
- raise I18n::ArgumentError if key.is_a?(String) && key.empty?
+ raise I18n::ArgumentError if (key.is_a?(String) && key.empty?) || key.nil?
+
config.backend.exists?(locale, key, options)
end
diff --git a/lib/i18n/backend/base.rb b/lib/i18n/backend/base.rb
index 7ca9c28a..0c59cd72 100644
--- a/lib/i18n/backend/base.rb
+++ b/lib/i18n/backend/base.rb
@@ -54,8 +54,9 @@ def translate(locale, key, options = EMPTY_HASH)
end
deep_interpolation = options[:deep_interpolation]
+ skip_interpolation = options[:skip_interpolation]
values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
- if values && !values.empty?
+ if !skip_interpolation && values && !values.empty?
entry = if deep_interpolation
deep_interpolate(locale, entry, values)
else
@@ -151,7 +152,14 @@ def resolve(locale, object, subject, options = EMPTY_HASH)
result = catch(:exception) do
case subject
when Symbol
- I18n.translate(subject, **options.merge(:locale => locale, :throw => true))
+ I18n.translate(
+ subject,
+ **options.merge(
+ :locale => locale,
+ :throw => true,
+ :skip_interpolation => true
+ )
+ )
when Proc
date_or_time = options.delete(:object) || object
resolve(locale, object, subject.call(date_or_time, **options))
@@ -244,7 +252,7 @@ def load_file(filename)
# Loads a plain Ruby translations file. eval'ing the file must yield
# a Hash containing translation data with locales as toplevel keys.
def load_rb(filename)
- translations = eval(IO.read(filename), binding, filename)
+ translations = eval(IO.read(filename), binding, filename.to_s)
[translations, false]
end
diff --git a/lib/i18n/backend/fallbacks.rb b/lib/i18n/backend/fallbacks.rb
index a88a4fca..caa4e66e 100644
--- a/lib/i18n/backend/fallbacks.rb
+++ b/lib/i18n/backend/fallbacks.rb
@@ -71,7 +71,11 @@ def resolve_entry(locale, object, subject, options = EMPTY_HASH)
case subject
when Symbol
- I18n.translate(subject, **options.merge(:locale => options[:fallback_original_locale], :throw => true))
+ I18n.translate(subject, **options.merge(
+ :locale => options[:fallback_original_locale],
+ :throw => true,
+ :skip_interpolation => true
+ ))
when Proc
date_or_time = options.delete(:object) || object
resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
diff --git a/lib/i18n/backend/simple.rb b/lib/i18n/backend/simple.rb
index 7caa7dd1..2cac2452 100644
--- a/lib/i18n/backend/simple.rb
+++ b/lib/i18n/backend/simple.rb
@@ -10,14 +10,14 @@ module Backend
# The implementation is provided by a Implementation module allowing to easily
# extend Simple backend's behavior by including modules. E.g.:
#
- # module I18n::Backend::Pluralization
- # def pluralize(*args)
- # # extended pluralization logic
- # super
- # end
- # end
- #
- # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
+ # module I18n::Backend::Pluralization
+ # def pluralize(*args)
+ # # extended pluralization logic
+ # super
+ # end
+ # end
+ #
+ # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
class Simple
module Implementation
include Base
diff --git a/lib/i18n/tests/defaults.rb b/lib/i18n/tests/defaults.rb
index 31fdb469..e5db3365 100644
--- a/lib/i18n/tests/defaults.rb
+++ b/lib/i18n/tests/defaults.rb
@@ -47,6 +47,13 @@ def setup
I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' })
assert_equal 'bar', I18n.t(nil, :default => :'foo|bar', :separator => '|')
end
+
+ # Addresses issue: #599
+ test "defaults: only interpolates once when resolving defaults" do
+ I18n.backend.store_translations(:en, :greeting => 'hey %{name}')
+ assert_equal 'hey %{dont_interpolate_me}',
+ I18n.t(:does_not_exist, :name => '%{dont_interpolate_me}', default: [:greeting])
+ end
end
end
end
diff --git a/lib/i18n/tests/lookup.rb b/lib/i18n/tests/lookup.rb
index bbd775f0..f1bee792 100644
--- a/lib/i18n/tests/lookup.rb
+++ b/lib/i18n/tests/lookup.rb
@@ -76,6 +76,12 @@ def setup
test "lookup: a resulting Hash is not frozen" do
assert !I18n.t(:hash).frozen?
end
+
+ # Addresses issue: #599
+ test "lookup: only interpolates once when resolving symbols" do
+ I18n.backend.store_translations(:en, foo: :bar, bar: '%{value}')
+ assert_equal '%{dont_interpolate_me}', I18n.t(:foo, value: '%{dont_interpolate_me}')
+ end
end
end
end
diff --git a/lib/i18n/tests/procs.rb b/lib/i18n/tests/procs.rb
index 6abd8612..db99c766 100644
--- a/lib/i18n/tests/procs.rb
+++ b/lib/i18n/tests/procs.rb
@@ -57,6 +57,7 @@ def self.filter_args(*args)
if arg.is_a?(Hash)
arg.delete(:fallback_in_progress)
arg.delete(:fallback_original_locale)
+ arg.delete(:skip_interpolation)
end
arg
end.inspect
diff --git a/lib/i18n/version.rb b/lib/i18n/version.rb
index 7c4b3edd..546bcbaa 100644
--- a/lib/i18n/version.rb
+++ b/lib/i18n/version.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module I18n
- VERSION = "1.14.5"
+ VERSION = "1.14.6"
end
diff --git a/test/i18n/load_path_test.rb b/test/i18n/load_path_test.rb
index 7d0d27a1..ab354c08 100644
--- a/test/i18n/load_path_test.rb
+++ b/test/i18n/load_path_test.rb
@@ -31,4 +31,14 @@ def setup
I18n.load_path << Dir[locales_dir + '/*.{rb,yml}']
assert_equal "baz", I18n.t(:'foo.bar')
end
+
+ test "adding Pathnames to the load path does not break YML file locale loading" do
+ I18n.load_path << Pathname.new(locales_dir + '/en.yml')
+ assert_equal "baz", I18n.t(:'foo.bar')
+ end
+
+ test "adding Pathnames to the load path does not break Ruby file locale loading" do
+ I18n.load_path << Pathname.new(locales_dir + '/en.rb')
+ assert_equal "bas", I18n.t(:'fuh.bah')
+ end
end
diff --git a/test/i18n_test.rb b/test/i18n_test.rb
index 85679d40..642920de 100644
--- a/test/i18n_test.rb
+++ b/test/i18n_test.rb
@@ -315,6 +315,10 @@ def setup
assert_raises(I18n::ArgumentError) { I18n.interpolation_keys(["bad-argument"]) }
end
+ test "exists? given nil raises I18n::ArgumentError" do
+ assert_raises(I18n::ArgumentError) { I18n.exists?(nil) }
+ end
+
test "exists? given an existing key will return true" do
assert_equal true, I18n.exists?(:currency)
end
diff --git a/test/test_data/locales/plurals.rb b/test/test_data/locales/plurals.rb
index 9dc3ef0e..6606c0dc 100644
--- a/test/test_data/locales/plurals.rb
+++ b/test/test_data/locales/plurals.rb
@@ -3,7 +3,7 @@
{
:af => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } },
:am => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } },
- :ar => { :i18n => { :plural => { :keys => [:zero, :one, :two, :few, :many, :other], :rule => lambda { |n| n == 0 ? :zero : n == 1 ? :one : n == 2 ? :two : [3, 4, 5, 6, 7, 8, 9, 10].include?(n % 100) ? :few : [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99].include?(n % 100) ? :many : :other } } } },
+ :ar => { :i18n => { :plural => { :keys => [:zero, :one, :two, :few, :many, :other], :rule => lambda { |n| n == 0 ? :zero : n == 1 ? :one : n == 2 ? :two : (3..10).cover?(n % 100) ? :few : (11..99).cover?(n % 100) ? :many : :other } } } },
:az => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } },
:be => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } },
:bg => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } },
@@ -52,7 +52,7 @@
:ku => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } },
:lb => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } },
:ln => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } },
- :lt => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n % 10 == 1 && ![11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :one : [2, 3, 4, 5, 6, 7, 8, 9].include?(n % 10) && ![11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :few : :other } } } },
+ :lt => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n % 10 == 1 && !(11..19).include?(n % 100) ? :one : [2, 3, 4, 5, 6, 7, 8, 9].include?(n % 10) && !(11..19).include?(n % 100) ? :few : :other } } } },
:lv => { :i18n => { :plural => { :keys => [:zero, :one, :other], :rule => lambda { |n| n == 0 ? :zero : n % 10 == 1 && n % 100 != 11 ? :one : :other } } } },
:mg => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } },
:mk => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n % 10 == 1 ? :one : :other } } } },
@@ -61,7 +61,7 @@
:mo => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n == 1 ? :one : n == 0 ? :few : :other } } } },
:mr => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } },
:ms => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } },
- :mt => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n == 1 ? :one : n == 0 || [2, 3, 4, 5, 6, 7, 8, 9, 10].include?(n % 100) ? :few : [11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :many : :other } } } },
+ :mt => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n == 1 ? :one : n == 0 || (2..10).include?(n % 100) ? :few : (11..19).include?(n % 100) ? :many : :other } } } },
:my => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } },
:nah => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } },
:nb => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } },