New Upstream Release - ruby-i18n

Ready changes

Summary

Merged new upstream version: 1.14.1 (was: 1.12.0).

Resulting package

Built on 2023-06-09T05:40 (took 5m27s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases ruby-i18n

Lintian Result

Diff

diff --git a/.github/funding.yml b/.github/funding.yml
new file mode 100644
index 0000000..2dda82a
--- /dev/null
+++ b/.github/funding.yml
@@ -0,0 +1 @@
+github: [radar]
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 81c1295..3e33190 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -17,7 +17,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        ruby_version: [3.1, "3.0", 2.7, 2.6, jruby]
+        ruby_version: [3.2, 3.1, "3.0", 2.7, 2.6, jruby]
         gemfile:
           - Gemfile
           - gemfiles/Gemfile.rails-5.2.x
@@ -26,6 +26,10 @@ jobs:
           - gemfiles/Gemfile.rails-7.0.x
           - gemfiles/Gemfile.rails-main
         exclude:
+          # Ruby 3.2 is not supported by Rails 5.2.x
+          - ruby_version: 3.2
+            gemfile: gemfiles/Gemfile.rails-5.2.x
+
           # Ruby 3.1 is not supported by Rails 5.2.x
           - ruby_version: 3.1
             gemfile: gemfiles/Gemfile.rails-5.2.x
@@ -42,6 +46,10 @@ jobs:
           - ruby_version: 2.6
             gemfile: gemfiles/Gemfile.rails-7.0.x
 
+          # JRuby 9.4.2.0 (3.1.0) is not supported by Rails 5.2.x
+          - ruby_version: jruby
+            gemfile: gemfiles/Gemfile.rails-5.2.x
+
           # JRuby is not supported by Rails 7.0.x
           - ruby_version: jruby
             gemfile: gemfiles/Gemfile.rails-7.0.x
@@ -53,7 +61,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - name: Set up Ruby
         uses: ruby/setup-ruby@v1
         with:
diff --git a/README.md b/README.md
index 318e05d..1d174dd 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ gem 'i18n'
 Then configure I18n with some translations, and a default locale:
 
 ```ruby
-I18n.load_path << Dir[File.expand_path("config/locales") + "/*.yml"]
+I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
 I18n.default_locale = :en # (note that `en` is already the default!)
 ```
 
diff --git a/debian/changelog b/debian/changelog
index 95bae5e..aac06d1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+ruby-i18n (1.14.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 09 Jun 2023 05:35:48 -0000
+
 ruby-i18n (1.10.0-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/debian/patches/0001-disable-bundler-on-build-time-do-not-install-stuff-a.patch b/debian/patches/0001-disable-bundler-on-build-time-do-not-install-stuff-a.patch
index 72de208..2a6c70b 100644
--- a/debian/patches/0001-disable-bundler-on-build-time-do-not-install-stuff-a.patch
+++ b/debian/patches/0001-disable-bundler-on-build-time-do-not-install-stuff-a.patch
@@ -7,8 +7,10 @@ Forwarded: not-needed
  test/test_helper.rb | 1 -
  1 file changed, 1 deletion(-)
 
---- a/test/test_helper.rb
-+++ b/test/test_helper.rb
+Index: ruby-i18n.git/test/test_helper.rb
+===================================================================
+--- ruby-i18n.git.orig/test/test_helper.rb
++++ ruby-i18n.git/test/test_helper.rb
 @@ -1,5 +1,4 @@
  require 'minitest/autorun'
 -require 'bundler/setup'
diff --git a/debian/patches/0002-gemspec-prepend-local-directory-to-the-LOAD_PATH.patch b/debian/patches/0002-gemspec-prepend-local-directory-to-the-LOAD_PATH.patch
index 53da29c..709971c 100644
--- a/debian/patches/0002-gemspec-prepend-local-directory-to-the-LOAD_PATH.patch
+++ b/debian/patches/0002-gemspec-prepend-local-directory-to-the-LOAD_PATH.patch
@@ -6,10 +6,10 @@ Subject: gemspec: prepend local directory to the $LOAD_PATH
  i18n.gemspec | 2 +-
  1 file changed, 1 insertion(+), 1 deletion(-)
 
-diff --git a/i18n.gemspec b/i18n.gemspec
-index 0e67209..f4211e9 100644
---- a/i18n.gemspec
-+++ b/i18n.gemspec
+Index: ruby-i18n.git/i18n.gemspec
+===================================================================
+--- ruby-i18n.git.orig/i18n.gemspec
++++ ruby-i18n.git/i18n.gemspec
 @@ -1,6 +1,6 @@
  # encoding: utf-8
  
diff --git a/debian/patches/0003-test_helper-load-gem-i18n-before-requiring-i18n.patch b/debian/patches/0003-test_helper-load-gem-i18n-before-requiring-i18n.patch
index 41d4393..599a770 100644
--- a/debian/patches/0003-test_helper-load-gem-i18n-before-requiring-i18n.patch
+++ b/debian/patches/0003-test_helper-load-gem-i18n-before-requiring-i18n.patch
@@ -10,8 +10,10 @@ in the require.
  test/test_helper.rb | 1 +
  1 file changed, 1 insertion(+)
 
---- a/test/test_helper.rb
-+++ b/test/test_helper.rb
+Index: ruby-i18n.git/test/test_helper.rb
+===================================================================
+--- ruby-i18n.git.orig/test/test_helper.rb
++++ ruby-i18n.git/test/test_helper.rb
 @@ -1,4 +1,5 @@
  require 'minitest/autorun'
 +gem "i18n"
diff --git a/lib/i18n.rb b/lib/i18n.rb
index e197e2b..d336970 100644
--- a/lib/i18n.rb
+++ b/lib/i18n.rb
@@ -214,18 +214,12 @@ module I18n
 
       backend = config.backend
 
-      result = catch(:exception) do
-        if key.is_a?(Array)
-          key.map { |k| backend.translate(locale, k, options) }
-        else
-          backend.translate(locale, key, options)
+      if key.is_a?(Array)
+        key.map do |k|
+          translate_key(k, throw, raise, locale, backend, options)
         end
-      end
-
-      if result.is_a?(MissingTranslation)
-        handle_exception((throw && :throw || raise && :raise), result, locale, key, options)
       else
-        result
+        translate_key(key, throw, raise, locale, backend, options)
       end
     end
     alias :t :translate
@@ -364,6 +358,18 @@ module I18n
 
   private
 
+    def translate_key(key, throw, raise, locale, backend, options)
+      result = catch(:exception) do
+        backend.translate(locale, key, options)
+      end
+
+      if result.is_a?(MissingTranslation)
+        handle_exception((throw && :throw || raise && :raise), result, locale, key, options)
+      else
+        result
+      end
+    end
+
     # Any exceptions thrown in translate will be sent to the @@exception_handler
     # which can be a Symbol, a Proc or any other Object unless they're forced to
     # be raised or thrown (MissingTranslation).
diff --git a/lib/i18n/backend/base.rb b/lib/i18n/backend/base.rb
index 4cbcc3c..5775675 100644
--- a/lib/i18n/backend/base.rb
+++ b/lib/i18n/backend/base.rb
@@ -54,7 +54,7 @@ module I18n
         end
 
         deep_interpolation = options[:deep_interpolation]
-        values = Utils.except(options, *RESERVED_KEYS)
+        values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
         if values
           entry = if deep_interpolation
             deep_interpolate(locale, entry, values)
@@ -66,7 +66,7 @@ module I18n
       end
 
       def exists?(locale, key, options = EMPTY_HASH)
-        lookup(locale, key) != nil
+        lookup(locale, key, options[:scope]) != nil
       end
 
       # Acts the same as +strftime+, but uses a localized version of the
@@ -123,7 +123,12 @@ module I18n
         # first translation that can be resolved. Otherwise it tries to resolve
         # the translation directly.
         def default(locale, object, subject, options = EMPTY_HASH)
-          options = options.reject { |key, value| key == :default }
+          if options.size == 1 && options.has_key?(:default)
+            options = {}
+          else
+            options = Utils.except(options, :default)
+          end
+
           case subject
           when Array
             subject.each do |item|
@@ -166,7 +171,7 @@ module I18n
         # Other backends can implement more flexible or complex pluralization rules.
         def pluralize(locale, entry, count)
           entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
-          return entry unless entry.is_a?(Hash) && count && entry.values.none? { |v| v.is_a?(Hash) }
+          return entry unless entry.is_a?(Hash) && count
 
           key = pluralization_key(entry, count)
           raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
@@ -282,8 +287,8 @@ module I18n
             when '%^b' then I18n.t!(:"date.abbr_month_names",               :locale => locale, :format => format)[object.mon].upcase
             when '%B' then I18n.t!(:"date.month_names",                     :locale => locale, :format => format)[object.mon]
             when '%^B' then I18n.t!(:"date.month_names",                    :locale => locale, :format => format)[object.mon].upcase
-            when '%p' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase if object.respond_to? :hour
-            when '%P' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase if object.respond_to? :hour
+            when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
+            when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
             end
           end
         rescue MissingTranslationData => e
diff --git a/lib/i18n/backend/chain.rb b/lib/i18n/backend/chain.rb
index 525dd2d..e081a91 100644
--- a/lib/i18n/backend/chain.rb
+++ b/lib/i18n/backend/chain.rb
@@ -16,6 +16,8 @@ module I18n
     #
     # The implementation assumes that all backends added to the Chain implement
     # a lookup method with the same API as Simple backend does.
+    # 
+    # Fallback translations using the :default option are only used by the last backend of a chain.
     class Chain
       module Implementation
         include Base
diff --git a/lib/i18n/backend/fallbacks.rb b/lib/i18n/backend/fallbacks.rb
index 7afbfe3..6d4d6e1 100644
--- a/lib/i18n/backend/fallbacks.rb
+++ b/lib/i18n/backend/fallbacks.rb
@@ -107,7 +107,7 @@ module I18n
       private
 
         # Overwrite on_fallback to add specified logic when the fallback succeeds.
-        def on_fallback(_original_locale, _fallback_locale, _key, _optoins)
+        def on_fallback(_original_locale, _fallback_locale, _key, _options)
           nil
         end
     end
diff --git a/lib/i18n/backend/lazy_loadable.rb b/lib/i18n/backend/lazy_loadable.rb
index 60f21fa..575b32b 100644
--- a/lib/i18n/backend/lazy_loadable.rb
+++ b/lib/i18n/backend/lazy_loadable.rb
@@ -98,7 +98,7 @@ module I18n
       # Parse the load path and extract all locales.
       def available_locales
         if lazy_load?
-          I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }
+          I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }.uniq
         else
           super
         end
diff --git a/lib/i18n/backend/pluralization.rb b/lib/i18n/backend/pluralization.rb
index b602657..1d3277b 100644
--- a/lib/i18n/backend/pluralization.rb
+++ b/lib/i18n/backend/pluralization.rb
@@ -16,26 +16,57 @@ module I18n
     module Pluralization
       # Overwrites the Base backend translate method so that it will check the
       # translation meta data space (:i18n) for a locale specific pluralization
-      # rule and use it to pluralize the given entry. I.e. the library expects
+      # rule and use it to pluralize the given entry. I.e., the library expects
       # pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
       #
       # Pluralization rules are expected to respond to #call(count) and
-      # return a pluralization key. Valid keys depend on the translation data
-      # hash (entry) but it is generally recommended to follow CLDR's style,
-      # i.e., return one of the keys :zero, :one, :few, :many, :other.
+      # return a pluralization key. Valid keys depend on the pluralization
+      # rules for the locale, as defined in the CLDR.
+      # As of v41, 6 locale-specific plural categories are defined:
+      #   :few, :many, :one, :other, :two, :zero
       #
-      # The :zero key is always picked directly when count equals 0 AND the
-      # translation data has the key :zero. This way translators are free to
-      # either pick a special :zero translation even for languages where the
-      # pluralizer does not return a :zero key.
+      # n.b., The :one plural category does not imply the number 1.
+      # Instead, :one is a category for any number that behaves like 1 in
+      # that locale. For example, in some locales, :one is used for numbers
+      # that end in "1" (like 1, 21, 151) but that don't end in
+      # 11 (like 11, 111, 10311).
+      # Similar notes apply to the :two, and :zero plural categories.
+      #
+      # If you want to have different strings for the categories of count == 0
+      # (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car")
+      # use the explicit `"0"` and `"1"` keys.
+      # https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
       def pluralize(locale, entry, count)
         return entry unless entry.is_a?(Hash) && count
 
         pluralizer = pluralizer(locale)
         if pluralizer.respond_to?(:call)
-          key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
-          raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
-          entry[key]
+          # Deprecation: The use of the `zero` key in this way is incorrect.
+          # Users that want a different string for the case of `count == 0` should use the explicit "0" key instead.
+          # We keep this incorrect behaviour for now for backwards compatibility until we can remove it.
+          # Ref: https://github.com/ruby-i18n/i18n/issues/629
+          return entry[:zero] if count == 0 && entry.has_key?(:zero)
+
+          # "0" and "1" are special cases
+          # https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
+          if count == 0 || count == 1
+            value = entry[symbolic_count(count)]
+            return value if value
+          end
+
+          # Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
+          # > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
+          # > 1. If "x" is numeric, the path falls back to the path with [@count=«the plural rules category for x for that locale»], within that the same locale.
+          # > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
+          # > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
+          # Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
+          plural_rule_category = pluralizer.call(count)
+
+          value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
+            entry[plural_rule_category] || entry[:other]
+          else
+            raise InvalidPluralizationData.new(entry, count, plural_rule_category)
+          end
         else
           super
         end
@@ -43,13 +74,23 @@ module I18n
 
       protected
 
-        def pluralizers
-          @pluralizers ||= {}
-        end
+      def pluralizers
+        @pluralizers ||= {}
+      end
 
-        def pluralizer(locale)
-          pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
-        end
+      def pluralizer(locale)
+        pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
+      end
+
+      private
+
+      # Normalizes categories of 0.0 and 1.0
+      # and returns the symbolic version
+      def symbolic_count(count)
+        count = 0 if count == 0
+        count = 1 if count == 1
+        count.to_s.to_sym
+      end
     end
   end
 end
diff --git a/lib/i18n/backend/simple.rb b/lib/i18n/backend/simple.rb
index 0c49de8..7caa7dd 100644
--- a/lib/i18n/backend/simple.rb
+++ b/lib/i18n/backend/simple.rb
@@ -21,6 +21,9 @@ module I18n
     class Simple
       module Implementation
         include Base
+        
+        # Mutex to ensure that concurrent translations loading will be thread-safe
+        MUTEX = Mutex.new
 
         def initialized?
           @initialized ||= false
@@ -68,7 +71,11 @@ module I18n
           # call `init_translations`
           init_translations if do_init && !initialized?
 
-          @translations ||= Concurrent::Hash.new { |h, k| h[k] = Concurrent::Hash.new }
+          @translations ||= Concurrent::Hash.new do |h, k|
+            MUTEX.synchronize do
+              h[k] = Concurrent::Hash.new
+            end
+          end
         end
 
       protected
@@ -94,7 +101,7 @@ module I18n
               return nil unless result.has_key?(_key)
             end
             result = result[_key]
-            result = resolve_entry(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
+            result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
             result
           end
         end
diff --git a/lib/i18n/backend/transliterator.rb b/lib/i18n/backend/transliterator.rb
index bb704ab..70c0df3 100644
--- a/lib/i18n/backend/transliterator.rb
+++ b/lib/i18n/backend/transliterator.rb
@@ -45,30 +45,30 @@ module I18n
           "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
           "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
           "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
-          "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a",
-          "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e",
-          "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d",
-          "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o",
-          "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y",
-          "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C",
-          "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c",
-          "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E",
-          "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e",
-          "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G",
-          "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i",
-          "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I",
-          "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k",
-          "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l",
-          "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N",
-          "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng",
-          "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE",
-          "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r",
-          "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S",
-          "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t",
-          "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U",
-          "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w",
-          "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z",
-          "Ž"=>"Z", "ž"=>"z"
+          "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "ẞ"=>"SS", "à"=>"a",
+          "á"=>"a", "â"=>"a", "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c",
+          "è"=>"e", "é"=>"e", "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i",
+          "ï"=>"i", "ð"=>"d", "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o",
+          "ö"=>"o", "ø"=>"o", "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y",
+          "þ"=>"th", "ÿ"=>"y", "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A",
+          "ą"=>"a", "Ć"=>"C", "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c",
+          "Č"=>"C", "č"=>"c", "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E",
+          "ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e",
+          "Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G",
+          "ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h",
+          "Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I",
+          "į"=>"i", "İ"=>"I", "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j",
+          "Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l",
+          "Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N",
+          "ń"=>"n", "Ņ"=>"N", "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG",
+          "ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o",
+          "Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R",
+          "ř"=>"r", "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s",
+          "Š"=>"S", "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T",
+          "ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u",
+          "Ů"=>"U", "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W",
+          "ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z",
+          "ż"=>"z", "Ž"=>"Z", "ž"=>"z"
         }.freeze
 
         def initialize(rule = nil)
diff --git a/lib/i18n/config.rb b/lib/i18n/config.rb
index ea3dd1e..9878e02 100644
--- a/lib/i18n/config.rb
+++ b/lib/i18n/config.rb
@@ -38,7 +38,7 @@ module I18n
     end
 
     # Returns an array of locales for which translations are available.
-    # Unless you explicitely set these through I18n.available_locales=
+    # Unless you explicitly set these through I18n.available_locales=
     # the call will be delegated to the backend.
     def available_locales
       @@available_locales ||= nil
@@ -106,7 +106,7 @@ module I18n
     # if you don't care about arity.
     #
     # == Example:
-    # You can supress raising an exception and return string instead:
+    # You can suppress raising an exception and return string instead:
     #
     #   I18n.config.missing_interpolation_argument_handler = Proc.new do |key|
     #     "#{key} is missing"
diff --git a/lib/i18n/exceptions.rb b/lib/i18n/exceptions.rb
index f66e207..23ca46e 100644
--- a/lib/i18n/exceptions.rb
+++ b/lib/i18n/exceptions.rb
@@ -24,7 +24,7 @@ module I18n
         been set is likely to display text from the wrong locale to some users.
 
         If you have a legitimate reason to access i18n data outside of the user flow, you can do so by passing
-        the desired locale explictly with the `locale` argument, e.g. `I18n.#{method}(..., locale: :en)`
+        the desired locale explicitly with the `locale` argument, e.g. `I18n.#{method}(..., locale: :en)`
       MESSAGE
     end
   end
@@ -47,7 +47,7 @@ module I18n
 
   class MissingTranslation < ArgumentError
     module Base
-      PERMITTED_KEYS = [:scope].freeze
+      PERMITTED_KEYS = [:scope, :default].freeze
 
       attr_reader :locale, :key, :options
 
@@ -63,8 +63,18 @@ module I18n
       end
 
       def message
-        "translation missing: #{keys.join('.')}"
+        if (default = options[:default]).is_a?(Array) && default.any?
+          other_options = ([key, *default]).map { |k| normalized_option(k).prepend('- ') }.join("\n")
+          "Translation missing. Options considered were:\n#{other_options}"
+        else
+          "Translation missing: #{keys.join('.')}"
+        end
+      end
+
+      def normalized_option(key)
+        I18n.normalize_keys(locale, key, options[:scope]).join('.')
       end
+
       alias :to_s :message
 
       def to_exception
diff --git a/lib/i18n/interpolate/ruby.rb b/lib/i18n/interpolate/ruby.rb
index dab8f0e..5b50593 100644
--- a/lib/i18n/interpolate/ruby.rb
+++ b/lib/i18n/interpolate/ruby.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # heavily based on Masao Mutoh's gettext String interpolation extension
 # http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
 
@@ -10,6 +12,11 @@ module I18n
   INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)
   deprecate_constant :INTERPOLATION_PATTERN
 
