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 } } } },