+  INTERPOLATION_PATTERNS_CACHE = Hash.new do |hash, patterns|
+    hash[patterns] = Regexp.union(patterns)
+  end
+  private_constant :INTERPOLATION_PATTERNS_CACHE
+
   class << self
     # Return String or raises MissingInterpolationArgument exception.
     # Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler.
@@ -20,7 +27,12 @@ module I18n
     end
 
     def interpolate_hash(string, values)
-      string.gsub(Regexp.union(config.interpolation_patterns)) do |match|
+      pattern = INTERPOLATION_PATTERNS_CACHE[config.interpolation_patterns]
+      interpolated = false
+
+      interpolated_string = string.gsub(pattern) do |match|
+        interpolated = true
+
         if match == '%%'
           '%'
         else
@@ -34,6 +46,8 @@ module I18n
           $3 ? sprintf("%#{$3}", value) : value
         end
       end
+
+      interpolated ? interpolated_string : string
     end
   end
 end
diff --git a/lib/i18n/locale/tag/simple.rb b/lib/i18n/locale/tag/simple.rb
index 6d9ab56..18d55c2 100644
--- a/lib/i18n/locale/tag/simple.rb
+++ b/lib/i18n/locale/tag/simple.rb
@@ -1,5 +1,5 @@
 # Simple Locale tag implementation that computes subtags by simply splitting
-# the locale tag at '-' occurences.
+# the locale tag at '-' occurrences.
 module I18n
   module Locale
     module Tag
diff --git a/lib/i18n/tests/basics.rb b/lib/i18n/tests/basics.rb
index be82430..833762b 100644
--- a/lib/i18n/tests/basics.rb
+++ b/lib/i18n/tests/basics.rb
@@ -26,7 +26,7 @@ module I18n
         assert_equal I18n.available_locales, I18n.backend.available_locales
       end
 
-      test "available_locales memoizes when set explicitely" do
+      test "available_locales memoizes when set explicitly" do
         I18n.backend.expects(:available_locales).never
         I18n.available_locales = [:foo]
         I18n.backend.store_translations('de', :bar => 'baz')
@@ -34,7 +34,7 @@ module I18n
         assert_equal [:foo], I18n.available_locales
       end
 
-      test "available_locales delegates to the backend when not set explicitely" do
+      test "available_locales delegates to the backend when not set explicitly" do
         original_available_locales_value = I18n.backend.available_locales
         I18n.backend.expects(:available_locales).returns(original_available_locales_value).twice
         assert_equal I18n.backend.available_locales, I18n.available_locales
diff --git a/lib/i18n/tests/localization/date.rb b/lib/i18n/tests/localization/date.rb
index 2a44371..c21fbbf 100644
--- a/lib/i18n/tests/localization/date.rb
+++ b/lib/i18n/tests/localization/date.rb
@@ -34,6 +34,11 @@ module I18n
           assert_equal 'Sa', I18n.l(@date, :format => '%a', :locale => :de)
         end
 
+        test "localize Date: given an meridian indicator format it returns the correct meridian indicator" do
+          assert_equal 'AM', I18n.l(@date, :format => '%p', :locale => :de)
+          assert_equal 'am', I18n.l(@date, :format => '%P', :locale => :de)
+        end
+
         test "localize Date: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do
           assert_equal 'sa'.upcase, I18n.l(@date, :format => '%^a', :locale => :de)
         end
@@ -59,7 +64,7 @@ module I18n
         end
 
         test "localize Date: given missing translations it returns the correct error message" do
-          assert_equal 'translation missing: fr.date.abbr_month_names', I18n.l(@date, :format => '%b', :locale => :fr)
+          assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@date, :format => '%b', :locale => :fr)
         end
 
         test "localize Date: given an unknown format it does not fail" do
diff --git a/lib/i18n/tests/localization/date_time.rb b/lib/i18n/tests/localization/date_time.rb
index b09b888..b5d3527 100644
--- a/lib/i18n/tests/localization/date_time.rb
+++ b/lib/i18n/tests/localization/date_time.rb
@@ -60,7 +60,7 @@ module I18n
         end
 
         test "localize DateTime: given missing translations it returns the correct error message" do
-          assert_equal 'translation missing: fr.date.abbr_month_names', I18n.l(@datetime, :format => '%b', :locale => :fr)
+          assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@datetime, :format => '%b', :locale => :fr)
         end
 
         test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator" do
diff --git a/lib/i18n/tests/localization/time.rb b/lib/i18n/tests/localization/time.rb
index 7afe176..456a760 100644
--- a/lib/i18n/tests/localization/time.rb
+++ b/lib/i18n/tests/localization/time.rb
@@ -61,7 +61,7 @@ module I18n
         end
 
         test "localize Time: given missing translations it returns the correct error message" do
-          assert_equal 'translation missing: fr.date.abbr_month_names', I18n.l(@time, :format => '%b', :locale => :fr)
+          assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@time, :format => '%b', :locale => :fr)
         end
 
         test "localize Time: given a meridian indicator format it returns the correct meridian indicator" do
diff --git a/lib/i18n/tests/lookup.rb b/lib/i18n/tests/lookup.rb
index 3bd46b8..bbd775f 100644
--- a/lib/i18n/tests/lookup.rb
+++ b/lib/i18n/tests/lookup.rb
@@ -30,7 +30,7 @@ module I18n
       end
 
       test "lookup: given a missing key, no default and no raise option it returns an error message" do
-        assert_equal "translation missing: en.missing", I18n.t(:missing)
+        assert_equal "Translation missing: en.missing", I18n.t(:missing)
       end
 
       test "lookup: given a missing key, no default and the raise option it raises MissingTranslationData" do
diff --git a/lib/i18n/version.rb b/lib/i18n/version.rb
index bc754ba..965f5dd 100644
--- a/lib/i18n/version.rb
+++ b/lib/i18n/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 module I18n
-  VERSION = "1.10.0"
+  VERSION = "1.14.1"
 end
diff --git a/test/backend/cache_test.rb b/test/backend/cache_test.rb
index e253393..3ca4f19 100644
--- a/test/backend/cache_test.rb
+++ b/test/backend/cache_test.rb
@@ -62,8 +62,9 @@ class I18nBackendCacheTest < I18n::TestCase
     I18n.t(:missing, :scope => :foo, :extra => true)
     assert_equal 1, I18n.cache_store.instance_variable_get(:@data).size
 
-    _, entry = I18n.cache_store.instance_variable_get(:@data).first
-    assert_equal({ scope: :foo }, entry.value.options)
+    value = I18n.cache_store.read(I18n.cache_store.instance_variable_get(:@data).keys.first)
+
+    assert_equal({ scope: :foo }, value.options)
   end
 
   test "uses 'i18n' as a cache key namespace by default" do
diff --git a/test/backend/chain_test.rb b/test/backend/chain_test.rb
index b563374..acb50ef 100644
--- a/test/backend/chain_test.rb
+++ b/test/backend/chain_test.rb
@@ -78,7 +78,7 @@ class I18nBackendChainTest < I18n::TestCase
       "Bah"], I18n.t([:formats, :plural_2, :bah], :default => 'Bah')
   end
 
-  test "store_translations options are not dropped while transfering to backend" do
+  test "store_translations options are not dropped while transferring to backend" do
     @first.expects(:store_translations).with(:foo, {:bar => :baz}, {:option => 'persists'})
     I18n.backend.store_translations :foo, {:bar => :baz}, {:option => 'persists'}
   end
diff --git a/test/backend/exceptions_test.rb b/test/backend/exceptions_test.rb
index cc22166..e19c812 100644
--- a/test/backend/exceptions_test.rb
+++ b/test/backend/exceptions_test.rb
@@ -10,7 +10,7 @@ class I18nBackendExceptionsTest < I18n::TestCase
     exception = catch(:exception) do
       I18n.t(:'baz.missing', :scope => :'foo.bar', :throw => true)
     end
-    assert_equal "translation missing: en.foo.bar.baz.missing", exception.message
+    assert_equal "Translation missing: en.foo.bar.baz.missing", exception.message
   end
 
   test "exceptions: MissingTranslationData message from #translate includes the given scope and full key" do
@@ -18,7 +18,7 @@ class I18nBackendExceptionsTest < I18n::TestCase
       I18n.t(:'baz.missing', :scope => :'foo.bar', :raise => true)
     rescue I18n::MissingTranslationData => exception
     end
-    assert_equal "translation missing: en.foo.bar.baz.missing", exception.message
+    assert_equal "Translation missing: en.foo.bar.baz.missing", exception.message
   end
 
   test "exceptions: MissingTranslationData message from #localize includes the given scope and full key" do
@@ -26,7 +26,7 @@ class I18nBackendExceptionsTest < I18n::TestCase
       I18n.l(Time.now, :format => :foo)
     rescue I18n::MissingTranslationData => exception
     end
-    assert_equal "translation missing: en.time.formats.foo", exception.message
+    assert_equal "Translation missing: en.time.formats.foo", exception.message
   end
 
   test "exceptions: MissingInterpolationArgument message includes missing key, provided keys and full string" do
diff --git a/test/backend/fallbacks_test.rb b/test/backend/fallbacks_test.rb
index a215f73..8c20a04 100644
--- a/test/backend/fallbacks_test.rb
+++ b/test/backend/fallbacks_test.rb
@@ -29,7 +29,7 @@ class I18nBackendFallbacksTranslateTest < I18n::TestCase
   end
 
   test "keeps the count option when defaulting to a different key" do
-    assert_equal 'Interpolate 5 10', I18n.t(:non_existant, default: :interpolate_count, count: 10, value: 5)
+    assert_equal 'Interpolate 5 10', I18n.t(:non_existent, default: :interpolate_count, count: 10, value: 5)
   end
 
   test "returns the :de translation for a missing :'de-DE' when :default is a String" do
@@ -60,8 +60,18 @@ class I18nBackendFallbacksTranslateTest < I18n::TestCase
     assert_nil I18n.t(:missing_bar, :locale => :'de-DE', :default => nil)
   end
 
-  test "returns the translation missing message if the default is also missing" do
-    assert_equal 'translation missing: de-DE.missing_bar', I18n.t(:missing_bar, :locale => :'de-DE', :default => [:missing_baz])
+  test "returns the Translation missing: message if the default is also missing" do
+    translation_missing_message = <<~MSG
+      Translation missing. Options considered were:
+      - de-DE.missing_bar
+      - de-DE.missing_baz
+    MSG
+
+    assert_equal translation_missing_message.chomp, I18n.t(:missing_bar, :locale => :'de-DE', :default => [:missing_baz])
+  end
+
+  test "returns the simple Translation missing: message when default is an empty Array" do
+    assert_equal "Translation missing: de-DE.missing_bar", I18n.t(:missing_bar, :locale => :'de-DE', :default => [])
   end
 
   test "returns the :'de-DE' default :baz translation for a missing :'de-DE' when defaults contains Symbol" do
@@ -192,7 +202,7 @@ class I18nBackendFallbacksLocalizeTestWithMultipleThreads < I18n::TestCase
 end
 
 # See Issue #590
-class I18nBackendFallbacksSymbolReolveRestartsLookupAtOriginalLocale < I18n::TestCase
+class I18nBackendFallbacksSymbolResolveRestartsLookupAtOriginalLocale < I18n::TestCase
   class Backend < I18n::Backend::Simple
     include I18n::Backend::Fallbacks
   end
diff --git a/test/backend/key_value_test.rb b/test/backend/key_value_test.rb
index aefa9ce..34bce7b 100644
--- a/test/backend/key_value_test.rb
+++ b/test/backend/key_value_test.rb
@@ -21,7 +21,7 @@ class I18nBackendKeyValueTest < I18n::TestCase
     assert_flattens({:"a.b"=>['a', 'b']}, {:a=>{:b =>['a', 'b']}}, true, false)
     assert_flattens({:"a.b" => "c"}, {:"a.b" => "c"}, false)
   end
-  
+
   test "store_translations supports numeric keys" do
     setup_backend!
     store_translations(:en, 1 => 'foo')
@@ -60,8 +60,8 @@ class I18nBackendKeyValueTest < I18n::TestCase
     I18n.backend.send(:translations)
     expected = { :en => {:foo => { :bar => 'bar', :baz => 'baz' }} }
     assert_equal expected, translations
-  end 
-  
+  end
+
   test "subtrees enabled: given incomplete pluralization data it raises I18n::InvalidPluralizationData" do
     setup_backend!
     store_translations(:en, :bar => { :one => "One" })
@@ -71,7 +71,7 @@ class I18nBackendKeyValueTest < I18n::TestCase
   test "subtrees disabled: given incomplete pluralization data it returns an error message" do
     setup_backend!(false)
     store_translations(:en, :bar => { :one => "One" })
-    assert_equal "translation missing: en.bar", I18n.t(:bar, :count => 2)
+    assert_equal "Translation missing: en.bar", I18n.t(:bar, :count => 2)
   end
 
   test "translate handles subtrees for pluralization" do
@@ -79,7 +79,7 @@ class I18nBackendKeyValueTest < I18n::TestCase
     store_translations(:en, :bar => { :one => "One" })
     assert_equal("One", I18n.t("bar", :count => 1))
   end
-  
+
   test "subtrees enabled: returns localized string given missing pluralization data" do
     setup_backend!(true)
     assert_equal 'bar', I18n.t("foo.bar", count: 1)
@@ -89,7 +89,7 @@ class I18nBackendKeyValueTest < I18n::TestCase
     setup_backend!(false)
     assert_equal 'bar', I18n.t("foo.bar", count: 1)
   end
-  
+
   test "subtrees enabled: Returns fallback default given missing pluralization data" do
     setup_backend!(true)
     I18n.backend.extend I18n::Backend::Fallbacks
diff --git a/test/backend/lazy_loadable_test.rb b/test/backend/lazy_loadable_test.rb
index a15ab00..01e2efc 100644
--- a/test/backend/lazy_loadable_test.rb
+++ b/test/backend/lazy_loadable_test.rb
@@ -7,7 +7,7 @@ class I18nBackendLazyLoadableTest < I18n::TestCase
     @lazy_mode_backend = I18n::Backend::LazyLoadable.new(lazy_load: true)
     @eager_mode_backend = I18n::Backend::LazyLoadable.new(lazy_load: false)
 
-    I18n.load_path = [File.join(locales_dir, '/en.yml'), File.join(locales_dir,  '/fr.yml')]
+    I18n.load_path = [File.join(locales_dir, '/en.yml'), File.join(locales_dir, '/en.yaml'), File.join(locales_dir,  '/fr.yml')]
   end
 
   test "lazy mode: only loads translations for current locale" do
diff --git a/test/backend/pluralization_test.rb b/test/backend/pluralization_test.rb
index d955818..0a7321a 100644
--- a/test/backend/pluralization_test.rb
+++ b/test/backend/pluralization_test.rb
@@ -9,16 +9,23 @@ class I18nBackendPluralizationTest < I18n::TestCase
   def setup
     super
     I18n.backend = Backend.new
-    @rule = lambda { |n| n == 1 ? :one : n == 0 || (2..10).include?(n % 100) ? :few : (11..19).include?(n % 100) ? :many : :other }
+    @rule = lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : n == 0 || (2..10).include?(n % 100) ? :few : (11..19).include?(n % 100) ? :many : :other }
     store_translations(:xx, :i18n => { :plural => { :rule => @rule } })
-    @entry = { :zero => 'zero', :one => 'one', :few => 'few', :many => 'many', :other => 'other' }
+    @entry = { :"0" => 'none', :"1" => 'single', :one => 'one', :few => 'few', :many => 'many', :other => 'other' }
+    @entry_with_zero = @entry.merge( { :zero => 'zero' } )
   end
 
   test "pluralization picks a pluralizer from :'i18n.pluralize'" do
     assert_equal @rule, I18n.backend.send(:pluralizer, :xx)
   end
 
-  test "pluralization picks :one for 1" do
+  test "pluralization picks the explicit 1 rule for count == 1, the explicit rule takes priority over the matching :one rule" do
+    assert_equal 'single', I18n.t(:count => 1, :default => @entry, :locale => :xx)
+    assert_equal 'single', I18n.t(:count => 1.0, :default => @entry, :locale => :xx)
+  end
+
+  test "pluralization picks :one for 1, since in this case that is the matching rule for 1 (when there is no explicit 1 rule)" do
+    @entry.delete(:"1")
     assert_equal 'one', I18n.t(:count => 1, :default => @entry, :locale => :xx)
   end
 
@@ -31,20 +38,58 @@ class I18nBackendPluralizationTest < I18n::TestCase
   end
 
   test "pluralization picks zero for 0 if the key is contained in the data" do
-    assert_equal 'zero', I18n.t(:count => 0, :default => @entry, :locale => :xx)
+    assert_equal 'zero', I18n.t(:count => 0, :default => @entry_with_zero, :locale => :xx)
   end
 
-  test "pluralization picks few for 0 if the key is not contained in the data" do
-    @entry.delete(:zero)
+  test "pluralization picks explicit 0 rule for count == 0, since the explicit rule takes priority over the matching :few rule" do
+    assert_equal 'none', I18n.t(:count => 0, :default => @entry, :locale => :xx)
+    assert_equal 'none', I18n.t(:count => 0.0, :default => @entry, :locale => :xx)
+    assert_equal 'none', I18n.t(:count => -0, :default => @entry, :locale => :xx)
+  end
+
+  test "pluralization picks :few for 0 (when there is no explicit 0 rule)" do
+    @entry.delete(:"0")
     assert_equal 'few', I18n.t(:count => 0, :default => @entry, :locale => :xx)
   end
 
+  test "pluralization does Lateral Inheritance to :other to cover missing data" do
+    @entry.delete(:many)
+    assert_equal 'other', I18n.t(:count => 11, :default => @entry, :locale => :xx)
+  end
+
   test "pluralization picks one for 1 if the entry has attributes hash on unknown locale" do
     @entry[:attributes] = { :field => 'field', :second => 'second' }
     assert_equal 'one', I18n.t(:count => 1, :default => @entry, :locale => :pirate)
   end
 
+  test "Nested keys within pluralization context" do
+    store_translations(:xx,
+      :stars => {
+        one: "%{count} star",
+        other: "%{count} stars",
+        special: {
+          one: "%{count} special star",
+          other: "%{count} special stars",
+        }
+      }
+    )
+    assert_equal "1 star", I18n.t('stars', count: 1, :locale => :xx)
+    assert_equal "20 stars", I18n.t('stars', count: 20, :locale => :xx)
+    assert_equal "1 special star", I18n.t('stars.special', count: 1, :locale => :xx)
+    assert_equal "20 special stars", I18n.t('stars.special', count: 20, :locale => :xx)
+  end
+
   test "Fallbacks can pick up rules from fallback locales, too" do
     assert_equal @rule, I18n.backend.send(:pluralizer, :'xx-XX')
   end
+
+  test "linked lookup works with pluralization backend" do
+    I18n.backend.store_translations(:xx, {
+      :automobiles => :autos,
+      :autos => :cars,
+      :cars => { :porsche => { :one => "I have %{count} Porsche 🚗", :other => "I have %{count} Porsches 🚗" } }
+    })
+    assert_equal "I have 1 Porsche 🚗", I18n.t(:'automobiles.porsche', count: 1, :locale => :xx)
+    assert_equal "I have 20 Porsches 🚗", I18n.t(:'automobiles.porsche', count: 20, :locale => :xx)
+  end
 end
diff --git a/test/backend/simple_test.rb b/test/backend/simple_test.rb
index 23de486..d568961 100644
--- a/test/backend/simple_test.rb
+++ b/test/backend/simple_test.rb
@@ -209,6 +209,23 @@ class I18nBackendSimpleTest < I18n::TestCase
     assert_equal true, I18n.backend.initialized?
   end
 
+  test "Nested keys within pluralization context" do
+    store_translations(:en,
+      :stars => {
+        one: "%{count} star",
+        other: "%{count} stars",
+        special: {
+          one: "%{count} special star",
+          other: "%{count} special stars",
+        }
+      }
+    )
+    assert_equal "1 star", I18n.t('stars', count: 1, :locale => :en)
+    assert_equal "20 stars", I18n.t('stars', count: 20, :locale => :en)
+    assert_equal "1 special star", I18n.t('stars.special', count: 1, :locale => :en)
+    assert_equal "20 special stars", I18n.t('stars.special', count: 20, :locale => :en)
+  end
+
   test "returns localized string given missing pluralization data" do
     assert_equal 'baz', I18n.t('foo.bar', count: 1)
   end
diff --git a/test/backend/transliterator_test.rb b/test/backend/transliterator_test.rb
index ebd446e..4c2bc22 100644
--- a/test/backend/transliterator_test.rb
+++ b/test/backend/transliterator_test.rb
@@ -42,7 +42,7 @@ class I18nBackendTransliterator < I18n::TestCase
     # create string with range of Unicode's western characters with
     # diacritics, excluding the division and multiplication signs which for
     # some reason or other are floating in the middle of all the letters.
-    string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include? c}.pack("U*")
+    string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include? c}.append(0x1E9E).pack("U*")
     string.split(//) do |char|
       assert_match %r{^[a-zA-Z']*$}, @transliterator.transliterate(string)
     end
diff --git a/test/i18n/interpolate_test.rb b/test/i18n/interpolate_test.rb
index 79e59a9..2d54090 100644
--- a/test/i18n/interpolate_test.rb
+++ b/test/i18n/interpolate_test.rb
@@ -65,9 +65,15 @@ class I18nInterpolateTest < I18n::TestCase
     end
 
   end
+
   test "with String subclass that redefined gsub method" do
     assert_equal "Hello mars world", I18n.interpolate(RailsSafeBuffer.new("Hello %{planet} world"), :planet => 'mars') 
   end
+
+  test "with String subclass that redefined gsub method returns same object if no interpolations" do
+    string = RailsSafeBuffer.new("Hello world")
+    assert_same string, I18n.interpolate(string, :planet => 'mars')
+  end
 end
 
 class I18nMissingInterpolationCustomHandlerTest < I18n::TestCase
diff --git a/test/i18n/middleware_test.rb b/test/i18n/middleware_test.rb
index c8f6d36..f0982cb 100644
--- a/test/i18n/middleware_test.rb
+++ b/test/i18n/middleware_test.rb
@@ -16,7 +16,7 @@ class I18nMiddlewareTest < I18n::TestCase
     refute_equal updated_i18n_config_object_id, old_i18n_config_object_id
   end
 
-  test "succesfully resets i18n locale to default locale by defining new config" do
+  test "successfully resets i18n locale to default locale by defining new config" do
     @middleware.call({})
 
     assert_equal :fr, I18n.locale
diff --git a/test/i18n_test.rb b/test/i18n_test.rb
index a0b8105..3f576fb 100644
--- a/test/i18n_test.rb
+++ b/test/i18n_test.rb
@@ -140,6 +140,10 @@ class I18nTest < I18n::TestCase
     assert_equal [:en, :foo, :bar, :baz, :buz], I18n.normalize_keys(:en, :'baz|buz', :'foo|bar', '|')
   end
 
+  test "normalize_keys normalizes given locale with separator" do
+    assert_equal [:en, :foo, :bar, :baz], I18n.normalize_keys(:"en.foo", :baz, :bar)
+  end
+
   test "can set the exception_handler" do
     begin
       previous_exception_handler = I18n.exception_handler
@@ -205,11 +209,18 @@ class I18nTest < I18n::TestCase
   end
 
   # def test_translate_given_no_args_raises_missing_translation_data
-  #   assert_equal "translation missing: en, no key", I18n.t
+  #   assert_equal "Translation missing: en, no key", I18n.t
   # end
 
   test "translate given a bogus key returns an error message" do
-    assert_equal "translation missing: en.bogus", I18n.t(:bogus)
+    assert_equal "Translation missing: en.bogus", I18n.t(:bogus)
+  end
+
+  test "translate given multiple bogus keys returns an array of error messages" do
+    assert_equal(
+      ["Translation missing: en.bogus", "Translation missing: en.also_bogus"],
+      I18n.t([:bogus, :also_bogus]),
+    )
   end
 
   test "translate given an empty string as a key raises an I18n::ArgumentError" do
@@ -255,7 +266,7 @@ class I18nTest < I18n::TestCase
         I18n.t('foo')
       end
 
-      assert_equal 'translation missing: en.foo', I18n.t('foo', locale: :en)
+      assert_equal 'Translation missing: en.foo', I18n.t('foo', locale: :en)
     end
   end
 
@@ -296,6 +307,10 @@ class I18nTest < I18n::TestCase
     assert_equal true, I18n.exists?(:currency, :nl)
   end
 
+  test "exists? given an existing key and a scope will return true" do
+    assert_equal true, I18n.exists?(:delimiter, scope: [:currency, :format])
+  end
+
   test "exists? given a non-existing key and an existing locale will return false" do
     assert_equal false, I18n.exists?(:bogus, :nl)
   end
@@ -379,7 +394,7 @@ class I18nTest < I18n::TestCase
     assert_equal I18n.default_locale, I18n.locale
   end
 
-  test "I18n.translitarate handles I18n::ArgumentError exception" do
+  test "I18n.transliterate handles I18n::ArgumentError exception" do
     I18n::Backend::Transliterator.stubs(:get).raises(I18n::ArgumentError)
     I18n.exception_handler.expects(:call).raises(I18n::ArgumentError)
     assert_raises(I18n::ArgumentError) {
@@ -387,7 +402,7 @@ class I18nTest < I18n::TestCase
     }
   end
 
-  test "I18n.translitarate raises I18n::ArgumentError exception" do
+  test "I18n.transliterate raises I18n::ArgumentError exception" do
     I18n::Backend::Transliterator.stubs(:get).raises(I18n::ArgumentError)
     I18n.exception_handler.expects(:call).never
     assert_raises(I18n::ArgumentError) {
@@ -490,18 +505,14 @@ class I18nTest < I18n::TestCase
 
   test "can reserve a key" do
     begin
-      reserved_keys_were = I18n::RESERVED_KEYS.dup
-
-      assert !I18n::RESERVED_KEYS.include?(:foo)
-      assert !I18n::RESERVED_KEYS.include?(:bar)
+      stub_const(I18n, :RESERVED_KEYS, []) do
+        I18n.reserve_key(:foo)
+        I18n.reserve_key("bar")
 
-      I18n.reserve_key(:foo)
-      I18n.reserve_key("bar")
-
-      assert I18n::RESERVED_KEYS.include?(:foo)
-      assert I18n::RESERVED_KEYS.include?(:bar)
+        assert I18n::RESERVED_KEYS.include?(:foo)
+        assert I18n::RESERVED_KEYS.include?(:bar)
+      end
     ensure
-      I18n::RESERVED_KEYS = reserved_keys_were
       I18n.instance_variable_set(:@reserved_keys_pattern, nil)
     end
   end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 78b9635..f36b20b 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -44,6 +44,16 @@ class I18n::TestCase < Minitest::Test
   def locales_dir
     File.dirname(__FILE__) + '/test_data/locales'
   end
+
+  def stub_const(klass, constant, new_value)
+    old_value = klass.const_get(constant)
+    klass.send(:remove_const, constant)
+    klass.const_set(constant, new_value)
+    yield
+  ensure
+    klass.send(:remove_const, constant)
+    klass.const_set(constant, old_value)
+  end
 end
 
 class DummyRackApp

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/base.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/cache.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/cache_file.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/cascade.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/chain.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/fallbacks.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/flatten.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/gettext.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/interpolation_compiler.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/key_value.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/lazy_loadable.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/memoize.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/metadata.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/pluralization.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/simple.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/backend/transliterator.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/config.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/exceptions.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/gettext.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/gettext/helpers.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/gettext/po_parser.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/interpolate/ruby.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/locale.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/locale/fallbacks.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/locale/tag.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/locale/tag/parents.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/locale/tag/rfc4646.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/locale/tag/simple.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/middleware.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/basics.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/defaults.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/interpolation.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/link.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/localization.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/localization/date.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/localization/date_time.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/localization/procs.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/localization/time.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/lookup.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/pluralization.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/tests/procs.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/utils.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.14.1/lib/i18n/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/i18n-1.14.1.gemspec

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/base.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/cache.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/cache_file.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/cascade.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/chain.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/fallbacks.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/flatten.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/gettext.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/interpolation_compiler.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/key_value.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/lazy_loadable.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/memoize.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/metadata.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/pluralization.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/simple.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/backend/transliterator.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/config.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/exceptions.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/gettext.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/gettext/helpers.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/gettext/po_parser.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/interpolate/ruby.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/locale.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/locale/fallbacks.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/locale/tag.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/locale/tag/parents.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/locale/tag/rfc4646.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/locale/tag/simple.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/middleware.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/basics.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/defaults.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/interpolation.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/link.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/localization.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/localization/date.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/localization/date_time.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/localization/procs.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/localization/time.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/lookup.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/pluralization.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/tests/procs.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/utils.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/i18n-1.10.0/lib/i18n/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/i18n-1.10.0.gemspec

No differences were encountered in the control files

More details

Full run details