├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── funding.yml └── workflows │ └── ruby.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── benchmark ├── example.yml └── run.rb ├── gemfiles ├── Gemfile.rails-6.0.x ├── Gemfile.rails-6.1.x ├── Gemfile.rails-7.0.x ├── Gemfile.rails-7.1.x ├── Gemfile.rails-7.2.x ├── Gemfile.rails-8.0.x └── Gemfile.rails-main ├── i18n.gemspec ├── lib ├── i18n.rb └── i18n │ ├── backend.rb │ ├── backend │ ├── base.rb │ ├── cache.rb │ ├── cache_file.rb │ ├── cascade.rb │ ├── chain.rb │ ├── fallbacks.rb │ ├── flatten.rb │ ├── gettext.rb │ ├── interpolation_compiler.rb │ ├── key_value.rb │ ├── lazy_loadable.rb │ ├── memoize.rb │ ├── metadata.rb │ ├── pluralization.rb │ ├── simple.rb │ └── transliterator.rb │ ├── config.rb │ ├── exceptions.rb │ ├── gettext.rb │ ├── gettext │ ├── helpers.rb │ └── po_parser.rb │ ├── interpolate │ └── ruby.rb │ ├── locale.rb │ ├── locale │ ├── fallbacks.rb │ ├── tag.rb │ └── tag │ │ ├── parents.rb │ │ ├── rfc4646.rb │ │ └── simple.rb │ ├── middleware.rb │ ├── tests.rb │ ├── tests │ ├── basics.rb │ ├── defaults.rb │ ├── interpolation.rb │ ├── link.rb │ ├── localization.rb │ ├── localization │ │ ├── date.rb │ │ ├── date_time.rb │ │ ├── procs.rb │ │ └── time.rb │ ├── lookup.rb │ ├── pluralization.rb │ └── procs.rb │ ├── utils.rb │ └── version.rb └── test ├── api ├── all_features_test.rb ├── cascade_test.rb ├── chain_test.rb ├── fallbacks_test.rb ├── key_value_test.rb ├── lazy_loadable_test.rb ├── memoize_test.rb ├── override_test.rb ├── pluralization_test.rb └── simple_test.rb ├── backend ├── cache_file_test.rb ├── cache_test.rb ├── cascade_test.rb ├── chain_test.rb ├── exceptions_test.rb ├── fallbacks_test.rb ├── interpolation_compiler_test.rb ├── key_value_test.rb ├── lazy_loadable_test.rb ├── memoize_test.rb ├── metadata_test.rb ├── pluralization_fallback_test.rb ├── pluralization_scope_test.rb ├── pluralization_test.rb ├── simple_test.rb └── transliterator_test.rb ├── gettext ├── api_test.rb └── backend_test.rb ├── i18n ├── exceptions_test.rb ├── gettext_plural_keys_test.rb ├── interpolate_test.rb ├── load_path_test.rb └── middleware_test.rb ├── i18n_test.rb ├── locale ├── fallbacks_test.rb └── tag │ ├── rfc4646_test.rb │ └── simple_test.rb ├── run_all.rb ├── run_one.rb ├── test_data └── locales │ ├── de.po │ ├── en.json │ ├── en.rb │ ├── en.yaml │ ├── en.yml │ ├── fr.yml │ ├── invalid │ ├── empty.yml │ └── syntax.yml │ └── plurals.rb ├── test_helper.rb └── utils_test.rb /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## What I tried to do 11 | 12 | * Fill this out! 13 | 14 | ## What I expected to happen 15 | 16 | * Fill this out! 17 | 18 | ## What actually happened 19 | 20 | * Fill this out! 21 | 22 | ## Versions of i18n, rails, and anything else you think is necessary 23 | 24 | * Fill this out! 25 | 26 | ---- 27 | 28 | Bonus points for providing an application or a small code example which reproduces the issue. 29 | 30 | Thanks! :heart: 31 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [radar] 2 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the master branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build: 15 | env: 16 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | ruby_version: [head, 3.3, 3.2, 3.1, '3.0', jruby] 21 | gemfile: 22 | - Gemfile 23 | - gemfiles/Gemfile.rails-6.0.x 24 | - gemfiles/Gemfile.rails-6.1.x 25 | - gemfiles/Gemfile.rails-7.0.x 26 | - gemfiles/Gemfile.rails-7.1.x 27 | - gemfiles/Gemfile.rails-7.2.x 28 | - gemfiles/Gemfile.rails-8.0.x 29 | - gemfiles/Gemfile.rails-main 30 | exclude: 31 | # Rails 8+ requires at least Ruby 3.2 32 | - ruby_version: '3.1' 33 | gemfile: gemfiles/Gemfile.rails-main 34 | - ruby_version: '3.0' 35 | gemfile: gemfiles/Gemfile.rails-main 36 | - ruby_version: '3.1' 37 | gemfile: gemfiles/Gemfile.rails-8.0.x 38 | - ruby_version: '3.0' 39 | gemfile: gemfiles/Gemfile.rails-8.0.x 40 | # Rails 7.2.x requires at least Ruby 3.1 41 | - ruby_version: '3.0' 42 | gemfile: gemfiles/Gemfile.rails-7.2.x 43 | # JRuby is not supported by Rails 7.0.x 44 | - ruby_version: jruby 45 | gemfile: gemfiles/Gemfile.rails-7.0.x 46 | # JRuby is not supported by Rails 8+ 47 | - ruby_version: jruby 48 | gemfile: gemfiles/Gemfile.rails-8.0.x 49 | - ruby_version: jruby 50 | gemfile: gemfiles/Gemfile.rails-main 51 | 52 | runs-on: ubuntu-latest 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Set up Ruby 57 | uses: ruby/setup-ruby@v1 58 | with: 59 | ruby-version: ${{ matrix.ruby_version }} 60 | bundler-cache: true # 'bundle install' and cache 61 | - name: Build and test with Rake 62 | run: bundle exec rake 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test/rails/fixtures 3 | nbproject/ 4 | vendor/**/* 5 | *.swp 6 | pkg 7 | .bundle 8 | .rvmrc 9 | .ruby-version 10 | .ruby-gemset 11 | .tool-versions 12 | Gemfile.lock 13 | gemfiles/*.lock 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog has moved 2 | 3 | For changes, please see our [Releases page](https://github.com/svenfuchs/i18n/releases). 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'mocha', '~> 2.1.0' 6 | gem 'test_declarative', '0.0.6' 7 | gem 'rake', '~> 13' 8 | gem 'minitest', '~> 5.14' 9 | gem 'json' 10 | gem 'racc' 11 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 The Ruby I18n team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby I18n 2 | 3 | [![Gem Version](https://badge.fury.io/rb/i18n.svg)](https://badge.fury.io/rb/i18n) 4 | [![Build Status](https://github.com/ruby-i18n/i18n/workflows/Ruby/badge.svg)](https://github.com/ruby-i18n/i18n/actions?query=workflow%3ARuby) 5 | 6 | Ruby internationalization and localization (i18n) solution. 7 | 8 | Currently maintained by @radar. 9 | 10 | ## Usage 11 | 12 | ### Rails 13 | 14 | You will most commonly use this library within a Rails app. 15 | 16 | We support Rails versions from 6.0 and up. 17 | 18 | [See the Rails Guide](https://guides.rubyonrails.org/i18n.html) for an example of its usage. 19 | 20 | ### Ruby (without Rails) 21 | 22 | We support Ruby versions from 3.0 and up. 23 | 24 | If you want to use this library without Rails, you can simply add `i18n` to your `Gemfile`: 25 | 26 | ```ruby 27 | gem 'i18n' 28 | ``` 29 | 30 | Then configure I18n with some translations, and a default locale: 31 | 32 | ```ruby 33 | I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"] 34 | I18n.default_locale = :en # (note that `en` is already the default!) 35 | ``` 36 | 37 | A simple translation file in your project might live at `config/locales/en.yml` and look like: 38 | 39 | ```yml 40 | en: 41 | test: "This is a test" 42 | ``` 43 | 44 | You can then access this translation by doing: 45 | 46 | ```ruby 47 | I18n.t(:test) 48 | ``` 49 | 50 | You can switch locales in your project by setting `I18n.locale` to a different value: 51 | 52 | ```ruby 53 | I18n.locale = :de 54 | I18n.t(:test) # => "Dies ist ein Test" 55 | ``` 56 | 57 | ## Features 58 | 59 | * Translation and localization 60 | * Interpolation of values to translations 61 | * Pluralization (CLDR compatible) 62 | * Customizable transliteration to ASCII 63 | * Flexible defaults 64 | * Bulk lookup 65 | * Lambdas as translation data 66 | * Custom key/scope separator 67 | * Custom exception handlers 68 | * Extensible architecture with a swappable backend 69 | 70 | ## Pluggable Features 71 | 72 | * Cache 73 | * Pluralization: lambda pluralizers stored as translation data 74 | * Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation) 75 | * [Gettext support](https://github.com/ruby-i18n/i18n/wiki/Gettext) 76 | * Translation metadata 77 | 78 | ## Alternative Backend 79 | 80 | * Chain 81 | * ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs) 82 | * KeyValue (uses active_support/json and cannot store procs) 83 | 84 | For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/ruby-i18n/i18n/wiki/Resources). 85 | 86 | ## Tests 87 | 88 | You can run tests both with 89 | 90 | * `rake test` or just `rake` 91 | * run any test file directly, e.g. `ruby -Ilib:test test/api/simple_test.rb` 92 | 93 | You can run all tests against all Gemfiles with 94 | 95 | * `ruby test/run_all.rb` 96 | 97 | The structure of the test suite is a bit unusual as it uses modules to reuse 98 | particular tests in different test cases. 99 | 100 | The reason for this is that we need to enforce the I18n API across various 101 | combinations of extensions. E.g. the Simple backend alone needs to support 102 | the same API as any combination of feature and/or optimization modules included 103 | to the Simple backend. We test this by reusing the same API definition (implemented 104 | as test methods) in test cases with different setups. 105 | 106 | You can find the test cases that enforce the API in test/api. And you can find 107 | the API definition test methods in test/api/tests. 108 | 109 | All other test cases (e.g. as defined in test/backend, test/core_ext) etc. 110 | follow the usual test setup and should be easy to grok. 111 | 112 | ## More Documentation 113 | 114 | Additional documentation can be found here: https://github.com/ruby-i18n/i18n/wiki 115 | 116 | ## Contributors 117 | 118 | * @radar 119 | * @carlosantoniodasilva 120 | * @josevalim 121 | * @knapo 122 | * @tigrish 123 | * [and many more](https://github.com/ruby-i18n/i18n/graphs/contributors) 124 | 125 | ## License 126 | 127 | MIT License. See the included MIT-LICENSE file. 128 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | task :default => [:test] 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << 'lib' 8 | t.libs << 'test' 9 | t.pattern = "test/**/*_test.rb" 10 | t.verbose = true 11 | t.warning = true 12 | end 13 | Rake::Task['test'].comment = "Run all i18n tests" 14 | -------------------------------------------------------------------------------- /benchmark/example.yml: -------------------------------------------------------------------------------- 1 | en: 2 | first: "First" 3 | 4 | activemodel: 5 | errors: 6 | messages: :"activerecord.errors.messages" 7 | 8 | activerecord: 9 | errors: 10 | messages: 11 | inclusion: "is not included in the list" 12 | exclusion: "is reserved" 13 | invalid: "is invalid" 14 | confirmation: "doesn't match confirmation" 15 | accepted: "must be accepted" 16 | empty: "can't be empty" 17 | blank: "can't be blank" 18 | too_long: "is too long (maximum is %{count} characters)" 19 | too_short: "is too short (minimum is %{count} characters)" 20 | wrong_length: "is the wrong length (should be %{count} characters)" 21 | taken: "has already been taken" 22 | not_a_number: "is not a number" 23 | greater_than: "must be greater than %{count}" 24 | greater_than_or_equal_to: "must be greater than or equal to %{count}" 25 | equal_to: "must be equal to %{count}" 26 | less_than: "must be less than %{count}" 27 | less_than_or_equal_to: "must be less than or equal to %{count}" 28 | odd: "must be odd" 29 | even: "must be even" 30 | record_invalid: "Validation failed: %{errors}" 31 | 32 | models: 33 | user: 34 | blank: "This is a custom blank message for %{model}: %{attribute}" 35 | attributes: 36 | login: 37 | blank: "This is a custom blank message for User login" 38 | 39 | models: 40 | user: "Dude" 41 | 42 | attributes: 43 | admins: 44 | user: 45 | login: "Handle" 46 | 47 | date: 48 | formats: 49 | # Use the strftime parameters for formats. 50 | # When no format has been given, it uses default. 51 | # You can provide other formats here if you like! 52 | default: "%Y-%m-%d" 53 | short: "%b %d" 54 | long: "%B %d, %Y" 55 | 56 | day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] 57 | abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] 58 | 59 | # Don't forget the nil at the beginning; there's no such thing as a 0th month 60 | month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] 61 | abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] 62 | # Used in date_select and datime_select. 63 | order: 64 | - :year, 65 | - :month, 66 | - :day 67 | 68 | time: 69 | formats: 70 | default: "%a, %d %b %Y %H:%M:%S %z" 71 | short: "%d %b %H:%M" 72 | long: "%B %d, %Y %H:%M" 73 | am: "am" 74 | pm: "pm" 75 | 76 | support: 77 | array: 78 | words_connector: ", " 79 | two_words_connector: " and " 80 | last_word_connector: ", and " 81 | 82 | activemodel: 83 | errors: 84 | messages: 85 | inclusion: "is not included in the list" 86 | exclusion: "is reserved" 87 | invalid: "is invalid" 88 | confirmation: "doesn't match confirmation" 89 | accepted: "must be accepted" 90 | empty: "can't be empty" 91 | blank: "can't be blank" 92 | too_long: "is too long (maximum is %{count} characters)" 93 | too_short: "is too short (minimum is %{count} characters)" 94 | wrong_length: "is the wrong length (should be %{count} characters)" 95 | taken: "has already been taken" 96 | not_a_number: "is not a number" 97 | greater_than: "must be greater than %{count}" 98 | greater_than_or_equal_to: "must be greater than or equal to %{count}" 99 | equal_to: "must be equal to %{count}" 100 | less_than: "must be less than %{count}" 101 | less_than_or_equal_to: "must be less than or equal to %{count}" 102 | odd: "must be odd" 103 | even: "must be even" 104 | record_invalid: "Validation failed: %{errors}" 105 | 106 | models: 107 | user: 108 | blank: "This is a custom blank message for %{model}: %{attribute}" 109 | attributes: 110 | login: 111 | blank: "This is a custom blank message for User login" 112 | 113 | models: 114 | user: "Dude" 115 | 116 | attributes: 117 | user: 118 | login: "Handle" 119 | 120 | model_data: 121 | date: 122 | formats: 123 | # Use the strftime parameters for formats. 124 | # When no format has been given, it uses default. 125 | # You can provide other formats here if you like! 126 | default: "%Y-%m-%d" 127 | short: "%b %d" 128 | long: "%B %d, %Y" 129 | 130 | day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] 131 | abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] 132 | 133 | # Don't forget the nil at the beginning; there's no such thing as a 0th month 134 | month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] 135 | abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] 136 | # Used in date_select and datime_select. 137 | order: 138 | - :year 139 | - :month 140 | - :day 141 | 142 | time: 143 | formats: 144 | default: "%a, %d %b %Y %H:%M:%S %z" 145 | short: "%d %b %H:%M" 146 | long: "%B %d, %Y %H:%M" 147 | am: "am" 148 | pm: "pm" 149 | 150 | support: 151 | array: 152 | words_connector: ", " 153 | two_words_connector: " and " 154 | last_word_connector: ", and " 155 | -------------------------------------------------------------------------------- /benchmark/run.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | $:.unshift File.expand_path('../../lib', __FILE__) 3 | 4 | require 'bundler/setup' 5 | require 'i18n' 6 | require 'benchmark' 7 | require 'yaml' 8 | 9 | N = (ARGV.shift || 1000).to_i 10 | YAML_HASH = YAML.load_file(File.expand_path("example.yml", File.dirname(__FILE__))) 11 | 12 | module Backends 13 | Simple = I18n::Backend::Simple.new 14 | 15 | Interpolation = Class.new(I18n::Backend::Simple) do 16 | include I18n::Backend::InterpolationCompiler 17 | end.new 18 | 19 | begin 20 | require 'active_support' 21 | KeyValue = I18n::Backend::KeyValue.new({}, true) 22 | puts "Running KeyValue with ActiveSupport #{ActiveSupport::VERSION::STRING}" 23 | rescue LoadError 24 | puts 'Skipping KeyValue since ActiveSupport could not be loaded.' 25 | end 26 | end 27 | 28 | ORDER = %w(Simple Interpolation KeyValue) 29 | ORDER.map!(&:to_sym) if RUBY_VERSION > '1.9' 30 | 31 | module Benchmark 32 | WIDTH = 20 33 | 34 | def self.rt(label = "", n=N, &blk) 35 | print label.ljust(WIDTH) 36 | time, objects = measure_objects(n, &blk) 37 | time = time.respond_to?(:real) ? time.real : time 38 | print format("%8.2f ms %8d objects\n", time * 1000, objects) 39 | rescue Exception => e 40 | print "FAILED: #{e.message}" 41 | end 42 | 43 | if ObjectSpace.respond_to?(:allocated_objects) 44 | def self.measure_objects(n, &blk) 45 | obj = ObjectSpace.allocated_objects 46 | t = Benchmark.realtime { n.times(&blk) } 47 | [t, ObjectSpace.allocated_objects - obj] 48 | end 49 | else 50 | def self.measure_objects(n, &blk) 51 | [Benchmark.measure { n.times(&blk) }, 0] 52 | end 53 | end 54 | end 55 | 56 | benchmarker = lambda do |backend_name| 57 | I18n.backend = Backends.const_get(backend_name) 58 | puts "=> #{backend_name}\n\n" 59 | 60 | Benchmark.rt "store", 1 do 61 | I18n.backend.store_translations(*YAML_HASH.to_a.first) 62 | end 63 | 64 | I18n.backend.translate :en, :first 65 | 66 | Benchmark.rt "available_locales" do 67 | I18n.backend.available_locales 68 | end 69 | 70 | Benchmark.rt "t (depth=3)" do 71 | I18n.backend.translate :en, :"activerecord.models.user" 72 | end 73 | 74 | Benchmark.rt "t (depth=5)" do 75 | I18n.backend.translate :en, :"activerecord.attributes.admins.user.login" 76 | end 77 | 78 | Benchmark.rt "t (depth=7)" do 79 | I18n.backend.translate :en, :"activerecord.errors.models.user.attributes.login.blank" 80 | end 81 | 82 | Benchmark.rt "t w/ default" do 83 | I18n.backend.translate :en, :"activerecord.models.another", :default => "Another" 84 | end 85 | 86 | Benchmark.rt "t w/ interpolation" do 87 | I18n.backend.translate :en, :"activerecord.errors.models.user.blank", :model => "User", :attribute => "name" 88 | end 89 | 90 | Benchmark.rt "t w/ link" do 91 | I18n.backend.translate :en, :"activemodel.errors.messages.blank" 92 | end 93 | 94 | Benchmark.rt "t subtree" do 95 | I18n.backend.translate :en, :"activerecord.errors.messages" 96 | end 97 | 98 | puts 99 | end 100 | 101 | # Run! 102 | puts 103 | puts "Running benchmarks with N = #{N}\n\n" 104 | (ORDER & Backends.constants).each(&benchmarker) 105 | 106 | Backends.constants.each do |backend_name| 107 | backend = Backends.const_get(backend_name) 108 | backend.reload! 109 | backend.extend I18n::Backend::Memoize 110 | end 111 | 112 | puts "Running memoized benchmarks with N = #{N}\n\n" 113 | (ORDER & Backends.constants).each(&benchmarker) 114 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-6.0.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'activesupport', '~> 6.0.0' 6 | gem 'mocha', '~> 2.1.0' 7 | gem 'test_declarative', '0.0.6' 8 | gem 'rake' 9 | gem 'minitest', '~> 5.14' 10 | gem 'racc' 11 | 12 | platforms :mri do 13 | gem 'oj' 14 | end 15 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-6.1.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'activesupport', '~> 6.1' 6 | gem 'mocha', '~> 2.1.0' 7 | gem 'test_declarative', '0.0.6' 8 | gem 'rake' 9 | gem 'minitest', '~> 5.14' 10 | gem 'racc' 11 | gem 'base64' 12 | gem 'mutex_m' 13 | 14 | platforms :mri do 15 | gem 'oj' 16 | end 17 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.0.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'activesupport', '~> 7.0' 6 | gem 'mocha', '~> 2.1.0' 7 | gem 'test_declarative', '0.0.6' 8 | gem 'rake' 9 | gem 'minitest', '~> 5.14' 10 | gem 'racc' 11 | 12 | platforms :mri do 13 | gem 'oj' 14 | end 15 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.1.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'activesupport', '~> 7.1' 6 | gem 'mocha', '~> 2' 7 | gem 'test_declarative', '0.0.6' 8 | gem 'rake' 9 | gem 'minitest', '~> 5.14' 10 | gem 'racc' 11 | 12 | platforms :mri do 13 | gem 'oj' 14 | end 15 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.2.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'activesupport', '~> 7.2' 6 | gem 'mocha', '~> 2' 7 | gem 'test_declarative', '0.0.6' 8 | gem 'rake' 9 | gem 'minitest', '~> 5.1' 10 | gem 'racc' 11 | 12 | platforms :mri do 13 | gem 'oj' 14 | end 15 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-8.0.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'activesupport', '~> 8.0' 6 | gem 'mocha', '~> 2' 7 | gem 'test_declarative', '0.0.6' 8 | gem 'rake' 9 | gem 'minitest', '~> 5.1' 10 | gem 'racc' 11 | 12 | platforms :mri do 13 | gem 'oj' 14 | end 15 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-main: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'activesupport', github: 'rails/rails', branch: 'main' 6 | gem 'mocha', '~> 2.1.0' 7 | gem 'test_declarative', '0.0.6' 8 | gem 'rake' 9 | gem 'minitest', '~> 5.1' 10 | gem 'racc' 11 | 12 | platforms :mri do 13 | gem 'oj' 14 | end 15 | -------------------------------------------------------------------------------- /i18n.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | $: << File.expand_path('../lib', __FILE__) 4 | require 'i18n/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "i18n" 8 | s.version = I18n::VERSION 9 | s.authors = ["Sven Fuchs", "Joshua Harvey", "Matt Aimonetti", "Stephan Soller", "Saimon Moore", "Ryan Bigg"] 10 | s.email = "rails-i18n@googlegroups.com" 11 | s.homepage = "https://github.com/ruby-i18n/i18n" 12 | s.summary = "New wave Internationalization support for Ruby" 13 | s.description = "New wave Internationalization support for Ruby." 14 | s.license = "MIT" 15 | 16 | s.metadata = { 17 | 'bug_tracker_uri' => 'https://github.com/ruby-i18n/i18n/issues', 18 | 'changelog_uri' => 'https://github.com/ruby-i18n/i18n/releases', 19 | 'documentation_uri' => 'https://guides.rubyonrails.org/i18n.html', 20 | 'source_code_uri' => 'https://github.com/ruby-i18n/i18n', 21 | } 22 | 23 | s.files = Dir.glob("lib/**/*") + %w(README.md MIT-LICENSE) 24 | s.platform = Gem::Platform::RUBY 25 | s.require_path = 'lib' 26 | s.required_rubygems_version = '>= 1.3.5' 27 | s.required_ruby_version = '>= 2.3.0' 28 | s.add_dependency 'concurrent-ruby', '~> 1.0' 29 | end 30 | -------------------------------------------------------------------------------- /lib/i18n/backend.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | module Backend 5 | autoload :Base, 'i18n/backend/base' 6 | autoload :Cache, 'i18n/backend/cache' 7 | autoload :CacheFile, 'i18n/backend/cache_file' 8 | autoload :Cascade, 'i18n/backend/cascade' 9 | autoload :Chain, 'i18n/backend/chain' 10 | autoload :Fallbacks, 'i18n/backend/fallbacks' 11 | autoload :Flatten, 'i18n/backend/flatten' 12 | autoload :Gettext, 'i18n/backend/gettext' 13 | autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler' 14 | autoload :KeyValue, 'i18n/backend/key_value' 15 | autoload :LazyLoadable, 'i18n/backend/lazy_loadable' 16 | autoload :Memoize, 'i18n/backend/memoize' 17 | autoload :Metadata, 'i18n/backend/metadata' 18 | autoload :Pluralization, 'i18n/backend/pluralization' 19 | autoload :Simple, 'i18n/backend/simple' 20 | autoload :Transliterator, 'i18n/backend/transliterator' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/i18n/backend/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This module allows you to easily cache all responses from the backend - thus 4 | # speeding up the I18n aspects of your application quite a bit. 5 | # 6 | # To enable caching you can simply include the Cache module to the Simple 7 | # backend - or whatever other backend you are using: 8 | # 9 | # I18n::Backend::Simple.send(:include, I18n::Backend::Cache) 10 | # 11 | # You will also need to set a cache store implementation that you want to use: 12 | # 13 | # I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store) 14 | # 15 | # You can use any cache implementation you want that provides the same API as 16 | # ActiveSupport::Cache (only the methods #fetch and #write are being used). 17 | # 18 | # The cache_key implementation by default assumes you pass values that return 19 | # a valid key from #hash (see 20 | # https://www.ruby-doc.org/core/classes/Object.html#M000337). However, you can 21 | # configure your own digest method via which responds to #hexdigest (see 22 | # https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/Digest.html): 23 | # 24 | # I18n.cache_key_digest = OpenSSL::Digest::SHA256.new 25 | # 26 | # If you use a lambda as a default value in your translation like this: 27 | # 28 | # I18n.t(:"date.order", :default => lambda {[:month, :day, :year]}) 29 | # 30 | # Then you will always have a cache miss, because each time this method 31 | # is called the lambda will have a different hash value. If you know 32 | # the result of the lambda is a constant as in the example above, then 33 | # to cache this you can make the lambda a constant, like this: 34 | # 35 | # DEFAULT_DATE_ORDER = lambda {[:month, :day, :year]} 36 | # ... 37 | # I18n.t(:"date.order", :default => DEFAULT_DATE_ORDER) 38 | # 39 | # If the lambda may result in different values for each call then consider 40 | # also using the Memoize backend. 41 | # 42 | module I18n 43 | class << self 44 | @@cache_store = nil 45 | @@cache_namespace = nil 46 | @@cache_key_digest = nil 47 | 48 | def cache_store 49 | @@cache_store 50 | end 51 | 52 | def cache_store=(store) 53 | @@cache_store = store 54 | end 55 | 56 | def cache_namespace 57 | @@cache_namespace 58 | end 59 | 60 | def cache_namespace=(namespace) 61 | @@cache_namespace = namespace 62 | end 63 | 64 | def cache_key_digest 65 | @@cache_key_digest 66 | end 67 | 68 | def cache_key_digest=(key_digest) 69 | @@cache_key_digest = key_digest 70 | end 71 | 72 | def perform_caching? 73 | !cache_store.nil? 74 | end 75 | end 76 | 77 | module Backend 78 | # TODO Should the cache be cleared if new translations are stored? 79 | module Cache 80 | def translate(locale, key, options = EMPTY_HASH) 81 | I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super 82 | end 83 | 84 | protected 85 | 86 | def fetch(cache_key, &block) 87 | result = _fetch(cache_key, &block) 88 | throw(:exception, result) if result.is_a?(MissingTranslation) 89 | result = result.dup if result.frozen? rescue result 90 | result 91 | end 92 | 93 | def _fetch(cache_key, &block) 94 | result = I18n.cache_store.read(cache_key) 95 | return result unless result.nil? 96 | result = catch(:exception, &block) 97 | I18n.cache_store.write(cache_key, result) unless result.is_a?(Proc) 98 | result 99 | end 100 | 101 | def cache_key(locale, key, options) 102 | # This assumes that only simple, native Ruby values are passed to I18n.translate. 103 | "i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{digest_item(options)}" 104 | end 105 | 106 | private 107 | 108 | def digest_item(key) 109 | I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.to_s.hash 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/i18n/backend/cache_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | 5 | module I18n 6 | module Backend 7 | # Overwrites the Base load_file method to cache loaded file contents. 8 | module CacheFile 9 | # Optionally provide path_roots array to normalize filename paths, 10 | # to make the cached i18n data portable across environments. 11 | attr_accessor :path_roots 12 | 13 | protected 14 | 15 | # Track loaded translation files in the `i18n.load_file` scope, 16 | # and skip loading the file if its contents are still up-to-date. 17 | def load_file(filename) 18 | initialized = !respond_to?(:initialized?) || initialized? 19 | key = I18n::Backend::Flatten.escape_default_separator(normalized_path(filename)) 20 | old_mtime, old_digest = initialized && lookup(:i18n, key, :load_file) 21 | return if (mtime = File.mtime(filename).to_i) == old_mtime || 22 | (digest = OpenSSL::Digest::SHA256.file(filename).hexdigest) == old_digest 23 | super 24 | store_translations(:i18n, load_file: { key => [mtime, digest] }) 25 | end 26 | 27 | # Translate absolute filename to relative path for i18n key. 28 | def normalized_path(file) 29 | return file unless path_roots 30 | path = path_roots.find(&file.method(:start_with?)) || 31 | raise(InvalidLocaleData.new(file, 'outside expected path roots')) 32 | file.sub(path, path_roots.index(path).to_s) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/i18n/backend/cascade.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The Cascade module adds the ability to do cascading lookups to backends that 4 | # are compatible to the Simple backend. 5 | # 6 | # By cascading lookups we mean that for any key that can not be found the 7 | # Cascade module strips one segment off the scope part of the key and then 8 | # tries to look up the key in that scope. 9 | # 10 | # E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then 11 | # the segment :bar will be stripped off the scope part :"foo.bar" and the new 12 | # scope :foo will be used to look up the key :baz. If that does not succeed 13 | # then the remaining scope segment :foo will be omitted, too, and again the 14 | # key :baz will be looked up (now with no scope). 15 | # 16 | # To enable a cascading lookup one passes the :cascade option: 17 | # 18 | # I18n.t(:'foo.bar.baz', :cascade => true) 19 | # 20 | # This will return the first translation found for :"foo.bar.baz", :"foo.baz" 21 | # or :baz in this order. 22 | # 23 | # The cascading lookup takes precedence over resolving any given defaults. 24 | # I.e. defaults will kick in after the cascading lookups haven't succeeded. 25 | # 26 | # This behavior is useful for libraries like ActiveRecord validations where 27 | # the library wants to give users a bunch of more or less fine-grained options 28 | # of scopes for a particular key. 29 | # 30 | # Thanks to Clemens Kofler for the initial idea and implementation! See 31 | # http://github.com/clemens/i18n-cascading-backend 32 | 33 | module I18n 34 | module Backend 35 | module Cascade 36 | def lookup(locale, key, scope = [], options = EMPTY_HASH) 37 | return super unless cascade = options[:cascade] 38 | 39 | cascade = { :step => 1 } unless cascade.is_a?(Hash) 40 | step = cascade[:step] || 1 41 | offset = cascade[:offset] || 1 42 | separator = options[:separator] || I18n.default_separator 43 | skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true 44 | 45 | scope = I18n.normalize_keys(nil, key, scope, separator) 46 | key = (scope.slice!(-offset, offset) || []).join(separator) 47 | 48 | begin 49 | result = super 50 | return result unless result.nil? 51 | scope = scope.dup 52 | end while (!scope.empty? || !skip_root) && scope.slice!(-step, step) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/i18n/backend/chain.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | module Backend 5 | # Backend that chains multiple other backends and checks each of them when 6 | # a translation needs to be looked up. This is useful when you want to use 7 | # standard translations with a Simple backend but store custom application 8 | # translations in a database or other backends. 9 | # 10 | # To use the Chain backend instantiate it and set it to the I18n module. 11 | # You can add chained backends through the initializer or backends 12 | # accessor: 13 | # 14 | # # preserves the existing Simple backend set to I18n.backend 15 | # I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend) 16 | # 17 | # The implementation assumes that all backends added to the Chain implement 18 | # a lookup method with the same API as Simple backend does. 19 | # 20 | # Fallback translations using the :default option are only used by the last backend of a chain. 21 | class Chain 22 | module Implementation 23 | include Base 24 | 25 | attr_accessor :backends 26 | 27 | def initialize(*backends) 28 | self.backends = backends 29 | end 30 | 31 | def initialized? 32 | backends.all? do |backend| 33 | backend.instance_eval do 34 | return false unless initialized? 35 | end 36 | end 37 | true 38 | end 39 | 40 | def reload! 41 | backends.each { |backend| backend.reload! } 42 | end 43 | 44 | def eager_load! 45 | backends.each { |backend| backend.eager_load! } 46 | end 47 | 48 | def store_translations(locale, data, options = EMPTY_HASH) 49 | backends.first.store_translations(locale, data, options) 50 | end 51 | 52 | def available_locales 53 | backends.map { |backend| backend.available_locales }.flatten.uniq 54 | end 55 | 56 | def translate(locale, key, default_options = EMPTY_HASH) 57 | namespace = nil 58 | options = Utils.except(default_options, :default) 59 | 60 | backends.each do |backend| 61 | catch(:exception) do 62 | options = default_options if backend == backends.last 63 | translation = backend.translate(locale, key, options) 64 | if namespace_lookup?(translation, options) 65 | namespace = _deep_merge(translation, namespace || {}) 66 | elsif !translation.nil? || (options.key?(:default) && options[:default].nil?) 67 | return translation 68 | end 69 | end 70 | end 71 | 72 | return namespace if namespace 73 | throw(:exception, I18n::MissingTranslation.new(locale, key, options)) 74 | end 75 | 76 | def exists?(locale, key, options = EMPTY_HASH) 77 | backends.any? do |backend| 78 | backend.exists?(locale, key, options) 79 | end 80 | end 81 | 82 | def localize(locale, object, format = :default, options = EMPTY_HASH) 83 | backends.each do |backend| 84 | catch(:exception) do 85 | result = backend.localize(locale, object, format, options) and return result 86 | end 87 | end 88 | throw(:exception, I18n::MissingTranslation.new(locale, format, options)) 89 | end 90 | 91 | protected 92 | def init_translations 93 | backends.each do |backend| 94 | backend.send(:init_translations) 95 | end 96 | end 97 | 98 | def translations 99 | backends.reverse.each_with_object({}) do |backend, memo| 100 | partial_translations = backend.instance_eval do 101 | init_translations unless initialized? 102 | translations 103 | end 104 | Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a } 105 | end 106 | end 107 | 108 | def namespace_lookup?(result, options) 109 | result.is_a?(Hash) && !options.has_key?(:count) 110 | end 111 | 112 | private 113 | # This is approximately what gets used in ActiveSupport. 114 | # However since we are not guaranteed to run in an ActiveSupport context 115 | # it is wise to have our own copy. We underscore it 116 | # to not pollute the namespace of the including class. 117 | def _deep_merge(hash, other_hash) 118 | copy = hash.dup 119 | other_hash.each_pair do |k,v| 120 | value_from_other = hash[k] 121 | copy[k] = value_from_other.is_a?(Hash) && v.is_a?(Hash) ? _deep_merge(value_from_other, v) : v 122 | end 123 | copy 124 | end 125 | end 126 | 127 | include Implementation 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/i18n/backend/fallbacks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # I18n locale fallbacks are useful when you want your application to use 4 | # translations from other locales when translations for the current locale are 5 | # missing. E.g. you might want to use :en translations when translations in 6 | # your applications main locale :de are missing. 7 | # 8 | # To enable locale fallbacks you can simply include the Fallbacks module to 9 | # the Simple backend - or whatever other backend you are using: 10 | # 11 | # I18n::Backend::Simple.include(I18n::Backend::Fallbacks) 12 | module I18n 13 | @@fallbacks = nil 14 | 15 | class << self 16 | # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+. 17 | def fallbacks 18 | @@fallbacks ||= I18n::Locale::Fallbacks.new 19 | Thread.current[:i18n_fallbacks] || @@fallbacks 20 | end 21 | 22 | # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation. 23 | def fallbacks=(fallbacks) 24 | @@fallbacks = fallbacks.is_a?(Array) ? I18n::Locale::Fallbacks.new(fallbacks) : fallbacks 25 | Thread.current[:i18n_fallbacks] = @@fallbacks 26 | end 27 | end 28 | 29 | module Backend 30 | module Fallbacks 31 | # Overwrites the Base backend translate method so that it will try each 32 | # locale given by I18n.fallbacks for the given locale. E.g. for the 33 | # locale :"de-DE" it might try the locales :"de-DE", :de and :en 34 | # (depends on the fallbacks implementation) until it finds a result with 35 | # the given options. If it does not find any result for any of the 36 | # locales it will then throw MissingTranslation as usual. 37 | # 38 | # The default option takes precedence over fallback locales only when 39 | # it's a Symbol. When the default contains a String, Proc or Hash 40 | # it is evaluated last after all the fallback locales have been tried. 41 | def translate(locale, key, options = EMPTY_HASH) 42 | return super unless options.fetch(:fallback, true) 43 | return super if options[:fallback_in_progress] 44 | default = extract_non_symbol_default!(options) if options[:default] 45 | 46 | fallback_options = options.merge(:fallback_in_progress => true, fallback_original_locale: locale) 47 | I18n.fallbacks[locale].each do |fallback| 48 | begin 49 | catch(:exception) do 50 | result = super(fallback, key, fallback_options) 51 | unless result.nil? 52 | on_fallback(locale, fallback, key, options) if locale.to_s != fallback.to_s 53 | return result 54 | end 55 | end 56 | rescue I18n::InvalidLocale 57 | # we do nothing when the locale is invalid, as this is a fallback anyways. 58 | end 59 | end 60 | 61 | return if options.key?(:default) && options[:default].nil? 62 | 63 | return super(locale, nil, options.merge(:default => default)) if default 64 | throw(:exception, I18n::MissingTranslation.new(locale, key, options)) 65 | end 66 | 67 | def resolve_entry(locale, object, subject, options = EMPTY_HASH) 68 | return subject if options[:resolve] == false 69 | result = catch(:exception) do 70 | options.delete(:fallback_in_progress) if options.key?(:fallback_in_progress) 71 | 72 | case subject 73 | when Symbol 74 | I18n.translate(subject, **options.merge( 75 | :locale => options[:fallback_original_locale], 76 | :throw => true, 77 | :skip_interpolation => true 78 | )) 79 | when Proc 80 | date_or_time = options.delete(:object) || object 81 | resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options)) 82 | else 83 | subject 84 | end 85 | end 86 | result unless result.is_a?(MissingTranslation) 87 | end 88 | 89 | def extract_non_symbol_default!(options) 90 | defaults = [options[:default]].flatten 91 | first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)} 92 | if first_non_symbol_default 93 | options[:default] = defaults[0, defaults.index(first_non_symbol_default)] 94 | end 95 | return first_non_symbol_default 96 | end 97 | 98 | def exists?(locale, key, options = EMPTY_HASH) 99 | return super unless options.fetch(:fallback, true) 100 | I18n.fallbacks[locale].each do |fallback| 101 | begin 102 | return true if super(fallback, key, options) 103 | rescue I18n::InvalidLocale 104 | # we do nothing when the locale is invalid, as this is a fallback anyways. 105 | end 106 | end 107 | 108 | false 109 | end 110 | 111 | private 112 | 113 | # Overwrite on_fallback to add specified logic when the fallback succeeds. 114 | def on_fallback(_original_locale, _fallback_locale, _key, _options) 115 | nil 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/i18n/backend/flatten.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | module Backend 5 | # This module contains several helpers to assist flattening translations. 6 | # You may want to flatten translations for: 7 | # 8 | # 1) speed up lookups, as in the Memoize backend; 9 | # 2) In case you want to store translations in a data store, as in ActiveRecord backend; 10 | # 11 | # You can check both backends above for some examples. 12 | # This module also keeps all links in a hash so they can be properly resolved when flattened. 13 | module Flatten 14 | SEPARATOR_ESCAPE_CHAR = "\001" 15 | FLATTEN_SEPARATOR = "." 16 | 17 | # normalize_keys the flatten way. This method is significantly faster 18 | # and creates way less objects than the one at I18n.normalize_keys. 19 | # It also handles escaping the translation keys. 20 | def self.normalize_flat_keys(locale, key, scope, separator) 21 | keys = [scope, key] 22 | keys.flatten! 23 | keys.compact! 24 | 25 | separator ||= I18n.default_separator 26 | 27 | if separator != FLATTEN_SEPARATOR 28 | from_str = "#{FLATTEN_SEPARATOR}#{separator}" 29 | to_str = "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}" 30 | 31 | keys.map! { |k| k.to_s.tr from_str, to_str } 32 | end 33 | 34 | keys.join(".") 35 | end 36 | 37 | # Receives a string and escape the default separator. 38 | def self.escape_default_separator(key) #:nodoc: 39 | key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR) 40 | end 41 | 42 | # Shortcut to I18n::Backend::Flatten.normalize_flat_keys 43 | # and then resolve_links. 44 | def normalize_flat_keys(locale, key, scope, separator) 45 | key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator) 46 | resolve_link(locale, key) 47 | end 48 | 49 | # Store flattened links. 50 | def links 51 | @links ||= I18n.new_double_nested_cache 52 | end 53 | 54 | # Flatten keys for nested Hashes by chaining up keys: 55 | # 56 | # >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind 57 | # => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" } 58 | # 59 | def flatten_keys(hash, escape, prev_key=nil, &block) 60 | hash.each_pair do |key, value| 61 | key = escape_default_separator(key) if escape 62 | curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym 63 | yield curr_key, value 64 | flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash) 65 | end 66 | end 67 | 68 | # Receives a hash of translations (where the key is a locale and 69 | # the value is another hash) and return a hash with all 70 | # translations flattened. 71 | # 72 | # Nested hashes are included in the flattened hash just if subtree 73 | # is true and Symbols are automatically stored as links. 74 | def flatten_translations(locale, data, escape, subtree) 75 | hash = {} 76 | flatten_keys(data, escape) do |key, value| 77 | if value.is_a?(Hash) 78 | hash[key] = value if subtree 79 | else 80 | store_link(locale, key, value) if value.is_a?(Symbol) 81 | hash[key] = value 82 | end 83 | end 84 | hash 85 | end 86 | 87 | protected 88 | 89 | def store_link(locale, key, link) 90 | links[locale.to_sym][key.to_s] = link.to_s 91 | end 92 | 93 | def resolve_link(locale, key) 94 | key, locale = key.to_s, locale.to_sym 95 | links = self.links[locale] 96 | 97 | if links.key?(key) 98 | links[key] 99 | elsif link = find_link(locale, key) 100 | store_link(locale, key, key.gsub(*link)) 101 | else 102 | key 103 | end 104 | end 105 | 106 | def find_link(locale, key) #:nodoc: 107 | links[locale].each_pair do |from, to| 108 | return [from, to] if key[0, from.length] == from 109 | end && nil 110 | end 111 | 112 | def escape_default_separator(key) #:nodoc: 113 | I18n::Backend::Flatten.escape_default_separator(key) 114 | end 115 | 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/i18n/backend/gettext.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'i18n/gettext' 4 | require 'i18n/gettext/po_parser' 5 | 6 | module I18n 7 | module Backend 8 | # Experimental support for using Gettext po files to store translations. 9 | # 10 | # To use this you can simply include the module to the Simple backend - or 11 | # whatever other backend you are using. 12 | # 13 | # I18n::Backend::Simple.include(I18n::Backend::Gettext) 14 | # 15 | # Now you should be able to include your Gettext translation (*.po) files to 16 | # the +I18n.load_path+ so they're loaded to the backend and you can use them as 17 | # usual: 18 | # 19 | # I18n.load_path += Dir["path/to/locales/*.po"] 20 | # 21 | # Following the Gettext convention this implementation expects that your 22 | # translation files are named by their locales. E.g. the file en.po would 23 | # contain the translations for the English locale. 24 | # 25 | # To translate text you must use one of the translate methods provided by 26 | # I18n::Gettext::Helpers. 27 | # 28 | # include I18n::Gettext::Helpers 29 | # puts _("some string") 30 | # 31 | # Without it strings containing periods (".") will not be translated. 32 | 33 | module Gettext 34 | class PoData < Hash 35 | def set_comment(msgid_or_sym, comment) 36 | # ignore 37 | end 38 | end 39 | 40 | protected 41 | def load_po(filename) 42 | locale = ::File.basename(filename, '.po').to_sym 43 | data = normalize(locale, parse(filename)) 44 | [{ locale => data }, false] 45 | end 46 | 47 | def parse(filename) 48 | GetText::PoParser.new.parse(::File.read(filename), PoData.new) 49 | end 50 | 51 | def normalize(locale, data) 52 | data.inject({}) do |result, (key, value)| 53 | unless key.nil? || key.empty? 54 | key = key.gsub(I18n::Gettext::CONTEXT_SEPARATOR, '|') 55 | key, value = normalize_pluralization(locale, key, value) if key.index("\000") 56 | 57 | parts = key.split('|').reverse 58 | normalized = parts.inject({}) do |_normalized, part| 59 | { part => _normalized.empty? ? value : _normalized } 60 | end 61 | 62 | Utils.deep_merge!(result, normalized) 63 | end 64 | result 65 | end 66 | end 67 | 68 | def normalize_pluralization(locale, key, value) 69 | # FIXME po_parser includes \000 chars that can not be turned into Symbols 70 | key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first 71 | 72 | keys = I18n::Gettext.plural_keys(locale) 73 | values = value.split("\000") 74 | raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect} on #{locale} locale for msgid #{key.inspect} with values #{values.inspect}" if values.size != keys.size 75 | 76 | result = {} 77 | values.each_with_index { |_value, ix| result[keys[ix]] = _value } 78 | [key, result] 79 | end 80 | 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/i18n/backend/interpolation_compiler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The InterpolationCompiler module contains optimizations that can tremendously 4 | # speed up the interpolation process on the Simple backend. 5 | # 6 | # It works by defining a pre-compiled method on stored translation Strings that 7 | # already bring all the knowledge about contained interpolation variables etc. 8 | # so that the actual recurring interpolation will be very fast. 9 | # 10 | # To enable pre-compiled interpolations you can simply include the 11 | # InterpolationCompiler module to the Simple backend: 12 | # 13 | # I18n::Backend::Simple.include(I18n::Backend::InterpolationCompiler) 14 | # 15 | # Note that InterpolationCompiler does not yield meaningful results and consequently 16 | # should not be used with Ruby 1.9 (YARV) but improves performance everywhere else 17 | # (jRuby, Rubinius). 18 | module I18n 19 | module Backend 20 | module InterpolationCompiler 21 | module Compiler 22 | extend self 23 | 24 | TOKENIZER = /(%%?\{[^}]+\})/ 25 | 26 | def compile_if_an_interpolation(string) 27 | if interpolated_str?(string) 28 | string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ 29 | def i18n_interpolate(v = {}) 30 | "#{compiled_interpolation_body(string)}" 31 | end 32 | RUBY_EVAL 33 | end 34 | 35 | string 36 | end 37 | 38 | def interpolated_str?(str) 39 | str.kind_of?(::String) && str =~ TOKENIZER 40 | end 41 | 42 | protected 43 | # tokenize("foo %{bar} baz %%{buz}") # => ["foo ", "%{bar}", " baz ", "%%{buz}"] 44 | def tokenize(str) 45 | str.split(TOKENIZER) 46 | end 47 | 48 | def compiled_interpolation_body(str) 49 | tokenize(str).map do |token| 50 | token.match(TOKENIZER) ? handle_interpolation_token(token) : escape_plain_str(token) 51 | end.join 52 | end 53 | 54 | def handle_interpolation_token(token) 55 | token.start_with?('%%') ? token[1..] : compile_interpolation_token(token[2..-2]) 56 | end 57 | 58 | def compile_interpolation_token(key) 59 | "\#{#{interpolate_or_raise_missing(key)}}" 60 | end 61 | 62 | def interpolate_or_raise_missing(key) 63 | escaped_key = escape_key_sym(key) 64 | RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key) 65 | end 66 | 67 | def interpolate_key(key) 68 | [direct_key(key), nil_key(key), missing_key(key)].join('||') 69 | end 70 | 71 | def direct_key(key) 72 | "((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)" 73 | end 74 | 75 | def nil_key(key) 76 | "(v.has_key?(#{key}) && '')" 77 | end 78 | 79 | def missing_key(key) 80 | "I18n.config.missing_interpolation_argument_handler.call(#{key}, v, self)" 81 | end 82 | 83 | def reserved_key(key) 84 | "raise(ReservedInterpolationKey.new(#{key}, self))" 85 | end 86 | 87 | def escape_plain_str(str) 88 | str.gsub(/"|\\|#/) {|x| "\\#{x}"} 89 | end 90 | 91 | def escape_key_sym(key) 92 | # rely on Ruby to do all the hard work :) 93 | key.to_sym.inspect 94 | end 95 | end 96 | 97 | def interpolate(locale, string, values) 98 | if string.respond_to?(:i18n_interpolate) 99 | string.i18n_interpolate(values) 100 | elsif values 101 | super 102 | else 103 | string 104 | end 105 | end 106 | 107 | def store_translations(locale, data, options = EMPTY_HASH) 108 | compile_all_strings_in(data) 109 | super 110 | end 111 | 112 | protected 113 | def compile_all_strings_in(data) 114 | data.each_value do |value| 115 | Compiler.compile_if_an_interpolation(value) 116 | compile_all_strings_in(value) if value.kind_of?(Hash) 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/i18n/backend/memoize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Memoize module simply memoizes the values returned by lookup using 4 | # a flat hash and can tremendously speed up the lookup process in a backend. 5 | # 6 | # To enable it you can simply include the Memoize module to your backend: 7 | # 8 | # I18n::Backend::Simple.include(I18n::Backend::Memoize) 9 | # 10 | # Notice that it's the responsibility of the backend to define whenever the 11 | # cache should be cleaned. 12 | module I18n 13 | module Backend 14 | module Memoize 15 | def available_locales 16 | @memoized_locales ||= super 17 | end 18 | 19 | def store_translations(locale, data, options = EMPTY_HASH) 20 | reset_memoizations!(locale) 21 | super 22 | end 23 | 24 | def reload! 25 | reset_memoizations! 26 | super 27 | end 28 | 29 | def eager_load! 30 | memoized_lookup 31 | available_locales 32 | super 33 | end 34 | 35 | protected 36 | 37 | def lookup(locale, key, scope = nil, options = EMPTY_HASH) 38 | flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale, 39 | key, scope, options[:separator]).to_sym 40 | flat_hash = memoized_lookup[locale.to_sym] 41 | flat_hash.key?(flat_key) ? flat_hash[flat_key] : (flat_hash[flat_key] = super) 42 | end 43 | 44 | def memoized_lookup 45 | @memoized_lookup ||= I18n.new_double_nested_cache 46 | end 47 | 48 | def reset_memoizations!(locale=nil) 49 | @memoized_locales = nil 50 | (locale ? memoized_lookup[locale.to_sym] : memoized_lookup).clear 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/i18n/backend/metadata.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # I18n translation metadata is useful when you want to access information 4 | # about how a translation was looked up, pluralized or interpolated in 5 | # your application. 6 | # 7 | # msg = I18n.t(:message, :default => 'Hi!', :scope => :foo) 8 | # msg.translation_metadata 9 | # # => { :key => :message, :scope => :foo, :default => 'Hi!' } 10 | # 11 | # If a :count option was passed to #translate it will be set to the metadata. 12 | # Likewise, if any interpolation variables were passed they will also be set. 13 | # 14 | # To enable translation metadata you can simply include the Metadata module 15 | # into the Simple backend class - or whatever other backend you are using: 16 | # 17 | # I18n::Backend::Simple.include(I18n::Backend::Metadata) 18 | # 19 | module I18n 20 | module Backend 21 | module Metadata 22 | class << self 23 | def included(base) 24 | Object.class_eval do 25 | def translation_metadata 26 | unless self.frozen? 27 | @translation_metadata ||= {} 28 | else 29 | {} 30 | end 31 | end 32 | 33 | def translation_metadata=(translation_metadata) 34 | @translation_metadata = translation_metadata unless self.frozen? 35 | end 36 | end unless Object.method_defined?(:translation_metadata) 37 | end 38 | end 39 | 40 | def translate(locale, key, options = EMPTY_HASH) 41 | metadata = { 42 | :locale => locale, 43 | :key => key, 44 | :scope => options[:scope], 45 | :default => options[:default], 46 | :separator => options[:separator], 47 | :values => options.reject { |name, _value| RESERVED_KEYS.include?(name) } 48 | } 49 | with_metadata(metadata) { super } 50 | end 51 | 52 | def interpolate(locale, entry, values = EMPTY_HASH) 53 | metadata = entry.translation_metadata.merge(:original => entry) 54 | with_metadata(metadata) { super } 55 | end 56 | 57 | def pluralize(locale, entry, count) 58 | with_metadata(:count => count) { super } 59 | end 60 | 61 | protected 62 | 63 | def with_metadata(metadata, &block) 64 | result = yield 65 | result.translation_metadata = result.translation_metadata.merge(metadata) if result 66 | result 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/i18n/backend/pluralization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # I18n Pluralization are useful when you want your application to 4 | # customize pluralization rules. 5 | # 6 | # To enable locale specific pluralizations you can simply include the 7 | # Pluralization module to the Simple backend - or whatever other backend you 8 | # are using. 9 | # 10 | # I18n::Backend::Simple.include(I18n::Backend::Pluralization) 11 | # 12 | # You also need to make sure to provide pluralization algorithms to the 13 | # backend, i.e. include them to your I18n.load_path accordingly. 14 | module I18n 15 | module Backend 16 | module Pluralization 17 | # Overwrites the Base backend translate method so that it will check the 18 | # translation meta data space (:i18n) for a locale specific pluralization 19 | # rule and use it to pluralize the given entry. I.e., the library expects 20 | # pluralization rules to be stored at I18n.t(:'i18n.plural.rule') 21 | # 22 | # Pluralization rules are expected to respond to #call(count) and 23 | # return a pluralization key. Valid keys depend on the pluralization 24 | # rules for the locale, as defined in the CLDR. 25 | # As of v41, 6 locale-specific plural categories are defined: 26 | # :few, :many, :one, :other, :two, :zero 27 | # 28 | # n.b., The :one plural category does not imply the number 1. 29 | # Instead, :one is a category for any number that behaves like 1 in 30 | # that locale. For example, in some locales, :one is used for numbers 31 | # that end in "1" (like 1, 21, 151) but that don't end in 32 | # 11 (like 11, 111, 10311). 33 | # Similar notes apply to the :two, and :zero plural categories. 34 | # 35 | # If you want to have different strings for the categories of count == 0 36 | # (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car") 37 | # use the explicit `"0"` and `"1"` keys. 38 | # https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules 39 | def pluralize(locale, entry, count) 40 | return entry unless entry.is_a?(Hash) && count 41 | 42 | pluralizer = pluralizer(locale) 43 | if pluralizer.respond_to?(:call) 44 | # Deprecation: The use of the `zero` key in this way is incorrect. 45 | # Users that want a different string for the case of `count == 0` should use the explicit "0" key instead. 46 | # We keep this incorrect behaviour for now for backwards compatibility until we can remove it. 47 | # Ref: https://github.com/ruby-i18n/i18n/issues/629 48 | return entry[:zero] if count == 0 && entry.has_key?(:zero) 49 | 50 | # "0" and "1" are special cases 51 | # https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules 52 | if count == 0 || count == 1 53 | value = entry[symbolic_count(count)] 54 | return value if value 55 | end 56 | 57 | # Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance): 58 | # > If there is no value for a path, and that path has a [@count="x"] attribute and value, then: 59 | # > 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. 60 | # > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale. 61 | # > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale. 62 | # Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented. 63 | plural_rule_category = pluralizer.call(count) 64 | 65 | value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other) 66 | entry[plural_rule_category] || entry[:other] 67 | else 68 | raise InvalidPluralizationData.new(entry, count, plural_rule_category) 69 | end 70 | else 71 | super 72 | end 73 | end 74 | 75 | protected 76 | 77 | def pluralizers 78 | @pluralizers ||= {} 79 | end 80 | 81 | def pluralizer(locale) 82 | pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false) 83 | end 84 | 85 | private 86 | 87 | # Normalizes categories of 0.0 and 1.0 88 | # and returns the symbolic version 89 | def symbolic_count(count) 90 | count = 0 if count == 0 91 | count = 1 if count == 1 92 | count.to_s.to_sym 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/i18n/backend/simple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'i18n/backend/base' 4 | 5 | module I18n 6 | module Backend 7 | # A simple backend that reads translations from YAML files and stores them in 8 | # an in-memory hash. Relies on the Base backend. 9 | # 10 | # The implementation is provided by a Implementation module allowing to easily 11 | # extend Simple backend's behavior by including modules. E.g.: 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) 21 | class Simple 22 | module Implementation 23 | include Base 24 | 25 | # Mutex to ensure that concurrent translations loading will be thread-safe 26 | MUTEX = Mutex.new 27 | 28 | def initialized? 29 | @initialized ||= false 30 | end 31 | 32 | # Stores translations for the given locale in memory. 33 | # This uses a deep merge for the translations hash, so existing 34 | # translations will be overwritten by new ones only at the deepest 35 | # level of the hash. 36 | def store_translations(locale, data, options = EMPTY_HASH) 37 | if I18n.enforce_available_locales && 38 | I18n.available_locales_initialized? && 39 | !I18n.locale_available?(locale) 40 | return data 41 | end 42 | locale = locale.to_sym 43 | translations[locale] ||= Concurrent::Hash.new 44 | data = Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false) 45 | Utils.deep_merge!(translations[locale], data) 46 | end 47 | 48 | # Get available locales from the translations hash 49 | def available_locales 50 | init_translations unless initialized? 51 | translations.inject([]) do |locales, (locale, data)| 52 | locales << locale unless data.size <= 1 && (data.empty? || data.has_key?(:i18n)) 53 | locales 54 | end 55 | end 56 | 57 | # Clean up translations hash and set initialized to false on reload! 58 | def reload! 59 | @initialized = false 60 | @translations = nil 61 | super 62 | end 63 | 64 | def eager_load! 65 | init_translations unless initialized? 66 | super 67 | end 68 | 69 | def translations(do_init: false) 70 | # To avoid returning empty translations, 71 | # call `init_translations` 72 | init_translations if do_init && !initialized? 73 | 74 | @translations ||= Concurrent::Hash.new do |h, k| 75 | MUTEX.synchronize do 76 | h[k] = Concurrent::Hash.new 77 | end 78 | end 79 | end 80 | 81 | protected 82 | 83 | def init_translations 84 | load_translations 85 | @initialized = true 86 | end 87 | 88 | # Looks up a translation from the translations hash. Returns nil if 89 | # either key is nil, or locale, scope or key do not exist as a key in the 90 | # nested translations hash. Splits keys or scopes containing dots 91 | # into multiple keys, i.e. currency.format is regarded the same as 92 | # %w(currency format). 93 | def lookup(locale, key, scope = [], options = EMPTY_HASH) 94 | init_translations unless initialized? 95 | keys = I18n.normalize_keys(locale, key, scope, options[:separator]) 96 | 97 | keys.inject(translations) do |result, _key| 98 | return nil unless result.is_a?(Hash) 99 | unless result.has_key?(_key) 100 | _key = _key.to_s.to_sym 101 | return nil unless result.has_key?(_key) 102 | end 103 | result = result[_key] 104 | result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol) 105 | result 106 | end 107 | end 108 | end 109 | 110 | include Implementation 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/i18n/backend/transliterator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module I18n 5 | module Backend 6 | module Transliterator 7 | DEFAULT_REPLACEMENT_CHAR = "?" 8 | 9 | # Given a locale and a UTF-8 string, return the locale's ASCII 10 | # approximation for the string. 11 | def transliterate(locale, string, replacement = nil) 12 | @transliterators ||= {} 13 | @transliterators[locale] ||= Transliterator.get I18n.t(:'i18n.transliterate.rule', 14 | :locale => locale, :resolve => false, :default => {}) 15 | @transliterators[locale].transliterate(string, replacement) 16 | end 17 | 18 | # Get a transliterator instance. 19 | def self.get(rule = nil) 20 | if !rule || rule.kind_of?(Hash) 21 | HashTransliterator.new(rule) 22 | elsif rule.kind_of? Proc 23 | ProcTransliterator.new(rule) 24 | else 25 | raise I18n::ArgumentError, "Transliteration rule must be a proc or a hash." 26 | end 27 | end 28 | 29 | # A transliterator which accepts a Proc as its transliteration rule. 30 | class ProcTransliterator 31 | def initialize(rule) 32 | @rule = rule 33 | end 34 | 35 | def transliterate(string, replacement = nil) 36 | @rule.call(string) 37 | end 38 | end 39 | 40 | # A transliterator which accepts a Hash of characters as its translation 41 | # rule. 42 | class HashTransliterator 43 | DEFAULT_APPROXIMATIONS = { 44 | "À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE", 45 | "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I", 46 | "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O", 47 | "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U", 48 | "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "ẞ"=>"SS", "à"=>"a", 49 | "á"=>"a", "â"=>"a", "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", 50 | "è"=>"e", "é"=>"e", "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", 51 | "ï"=>"i", "ð"=>"d", "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", 52 | "ö"=>"o", "ø"=>"o", "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", 53 | "þ"=>"th", "ÿ"=>"y", "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", 54 | "ą"=>"a", "Ć"=>"C", "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", 55 | "Č"=>"C", "č"=>"c", "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", 56 | "ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", 57 | "Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", 58 | "ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", 59 | "Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", 60 | "į"=>"i", "İ"=>"I", "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", 61 | "Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", 62 | "Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", 63 | "ń"=>"n", "Ņ"=>"N", "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", 64 | "ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", 65 | "Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", 66 | "ř"=>"r", "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", 67 | "Š"=>"S", "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", 68 | "ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", 69 | "Ů"=>"U", "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", 70 | "ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", 71 | "ż"=>"z", "Ž"=>"Z", "ž"=>"z" 72 | }.freeze 73 | 74 | def initialize(rule = nil) 75 | @rule = rule 76 | add_default_approximations 77 | add rule if rule 78 | end 79 | 80 | def transliterate(string, replacement = nil) 81 | replacement ||= DEFAULT_REPLACEMENT_CHAR 82 | string.gsub(/[^\x00-\x7f]/u) do |char| 83 | approximations[char] || replacement 84 | end 85 | end 86 | 87 | private 88 | 89 | def approximations 90 | @approximations ||= {} 91 | end 92 | 93 | def add_default_approximations 94 | DEFAULT_APPROXIMATIONS.each do |key, value| 95 | approximations[key] = value 96 | end 97 | end 98 | 99 | # Add transliteration rules to the approximations hash. 100 | def add(hash) 101 | hash.each do |key, value| 102 | approximations[key.to_s] = value.to_s 103 | end 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/i18n/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'set' 4 | 5 | module I18n 6 | class Config 7 | # The only configuration value that is not global and scoped to thread is :locale. 8 | # It defaults to the default_locale. 9 | def locale 10 | defined?(@locale) && @locale != nil ? @locale : default_locale 11 | end 12 | 13 | # Sets the current locale pseudo-globally, i.e. in the Thread.current hash. 14 | def locale=(locale) 15 | I18n.enforce_available_locales!(locale) 16 | @locale = locale && locale.to_sym 17 | end 18 | 19 | # Returns the current backend. Defaults to +Backend::Simple+. 20 | def backend 21 | @@backend ||= Backend::Simple.new 22 | end 23 | 24 | # Sets the current backend. Used to set a custom backend. 25 | def backend=(backend) 26 | @@backend = backend 27 | end 28 | 29 | # Returns the current default locale. Defaults to :'en' 30 | def default_locale 31 | @@default_locale ||= :en 32 | end 33 | 34 | # Sets the current default locale. Used to set a custom default locale. 35 | def default_locale=(locale) 36 | I18n.enforce_available_locales!(locale) 37 | @@default_locale = locale && locale.to_sym 38 | end 39 | 40 | # Returns an array of locales for which translations are available. 41 | # Unless you explicitly set these through I18n.available_locales= 42 | # the call will be delegated to the backend. 43 | def available_locales 44 | @@available_locales ||= nil 45 | @@available_locales || backend.available_locales 46 | end 47 | 48 | # Caches the available locales list as both strings and symbols in a Set, so 49 | # that we can have faster lookups to do the available locales enforce check. 50 | def available_locales_set #:nodoc: 51 | @@available_locales_set ||= available_locales.inject(Set.new) do |set, locale| 52 | set << locale.to_s << locale.to_sym 53 | end 54 | end 55 | 56 | # Sets the available locales. 57 | def available_locales=(locales) 58 | @@available_locales = Array(locales).map { |locale| locale.to_sym } 59 | @@available_locales = nil if @@available_locales.empty? 60 | @@available_locales_set = nil 61 | end 62 | 63 | # Returns true if the available_locales have been initialized 64 | def available_locales_initialized? 65 | ( !!defined?(@@available_locales) && !!@@available_locales ) 66 | end 67 | 68 | # Clears the available locales set so it can be recomputed again after I18n 69 | # gets reloaded. 70 | def clear_available_locales_set #:nodoc: 71 | @@available_locales_set = nil 72 | end 73 | 74 | # Returns the current default scope separator. Defaults to '.' 75 | def default_separator 76 | @@default_separator ||= '.' 77 | end 78 | 79 | # Sets the current default scope separator. 80 | def default_separator=(separator) 81 | @@default_separator = separator 82 | end 83 | 84 | # Returns the current exception handler. Defaults to an instance of 85 | # I18n::ExceptionHandler. 86 | def exception_handler 87 | @@exception_handler ||= ExceptionHandler.new 88 | end 89 | 90 | # Sets the exception handler. 91 | def exception_handler=(exception_handler) 92 | @@exception_handler = exception_handler 93 | end 94 | 95 | # Returns the current handler for situations when interpolation argument 96 | # is missing. MissingInterpolationArgument will be raised by default. 97 | def missing_interpolation_argument_handler 98 | @@missing_interpolation_argument_handler ||= lambda do |missing_key, provided_hash, string| 99 | raise MissingInterpolationArgument.new(missing_key, provided_hash, string) 100 | end 101 | end 102 | 103 | # Sets the missing interpolation argument handler. It can be any 104 | # object that responds to #call. The arguments that will be passed to #call 105 | # are the same as for MissingInterpolationArgument initializer. Use +Proc.new+ 106 | # if you don't care about arity. 107 | # 108 | # == Example: 109 | # You can suppress raising an exception and return string instead: 110 | # 111 | # I18n.config.missing_interpolation_argument_handler = Proc.new do |key| 112 | # "#{key} is missing" 113 | # end 114 | def missing_interpolation_argument_handler=(exception_handler) 115 | @@missing_interpolation_argument_handler = exception_handler 116 | end 117 | 118 | # Allow clients to register paths providing translation data sources. The 119 | # backend defines acceptable sources. 120 | # 121 | # E.g. the provided SimpleBackend accepts a list of paths to translation 122 | # files which are either named *.rb and contain plain Ruby Hashes or are 123 | # named *.yml and contain YAML data. So for the SimpleBackend clients may 124 | # register translation files like this: 125 | # I18n.load_path << 'path/to/locale/en.yml' 126 | def load_path 127 | @@load_path ||= [] 128 | end 129 | 130 | # Sets the load path instance. Custom implementations are expected to 131 | # behave like a Ruby Array. 132 | def load_path=(load_path) 133 | @@load_path = load_path 134 | @@available_locales_set = nil 135 | backend.reload! 136 | end 137 | 138 | # Whether or not to verify if locales are in the list of available locales. 139 | # Defaults to true. 140 | @@enforce_available_locales = true 141 | def enforce_available_locales 142 | @@enforce_available_locales 143 | end 144 | 145 | def enforce_available_locales=(enforce_available_locales) 146 | @@enforce_available_locales = enforce_available_locales 147 | end 148 | 149 | # Returns the current interpolation patterns. Defaults to 150 | # I18n::DEFAULT_INTERPOLATION_PATTERNS. 151 | def interpolation_patterns 152 | @@interpolation_patterns ||= I18n::DEFAULT_INTERPOLATION_PATTERNS.dup 153 | end 154 | 155 | # Sets the current interpolation patterns. Used to set a interpolation 156 | # patterns. 157 | # 158 | # E.g. using {{}} as a placeholder like "{{hello}}, world!": 159 | # 160 | # I18n.config.interpolation_patterns << /\{\{(\w+)\}\}/ 161 | def interpolation_patterns=(interpolation_patterns) 162 | @@interpolation_patterns = interpolation_patterns 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/i18n/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cgi' 4 | 5 | module I18n 6 | class ExceptionHandler 7 | def call(exception, _locale, _key, _options) 8 | if exception.is_a?(MissingTranslation) 9 | exception.message 10 | else 11 | raise exception 12 | end 13 | end 14 | end 15 | 16 | class ArgumentError < ::ArgumentError; end 17 | 18 | class Disabled < ArgumentError 19 | def initialize(method) 20 | super(<<~MESSAGE) 21 | I18n.#{method} is currently disabled, likely because your application is still in its loading phase. 22 | 23 | This method is meant to display text in the user locale, so calling it before the user locale has 24 | been set is likely to display text from the wrong locale to some users. 25 | 26 | If you have a legitimate reason to access i18n data outside of the user flow, you can do so by passing 27 | the desired locale explicitly with the `locale` argument, e.g. `I18n.#{method}(..., locale: :en)` 28 | MESSAGE 29 | end 30 | end 31 | 32 | class InvalidLocale < ArgumentError 33 | attr_reader :locale 34 | def initialize(locale) 35 | @locale = locale 36 | super "#{locale.inspect} is not a valid locale" 37 | end 38 | end 39 | 40 | class InvalidLocaleData < ArgumentError 41 | attr_reader :filename 42 | def initialize(filename, exception_message) 43 | @filename, @exception_message = filename, exception_message 44 | super "can not load translations from #{filename}: #{exception_message}" 45 | end 46 | end 47 | 48 | class MissingTranslation < ArgumentError 49 | module Base 50 | PERMITTED_KEYS = [:scope, :default].freeze 51 | 52 | attr_reader :locale, :key, :options 53 | 54 | def initialize(locale, key, options = EMPTY_HASH) 55 | @key, @locale, @options = key, locale, options.slice(*PERMITTED_KEYS) 56 | options.each { |k, v| self.options[k] = v.inspect if v.is_a?(Proc) } 57 | end 58 | 59 | def keys 60 | @keys ||= I18n.normalize_keys(locale, key, options[:scope]).tap do |keys| 61 | keys << 'no key' if keys.size < 2 62 | end 63 | end 64 | 65 | def message 66 | if (default = options[:default]).is_a?(Array) && default.any? 67 | other_options = ([key, *default]).map { |k| normalized_option(k).prepend('- ') }.join("\n") 68 | "Translation missing. Options considered were:\n#{other_options}" 69 | else 70 | "Translation missing: #{keys.join('.')}" 71 | end 72 | end 73 | 74 | def normalized_option(key) 75 | I18n.normalize_keys(locale, key, options[:scope]).join('.') 76 | end 77 | 78 | alias :to_s :message 79 | 80 | def to_exception 81 | MissingTranslationData.new(locale, key, options) 82 | end 83 | end 84 | 85 | include Base 86 | end 87 | 88 | class MissingTranslationData < ArgumentError 89 | include MissingTranslation::Base 90 | end 91 | 92 | class InvalidPluralizationData < ArgumentError 93 | attr_reader :entry, :count, :key 94 | def initialize(entry, count, key) 95 | @entry, @count, @key = entry, count, key 96 | super "translation data #{entry.inspect} can not be used with :count => #{count}. key '#{key}' is missing." 97 | end 98 | end 99 | 100 | class MissingInterpolationArgument < ArgumentError 101 | attr_reader :key, :values, :string 102 | def initialize(key, values, string) 103 | @key, @values, @string = key, values, string 104 | super "missing interpolation argument #{key.inspect} in #{string.inspect} (#{values.inspect} given)" 105 | end 106 | end 107 | 108 | class ReservedInterpolationKey < ArgumentError 109 | attr_reader :key, :string 110 | def initialize(key, string) 111 | @key, @string = key, string 112 | super "reserved key #{key.inspect} used in #{string.inspect}" 113 | end 114 | end 115 | 116 | class UnknownFileType < ArgumentError 117 | attr_reader :type, :filename 118 | def initialize(type, filename) 119 | @type, @filename = type, filename 120 | super "can not load translations from #{filename}, the file type #{type} is not known" 121 | end 122 | end 123 | 124 | class UnsupportedMethod < ArgumentError 125 | attr_reader :method, :backend_klass, :msg 126 | def initialize(method, backend_klass, msg) 127 | @method = method 128 | @backend_klass = backend_klass 129 | @msg = msg 130 | super "#{backend_klass} does not support the ##{method} method. #{msg}" 131 | end 132 | end 133 | 134 | class InvalidFilenames < ArgumentError 135 | NUMBER_OF_ERRORS_SHOWN = 20 136 | def initialize(file_errors) 137 | super <<~MSG 138 | Found #{file_errors.count} error(s). 139 | The first #{[file_errors.count, NUMBER_OF_ERRORS_SHOWN].min} error(s): 140 | #{file_errors.map(&:message).first(NUMBER_OF_ERRORS_SHOWN).join("\n")} 141 | 142 | To use the LazyLoadable backend: 143 | 1. Filenames must start with the locale. 144 | 2. An underscore must separate the locale with any optional text that follows. 145 | 3. The file must only contain translation data for the single locale. 146 | 147 | Example: 148 | "/config/locales/fr.yml" which contains: 149 | ```yml 150 | fr: 151 | dog: 152 | chien 153 | ``` 154 | MSG 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/i18n/gettext.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | module Gettext 5 | PLURAL_SEPARATOR = "\001" 6 | CONTEXT_SEPARATOR = "\004" 7 | 8 | autoload :Helpers, 'i18n/gettext/helpers' 9 | 10 | @@plural_keys = { :en => [:one, :other] } 11 | 12 | class << self 13 | # returns an array of plural keys for the given locale or the whole hash 14 | # of locale mappings to plural keys so that we can convert from gettext's 15 | # integer-index based style 16 | # TODO move this information to the pluralization module 17 | def plural_keys(*args) 18 | args.empty? ? @@plural_keys : @@plural_keys[args.first] || @@plural_keys[:en] 19 | end 20 | 21 | def extract_scope(msgid, separator) 22 | scope = msgid.to_s.split(separator) 23 | msgid = scope.pop 24 | [scope, msgid] 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/i18n/gettext/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'i18n/gettext' 4 | 5 | module I18n 6 | module Gettext 7 | # Implements classical Gettext style accessors. To use this include the 8 | # module to the global namespace or wherever you want to use it. 9 | # 10 | # include I18n::Gettext::Helpers 11 | module Helpers 12 | # Makes dynamic translation messages readable for the gettext parser. 13 | # _(fruit) cannot be understood by the gettext parser. To help the parser find all your translations, 14 | # you can add fruit = N_("Apple") which does not translate, but tells the parser: "Apple" needs translation. 15 | # * msgid: the message id. 16 | # * Returns: msgid. 17 | def N_(msgsid) 18 | msgsid 19 | end 20 | 21 | def gettext(msgid, options = EMPTY_HASH) 22 | I18n.t(msgid, **{:default => msgid, :separator => '|'}.merge(options)) 23 | end 24 | alias _ gettext 25 | 26 | def sgettext(msgid, separator = '|') 27 | scope, msgid = I18n::Gettext.extract_scope(msgid, separator) 28 | I18n.t(msgid, :scope => scope, :default => msgid, :separator => separator) 29 | end 30 | alias s_ sgettext 31 | 32 | def pgettext(msgctxt, msgid) 33 | separator = I18n::Gettext::CONTEXT_SEPARATOR 34 | sgettext([msgctxt, msgid].join(separator), separator) 35 | end 36 | alias p_ pgettext 37 | 38 | def ngettext(msgid, msgid_plural, n = 1) 39 | nsgettext(msgid, msgid_plural, n) 40 | end 41 | alias n_ ngettext 42 | 43 | # Method signatures: 44 | # nsgettext('Fruits|apple', 'apples', 2) 45 | # nsgettext(['Fruits|apple', 'apples'], 2) 46 | def nsgettext(msgid, msgid_plural, n = 1, separator = '|') 47 | if msgid.is_a?(Array) 48 | msgid, msgid_plural, n, separator = msgid[0], msgid[1], msgid_plural, n 49 | separator = '|' unless separator.is_a?(::String) 50 | end 51 | 52 | scope, msgid = I18n::Gettext.extract_scope(msgid, separator) 53 | default = { :one => msgid, :other => msgid_plural } 54 | I18n.t(msgid, :default => default, :count => n, :scope => scope, :separator => separator) 55 | end 56 | alias ns_ nsgettext 57 | 58 | # Method signatures: 59 | # npgettext('Fruits', 'apple', 'apples', 2) 60 | # npgettext('Fruits', ['apple', 'apples'], 2) 61 | def npgettext(msgctxt, msgid, msgid_plural, n = 1) 62 | separator = I18n::Gettext::CONTEXT_SEPARATOR 63 | 64 | if msgid.is_a?(Array) 65 | msgid_plural, msgid, n = msgid[1], [msgctxt, msgid[0]].join(separator), msgid_plural 66 | else 67 | msgid = [msgctxt, msgid].join(separator) 68 | end 69 | 70 | nsgettext(msgid, msgid_plural, n, separator) 71 | end 72 | alias np_ npgettext 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/i18n/interpolate/ruby.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # heavily based on Masao Mutoh's gettext String interpolation extension 4 | # http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb 5 | 6 | module I18n 7 | DEFAULT_INTERPOLATION_PATTERNS = [ 8 | /%%/, 9 | /%\{([\w|]+)\}/, # matches placeholders like "%{foo} or %{foo|word}" 10 | /%<(\w+)>([^\d]*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%.d" 11 | ].freeze 12 | INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS) 13 | deprecate_constant :INTERPOLATION_PATTERN 14 | 15 | INTERPOLATION_PATTERNS_CACHE = Hash.new do |hash, patterns| 16 | hash[patterns] = Regexp.union(patterns) 17 | end 18 | private_constant :INTERPOLATION_PATTERNS_CACHE 19 | 20 | class << self 21 | # Return String or raises MissingInterpolationArgument exception. 22 | # Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler. 23 | def interpolate(string, values) 24 | raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ I18n.reserved_keys_pattern 25 | raise ArgumentError.new('Interpolation values must be a Hash.') unless values.kind_of?(Hash) 26 | interpolate_hash(string, values) 27 | end 28 | 29 | def interpolate_hash(string, values) 30 | pattern = INTERPOLATION_PATTERNS_CACHE[config.interpolation_patterns] 31 | interpolated = false 32 | 33 | interpolated_string = string.gsub(pattern) do |match| 34 | interpolated = true 35 | 36 | if match == '%%' 37 | '%' 38 | else 39 | key = ($1 || $2 || match.tr("%{}", "")).to_sym 40 | value = if values.key?(key) 41 | values[key] 42 | else 43 | config.missing_interpolation_argument_handler.call(key, values, string) 44 | end 45 | value = value.call(values) if value.respond_to?(:call) 46 | $3 ? sprintf("%#{$3}", value) : value 47 | end 48 | end 49 | 50 | interpolated ? interpolated_string : string 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/i18n/locale.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | module Locale 5 | autoload :Fallbacks, 'i18n/locale/fallbacks' 6 | autoload :Tag, 'i18n/locale/tag' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/i18n/locale/fallbacks.rb: -------------------------------------------------------------------------------- 1 | # Locale Fallbacks 2 | # 3 | # Extends the I18n module to hold a fallbacks instance which is set to an 4 | # instance of I18n::Locale::Fallbacks by default but can be swapped with a 5 | # different implementation. 6 | # 7 | # Locale fallbacks will compute a number of fallback locales for a given locale. 8 | # For example: 9 | # 10 | #

 11 | # I18n.fallbacks[:"es-MX"] # => [:"es-MX", :es, :en] 
12 | # 13 | # Locale fallbacks always fall back to 14 | # 15 | # * all parent locales of a given locale (e.g. :es for :"es-MX") first, 16 | # * the current default locales and all of their parents second 17 | # 18 | # The default locales are set to [] by default but can be set to something else. 19 | # 20 | # One can additionally add any number of additional fallback locales manually. 21 | # These will be added before the default locales to the fallback chain. For 22 | # example: 23 | # 24 | # # using a custom locale as default fallback locale 25 | # 26 | # I18n.fallbacks = I18n::Locale::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de) 27 | # I18n.fallbacks[:"de-AT"] # => [:"de-AT", :de, :"en-GB", :en] 28 | # I18n.fallbacks[:"de-CH"] # => [:"de-CH", :de, :"en-GB", :en] 29 | # 30 | # # mapping fallbacks to an existing instance 31 | # 32 | # # people speaking Catalan also speak Spanish as spoken in Spain 33 | # fallbacks = I18n.fallbacks 34 | # fallbacks.map(:ca => :"es-ES") 35 | # fallbacks[:ca] # => [:ca, :"es-ES", :es, :"en-US", :en] 36 | # 37 | # # people speaking Arabian as spoken in Palestine also speak Hebrew as spoken in Israel 38 | # fallbacks.map(:"ar-PS" => :"he-IL") 39 | # fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en] 40 | # fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en] 41 | # 42 | # # people speaking Sami as spoken in Finland also speak Swedish and Finnish as spoken in Finland 43 | # fallbacks.map(:sms => [:"se-FI", :"fi-FI"]) 44 | # fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en] 45 | 46 | module I18n 47 | module Locale 48 | class Fallbacks < Hash 49 | def initialize(*mappings) 50 | @map = {} 51 | map(mappings.pop) if mappings.last.is_a?(Hash) 52 | self.defaults = mappings.empty? ? [] : mappings 53 | end 54 | 55 | def defaults=(defaults) 56 | @defaults = defaults.flat_map { |default| compute(default, false) } 57 | end 58 | attr_reader :defaults 59 | 60 | def [](locale) 61 | raise InvalidLocale.new(locale) if locale.nil? 62 | raise Disabled.new('fallback#[]') if locale == false 63 | locale = locale.to_sym 64 | super || store(locale, compute(locale)) 65 | end 66 | 67 | def map(*args, &block) 68 | if args.count == 1 && !block_given? 69 | mappings = args.first 70 | mappings.each do |from, to| 71 | from, to = from.to_sym, Array(to) 72 | to.each do |_to| 73 | @map[from] ||= [] 74 | @map[from] << _to.to_sym 75 | end 76 | end 77 | else 78 | @map.map(*args, &block) 79 | end 80 | end 81 | 82 | def empty? 83 | @map.empty? && @defaults.empty? 84 | end 85 | 86 | def inspect 87 | "#<#{self.class.name} @map=#{@map.inspect} @defaults=#{@defaults.inspect}>" 88 | end 89 | 90 | protected 91 | 92 | def compute(tags, include_defaults = true, exclude = []) 93 | result = [] 94 | Array(tags).each do |tag| 95 | tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude 96 | result += tags 97 | tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] } 98 | end 99 | 100 | result.push(*defaults) if include_defaults 101 | result.uniq! 102 | result.compact! 103 | result 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/i18n/locale/tag.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Locale 5 | module Tag 6 | autoload :Parents, 'i18n/locale/tag/parents' 7 | autoload :Rfc4646, 'i18n/locale/tag/rfc4646' 8 | autoload :Simple, 'i18n/locale/tag/simple' 9 | 10 | class << self 11 | # Returns the current locale tag implementation. Defaults to +I18n::Locale::Tag::Simple+. 12 | def implementation 13 | @@implementation ||= Simple 14 | end 15 | 16 | # Sets the current locale tag implementation. Use this to set a different locale tag implementation. 17 | def implementation=(implementation) 18 | @@implementation = implementation 19 | end 20 | 21 | # Factory method for locale tags. Delegates to the current locale tag implementation. 22 | def tag(tag) 23 | implementation.tag(tag) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/i18n/locale/tag/parents.rb: -------------------------------------------------------------------------------- 1 | module I18n 2 | module Locale 3 | module Tag 4 | module Parents 5 | def parent 6 | @parent ||= 7 | begin 8 | segs = to_a 9 | segs.compact! 10 | segs.length > 1 ? self.class.tag(*segs[0..(segs.length - 2)].join('-')) : nil 11 | end 12 | end 13 | 14 | def self_and_parents 15 | @self_and_parents ||= [self].concat parents 16 | end 17 | 18 | def parents 19 | @parents ||= parent ? [parent].concat(parent.parents) : [] 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/i18n/locale/tag/rfc4646.rb: -------------------------------------------------------------------------------- 1 | # RFC 4646/47 compliant Locale tag implementation that parses locale tags to 2 | # subtags such as language, script, region, variant etc. 3 | # 4 | # For more information see by http://en.wikipedia.org/wiki/IETF_language_tag 5 | # 6 | # Rfc4646::Parser does not implement grandfathered tags. 7 | 8 | module I18n 9 | module Locale 10 | module Tag 11 | RFC4646_SUBTAGS = [ :language, :script, :region, :variant, :extension, :privateuse, :grandfathered ] 12 | RFC4646_FORMATS = { :language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase } 13 | 14 | class Rfc4646 < Struct.new(*RFC4646_SUBTAGS) 15 | class << self 16 | # Parses the given tag and returns a Tag instance if it is valid. 17 | # Returns false if the given tag is not valid according to RFC 4646. 18 | def tag(tag) 19 | matches = parser.match(tag) 20 | new(*matches) if matches 21 | end 22 | 23 | def parser 24 | @@parser ||= Rfc4646::Parser 25 | end 26 | 27 | def parser=(parser) 28 | @@parser = parser 29 | end 30 | end 31 | 32 | include Parents 33 | 34 | RFC4646_FORMATS.each do |name, format| 35 | define_method(name) { self[name].send(format) unless self[name].nil? } 36 | end 37 | 38 | def to_sym 39 | to_s.to_sym 40 | end 41 | 42 | def to_s 43 | @tag ||= to_a.compact.join("-") 44 | end 45 | 46 | def to_a 47 | members.collect { |attr| self.send(attr) } 48 | end 49 | 50 | module Parser 51 | PATTERN = %r{\A(?: 52 | ([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language 53 | (?:-([a-z]{4}))? # script 54 | (?:-([a-z]{2}|\d{3}))? # region 55 | (?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant 56 | (?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension 57 | (?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag 58 | (x(?:-[0-9a-z]{1,8})+)| # privateuse tag 59 | /* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered 60 | )\z}xi 61 | 62 | class << self 63 | def match(tag) 64 | c = PATTERN.match(tag.to_s).captures 65 | c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here? 66 | rescue 67 | false 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/i18n/locale/tag/simple.rb: -------------------------------------------------------------------------------- 1 | # Simple Locale tag implementation that computes subtags by simply splitting 2 | # the locale tag at '-' occurrences. 3 | module I18n 4 | module Locale 5 | module Tag 6 | class Simple 7 | class << self 8 | def tag(tag) 9 | new(tag) 10 | end 11 | end 12 | 13 | include Parents 14 | 15 | attr_reader :tag 16 | 17 | def initialize(*tag) 18 | @tag = tag.join('-').to_sym 19 | end 20 | 21 | def subtags 22 | @subtags = tag.to_s.split('-').map!(&:to_s) 23 | end 24 | 25 | def to_sym 26 | tag 27 | end 28 | 29 | def to_s 30 | tag.to_s 31 | end 32 | 33 | def to_a 34 | subtags 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/i18n/middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | class Middleware 5 | 6 | def initialize(app) 7 | @app = app 8 | end 9 | 10 | def call(env) 11 | @app.call(env) 12 | ensure 13 | Thread.current[:i18n_config] = I18n::Config.new 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/i18n/tests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | module Tests 5 | autoload :Basics, 'i18n/tests/basics' 6 | autoload :Defaults, 'i18n/tests/defaults' 7 | autoload :Interpolation, 'i18n/tests/interpolation' 8 | autoload :Link, 'i18n/tests/link' 9 | autoload :Localization, 'i18n/tests/localization' 10 | autoload :Lookup, 'i18n/tests/lookup' 11 | autoload :Pluralization, 'i18n/tests/pluralization' 12 | autoload :Procs, 'i18n/tests/procs' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/i18n/tests/basics.rb: -------------------------------------------------------------------------------- 1 | module I18n 2 | module Tests 3 | module Basics 4 | def teardown 5 | I18n.available_locales = nil 6 | end 7 | 8 | test "available_locales returns the available_locales produced by the backend, by default" do 9 | I18n.backend.store_translations('de', :foo => 'bar') 10 | I18n.backend.store_translations('en', :foo => 'foo') 11 | 12 | assert_equal I18n.available_locales, I18n.backend.available_locales 13 | end 14 | 15 | test "available_locales can be set to something else independently from the actual locale data" do 16 | I18n.backend.store_translations('de', :foo => 'bar') 17 | I18n.backend.store_translations('en', :foo => 'foo') 18 | 19 | I18n.available_locales = :foo 20 | assert_equal [:foo], I18n.available_locales 21 | 22 | I18n.available_locales = [:foo, 'bar'] 23 | assert_equal [:foo, :bar], I18n.available_locales 24 | 25 | I18n.available_locales = nil 26 | assert_equal I18n.available_locales, I18n.backend.available_locales 27 | end 28 | 29 | test "available_locales memoizes when set explicitly" do 30 | I18n.backend.expects(:available_locales).never 31 | I18n.available_locales = [:foo] 32 | I18n.backend.store_translations('de', :bar => 'baz') 33 | I18n.reload! 34 | assert_equal [:foo], I18n.available_locales 35 | end 36 | 37 | test "available_locales delegates to the backend when not set explicitly" do 38 | original_available_locales_value = I18n.backend.available_locales 39 | I18n.backend.expects(:available_locales).returns(original_available_locales_value).twice 40 | assert_equal I18n.backend.available_locales, I18n.available_locales 41 | end 42 | 43 | test "exists? is implemented by the backend" do 44 | I18n.backend.store_translations(:foo, :bar => 'baz') 45 | assert I18n.exists?(:bar, :foo) 46 | end 47 | 48 | test "storing a nil value as a translation removes it from the available locale data" do 49 | I18n.backend.store_translations(:en, :to_be_deleted => 'bar') 50 | assert_equal 'bar', I18n.t(:to_be_deleted, :default => 'baz') 51 | 52 | I18n.cache_store.clear if I18n.respond_to?(:cache_store) && I18n.cache_store 53 | I18n.backend.store_translations(:en, :to_be_deleted => nil) 54 | assert_equal 'baz', I18n.t(:to_be_deleted, :default => 'baz') 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/i18n/tests/defaults.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Defaults 6 | def setup 7 | super 8 | I18n.backend.store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' }) 9 | end 10 | 11 | test "defaults: given nil as a key it returns the given default" do 12 | assert_equal 'default', I18n.t(nil, :default => 'default') 13 | end 14 | 15 | test "defaults: given a symbol as a default it translates the symbol" do 16 | assert_equal 'bar', I18n.t(nil, :default => :'foo.bar') 17 | end 18 | 19 | test "defaults: given a symbol as a default and a scope it stays inside the scope when looking up the symbol" do 20 | assert_equal 'bar', I18n.t(:missing, :default => :bar, :scope => :foo) 21 | end 22 | 23 | test "defaults: given an array as a default it returns the first match" do 24 | assert_equal 'bar', I18n.t(:does_not_exist, :default => [:does_not_exist_2, :'foo.bar']) 25 | end 26 | 27 | test "defaults: given an array as a default with false it returns false" do 28 | assert_equal false, I18n.t(:does_not_exist, :default => [false]) 29 | end 30 | 31 | test "defaults: given false it returns false" do 32 | assert_equal false, I18n.t(:does_not_exist, :default => false) 33 | end 34 | 35 | test "defaults: given nil it returns nil" do 36 | assert_nil I18n.t(:does_not_exist, :default => nil) 37 | end 38 | 39 | test "defaults: given an array of missing keys it raises a MissingTranslationData exception" do 40 | assert_raises I18n::MissingTranslationData do 41 | I18n.t(:does_not_exist, :default => [:does_not_exist_2, :does_not_exist_3], :raise => true) 42 | end 43 | end 44 | 45 | test "defaults: using a custom scope separator" do 46 | # data must have been stored using the custom separator when using the ActiveRecord backend 47 | I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' }) 48 | assert_equal 'bar', I18n.t(nil, :default => :'foo|bar', :separator => '|') 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 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/i18n/tests/link.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Link 6 | test "linked lookup: if a key resolves to a symbol it looks up the symbol" do 7 | I18n.backend.store_translations 'en', { 8 | :link => :linked, 9 | :linked => 'linked' 10 | } 11 | assert_equal 'linked', I18n.backend.translate('en', :link) 12 | end 13 | 14 | test "linked lookup: if a key resolves to a dot-separated symbol it looks up the symbol" do 15 | I18n.backend.store_translations 'en', { 16 | :link => :"foo.linked", 17 | :foo => { :linked => 'linked' } 18 | } 19 | assert_equal('linked', I18n.backend.translate('en', :link)) 20 | end 21 | 22 | test "linked lookup: if a dot-separated key resolves to a symbol it looks up the symbol" do 23 | I18n.backend.store_translations 'en', { 24 | :foo => { :link => :linked }, 25 | :linked => 'linked' 26 | } 27 | assert_equal('linked', I18n.backend.translate('en', :'foo.link')) 28 | end 29 | 30 | test "linked lookup: if a dot-separated key resolves to a dot-separated symbol it looks up the symbol" do 31 | I18n.backend.store_translations 'en', { 32 | :foo => { :link => :"bar.linked" }, 33 | :bar => { :linked => 'linked' } 34 | } 35 | assert_equal('linked', I18n.backend.translate('en', :'foo.link')) 36 | end 37 | 38 | test "linked lookup: links always refer to the absolute key" do 39 | I18n.backend.store_translations 'en', { 40 | :foo => { :link => :linked, :linked => 'linked in foo' }, 41 | :linked => 'linked absolutely' 42 | } 43 | assert_equal 'linked absolutely', I18n.backend.translate('en', :link, :scope => :foo) 44 | end 45 | 46 | test "linked lookup: a link can resolve to a namespace in the middle of a dot-separated key" do 47 | I18n.backend.store_translations 'en', { 48 | :activemodel => { :errors => { :messages => { :blank => "can't be blank" } } }, 49 | :activerecord => { :errors => { :messages => :"activemodel.errors.messages" } } 50 | } 51 | assert_equal "can't be blank", I18n.t(:"activerecord.errors.messages.blank") 52 | assert_equal "can't be blank", I18n.t(:"activerecord.errors.messages.blank") 53 | end 54 | 55 | test "linked lookup: a link can resolve with option :count" do 56 | I18n.backend.store_translations 'en', { 57 | :counter => :counted, 58 | :counted => { :foo => { :one => "one", :other => "other" }, :bar => "bar" } 59 | } 60 | assert_equal "one", I18n.t(:'counter.foo', count: 1) 61 | assert_equal "other", I18n.t(:'counter.foo', count: 2) 62 | assert_equal "bar", I18n.t(:'counter.bar', count: 3) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/i18n/tests/localization.rb: -------------------------------------------------------------------------------- 1 | module I18n 2 | module Tests 3 | module Localization 4 | autoload :Date, 'i18n/tests/localization/date' 5 | autoload :DateTime, 'i18n/tests/localization/date_time' 6 | autoload :Time, 'i18n/tests/localization/time' 7 | autoload :Procs, 'i18n/tests/localization/procs' 8 | 9 | def self.included(base) 10 | base.class_eval do 11 | include I18n::Tests::Localization::Date 12 | include I18n::Tests::Localization::DateTime 13 | include I18n::Tests::Localization::Procs 14 | include I18n::Tests::Localization::Time 15 | end 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/i18n/tests/localization/date.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Localization 6 | module Date 7 | def setup 8 | super 9 | setup_date_translations 10 | @date = ::Date.new(2008, 3, 1) 11 | end 12 | 13 | test "localize Date: given the short format it uses it" do 14 | assert_equal '01. Mär', I18n.l(@date, :format => :short, :locale => :de) 15 | end 16 | 17 | test "localize Date: given the long format it uses it" do 18 | assert_equal '01. März 2008', I18n.l(@date, :format => :long, :locale => :de) 19 | end 20 | 21 | test "localize Date: given the default format it uses it" do 22 | assert_equal '01.03.2008', I18n.l(@date, :format => :default, :locale => :de) 23 | end 24 | 25 | test "localize Date: given a day name format it returns the correct day name" do 26 | assert_equal 'Samstag', I18n.l(@date, :format => '%A', :locale => :de) 27 | end 28 | 29 | test "localize Date: given a uppercased day name format it returns the correct day name in upcase" do 30 | assert_equal 'samstag'.upcase, I18n.l(@date, :format => '%^A', :locale => :de) 31 | end 32 | 33 | test "localize Date: given an abbreviated day name format it returns the correct abbreviated day name" do 34 | assert_equal 'Sa', I18n.l(@date, :format => '%a', :locale => :de) 35 | end 36 | 37 | test "localize Date: given an meridian indicator format it returns the correct meridian indicator" do 38 | assert_equal 'AM', I18n.l(@date, :format => '%p', :locale => :de) 39 | assert_equal 'am', I18n.l(@date, :format => '%P', :locale => :de) 40 | end 41 | 42 | test "localize Date: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do 43 | assert_equal 'sa'.upcase, I18n.l(@date, :format => '%^a', :locale => :de) 44 | end 45 | 46 | test "localize Date: given a month name format it returns the correct month name" do 47 | assert_equal 'März', I18n.l(@date, :format => '%B', :locale => :de) 48 | end 49 | 50 | test "localize Date: given a uppercased month name format it returns the correct month name in upcase" do 51 | assert_equal 'märz'.upcase, I18n.l(@date, :format => '%^B', :locale => :de) 52 | end 53 | 54 | test "localize Date: given an abbreviated month name format it returns the correct abbreviated month name" do 55 | assert_equal 'Mär', I18n.l(@date, :format => '%b', :locale => :de) 56 | end 57 | 58 | test "localize Date: given an abbreviated and uppercased month name format it returns the correct abbreviated month name in upcase" do 59 | assert_equal 'mär'.upcase, I18n.l(@date, :format => '%^b', :locale => :de) 60 | end 61 | 62 | test "localize Date: given a date format with the month name upcased it returns the correct value" do 63 | assert_equal '1. FEBRUAR 2008', I18n.l(::Date.new(2008, 2, 1), :format => "%-d. %^B %Y", :locale => :de) 64 | end 65 | 66 | test "localize Date: given missing translations it returns the correct error message" do 67 | assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@date, :format => '%b', :locale => :fr) 68 | end 69 | 70 | test "localize Date: given an unknown format it does not fail" do 71 | assert_nothing_raised { I18n.l(@date, :format => '%x') } 72 | end 73 | 74 | test "localize Date: does not modify the options hash" do 75 | options = { :format => '%b', :locale => :de } 76 | assert_equal 'Mär', I18n.l(@date, **options) 77 | assert_equal({ :format => '%b', :locale => :de }, options) 78 | assert_nothing_raised { I18n.l(@date, **options.freeze) } 79 | end 80 | 81 | test "localize Date: given nil with default value it returns default" do 82 | assert_equal 'default', I18n.l(nil, :default => 'default') 83 | end 84 | 85 | test "localize Date: given nil it raises I18n::ArgumentError" do 86 | assert_raises(I18n::ArgumentError) { I18n.l(nil) } 87 | end 88 | 89 | test "localize Date: given a plain Object it raises I18n::ArgumentError" do 90 | assert_raises(I18n::ArgumentError) { I18n.l(Object.new) } 91 | end 92 | 93 | test "localize Date: given a format is missing it raises I18n::MissingTranslationData" do 94 | assert_raises(I18n::MissingTranslationData) { I18n.l(@date, :format => :missing) } 95 | end 96 | 97 | test "localize Date: it does not alter the format string" do 98 | assert_equal '01. Februar 2009', I18n.l(::Date.parse('2009-02-01'), :format => :long, :locale => :de) 99 | assert_equal '01. Oktober 2009', I18n.l(::Date.parse('2009-10-01'), :format => :long, :locale => :de) 100 | end 101 | 102 | protected 103 | 104 | def setup_date_translations 105 | I18n.backend.store_translations :de, { 106 | :date => { 107 | :formats => { 108 | :default => "%d.%m.%Y", 109 | :short => "%d. %b", 110 | :long => "%d. %B %Y", 111 | }, 112 | :day_names => %w(Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag), 113 | :abbr_day_names => %w(So Mo Di Mi Do Fr Sa), 114 | :month_names => %w(Januar Februar März April Mai Juni Juli August September Oktober November Dezember).unshift(nil), 115 | :abbr_month_names => %w(Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez).unshift(nil) 116 | } 117 | } 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/i18n/tests/localization/date_time.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Localization 6 | module DateTime 7 | def setup 8 | super 9 | setup_datetime_translations 10 | @datetime = ::DateTime.new(2008, 3, 1, 6) 11 | @other_datetime = ::DateTime.new(2008, 3, 1, 18) 12 | end 13 | 14 | test "localize DateTime: given the short format it uses it" do 15 | assert_equal '01. Mär 06:00', I18n.l(@datetime, :format => :short, :locale => :de) 16 | end 17 | 18 | test "localize DateTime: given the long format it uses it" do 19 | assert_equal '01. März 2008 06:00', I18n.l(@datetime, :format => :long, :locale => :de) 20 | end 21 | 22 | test "localize DateTime: given the default format it uses it" do 23 | assert_equal 'Sa, 01. Mär 2008 06:00:00 +0000', I18n.l(@datetime, :format => :default, :locale => :de) 24 | end 25 | 26 | test "localize DateTime: given a day name format it returns the correct day name" do 27 | assert_equal 'Samstag', I18n.l(@datetime, :format => '%A', :locale => :de) 28 | end 29 | 30 | test "localize DateTime: given a uppercased day name format it returns the correct day name in upcase" do 31 | assert_equal 'samstag'.upcase, I18n.l(@datetime, :format => '%^A', :locale => :de) 32 | end 33 | 34 | test "localize DateTime: given an abbreviated day name format it returns the correct abbreviated day name" do 35 | assert_equal 'Sa', I18n.l(@datetime, :format => '%a', :locale => :de) 36 | end 37 | 38 | test "localize DateTime: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do 39 | assert_equal 'sa'.upcase, I18n.l(@datetime, :format => '%^a', :locale => :de) 40 | end 41 | 42 | test "localize DateTime: given a month name format it returns the correct month name" do 43 | assert_equal 'März', I18n.l(@datetime, :format => '%B', :locale => :de) 44 | end 45 | 46 | test "localize DateTime: given a uppercased month name format it returns the correct month name in upcase" do 47 | assert_equal 'märz'.upcase, I18n.l(@datetime, :format => '%^B', :locale => :de) 48 | end 49 | 50 | test "localize DateTime: given an abbreviated month name format it returns the correct abbreviated month name" do 51 | assert_equal 'Mär', I18n.l(@datetime, :format => '%b', :locale => :de) 52 | end 53 | 54 | test "localize DateTime: given an abbreviated and uppercased month name format it returns the correct abbreviated month name in upcase" do 55 | assert_equal 'mär'.upcase, I18n.l(@datetime, :format => '%^b', :locale => :de) 56 | end 57 | 58 | test "localize DateTime: given a date format with the month name upcased it returns the correct value" do 59 | assert_equal '1. FEBRUAR 2008', I18n.l(::DateTime.new(2008, 2, 1, 6), :format => "%-d. %^B %Y", :locale => :de) 60 | end 61 | 62 | test "localize DateTime: given missing translations it returns the correct error message" do 63 | assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@datetime, :format => '%b', :locale => :fr) 64 | end 65 | 66 | test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator" do 67 | assert_equal 'AM', I18n.l(@datetime, :format => '%p', :locale => :de) 68 | assert_equal 'PM', I18n.l(@other_datetime, :format => '%p', :locale => :de) 69 | end 70 | 71 | test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator in downcase" do 72 | assert_equal 'am', I18n.l(@datetime, :format => '%P', :locale => :de) 73 | assert_equal 'pm', I18n.l(@other_datetime, :format => '%P', :locale => :de) 74 | end 75 | 76 | test "localize DateTime: given an unknown format it does not fail" do 77 | assert_nothing_raised { I18n.l(@datetime, :format => '%x') } 78 | end 79 | 80 | test "localize DateTime: given a format is missing it raises I18n::MissingTranslationData" do 81 | assert_raises(I18n::MissingTranslationData) { I18n.l(@datetime, :format => :missing) } 82 | end 83 | 84 | protected 85 | 86 | def setup_datetime_translations 87 | # time translations might have been set up in Tests::Api::Localization::Time 88 | I18n.backend.store_translations :de, { 89 | :time => { 90 | :formats => { 91 | :default => "%a, %d. %b %Y %H:%M:%S %z", 92 | :short => "%d. %b %H:%M", 93 | :long => "%d. %B %Y %H:%M" 94 | }, 95 | :am => 'am', 96 | :pm => 'pm' 97 | } 98 | } 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/i18n/tests/localization/procs.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Localization 6 | module Procs 7 | test "localize: using day names from lambdas" do 8 | setup_time_proc_translations 9 | time = ::Time.utc(2008, 3, 1, 6, 0) 10 | assert_match(/Суббота/, I18n.l(time, :format => "%A, %d %B", :locale => :ru)) 11 | assert_match(/суббота/, I18n.l(time, :format => "%d %B (%A)", :locale => :ru)) 12 | end 13 | 14 | test "localize: using month names from lambdas" do 15 | setup_time_proc_translations 16 | time = ::Time.utc(2008, 3, 1, 6, 0) 17 | assert_match(/марта/, I18n.l(time, :format => "%d %B %Y", :locale => :ru)) 18 | assert_match(/Март /, I18n.l(time, :format => "%B %Y", :locale => :ru)) 19 | end 20 | 21 | test "localize: using abbreviated day names from lambdas" do 22 | setup_time_proc_translations 23 | time = ::Time.utc(2008, 3, 1, 6, 0) 24 | assert_match(/марта/, I18n.l(time, :format => "%d %b %Y", :locale => :ru)) 25 | assert_match(/март /, I18n.l(time, :format => "%b %Y", :locale => :ru)) 26 | end 27 | 28 | test "localize Date: given a format that resolves to a Proc it calls the Proc with the object" do 29 | setup_time_proc_translations 30 | date = ::Date.new(2008, 3, 1) 31 | assert_equal '[Sat, 01 Mar 2008, {}]', I18n.l(date, :format => :proc, :locale => :ru) 32 | end 33 | 34 | test "localize Date: given a format that resolves to a Proc it calls the Proc with the object and extra options" do 35 | setup_time_proc_translations 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) 38 | end 39 | 40 | test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object" do 41 | setup_time_proc_translations 42 | datetime = ::DateTime.new(2008, 3, 1, 6) 43 | assert_equal '[Sat, 01 Mar 2008 06:00:00 +00:00, {}]', I18n.l(datetime, :format => :proc, :locale => :ru) 44 | end 45 | 46 | test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object and extra options" do 47 | setup_time_proc_translations 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) 50 | end 51 | 52 | test "localize Time: given a format that resolves to a Proc it calls the Proc with the object" do 53 | setup_time_proc_translations 54 | time = ::Time.utc(2008, 3, 1, 6, 0) 55 | assert_equal I18n::Tests::Localization::Procs.inspect_args([time], {}), I18n.l(time, :format => :proc, :locale => :ru) 56 | end 57 | 58 | test "localize Time: given a format that resolves to a Proc it calls the Proc with the object and extra options" do 59 | setup_time_proc_translations 60 | time = ::Time.utc(2008, 3, 1, 6, 0) 61 | options = { :foo => 'foo' } 62 | assert_equal I18n::Tests::Localization::Procs.inspect_args([time], options), I18n.l(time, **options.merge(:format => :proc, :locale => :ru)) 63 | end 64 | 65 | protected 66 | 67 | def self.inspect_args(args, kwargs) 68 | args << kwargs 69 | args = args.map do |arg| 70 | case arg 71 | when ::Time, ::DateTime 72 | arg.strftime('%a, %d %b %Y %H:%M:%S %Z').sub('+0000', '+00:00') 73 | when ::Date 74 | arg.strftime('%a, %d %b %Y') 75 | when Hash 76 | arg.delete(:fallback_in_progress) 77 | arg.delete(:fallback_original_locale) 78 | arg.inspect 79 | else 80 | arg.inspect 81 | end 82 | end 83 | "[#{args.join(', ')}]" 84 | end 85 | 86 | def setup_time_proc_translations 87 | I18n.backend.store_translations :ru, { 88 | :time => { 89 | :formats => { 90 | :proc => lambda { |*args, **kwargs| I18n::Tests::Localization::Procs.inspect_args(args, kwargs) } 91 | } 92 | }, 93 | :date => { 94 | :formats => { 95 | :proc => lambda { |*args, **kwargs| I18n::Tests::Localization::Procs.inspect_args(args, kwargs) } 96 | }, 97 | :'day_names' => lambda { |key, options| 98 | (options[:format] =~ /^%A/) ? 99 | %w(Воскресенье Понедельник Вторник Среда Четверг Пятница Суббота) : 100 | %w(воскресенье понедельник вторник среда четверг пятница суббота) 101 | }, 102 | :'month_names' => lambda { |key, options| 103 | (options[:format] =~ /(%d|%e)(\s*)?(%B)/) ? 104 | %w(января февраля марта апреля мая июня июля августа сентября октября ноября декабря).unshift(nil) : 105 | %w(Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь).unshift(nil) 106 | }, 107 | :'abbr_month_names' => lambda { |key, options| 108 | (options[:format] =~ /(%d|%e)(\s*)(%b)/) ? 109 | %w(янв. февр. марта апр. мая июня июля авг. сент. окт. нояб. дек.).unshift(nil) : 110 | %w(янв. февр. март апр. май июнь июль авг. сент. окт. нояб. дек.).unshift(nil) 111 | }, 112 | } 113 | } 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/i18n/tests/localization/time.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Localization 6 | module Time 7 | def setup 8 | super 9 | setup_time_translations 10 | @time = ::Time.utc(2008, 3, 1, 6, 0) 11 | @other_time = ::Time.utc(2008, 3, 1, 18, 0) 12 | end 13 | 14 | test "localize Time: given the short format it uses it" do 15 | assert_equal '01. Mär 06:00', I18n.l(@time, :format => :short, :locale => :de) 16 | end 17 | 18 | test "localize Time: given the long format it uses it" do 19 | assert_equal '01. März 2008 06:00', I18n.l(@time, :format => :long, :locale => :de) 20 | end 21 | 22 | # TODO Seems to break on Windows because ENV['TZ'] is ignored. What's a better way to do this? 23 | # def test_localize_given_the_default_format_it_uses_it 24 | # assert_equal 'Sa, 01. Mar 2008 06:00:00 +0000', I18n.l(@time, :format => :default, :locale => :de) 25 | # end 26 | 27 | test "localize Time: given a day name format it returns the correct day name" do 28 | assert_equal 'Samstag', I18n.l(@time, :format => '%A', :locale => :de) 29 | end 30 | 31 | test "localize Time: given a uppercased day name format it returns the correct day name in upcase" do 32 | assert_equal 'samstag'.upcase, I18n.l(@time, :format => '%^A', :locale => :de) 33 | end 34 | 35 | test "localize Time: given an abbreviated day name format it returns the correct abbreviated day name" do 36 | assert_equal 'Sa', I18n.l(@time, :format => '%a', :locale => :de) 37 | end 38 | 39 | test "localize Time: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do 40 | assert_equal 'sa'.upcase, I18n.l(@time, :format => '%^a', :locale => :de) 41 | end 42 | 43 | test "localize Time: given a month name format it returns the correct month name" do 44 | assert_equal 'März', I18n.l(@time, :format => '%B', :locale => :de) 45 | end 46 | 47 | test "localize Time: given a uppercased month name format it returns the correct month name in upcase" do 48 | assert_equal 'märz'.upcase, I18n.l(@time, :format => '%^B', :locale => :de) 49 | end 50 | 51 | test "localize Time: given an abbreviated month name format it returns the correct abbreviated month name" do 52 | assert_equal 'Mär', I18n.l(@time, :format => '%b', :locale => :de) 53 | end 54 | 55 | test "localize Time: given an abbreviated and uppercased month name format it returns the correct abbreviated month name in upcase" do 56 | assert_equal 'mär'.upcase, I18n.l(@time, :format => '%^b', :locale => :de) 57 | end 58 | 59 | test "localize Time: given a date format with the month name upcased it returns the correct value" do 60 | assert_equal '1. FEBRUAR 2008', I18n.l(::Time.utc(2008, 2, 1, 6, 0), :format => "%-d. %^B %Y", :locale => :de) 61 | end 62 | 63 | test "localize Time: given missing translations it returns the correct error message" do 64 | assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@time, :format => '%b', :locale => :fr) 65 | end 66 | 67 | test "localize Time: given a meridian indicator format it returns the correct meridian indicator" do 68 | assert_equal 'AM', I18n.l(@time, :format => '%p', :locale => :de) 69 | assert_equal 'PM', I18n.l(@other_time, :format => '%p', :locale => :de) 70 | end 71 | 72 | test "localize Time: given a meridian indicator format it returns the correct meridian indicator in upcase" do 73 | assert_equal 'am', I18n.l(@time, :format => '%P', :locale => :de) 74 | assert_equal 'pm', I18n.l(@other_time, :format => '%P', :locale => :de) 75 | end 76 | 77 | test "localize Time: given an unknown format it does not fail" do 78 | assert_nothing_raised { I18n.l(@time, :format => '%x') } 79 | end 80 | 81 | test "localize Time: given a format is missing it raises I18n::MissingTranslationData" do 82 | assert_raises(I18n::MissingTranslationData) { I18n.l(@time, :format => :missing) } 83 | end 84 | 85 | protected 86 | 87 | def setup_time_translations 88 | I18n.backend.store_translations :de, { 89 | :time => { 90 | :formats => { 91 | :default => "%a, %d. %b %Y %H:%M:%S %z", 92 | :short => "%d. %b %H:%M", 93 | :long => "%d. %B %Y %H:%M", 94 | }, 95 | :am => 'am', 96 | :pm => 'pm' 97 | } 98 | } 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/i18n/tests/lookup.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Lookup 6 | def setup 7 | super 8 | I18n.backend.store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' }, :falsy => false, :truthy => true, 9 | :string => "a", :array => %w(a b c), :hash => { "a" => "b" }) 10 | end 11 | 12 | test "lookup: it returns a string" do 13 | assert_equal("a", I18n.t(:string)) 14 | end 15 | 16 | test "lookup: it returns hash" do 17 | assert_equal({ :a => "b" }, I18n.t(:hash)) 18 | end 19 | 20 | test "lookup: it returns an array" do 21 | assert_equal(%w(a b c), I18n.t(:array)) 22 | end 23 | 24 | test "lookup: it returns a native true" do 25 | assert I18n.t(:truthy) === true 26 | end 27 | 28 | test "lookup: it returns a native false" do 29 | assert I18n.t(:falsy) === false 30 | end 31 | 32 | test "lookup: given a missing key, no default and no raise option it returns an error message" do 33 | assert_equal "Translation missing: en.missing", I18n.t(:missing) 34 | end 35 | 36 | test "lookup: given a missing key, no default and the raise option it raises MissingTranslationData" do 37 | assert_raises(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) } 38 | end 39 | 40 | test "lookup: does not raise an exception if no translation data is present for the given locale" do 41 | assert_nothing_raised { I18n.t(:foo, :locale => :xx) } 42 | end 43 | 44 | test "lookup: does not modify the options hash" do 45 | options = {} 46 | assert_equal "a", I18n.t(:string, **options) 47 | assert_equal({}, options) 48 | assert_nothing_raised { I18n.t(:string, **options.freeze) } 49 | end 50 | 51 | test "lookup: given an array of keys it translates all of them" do 52 | assert_equal %w(bar baz), I18n.t([:bar, :baz], :scope => [:foo]) 53 | end 54 | 55 | test "lookup: using a custom scope separator" do 56 | # data must have been stored using the custom separator when using the ActiveRecord backend 57 | I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' }) 58 | assert_equal 'bar', I18n.t('foo|bar', :separator => '|') 59 | end 60 | 61 | # In fact it probably *should* fail but Rails currently relies on using the default locale instead. 62 | # So we'll stick to this for now until we get it fixed in Rails. 63 | test "lookup: given nil as a locale it does not raise but use the default locale" do 64 | # assert_raises(I18n::InvalidLocale) { I18n.t(:bar, :locale => nil) } 65 | assert_nothing_raised { I18n.t(:bar, :locale => nil) } 66 | end 67 | 68 | test "lookup: a resulting String is not frozen" do 69 | assert !I18n.t(:string).frozen? 70 | end 71 | 72 | test "lookup: a resulting Array is not frozen" do 73 | assert !I18n.t(:array).frozen? 74 | end 75 | 76 | test "lookup: a resulting Hash is not frozen" do 77 | assert !I18n.t(:hash).frozen? 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 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/i18n/tests/pluralization.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Pluralization 6 | test "pluralization: given 0 it returns the :zero translation if it is defined" do 7 | assert_equal 'zero', I18n.t(:default => { :zero => 'zero' }, :count => 0) 8 | end 9 | 10 | test "pluralization: given 0 it returns the :other translation if :zero is not defined" do 11 | assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 0) 12 | end 13 | 14 | test "pluralization: given 1 it returns the singular translation" do 15 | assert_equal 'bar', I18n.t(:default => { :one => 'bar' }, :count => 1) 16 | end 17 | 18 | test "pluralization: given 2 it returns the :other translation" do 19 | assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 2) 20 | end 21 | 22 | test "pluralization: given 3 it returns the :other translation" do 23 | assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 3) 24 | end 25 | 26 | test "pluralization: given nil it returns the whole entry" do 27 | assert_equal({ :one => 'bar' }, I18n.t(:default => { :one => 'bar' }, :count => nil)) 28 | end 29 | 30 | test "pluralization: given incomplete pluralization data it raises I18n::InvalidPluralizationData" do 31 | assert_raises(I18n::InvalidPluralizationData) { I18n.t(:default => { :one => 'bar' }, :count => 2) } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/i18n/tests/procs.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module I18n 4 | module Tests 5 | module Procs 6 | test "lookup: given a translation is a proc it calls the proc with the key and interpolation values" do 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') 9 | end 10 | 11 | test "lookup: given a translation is a proc it passes the interpolation values as keyword arguments" do 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') 14 | end 15 | 16 | test "defaults: given a default is a Proc it calls it with the key and interpolation values" do 17 | proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) } 18 | assert_equal %|[nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo') 19 | end 20 | 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 | the_lambda = lambda { |*args| I18n::Tests::Procs.filter_args(*args) } 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') 26 | end 27 | 28 | test "interpolation: given an interpolation value is a lambda it calls it with key and values before interpolating it" do 29 | proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) } 30 | if RUBY_VERSION < "3.4" 31 | assert_match %r(\[\{:foo=>#\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc) 32 | else 33 | assert_match %r(\[\{foo: #\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc) 34 | end 35 | end 36 | 37 | test "interpolation: given a key resolves to a Proc that returns a string then interpolation still works" do 38 | proc = lambda { |*args| "%{foo}: " + I18n::Tests::Procs.filter_args(*args) } 39 | assert_equal %|foo: [nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo') 40 | end 41 | 42 | test "pluralization: given a key resolves to a Proc that returns valid data then pluralization still works" do 43 | proc = lambda { |*args| { :zero => 'zero', :one => 'one', :other => 'other' } } 44 | assert_equal 'zero', I18n.t(:default => proc, :count => 0) 45 | assert_equal 'one', I18n.t(:default => proc, :count => 1) 46 | assert_equal 'other', I18n.t(:default => proc, :count => 2) 47 | end 48 | 49 | test "lookup: given the option :resolve => false was passed it does not resolve proc translations" do 50 | I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| I18n::Tests::Procs.filter_args(*args) }) 51 | assert_equal Proc, I18n.t(:a_lambda, :resolve => false).class 52 | end 53 | 54 | test "lookup: given the option :resolve => false was passed it does not resolve proc default" do 55 | assert_equal Proc, I18n.t(nil, :default => lambda { |*args| I18n::Tests::Procs.filter_args(*args) }, :resolve => false).class 56 | end 57 | 58 | 59 | def self.filter_args(*args) 60 | args.map do |arg| 61 | if arg.is_a?(Hash) 62 | arg.delete(:fallback_in_progress) 63 | arg.delete(:fallback_original_locale) 64 | arg.delete(:skip_interpolation) 65 | end 66 | arg 67 | end.inspect 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/i18n/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | module Utils 5 | class << self 6 | if Hash.method_defined?(:except) 7 | def except(hash, *keys) 8 | hash.except(*keys) 9 | end 10 | else 11 | def except(hash, *keys) 12 | hash = hash.dup 13 | keys.each { |k| hash.delete(k) } 14 | hash 15 | end 16 | end 17 | 18 | def deep_merge(hash, other_hash, &block) 19 | deep_merge!(hash.dup, other_hash, &block) 20 | end 21 | 22 | def deep_merge!(hash, other_hash, &block) 23 | hash.merge!(other_hash) do |key, this_val, other_val| 24 | if this_val.is_a?(Hash) && other_val.is_a?(Hash) 25 | deep_merge(this_val, other_val, &block) 26 | elsif block_given? 27 | yield key, this_val, other_val 28 | else 29 | other_val 30 | end 31 | end 32 | end 33 | 34 | def deep_symbolize_keys(hash) 35 | hash.each_with_object({}) do |(key, value), result| 36 | result[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize_keys_in_object(value) 37 | result 38 | end 39 | end 40 | 41 | private 42 | 43 | def deep_symbolize_keys_in_object(value) 44 | case value 45 | when Hash 46 | deep_symbolize_keys(value) 47 | when Array 48 | value.map { |e| deep_symbolize_keys_in_object(e) } 49 | else 50 | value 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/i18n/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module I18n 4 | VERSION = "1.14.7" 5 | end 6 | -------------------------------------------------------------------------------- /test/api/all_features_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | begin 4 | require 'rubygems' 5 | require 'active_support' 6 | rescue LoadError 7 | puts "not testing with Cache enabled because active_support can not be found" 8 | end 9 | 10 | class I18nAllFeaturesApiTest < I18n::TestCase 11 | class Backend < I18n::Backend::Simple 12 | include I18n::Backend::Metadata 13 | include I18n::Backend::Cache 14 | include I18n::Backend::Cascade 15 | include I18n::Backend::Fallbacks 16 | include I18n::Backend::Pluralization 17 | include I18n::Backend::Memoize 18 | end 19 | 20 | def setup 21 | I18n.backend = I18n::Backend::Chain.new(Backend.new, I18n::Backend::Simple.new) 22 | I18n.cache_store = cache_store 23 | super 24 | end 25 | 26 | def teardown 27 | I18n.cache_store.clear if I18n.cache_store 28 | I18n.cache_store = nil 29 | super 30 | end 31 | 32 | def cache_store 33 | ActiveSupport::Cache.lookup_store(:memory_store) if cache_available? 34 | end 35 | 36 | def cache_available? 37 | defined?(ActiveSupport) && defined?(ActiveSupport::Cache) 38 | end 39 | 40 | include I18n::Tests::Basics 41 | include I18n::Tests::Defaults 42 | include I18n::Tests::Interpolation 43 | include I18n::Tests::Link 44 | include I18n::Tests::Lookup 45 | include I18n::Tests::Pluralization 46 | include I18n::Tests::Procs 47 | include I18n::Tests::Localization::Date 48 | include I18n::Tests::Localization::DateTime 49 | include I18n::Tests::Localization::Time 50 | include I18n::Tests::Localization::Procs 51 | 52 | test "make sure we use a Chain backend with an all features backend" do 53 | assert_equal I18n::Backend::Chain, I18n.backend.class 54 | assert_equal Backend, I18n.backend.backends.first.class 55 | end 56 | 57 | # links: test that keys stored on one backend can link to keys stored on another backend 58 | end 59 | -------------------------------------------------------------------------------- /test/api/cascade_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nCascadeApiTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Cascade 6 | end 7 | 8 | def setup 9 | I18n.backend = Backend.new 10 | super 11 | end 12 | 13 | include I18n::Tests::Basics 14 | include I18n::Tests::Defaults 15 | include I18n::Tests::Interpolation 16 | include I18n::Tests::Link 17 | include I18n::Tests::Lookup 18 | include I18n::Tests::Pluralization 19 | include I18n::Tests::Procs 20 | include I18n::Tests::Localization::Date 21 | include I18n::Tests::Localization::DateTime 22 | include I18n::Tests::Localization::Time 23 | include I18n::Tests::Localization::Procs 24 | 25 | test "make sure we use a backend with Cascade included" do 26 | assert_equal Backend, I18n.backend.class 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/api/chain_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nApiChainTest < I18n::TestCase 4 | def setup 5 | super 6 | I18n.backend = I18n::Backend::Chain.new(I18n::Backend::Simple.new, I18n.backend) 7 | end 8 | 9 | include I18n::Tests::Basics 10 | include I18n::Tests::Defaults 11 | include I18n::Tests::Interpolation 12 | include I18n::Tests::Link 13 | include I18n::Tests::Lookup 14 | include I18n::Tests::Pluralization 15 | include I18n::Tests::Procs 16 | include I18n::Tests::Localization::Date 17 | include I18n::Tests::Localization::DateTime 18 | include I18n::Tests::Localization::Time 19 | include I18n::Tests::Localization::Procs 20 | 21 | test "make sure we use the Chain backend" do 22 | assert_equal I18n::Backend::Chain, I18n.backend.class 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/api/fallbacks_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nFallbacksApiTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Fallbacks 6 | end 7 | 8 | def setup 9 | I18n.backend = Backend.new 10 | super 11 | end 12 | 13 | include I18n::Tests::Basics 14 | include I18n::Tests::Defaults 15 | include I18n::Tests::Interpolation 16 | include I18n::Tests::Link 17 | include I18n::Tests::Lookup 18 | include I18n::Tests::Pluralization 19 | include I18n::Tests::Procs 20 | include I18n::Tests::Localization::Date 21 | include I18n::Tests::Localization::DateTime 22 | include I18n::Tests::Localization::Time 23 | include I18n::Tests::Localization::Procs 24 | 25 | test "make sure we use a backend with Fallbacks included" do 26 | assert_equal Backend, I18n.backend.class 27 | end 28 | 29 | # links: test that keys stored on one backend can link to keys stored on another backend 30 | end 31 | -------------------------------------------------------------------------------- /test/api/key_value_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nKeyValueApiTest < I18n::TestCase 4 | include I18n::Tests::Basics 5 | include I18n::Tests::Defaults 6 | include I18n::Tests::Interpolation 7 | include I18n::Tests::Link 8 | include I18n::Tests::Lookup 9 | include I18n::Tests::Pluralization 10 | # include Tests::Api::Procs 11 | include I18n::Tests::Localization::Date 12 | include I18n::Tests::Localization::DateTime 13 | include I18n::Tests::Localization::Time 14 | # include Tests::Api::Localization::Procs 15 | 16 | def setup 17 | I18n.backend = I18n::Backend::KeyValue.new({}) 18 | super 19 | end 20 | 21 | test "make sure we use the KeyValue backend" do 22 | assert_equal I18n::Backend::KeyValue, I18n.backend.class 23 | end 24 | end if I18n::TestCase.key_value? 25 | -------------------------------------------------------------------------------- /test/api/lazy_loadable_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nLazyLoadableBackendApiTest < I18n::TestCase 4 | def setup 5 | I18n.backend = I18n::Backend::LazyLoadable.new 6 | super 7 | end 8 | 9 | include I18n::Tests::Basics 10 | include I18n::Tests::Defaults 11 | include I18n::Tests::Interpolation 12 | include I18n::Tests::Link 13 | include I18n::Tests::Lookup 14 | include I18n::Tests::Pluralization 15 | include I18n::Tests::Procs 16 | include I18n::Tests::Localization::Date 17 | include I18n::Tests::Localization::DateTime 18 | include I18n::Tests::Localization::Time 19 | include I18n::Tests::Localization::Procs 20 | 21 | test "make sure we use the LazyLoadable backend" do 22 | assert_equal I18n::Backend::LazyLoadable, I18n.backend.class 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/api/memoize_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nMemoizeBackendWithSimpleApiTest < I18n::TestCase 4 | include I18n::Tests::Basics 5 | include I18n::Tests::Defaults 6 | include I18n::Tests::Interpolation 7 | include I18n::Tests::Link 8 | include I18n::Tests::Lookup 9 | include I18n::Tests::Pluralization 10 | include I18n::Tests::Procs 11 | include I18n::Tests::Localization::Date 12 | include I18n::Tests::Localization::DateTime 13 | include I18n::Tests::Localization::Time 14 | include I18n::Tests::Localization::Procs 15 | 16 | class MemoizeBackend < I18n::Backend::Simple 17 | include I18n::Backend::Memoize 18 | end 19 | 20 | def setup 21 | I18n.backend = MemoizeBackend.new 22 | super 23 | end 24 | 25 | test "make sure we use the MemoizeBackend backend" do 26 | assert_equal MemoizeBackend, I18n.backend.class 27 | end 28 | end 29 | 30 | class I18nMemoizeBackendWithKeyValueApiTest < I18n::TestCase 31 | include I18n::Tests::Basics 32 | include I18n::Tests::Defaults 33 | include I18n::Tests::Interpolation 34 | include I18n::Tests::Link 35 | include I18n::Tests::Lookup 36 | include I18n::Tests::Pluralization 37 | include I18n::Tests::Localization::Date 38 | include I18n::Tests::Localization::DateTime 39 | include I18n::Tests::Localization::Time 40 | 41 | # include I18n::Tests::Procs 42 | # include I18n::Tests::Localization::Procs 43 | 44 | class MemoizeBackend < I18n::Backend::KeyValue 45 | include I18n::Backend::Memoize 46 | end 47 | 48 | def setup 49 | I18n.backend = MemoizeBackend.new({}) 50 | super 51 | end 52 | 53 | test "make sure we use the MemoizeBackend backend" do 54 | assert_equal MemoizeBackend, I18n.backend.class 55 | end 56 | end if I18n::TestCase.key_value? 57 | -------------------------------------------------------------------------------- /test/api/override_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nOverrideTest < I18n::TestCase 4 | module OverrideInverse 5 | def translate(key, **options) 6 | super(key, **options).reverse 7 | end 8 | alias :t :translate 9 | end 10 | 11 | module OverrideSignature 12 | def translate(*args) 13 | args.first + args[1] 14 | end 15 | alias :t :translate 16 | end 17 | 18 | def setup 19 | super 20 | @I18n = I18n.dup 21 | @I18n.backend = I18n::Backend::Simple.new 22 | end 23 | 24 | test "make sure modules can overwrite I18n methods" do 25 | @I18n.extend OverrideInverse 26 | @I18n.backend.store_translations('en', :foo => 'bar') 27 | 28 | assert_equal 'rab', @I18n.translate(:foo, :locale => 'en') 29 | assert_equal 'rab', @I18n.t(:foo, :locale => 'en') 30 | assert_equal 'rab', @I18n.translate!(:foo, :locale => 'en') 31 | assert_equal 'rab', @I18n.t!(:foo, :locale => 'en') 32 | end 33 | 34 | test "make sure modules can overwrite I18n signature" do 35 | exception = catch(:exception) do 36 | @I18n.t('Hello', :tokenize => true, :throw => true) 37 | end 38 | assert exception.message 39 | @I18n.extend OverrideSignature 40 | assert_equal 'HelloWelcome message on home page', @I18n.translate('Hello', 'Welcome message on home page', :tokenize => true) # tr8n example 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/api/pluralization_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nPluralizationApiTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Pluralization 6 | end 7 | 8 | def setup 9 | I18n.backend = Backend.new 10 | super 11 | end 12 | 13 | include I18n::Tests::Basics 14 | include I18n::Tests::Defaults 15 | include I18n::Tests::Interpolation 16 | include I18n::Tests::Link 17 | include I18n::Tests::Lookup 18 | include I18n::Tests::Pluralization 19 | include I18n::Tests::Procs 20 | include I18n::Tests::Localization::Date 21 | include I18n::Tests::Localization::DateTime 22 | include I18n::Tests::Localization::Time 23 | include I18n::Tests::Localization::Procs 24 | 25 | test "make sure we use a backend with Pluralization included" do 26 | assert_equal Backend, I18n.backend.class 27 | end 28 | 29 | # links: test that keys stored on one backend can link to keys stored on another backend 30 | end 31 | -------------------------------------------------------------------------------- /test/api/simple_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nSimpleBackendApiTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Pluralization 6 | end 7 | 8 | def setup 9 | I18n.backend = I18n::Backend::Simple.new 10 | super 11 | end 12 | 13 | include I18n::Tests::Basics 14 | include I18n::Tests::Defaults 15 | include I18n::Tests::Interpolation 16 | include I18n::Tests::Link 17 | include I18n::Tests::Lookup 18 | include I18n::Tests::Pluralization 19 | include I18n::Tests::Procs 20 | include I18n::Tests::Localization::Date 21 | include I18n::Tests::Localization::DateTime 22 | include I18n::Tests::Localization::Time 23 | include I18n::Tests::Localization::Procs 24 | 25 | test "make sure we use the Simple backend" do 26 | assert_equal I18n::Backend::Simple, I18n.backend.class 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/backend/cache_file_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'fileutils' 3 | require 'tempfile' 4 | 5 | module CountWrites 6 | attr_reader :writes 7 | 8 | def initialize(*args) 9 | super 10 | @writes = [] 11 | end 12 | 13 | def store_translations(*args) 14 | super.tap { @writes << args } 15 | end 16 | end 17 | 18 | module CacheFileTest 19 | test 'load_translations caches loaded file contents' do 20 | setup_backend! 21 | I18n.load_path = [locales_dir + '/en.yml'] 22 | assert_equal 0, @backend.writes.count 23 | 24 | @backend.load_translations unless @backend.is_a?(I18n::Backend::Simple) 25 | assert_equal('baz', I18n.t('foo.bar')) 26 | assert_equal 2, @backend.writes.count 27 | 28 | @backend.load_translations 29 | assert_equal('baz', I18n.t('foo.bar')) 30 | assert_equal 2, @backend.writes.count 31 | end 32 | 33 | test 'setting path_roots normalizes write key' do 34 | setup_backend! 35 | I18n.load_path = [locales_dir + '/en.yml'] 36 | @backend.path_roots = [locales_dir] 37 | @backend.load_translations 38 | refute_nil I18n.t("0/en\x01yml", scope: :load_file, locale: :i18n, default: nil) 39 | end 40 | 41 | test 'load_translations caches file through updated modification time' do 42 | setup_backend! 43 | Tempfile.open(['test', '.yml']) do |file| 44 | I18n.load_path = [file.path] 45 | 46 | File.write(file, { :en => { :foo => { :bar => 'baz' } } }.to_yaml) 47 | assert_equal 0, @backend.writes.count 48 | 49 | @backend.load_translations unless @backend.is_a?(I18n::Backend::Simple) 50 | assert_equal('baz', I18n.t('foo.bar')) 51 | assert_equal 2, @backend.writes.count 52 | 53 | FileUtils.touch(file, :mtime => Time.now + 1) 54 | @backend.load_translations 55 | assert_equal('baz', I18n.t('foo.bar')) 56 | assert_equal 2, @backend.writes.count 57 | 58 | File.write(file, { :en => { :foo => { :bar => 'baa' } } }.to_yaml) 59 | FileUtils.touch(file, :mtime => Time.now + 1) 60 | @backend.load_translations 61 | assert_equal('baa', I18n.t('foo.bar')) 62 | assert_equal 4, @backend.writes.count 63 | end 64 | end 65 | end 66 | 67 | class SimpleCacheFileTest < I18n::TestCase 68 | include CacheFileTest 69 | 70 | class Backend < I18n::Backend::Simple 71 | include CountWrites 72 | include I18n::Backend::CacheFile 73 | end 74 | 75 | def setup_backend! 76 | @backend = I18n.backend = Backend.new 77 | end 78 | end 79 | 80 | class KeyValueCacheFileTest < I18n::TestCase 81 | include CacheFileTest 82 | 83 | class Backend < I18n::Backend::KeyValue 84 | include CountWrites 85 | include I18n::Backend::CacheFile 86 | def initialize 87 | super({}) 88 | end 89 | end 90 | 91 | def setup_backend! 92 | @backend = I18n.backend = Backend.new 93 | end 94 | end if I18n::TestCase.key_value? 95 | -------------------------------------------------------------------------------- /test/backend/cache_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'openssl' 3 | 4 | begin 5 | require 'active_support' 6 | rescue LoadError 7 | $stderr.puts "Skipping cache tests using ActiveSupport" 8 | else 9 | 10 | class I18nBackendCacheTest < I18n::TestCase 11 | class Backend < I18n::Backend::Simple 12 | include I18n::Backend::Cache 13 | end 14 | 15 | def setup 16 | I18n.backend = Backend.new 17 | super 18 | I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store) 19 | I18n.cache_store.clear 20 | I18n.cache_key_digest = nil 21 | end 22 | 23 | def teardown 24 | super 25 | I18n.cache_store.clear 26 | I18n.cache_store = nil 27 | end 28 | 29 | test "it uses the cache" do 30 | assert I18n.cache_store.is_a?(ActiveSupport::Cache::MemoryStore) 31 | end 32 | 33 | test "translate hits the backend and caches the response" do 34 | I18n.backend.expects(:lookup).returns('Foo') 35 | assert_equal 'Foo', I18n.t(:foo) 36 | 37 | I18n.backend.expects(:lookup).never 38 | assert_equal 'Foo', I18n.t(:foo) 39 | 40 | I18n.backend.expects(:lookup).returns('Bar') 41 | assert_equal 'Bar', I18n.t(:bar) 42 | end 43 | 44 | test "translate returns a cached false response" do 45 | I18n.backend.expects(:lookup).never 46 | I18n.cache_store.expects(:read).returns(false) 47 | assert_equal false, I18n.t(:foo) 48 | end 49 | 50 | test "still raises MissingTranslationData but also caches it" do 51 | assert_raises(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) } 52 | assert_raises(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) } 53 | assert_equal 1, I18n.cache_store.instance_variable_get(:@data).size 54 | 55 | # I18n.backend.expects(:lookup).returns(nil) 56 | # assert_raises(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) } 57 | # I18n.backend.expects(:lookup).never 58 | # assert_raises(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) } 59 | end 60 | 61 | test "MissingTranslationData does not cache custom options" do 62 | I18n.t(:missing, :scope => :foo, :extra => true) 63 | assert_equal 1, I18n.cache_store.instance_variable_get(:@data).size 64 | 65 | value = I18n.cache_store.read(I18n.cache_store.instance_variable_get(:@data).keys.first) 66 | 67 | assert_equal({ scope: :foo }, value.options) 68 | end 69 | 70 | test "uses 'i18n' as a cache key namespace by default" do 71 | assert_equal 0, I18n.backend.send(:cache_key, :en, :foo, {}).index('i18n') 72 | end 73 | 74 | test "adds a custom cache key namespace" do 75 | with_cache_namespace('bar') do 76 | assert_equal 0, I18n.backend.send(:cache_key, :en, :foo, {}).index('i18n/bar/') 77 | end 78 | end 79 | 80 | test "adds locale and hash of key and hash of options" do 81 | options = { :bar => 1 } 82 | assert_equal "i18n//en/#{:foo.to_s.hash}/#{options.to_s.hash}", I18n.backend.send(:cache_key, :en, :foo, options) 83 | end 84 | 85 | test "cache_key uses configured digest method" do 86 | digest = OpenSSL::Digest::SHA256.new 87 | options = { :bar => 1 } 88 | options_hash = options.inspect 89 | with_cache_key_digest(digest) do 90 | assert_equal "i18n//en/#{digest.hexdigest(:foo.to_s)}/#{digest.hexdigest(options_hash)}", I18n.backend.send(:cache_key, :en, :foo, options) 91 | end 92 | end 93 | 94 | test "keys should not be equal" do 95 | interpolation_values1 = { :foo => 1, :bar => 2 } 96 | interpolation_values2 = { :foo => 2, :bar => 1 } 97 | 98 | key1 = I18n.backend.send(:cache_key, :en, :some_key, interpolation_values1) 99 | key2 = I18n.backend.send(:cache_key, :en, :some_key, interpolation_values2) 100 | 101 | assert key1 != key2 102 | end 103 | 104 | protected 105 | 106 | def with_cache_namespace(namespace) 107 | I18n.cache_namespace = namespace 108 | yield 109 | I18n.cache_namespace = nil 110 | end 111 | 112 | def with_cache_key_digest(digest) 113 | I18n.cache_key_digest = digest 114 | yield 115 | I18n.cache_key_digest = nil 116 | end 117 | end 118 | 119 | end # AS cache check 120 | -------------------------------------------------------------------------------- /test/backend/cascade_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nBackendCascadeTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Cascade 6 | end 7 | 8 | def setup 9 | super 10 | I18n.backend = Backend.new 11 | store_translations(:en, :foo => 'foo', :bar => { :baz => 'baz' }) 12 | @cascade_options = { :step => 1, :offset => 1, :skip_root => false } 13 | end 14 | 15 | def lookup(key, options = {}) 16 | I18n.t(key, **options.merge(:cascade => @cascade_options)) 17 | end 18 | 19 | test "still returns an existing translation as usual" do 20 | assert_equal 'foo', lookup(:foo) 21 | assert_equal 'baz', lookup(:'bar.baz') 22 | end 23 | 24 | test "falls back by cutting keys off the end of the scope" do 25 | assert_equal 'foo', lookup(:foo, :scope => :'missing') 26 | assert_equal 'foo', lookup(:foo, :scope => :'missing.missing') 27 | assert_equal 'baz', lookup(:baz, :scope => :'bar.missing') 28 | assert_equal 'baz', lookup(:baz, :scope => :'bar.missing.missing') 29 | end 30 | 31 | test "raises I18n::MissingTranslationData exception when no translation was found" do 32 | assert_raises(I18n::MissingTranslationData) { lookup(:'foo.missing', :raise => true) } 33 | assert_raises(I18n::MissingTranslationData) { lookup(:'bar.baz.missing', :raise => true) } 34 | assert_raises(I18n::MissingTranslationData) { lookup(:'missing.bar.baz', :raise => true) } 35 | end 36 | 37 | test "cascades before evaluating the default" do 38 | assert_equal 'foo', lookup(:foo, :scope => :missing, :default => 'default') 39 | end 40 | 41 | test "cascades defaults, too" do 42 | assert_equal 'foo', lookup(nil, :default => [:'missing.missing', :'missing.foo']) 43 | end 44 | 45 | test "works with :offset => 2 and a single key" do 46 | @cascade_options[:offset] = 2 47 | lookup(:foo) 48 | end 49 | 50 | test "assemble required fallbacks for ActiveRecord validation messages" do 51 | store_translations(:en, 52 | :errors => { 53 | :odd => 'errors.odd', 54 | :reply => { :title => { :blank => 'errors.reply.title.blank' }, :taken => 'errors.reply.taken' }, 55 | :topic => { :title => { :format => 'errors.topic.title.format' }, :length => 'errors.topic.length' } 56 | } 57 | ) 58 | assert_equal 'errors.reply.title.blank', lookup(:'errors.reply.title.blank', :default => :'errors.topic.title.blank') 59 | assert_equal 'errors.reply.taken', lookup(:'errors.reply.title.taken', :default => :'errors.topic.title.taken') 60 | assert_equal 'errors.topic.title.format', lookup(:'errors.reply.title.format', :default => :'errors.topic.title.format') 61 | assert_equal 'errors.topic.length', lookup(:'errors.reply.title.length', :default => :'errors.topic.title.length') 62 | assert_equal 'errors.odd', lookup(:'errors.reply.title.odd', :default => :'errors.topic.title.odd') 63 | end 64 | 65 | test "assemble action view translation helper lookup cascade" do 66 | @cascade_options[:offset] = 2 67 | 68 | store_translations(:en, 69 | :menu => { :show => 'menu.show' }, 70 | :namespace => { 71 | :menu => { :new => 'namespace.menu.new' }, 72 | :controller => { 73 | :menu => { :edit => 'namespace.controller.menu.edit' }, 74 | :action => { 75 | :menu => { :destroy => 'namespace.controller.action.menu.destroy' } 76 | } 77 | } 78 | } 79 | ) 80 | 81 | assert_equal 'menu.show', lookup(:'namespace.controller.action.menu.show') 82 | assert_equal 'namespace.menu.new', lookup(:'namespace.controller.action.menu.new') 83 | assert_equal 'namespace.controller.menu.edit', lookup(:'namespace.controller.action.menu.edit') 84 | assert_equal 'namespace.controller.action.menu.destroy', lookup(:'namespace.controller.action.menu.destroy') 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/backend/exceptions_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nBackendExceptionsTest < I18n::TestCase 4 | def setup 5 | super 6 | I18n.backend = I18n::Backend::Simple.new 7 | end 8 | 9 | test "throw message: MissingTranslation message from #translate includes the given scope and full key" do 10 | exception = catch(:exception) do 11 | I18n.t(:'baz.missing', :scope => :'foo.bar', :throw => true) 12 | end 13 | assert_equal "Translation missing: en.foo.bar.baz.missing", exception.message 14 | end 15 | 16 | test "exceptions: MissingTranslationData message from #translate includes the given scope and full key" do 17 | begin 18 | I18n.t(:'baz.missing', :scope => :'foo.bar', :raise => true) 19 | rescue I18n::MissingTranslationData => exception 20 | end 21 | assert_equal "Translation missing: en.foo.bar.baz.missing", exception.message 22 | end 23 | 24 | test "exceptions: MissingTranslationData message from #localize includes the given scope and full key" do 25 | begin 26 | I18n.l(Time.now, :format => :foo) 27 | rescue I18n::MissingTranslationData => exception 28 | end 29 | assert_equal "Translation missing: en.time.formats.foo", exception.message 30 | end 31 | 32 | test "exceptions: MissingInterpolationArgument message includes missing key, provided keys and full string" do 33 | exception = I18n::MissingInterpolationArgument.new('key', {:this => 'was given'}, 'string') 34 | assert_equal %|missing interpolation argument "key" in "string" (#{{:this=>"was given"}} given)|, exception.message 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/backend/interpolation_compiler_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class InterpolationCompilerTest < I18n::TestCase 4 | Compiler = I18n::Backend::InterpolationCompiler::Compiler 5 | 6 | def compile_and_interpolate(str, values = {}) 7 | Compiler.compile_if_an_interpolation(str).i18n_interpolate(values) 8 | end 9 | 10 | def assert_escapes_interpolation_key(expected, malicious_str) 11 | assert_equal(expected, Compiler.send(:escape_key_sym, malicious_str)) 12 | end 13 | 14 | def test_escape_key_properly_escapes 15 | assert_escapes_interpolation_key ':"\""', '"' 16 | assert_escapes_interpolation_key ':"\\\\"', '\\' 17 | assert_escapes_interpolation_key ':"\\\\\""', '\\"' 18 | assert_escapes_interpolation_key ':"\#{}"', '#{}' 19 | assert_escapes_interpolation_key ':"\\\\\#{}"', '\#{}' 20 | end 21 | 22 | def assert_escapes_plain_string(expected, plain_str) 23 | assert_equal expected, Compiler.send(:escape_plain_str, plain_str) 24 | end 25 | 26 | def test_escape_plain_string_properly_escapes 27 | assert_escapes_plain_string '\\"', '"' 28 | assert_escapes_plain_string '\'', '\'' 29 | assert_escapes_plain_string '\\#', '#' 30 | assert_escapes_plain_string '\\#{}', '#{}' 31 | assert_escapes_plain_string '\\\\\\"','\\"' 32 | end 33 | 34 | def test_non_interpolated_strings_or_arrays_dont_get_compiled 35 | ['abc', '\\{a}}', '{a}}', []].each do |obj| 36 | Compiler.compile_if_an_interpolation(obj) 37 | assert_equal false, obj.respond_to?(:i18n_interpolate) 38 | end 39 | end 40 | 41 | def test_interpolated_string_gets_compiled 42 | assert_equal '-A-', compile_and_interpolate('-%{a}-', :a => 'A') 43 | end 44 | 45 | def assert_handles_key(str, key) 46 | assert_equal 'A', compile_and_interpolate(str, key => 'A') 47 | end 48 | 49 | def test_compiles_fancy_keys 50 | assert_handles_key('%{\}', :'\\' ) 51 | assert_handles_key('%{#}', :'#' ) 52 | assert_handles_key('%{#{}', :'#{' ) 53 | assert_handles_key('%{#$SAFE}', :'#$SAFE') 54 | assert_handles_key('%{\000}', :'\000' ) 55 | assert_handles_key('%{\'}', :'\'' ) 56 | assert_handles_key('%{\'\'}', :'\'\'' ) 57 | assert_handles_key('%{a.b}', :'a.b' ) 58 | assert_handles_key('%{ }', :' ' ) 59 | assert_handles_key('%{:}', :':' ) 60 | assert_handles_key("%{:''}", :":''" ) 61 | assert_handles_key('%{:"}', :':"' ) 62 | end 63 | 64 | def test_str_containing_only_escaped_interpolation_is_handled_correctly 65 | assert_equal 'abc %{x}', compile_and_interpolate('abc %%{x}') 66 | end 67 | 68 | def test_handles_weird_strings 69 | assert_equal '#{} a', compile_and_interpolate('#{} %{a}', :a => 'a') 70 | assert_equal '"#{abc}"', compile_and_interpolate('"#{ab%{a}c}"', :a => '' ) 71 | assert_equal 'a}', compile_and_interpolate('%{{a}}', :'{a' => 'a') 72 | assert_equal '"', compile_and_interpolate('"%{a}', :a => '' ) 73 | assert_equal 'a%{a}', compile_and_interpolate('%{a}%%{a}', :a => 'a') 74 | assert_equal '%%{a}', compile_and_interpolate('%%%{a}') 75 | assert_equal '\";eval("a")', compile_and_interpolate('\";eval("%{a}")', :a => 'a') 76 | assert_equal '\";eval("a")', compile_and_interpolate('\";eval("a")%{a}', :a => '' ) 77 | assert_equal "\na", compile_and_interpolate("\n%{a}", :a => 'a') 78 | end 79 | 80 | def test_raises_exception_when_argument_is_missing 81 | assert_raises(I18n::MissingInterpolationArgument) do 82 | compile_and_interpolate('%{first} %{last}', :first => 'first') 83 | end 84 | end 85 | 86 | def test_custom_missing_interpolation_argument_handler 87 | old_handler = I18n.config.missing_interpolation_argument_handler 88 | I18n.config.missing_interpolation_argument_handler = lambda do |key, values, string| 89 | "missing key is #{key}, values are #{values.inspect}, given string is '#{string}'" 90 | end 91 | assert_equal %|first missing key is last, values are #{{:first=>"first"}.to_s}, given string is '%{first} %{last}'|, 92 | compile_and_interpolate('%{first} %{last}', :first => 'first') 93 | ensure 94 | I18n.config.missing_interpolation_argument_handler = old_handler 95 | end 96 | end 97 | 98 | class I18nBackendInterpolationCompilerTest < I18n::TestCase 99 | class Backend < I18n::Backend::Simple 100 | include I18n::Backend::InterpolationCompiler 101 | end 102 | 103 | include I18n::Tests::Interpolation 104 | 105 | def setup 106 | I18n.backend = Backend.new 107 | super 108 | end 109 | 110 | # pre-compile default strings to make sure we are testing I18n::Backend::InterpolationCompiler 111 | def interpolate(*args) 112 | options = args.last.kind_of?(Hash) ? args.last : {} 113 | if default_str = options[:default] 114 | I18n::Backend::InterpolationCompiler::Compiler.compile_if_an_interpolation(default_str) 115 | end 116 | super 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /test/backend/key_value_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nBackendKeyValueTest < I18n::TestCase 4 | def setup_backend!(subtree=true) 5 | I18n.backend = I18n::Backend::KeyValue.new({}, subtree) 6 | store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' }) 7 | end 8 | 9 | def assert_flattens(expected, nested, escape=true, subtree=true) 10 | assert_equal expected, I18n.backend.flatten_translations("en", nested, escape, subtree) 11 | end 12 | 13 | test "hash flattening works" do 14 | setup_backend! 15 | assert_flattens( 16 | {:a=>'a', :b=>{:c=>'c', :d=>'d', :f=>{:x=>'x'}}, :"b.f" => {:x=>"x"}, :"b.c"=>"c", :"b.f.x"=>"x", :"b.d"=>"d"}, 17 | {:a=>'a', :b=>{:c=>'c', :d=>'d', :f=>{:x=>'x'}}} 18 | ) 19 | assert_flattens({:a=>{:b =>['a', 'b']}, :"a.b"=>['a', 'b']}, {:a=>{:b =>['a', 'b']}}) 20 | assert_flattens({:"a\001b" => "c"}, {:"a.b" => "c"}) 21 | assert_flattens({:"a.b"=>['a', 'b']}, {:a=>{:b =>['a', 'b']}}, true, false) 22 | assert_flattens({:"a.b" => "c"}, {:"a.b" => "c"}, false) 23 | end 24 | 25 | test "store_translations supports numeric keys" do 26 | setup_backend! 27 | store_translations(:en, 1 => 'foo') 28 | assert_equal 'foo', I18n.t('1') 29 | assert_equal 'foo', I18n.t(1) 30 | assert_equal 'foo', I18n.t(:'1') 31 | end 32 | 33 | test "store_translations handle subtrees by default" do 34 | setup_backend! 35 | assert_equal({ :bar => 'bar', :baz => 'baz' }, I18n.t("foo")) 36 | end 37 | 38 | test "store_translations merge subtrees accordingly" do 39 | setup_backend! 40 | store_translations(:en, :foo => { :baz => "BAZ"}) 41 | assert_equal('BAZ', I18n.t("foo.baz")) 42 | assert_equal({ :bar => 'bar', :baz => 'BAZ' }, I18n.t("foo")) 43 | end 44 | 45 | test "store_translations does not handle subtrees if desired" do 46 | setup_backend!(false) 47 | assert_raises I18n::MissingTranslationData do 48 | I18n.t("foo", :raise => true) 49 | end 50 | end 51 | 52 | test 'initialized? checks that a store is available' do 53 | setup_backend! 54 | I18n.backend.reload! 55 | assert_equal I18n.backend.initialized?, true 56 | end 57 | 58 | test 'translations gets the translations from the store' do 59 | setup_backend! 60 | I18n.backend.send(:translations) 61 | expected = { :en => {:foo => { :bar => 'bar', :baz => 'baz' }} } 62 | assert_equal expected, translations 63 | end 64 | 65 | test "subtrees enabled: given incomplete pluralization data it raises I18n::InvalidPluralizationData" do 66 | setup_backend! 67 | store_translations(:en, :bar => { :one => "One" }) 68 | assert_raises(I18n::InvalidPluralizationData) { I18n.t(:bar, :count => 2) } 69 | end 70 | 71 | test "subtrees disabled: given incomplete pluralization data it returns an error message" do 72 | setup_backend!(false) 73 | store_translations(:en, :bar => { :one => "One" }) 74 | assert_equal "Translation missing: en.bar", I18n.t(:bar, :count => 2) 75 | end 76 | 77 | test "translate handles subtrees for pluralization" do 78 | setup_backend!(false) 79 | store_translations(:en, :bar => { :one => "One" }) 80 | assert_equal("One", I18n.t("bar", :count => 1)) 81 | end 82 | 83 | test "subtrees enabled: returns localized string given missing pluralization data" do 84 | setup_backend!(true) 85 | assert_equal 'bar', I18n.t("foo.bar", count: 1) 86 | end 87 | 88 | test "subtrees disabled: returns localized string given missing pluralization data" do 89 | setup_backend!(false) 90 | assert_equal 'bar', I18n.t("foo.bar", count: 1) 91 | end 92 | 93 | test "subtrees enabled: Returns fallback default given missing pluralization data" do 94 | setup_backend!(true) 95 | I18n.backend.extend I18n::Backend::Fallbacks 96 | assert_equal 'default', I18n.t(:missing_bar, count: 1, default: 'default') 97 | assert_equal 'default', I18n.t(:missing_bar, count: 0, default: 'default') 98 | end 99 | 100 | test "subtrees disabled: Returns fallback default given missing pluralization data" do 101 | setup_backend!(false) 102 | I18n.backend.extend I18n::Backend::Fallbacks 103 | assert_equal 'default', I18n.t(:missing_bar, count: 1, default: 'default') 104 | assert_equal 'default', I18n.t(:missing_bar, count: 0, default: 'default') 105 | end 106 | end if I18n::TestCase.key_value? 107 | -------------------------------------------------------------------------------- /test/backend/memoize_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'backend/simple_test' 3 | 4 | class I18nBackendMemoizeTest < I18nBackendSimpleTest 5 | module MemoizeSpy 6 | attr_accessor :spy_calls 7 | 8 | def available_locales 9 | self.spy_calls = (self.spy_calls || 0) + 1 10 | super 11 | end 12 | end 13 | 14 | class MemoizeBackend < I18n::Backend::Simple 15 | include MemoizeSpy 16 | include I18n::Backend::Memoize 17 | end 18 | 19 | def setup 20 | super 21 | I18n.backend = MemoizeBackend.new 22 | end 23 | 24 | def test_memoizes_available_locales 25 | I18n.backend.spy_calls = 0 26 | assert_equal I18n.available_locales, I18n.available_locales 27 | assert_equal 1, I18n.backend.spy_calls 28 | end 29 | 30 | def test_resets_available_locales_on_reload! 31 | I18n.available_locales 32 | I18n.backend.spy_calls = 0 33 | I18n.reload! 34 | assert_equal I18n.available_locales, I18n.available_locales 35 | assert_equal 1, I18n.backend.spy_calls 36 | end 37 | 38 | def test_resets_available_locales_on_store_translations 39 | I18n.available_locales 40 | I18n.backend.spy_calls = 0 41 | I18n.backend.store_translations(:copa, :ca => :bana) 42 | assert_equal I18n.available_locales, I18n.available_locales 43 | assert I18n.available_locales.include?(:copa) 44 | assert_equal 1, I18n.backend.spy_calls 45 | end 46 | 47 | def test_eager_load 48 | I18n.eager_load! 49 | I18n.backend.spy_calls = 0 50 | assert_equal I18n.available_locales, I18n.available_locales 51 | assert_equal 0, I18n.backend.spy_calls 52 | end 53 | 54 | module TestLookup 55 | def lookup(locale, key, scope = [], options = {}) 56 | keys = I18n.normalize_keys(locale, key, scope, options[:separator]) 57 | keys.inspect 58 | end 59 | end 60 | 61 | def test_lookup_concurrent_consistency 62 | backend_impl = Class.new(I18n::Backend::Simple) do 63 | include TestLookup 64 | include I18n::Backend::Memoize 65 | end 66 | backend = backend_impl.new 67 | 68 | memoized_lookup = backend.send(:memoized_lookup) 69 | 70 | assert_equal "[:foo, :scoped, :sample]", backend.translate('foo', scope = [:scoped, :sample]) 71 | 72 | 30.times.inject([]) do |memo, i| 73 | memo << Thread.new do 74 | backend.translate('bar', scope); backend.translate(:baz, scope) 75 | end 76 | end.each(&:join) 77 | 78 | memoized_lookup = backend.send(:memoized_lookup) 79 | puts memoized_lookup.inspect if $VERBOSE 80 | assert_equal 3, memoized_lookup.size, "NON-THREAD-SAFE lookup memoization backend: #{memoized_lookup.class}" 81 | # if a plain Hash is used might eventually end up in a weird (inconsistent) state 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /test/backend/metadata_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nBackendMetadataTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Metadata 6 | end 7 | 8 | def setup 9 | super 10 | I18n.backend = Backend.new 11 | store_translations(:en, :foo => 'Hi %{name}') 12 | end 13 | 14 | test "translation strings carry metadata" do 15 | translation = I18n.t(:foo, :name => 'David') 16 | assert translation.respond_to?(:translation_metadata) 17 | assert translation.translation_metadata.is_a?(Hash) 18 | end 19 | 20 | test "translate adds the locale to metadata on Strings" do 21 | assert_equal :en, I18n.t(:foo, :name => 'David', :locale => :en).translation_metadata[:locale] 22 | end 23 | 24 | test "translate adds the key to metadata on Strings" do 25 | assert_equal :foo, I18n.t(:foo, :name => 'David').translation_metadata[:key] 26 | end 27 | 28 | test "translate adds the default to metadata on Strings" do 29 | assert_equal 'bar', I18n.t(:foo, :default => 'bar', :name => '').translation_metadata[:default] 30 | end 31 | 32 | test "translation adds the interpolation values to metadata on Strings" do 33 | assert_equal({:name => 'David'}, I18n.t(:foo, :name => 'David').translation_metadata[:values]) 34 | end 35 | 36 | test "interpolation adds the original string to metadata on Strings" do 37 | assert_equal('Hi %{name}', I18n.t(:foo, :name => 'David').translation_metadata[:original]) 38 | end 39 | 40 | test "pluralization adds the count to metadata on Strings" do 41 | assert_equal(1, I18n.t(:missing, :count => 1, :default => { :one => 'foo' }).translation_metadata[:count]) 42 | end 43 | 44 | test "metadata works with frozen values" do 45 | assert_equal(1, I18n.t(:missing, :count => 1, :default => 'foo'.freeze).translation_metadata[:count]) 46 | end 47 | end 48 | 49 | -------------------------------------------------------------------------------- /test/backend/pluralization_fallback_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nBackendPluralizationFallbackTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Pluralization 6 | include I18n::Backend::Fallbacks 7 | end 8 | 9 | def setup 10 | super 11 | I18n.default_locale = :'en' 12 | I18n.backend = Backend.new 13 | 14 | store_translations('en', cat: { zero: 'cat', one: 'cat', other: 'cats' }) 15 | store_translations('en-US', cat: { zero: 'no cat', one: nil, other: 'lots of cats' }) 16 | 17 | store_translations('ru', cat: { one: 'кот', few: 'кошек', many: 'кошка', other: 'кошек' }) 18 | # probably not a real locale but just to demonstrate 19 | store_translations('ru-US', cat: { one: nil, few: nil, many: nil, other: nil }) 20 | store_translations('ru', i18n: { plural: { rule: russian_rule }}) 21 | end 22 | 23 | test "fallbacks: nils are ignored and fallback is applied" do 24 | assert_equal "no cat", I18n.t("cat", count: 0, locale: "en-US") 25 | assert_equal "cat", I18n.t("cat", count: 0, locale: "en") 26 | 27 | assert_equal "cat", I18n.t("cat", count: 1, locale: "en-US") 28 | assert_equal "cat", I18n.t("cat", count: 1, locale: "en") 29 | 30 | assert_equal "lots of cats", I18n.t("cat", count: 2, locale: "en-US") 31 | assert_equal "cats", I18n.t("cat", count: 2, locale: "en") 32 | end 33 | 34 | test "fallbacks: nils are ignored and fallback is applied, with custom rule" do 35 | # more specs: https://github.com/svenfuchs/rails-i18n/blob/master/spec/unit/pluralization/east_slavic.rb 36 | assert_equal "кошка", I18n.t("cat", count: 0, locale: "ru") 37 | assert_equal "кошка", I18n.t("cat", count: 0, locale: "ru-US") 38 | 39 | assert_equal "кот", I18n.t("cat", count: 1, locale: "ru") 40 | assert_equal "кот", I18n.t("cat", count: 1, locale: "ru-US") 41 | 42 | assert_equal "кошек", I18n.t("cat", count: 2, locale: "ru") 43 | assert_equal "кошек", I18n.t("cat", count: 2, locale: "ru-US") 44 | 45 | assert_equal "кошек", I18n.t("cat", count: 1.5, locale: "ru") 46 | assert_equal "кошек", I18n.t("cat", count: 1.5, locale: "ru-US") 47 | end 48 | 49 | private 50 | 51 | # copied from https://github.com/svenfuchs/rails-i18n/blob/master/lib/rails_i18n/common_pluralizations/east_slavic.rb 52 | def russian_rule 53 | lambda do |n| 54 | n ||= 0 55 | mod10 = n % 10 56 | mod100 = n % 100 57 | 58 | if mod10 == 1 && mod100 != 11 59 | :one 60 | elsif (2..4).include?(mod10) && !(12..14).include?(mod100) 61 | :few 62 | elsif mod10 == 0 || (5..9).include?(mod10) || (11..14).include?(mod100) 63 | :many 64 | else 65 | :other 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/backend/pluralization_scope_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nBackendPluralizationScopeTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Pluralization 6 | include I18n::Backend::Fallbacks 7 | end 8 | 9 | def setup 10 | super 11 | I18n.default_locale = :'en' 12 | I18n.backend = Backend.new 13 | 14 | translations = { 15 | i18n: { 16 | plural: { 17 | keys: [:one, :other], 18 | rule: lambda { |n| n == 1 ? :one : :other }, 19 | } 20 | }, 21 | activerecord: { 22 | models: { 23 | my_model: { 24 | one: 'one model', 25 | other: 'more models', 26 | some_other_key: { 27 | key: 'value' 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | store_translations('en', translations) 35 | end 36 | 37 | test "pluralization picks :other for 2" do 38 | args = { 39 | scope: [:activerecord, :models], 40 | count: 2, 41 | default: ["My model"] 42 | } 43 | assert_equal 'more models', I18n.translate(:my_model, **args) 44 | end 45 | 46 | test "pluralization picks :one for 1" do 47 | args = { 48 | scope: [:activerecord, :models], 49 | count: 1, 50 | default: ["My model"] 51 | } 52 | assert_equal 'one model', I18n.translate(:my_model, **args) 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /test/backend/pluralization_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nBackendPluralizationTest < I18n::TestCase 4 | class Backend < I18n::Backend::Simple 5 | include I18n::Backend::Pluralization 6 | include I18n::Backend::Fallbacks 7 | end 8 | 9 | def setup 10 | super 11 | I18n.backend = Backend.new 12 | @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 } 13 | store_translations(:xx, :i18n => { :plural => { :rule => @rule } }) 14 | @entry = { :"0" => 'none', :"1" => 'single', :one => 'one', :few => 'few', :many => 'many', :other => 'other' } 15 | @entry_with_zero = @entry.merge( { :zero => 'zero' } ) 16 | end 17 | 18 | test "pluralization picks a pluralizer from :'i18n.pluralize'" do 19 | assert_equal @rule, I18n.backend.send(:pluralizer, :xx) 20 | end 21 | 22 | test "pluralization picks the explicit 1 rule for count == 1, the explicit rule takes priority over the matching :one rule" do 23 | assert_equal 'single', I18n.t(:count => 1, :default => @entry, :locale => :xx) 24 | assert_equal 'single', I18n.t(:count => 1.0, :default => @entry, :locale => :xx) 25 | end 26 | 27 | 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 28 | @entry.delete(:"1") 29 | assert_equal 'one', I18n.t(:count => 1, :default => @entry, :locale => :xx) 30 | end 31 | 32 | test "pluralization picks :few for 2" do 33 | assert_equal 'few', I18n.t(:count => 2, :default => @entry, :locale => :xx) 34 | end 35 | 36 | test "pluralization picks :many for 11" do 37 | assert_equal 'many', I18n.t(:count => 11, :default => @entry, :locale => :xx) 38 | end 39 | 40 | test "pluralization picks zero for 0 if the key is contained in the data" do 41 | assert_equal 'zero', I18n.t(:count => 0, :default => @entry_with_zero, :locale => :xx) 42 | end 43 | 44 | test "pluralization picks explicit 0 rule for count == 0, since the explicit rule takes priority over the matching :few rule" do 45 | assert_equal 'none', I18n.t(:count => 0, :default => @entry, :locale => :xx) 46 | assert_equal 'none', I18n.t(:count => 0.0, :default => @entry, :locale => :xx) 47 | assert_equal 'none', I18n.t(:count => -0, :default => @entry, :locale => :xx) 48 | end 49 | 50 | test "pluralization picks :few for 0 (when there is no explicit 0 rule)" do 51 | @entry.delete(:"0") 52 | assert_equal 'few', I18n.t(:count => 0, :default => @entry, :locale => :xx) 53 | end 54 | 55 | test "pluralization does Lateral Inheritance to :other to cover missing data" do 56 | @entry.delete(:many) 57 | assert_equal 'other', I18n.t(:count => 11, :default => @entry, :locale => :xx) 58 | end 59 | 60 | test "pluralization picks one for 1 if the entry has attributes hash on unknown locale" do 61 | @entry[:attributes] = { :field => 'field', :second => 'second' } 62 | assert_equal 'one', I18n.t(:count => 1, :default => @entry, :locale => :pirate) 63 | end 64 | 65 | test "Nested keys within pluralization context" do 66 | store_translations(:xx, 67 | :stars => { 68 | one: "%{count} star", 69 | other: "%{count} stars", 70 | special: { 71 | one: "%{count} special star", 72 | other: "%{count} special stars", 73 | } 74 | } 75 | ) 76 | assert_equal "1 star", I18n.t('stars', count: 1, :locale => :xx) 77 | assert_equal "20 stars", I18n.t('stars', count: 20, :locale => :xx) 78 | assert_equal "1 special star", I18n.t('stars.special', count: 1, :locale => :xx) 79 | assert_equal "20 special stars", I18n.t('stars.special', count: 20, :locale => :xx) 80 | end 81 | 82 | test "Fallbacks can pick up rules from fallback locales, too" do 83 | assert_equal @rule, I18n.backend.send(:pluralizer, :'xx-XX') 84 | end 85 | 86 | test "linked lookup works with pluralization backend" do 87 | I18n.backend.store_translations(:xx, { 88 | :automobiles => :autos, 89 | :autos => :cars, 90 | :cars => { :porsche => { :one => "I have %{count} Porsche 🚗", :other => "I have %{count} Porsches 🚗" } } 91 | }) 92 | assert_equal "I have 1 Porsche 🚗", I18n.t(:'automobiles.porsche', count: 1, :locale => :xx) 93 | assert_equal "I have 20 Porsches 🚗", I18n.t(:'automobiles.porsche', count: 20, :locale => :xx) 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/backend/transliterator_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'test_helper' 3 | 4 | class I18nBackendTransliterator < I18n::TestCase 5 | def setup 6 | super 7 | I18n.backend = I18n::Backend::Simple.new 8 | @proc = lambda { |n| n.upcase } 9 | @hash = { "ü" => "ue", "ö" => "oe", "a" => "a" } 10 | @transliterator = I18n::Backend::Transliterator.get 11 | end 12 | 13 | test "transliteration rule can be a proc" do 14 | store_translations(:xx, :i18n => {:transliterate => {:rule => @proc}}) 15 | assert_equal "HELLO", I18n.backend.transliterate(:xx, "hello") 16 | end 17 | 18 | test "transliteration rule can be a hash" do 19 | store_translations(:xx, :i18n => {:transliterate => {:rule => @hash}}) 20 | assert_equal "ue", I18n.backend.transliterate(:xx, "ü") 21 | end 22 | 23 | test "transliteration rule must be a proc or hash" do 24 | store_translations(:xx, :i18n => {:transliterate => {:rule => ""}}) 25 | assert_raises I18n::ArgumentError do 26 | I18n.backend.transliterate(:xx, "ü") 27 | end 28 | end 29 | 30 | test "transliterator defaults to latin => ascii when no rule is given" do 31 | assert_equal "AEroskobing", I18n.backend.transliterate(:xx, "Ærøskøbing") 32 | end 33 | 34 | test "default transliterator should not modify ascii characters" do 35 | (0..127).each do |byte| 36 | char = [byte].pack("U") 37 | assert_equal char, @transliterator.transliterate(char) 38 | end 39 | end 40 | 41 | test "default transliterator correctly transliterates latin characters" do 42 | # create string with range of Unicode's western characters with 43 | # diacritics, excluding the division and multiplication signs which for 44 | # some reason or other are floating in the middle of all the letters. 45 | string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include? c}.append(0x1E9E).pack("U*") 46 | string.split(//) do |char| 47 | assert_match %r{^[a-zA-Z']*$}, @transliterator.transliterate(string) 48 | end 49 | end 50 | 51 | test "should replace non-ASCII chars not in map with a replacement char" do 52 | assert_equal "abc?", @transliterator.transliterate("abcſ") 53 | end 54 | 55 | test "can replace non-ASCII chars not in map with a custom replacement string" do 56 | assert_equal "abc#", @transliterator.transliterate("abcſ", "#") 57 | end 58 | 59 | test "default transliterator raises errors for invalid UTF-8" do 60 | assert_raises ArgumentError do 61 | @transliterator.transliterate("a\x92b") 62 | end 63 | end 64 | 65 | test "I18n.transliterate should transliterate using a default transliterator" do 66 | assert_equal "aeo", I18n.transliterate("áèö") 67 | end 68 | 69 | test "I18n.transliterate should transliterate using a locale" do 70 | store_translations(:xx, :i18n => {:transliterate => {:rule => @hash}}) 71 | assert_equal "ue", I18n.transliterate("ü", :locale => :xx) 72 | end 73 | 74 | test "default transliterator fails with custom rules with uncomposed input" do 75 | char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS 76 | transliterator = I18n::Backend::Transliterator.get(@hash) 77 | refute_equal "ue", transliterator.transliterate(char) 78 | end 79 | 80 | test "DEFAULT_APPROXIMATIONS is frozen to prevent concurrency issues" do 81 | assert I18n::Backend::Transliterator::HashTransliterator::DEFAULT_APPROXIMATIONS.frozen? 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /test/gettext/backend_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'test_helper' 4 | 5 | class I18nGettextBackendTest < I18n::TestCase 6 | include I18n::Gettext::Helpers 7 | 8 | class Backend < I18n::Backend::Simple 9 | include I18n::Backend::Gettext 10 | end 11 | 12 | def setup 13 | super 14 | I18n.backend = Backend.new 15 | I18n.locale = :en 16 | I18n.load_path = ["#{locales_dir}/de.po"] 17 | I18n.default_separator = '|' 18 | end 19 | 20 | def test_backend_loads_po_file 21 | I18n.backend.send(:init_translations) 22 | assert I18n.backend.send(:translations)[:de][:"Axis"] 23 | end 24 | 25 | def test_looks_up_a_translation 26 | I18n.locale = :de 27 | assert_equal 'Auto', gettext('car') 28 | end 29 | 30 | def test_uses_default_translation 31 | assert_equal 'car', gettext('car') 32 | end 33 | 34 | def test_looks_up_a_namespaced_translation 35 | I18n.locale = :de 36 | assert_equal 'Räderzahl', sgettext('Car|Wheels count') 37 | assert_equal 'Räderzahl', pgettext('Car', 'Wheels count') 38 | assert_equal 'Räderzahl!', pgettext('New car', 'Wheels count') 39 | end 40 | 41 | def test_uses_namespaced_default_translation 42 | assert_equal 'Wheels count', sgettext('Car|Wheels count') 43 | assert_equal 'Wheels count', pgettext('Car', 'Wheels count') 44 | assert_equal 'Wheels count', pgettext('New car', 'Wheels count') 45 | end 46 | 47 | def test_pluralizes_entry 48 | I18n.locale = :de 49 | assert_equal 'Achse', ngettext('Axis', 'Axis', 1) 50 | assert_equal 'Achsen', ngettext('Axis', 'Axis', 2) 51 | end 52 | 53 | def test_pluralizes_default_entry 54 | assert_equal 'Axis', ngettext('Axis', 'Axis', 1) 55 | assert_equal 'Axis', ngettext('Axis', 'Axis', 2) 56 | end 57 | 58 | def test_pluralizes_namespaced_entry 59 | I18n.locale = :de 60 | assert_equal 'Rad', nsgettext('Car|wheel', 'wheels', 1) 61 | assert_equal 'Räder', nsgettext('Car|wheel', 'wheels', 2) 62 | assert_equal 'Rad', npgettext('Car', 'wheel', 'wheels', 1) 63 | assert_equal 'Räder', npgettext('Car', 'wheel', 'wheels', 2) 64 | assert_equal 'Rad!', npgettext('New car', 'wheel', 'wheels', 1) 65 | assert_equal 'Räder!', npgettext('New car', 'wheel', 'wheels', 2) 66 | end 67 | 68 | def test_pluralizes_namespaced_default_entry 69 | assert_equal 'wheel', nsgettext('Car|wheel', 'wheels', 1) 70 | assert_equal 'wheels', nsgettext('Car|wheel', 'wheels', 2) 71 | assert_equal 'wheel', npgettext('Car', 'wheel', 'wheels', 1) 72 | assert_equal 'wheels', npgettext('Car', 'wheel', 'wheels', 2) 73 | assert_equal 'wheel', npgettext('New car', 'wheel', 'wheels', 1) 74 | assert_equal 'wheels', npgettext('New car', 'wheel', 'wheels', 2) 75 | end 76 | 77 | def test_pluralizes_namespaced_entry_with_alternative_syntax 78 | I18n.locale = :de 79 | assert_equal 'Rad', nsgettext(['Car|wheel', 'wheels'], 1) 80 | assert_equal 'Räder', nsgettext(['Car|wheel', 'wheels'], 2) 81 | assert_equal 'Rad', npgettext('Car', ['wheel', 'wheels'], 1) 82 | assert_equal 'Räder', npgettext('Car', ['wheel', 'wheels'], 2) 83 | assert_equal 'Rad!', npgettext('New car', ['wheel', 'wheels'], 1) 84 | assert_equal 'Räder!', npgettext('New car', ['wheel', 'wheels'], 2) 85 | end 86 | 87 | def test_ngettextpluralizes_entry_with_dots 88 | I18n.locale = :de 89 | assert_equal 'Auf 1 Achse.', n_("On %{count} wheel.", "On %{count} wheels.", 1) 90 | assert_equal 'Auf 2 Achsen.', n_("On %{count} wheel.", "On %{count} wheels.", 2) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/i18n/exceptions_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nExceptionsTest < I18n::TestCase 4 | def test_invalid_locale_stores_locale 5 | force_invalid_locale 6 | rescue I18n::ArgumentError => exception 7 | assert_nil exception.locale 8 | end 9 | 10 | test "passing an invalid locale raises an InvalidLocale exception" do 11 | force_invalid_locale do |exception| 12 | assert_equal 'nil is not a valid locale', exception.message 13 | end 14 | end 15 | 16 | test "MissingTranslation can be initialized without options" do 17 | exception = I18n::MissingTranslation.new(:en, 'foo') 18 | assert_equal({}, exception.options) 19 | end 20 | 21 | test "MissingTranslationData exception stores locale, key and options" do 22 | force_missing_translation_data do |exception| 23 | assert_equal 'de', exception.locale 24 | assert_equal :foo, exception.key 25 | assert_equal({:scope => :bar}, exception.options) 26 | end 27 | end 28 | 29 | test "MissingTranslationData message contains the locale and scoped key" do 30 | force_missing_translation_data do |exception| 31 | assert_equal 'translation missing: de.bar.foo', exception.message 32 | end 33 | end 34 | 35 | test "InvalidPluralizationData stores entry, count and key" do 36 | force_invalid_pluralization_data do |exception| 37 | assert_equal({:other => "bar"}, exception.entry) 38 | assert_equal 1, exception.count 39 | assert_equal :one, exception.key 40 | end 41 | end 42 | 43 | test "InvalidPluralizationData message contains count, data and missing key" do 44 | force_invalid_pluralization_data do |exception| 45 | assert_match '1', exception.message 46 | assert_match %|#{{:other=>"bar"}}|, exception.message 47 | assert_match 'one', exception.message 48 | end 49 | end 50 | 51 | test "MissingInterpolationArgument stores key and string" do 52 | assert_raises(I18n::MissingInterpolationArgument) { force_missing_interpolation_argument } 53 | force_missing_interpolation_argument do |exception| 54 | assert_equal :bar, exception.key 55 | assert_equal "%{bar}", exception.string 56 | end 57 | end 58 | 59 | test "MissingInterpolationArgument message contains the missing and given arguments" do 60 | force_missing_interpolation_argument do |exception| 61 | assert_equal %|missing interpolation argument :bar in "%{bar}" (#{{:baz=>"baz"}.to_s} given)|, exception.message 62 | end 63 | end 64 | 65 | test "ReservedInterpolationKey stores key and string" do 66 | force_reserved_interpolation_key do |exception| 67 | assert_equal :scope, exception.key 68 | assert_equal "%{scope}", exception.string 69 | end 70 | end 71 | 72 | test "ReservedInterpolationKey message contains the reserved key" do 73 | force_reserved_interpolation_key do |exception| 74 | assert_equal 'reserved key :scope used in "%{scope}"', exception.message 75 | end 76 | end 77 | 78 | test "MissingTranslationData#new can be initialized with just two arguments" do 79 | assert I18n::MissingTranslationData.new('en', 'key') 80 | end 81 | 82 | private 83 | 84 | def force_invalid_locale 85 | I18n.translate(:foo, :locale => nil) 86 | rescue I18n::ArgumentError => e 87 | block_given? ? yield(e) : raise(e) 88 | end 89 | 90 | def force_missing_translation_data(options = {}) 91 | store_translations('de', :bar => nil) 92 | I18n.translate(:foo, **options.merge(:scope => :bar, :locale => :de)) 93 | rescue I18n::ArgumentError => e 94 | block_given? ? yield(e) : raise(e) 95 | end 96 | 97 | def force_invalid_pluralization_data 98 | store_translations('de', :foo => { :other => 'bar' }) 99 | I18n.translate(:foo, :count => 1, :locale => :de) 100 | rescue I18n::ArgumentError => e 101 | block_given? ? yield(e) : raise(e) 102 | end 103 | 104 | def force_missing_interpolation_argument 105 | store_translations('de', :foo => "%{bar}") 106 | I18n.translate(:foo, :baz => 'baz', :locale => :de) 107 | rescue I18n::ArgumentError => e 108 | block_given? ? yield(e) : raise(e) 109 | end 110 | 111 | def force_reserved_interpolation_key 112 | store_translations('de', :foo => "%{scope}") 113 | I18n.translate(:foo, :baz => 'baz', :locale => :de) 114 | rescue I18n::ArgumentError => e 115 | block_given? ? yield(e) : raise(e) 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/i18n/gettext_plural_keys_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nGettextPluralKeysTest < I18n::TestCase 4 | def setup 5 | super 6 | I18n::Gettext.plural_keys[:zz] = [:value1, :value2] 7 | end 8 | 9 | test "Returns the plural keys of the given locale if present" do 10 | assert_equal I18n::Gettext.plural_keys(:zz), [:value1, :value2] 11 | end 12 | 13 | test "Returns the plural keys of :en if given locale not present" do 14 | assert_equal I18n::Gettext.plural_keys(:yy), [:one, :other] 15 | end 16 | 17 | test "Returns the whole hash with no arguments" do 18 | assert_equal I18n::Gettext.plural_keys, { :en => [:one, :other], :zz => [:value1, :value2] } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/i18n/interpolate_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | # thanks to Masao's String extensions, some tests taken from Masao's tests 4 | # http://github.com/mutoh/gettext/blob/edbbe1fa8238fa12c7f26f2418403015f0270e47/test/test_string.rb 5 | 6 | class I18nInterpolateTest < I18n::TestCase 7 | test "String interpolates a hash argument w/ named placeholders" do 8 | assert_equal "Masao Mutoh", I18n.interpolate("%{first} %{last}", :first => 'Masao', :last => 'Mutoh' ) 9 | end 10 | 11 | test "String interpolates a hash argument w/ named placeholders (reverse order)" do 12 | assert_equal "Mutoh, Masao", I18n.interpolate("%{last}, %{first}", :first => 'Masao', :last => 'Mutoh' ) 13 | end 14 | 15 | test "String interpolates named placeholders with sprintf syntax" do 16 | assert_equal "10, 43.4", I18n.interpolate("%d, %.1f", :integer => 10, :float => 43.4) 17 | end 18 | 19 | test "String interpolates named placeholders with sprintf syntax, does not recurse" do 20 | assert_equal "%s", I18n.interpolate("%{msg}", :msg => '%s', :not_translated => 'should not happen' ) 21 | end 22 | 23 | test "String interpolation does not replace anything when no placeholders are given" do 24 | assert_equal "aaa", I18n.interpolate("aaa", :num => 1) 25 | end 26 | 27 | test "String interpolation sprintf behaviour equals Ruby 1.9 behaviour" do 28 | assert_equal "1", I18n.interpolate("%d", :num => 1) 29 | assert_equal "0b1", I18n.interpolate("%#b", :num => 1) 30 | assert_equal "foo", I18n.interpolate("%s", :msg => "foo") 31 | assert_equal "1.000000", I18n.interpolate("%f", :num => 1.0) 32 | assert_equal " 1", I18n.interpolate("%3.0f", :num => 1.0) 33 | assert_equal "100.00", I18n.interpolate("%2.2f", :num => 100.0) 34 | assert_equal "0x64", I18n.interpolate("%#x", :num => 100.0) 35 | assert_raises(ArgumentError) { I18n.interpolate("%,d", :num => 100) } 36 | assert_raises(ArgumentError) { I18n.interpolate("%/d", :num => 100) } 37 | end 38 | 39 | test "String interpolation raises an I18n::MissingInterpolationArgument when the string has extra placeholders" do 40 | assert_raises(I18n::MissingInterpolationArgument, "key not found") do 41 | I18n.interpolate("%{first} %{last}", :first => 'Masao') 42 | end 43 | end 44 | 45 | test "String interpolation does not raise when extra values were passed" do 46 | assert_nothing_raised do 47 | assert_equal "Masao Mutoh", I18n.interpolate("%{first} %{last}", :first => 'Masao', :last => 'Mutoh', :salutation => 'Mr.' ) 48 | end 49 | end 50 | 51 | test "% acts as escape character in String interpolation" do 52 | assert_equal "%{first}", I18n.interpolate("%%{first}", :first => 'Masao') 53 | assert_equal "% 1", I18n.interpolate("%% %d", :num => 1.0) 54 | assert_equal "%{num} %d", I18n.interpolate("%%{num} %%d", :num => 1) 55 | end 56 | 57 | def test_sprintf_mix_unformatted_and_formatted_named_placeholders 58 | assert_equal "foo 1.000000", I18n.interpolate("%{name} %f", :name => "foo", :num => 1.0) 59 | end 60 | 61 | class RailsSafeBuffer < String 62 | 63 | def gsub(*args, &block) 64 | to_str.gsub(*args, &block) 65 | end 66 | 67 | end 68 | 69 | test "with String subclass that redefined gsub method" do 70 | assert_equal "Hello mars world", I18n.interpolate(RailsSafeBuffer.new("Hello %{planet} world"), :planet => 'mars') 71 | end 72 | 73 | test "with String subclass that redefined gsub method returns same object if no interpolations" do 74 | string = RailsSafeBuffer.new("Hello world") 75 | assert_same string, I18n.interpolate(string, :planet => 'mars') 76 | end 77 | end 78 | 79 | class I18nMissingInterpolationCustomHandlerTest < I18n::TestCase 80 | def setup 81 | super 82 | @old_handler = I18n.config.missing_interpolation_argument_handler 83 | I18n.config.missing_interpolation_argument_handler = lambda do |key, values, string| 84 | "missing key is #{key}, values are #{values.inspect}, given string is '#{string}'" 85 | end 86 | end 87 | 88 | def teardown 89 | I18n.config.missing_interpolation_argument_handler = @old_handler 90 | super 91 | end 92 | 93 | test "String interpolation can use custom missing interpolation handler" do 94 | assert_equal %|Masao missing key is last, values are #{{:first=>"Masao"}.to_s}, given string is '%{first} %{last}'|, 95 | I18n.interpolate("%{first} %{last}", :first => 'Masao') 96 | end 97 | end 98 | 99 | class I18nCustomInterpolationPatternTest < I18n::TestCase 100 | def setup 101 | super 102 | @old_interpolation_patterns = I18n.config.interpolation_patterns 103 | I18n.config.interpolation_patterns << /\{\{(\w+)\}\}/ 104 | end 105 | 106 | def teardown 107 | I18n.config.interpolation_patterns = @old_interpolation_patterns 108 | super 109 | end 110 | 111 | test "String interpolation can use custom interpolation pattern" do 112 | assert_equal "Masao Mutoh", I18n.interpolate("{{first}} {{last}}", :first => "Masao", :last => "Mutoh") 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/i18n/load_path_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nLoadPathTest < I18n::TestCase 4 | def setup 5 | super 6 | I18n.locale = :en 7 | I18n.backend = I18n::Backend::Simple.new 8 | store_translations(:en, :foo => {:bar => 'bar', :baz => 'baz'}) 9 | end 10 | 11 | test "nested load paths do not break locale loading" do 12 | I18n.load_path = [[locales_dir + '/en.yml']] 13 | assert_equal "baz", I18n.t(:'foo.bar') 14 | end 15 | 16 | test "loading an empty yml file raises an InvalidLocaleData exception" do 17 | assert_raises I18n::InvalidLocaleData do 18 | I18n.load_path = [[locales_dir + '/invalid/empty.yml']] 19 | I18n.t(:'foo.bar', :default => "baz") 20 | end 21 | end 22 | 23 | test "loading an invalid yml file raises an InvalidLocaleData exception" do 24 | assert_raises I18n::InvalidLocaleData do 25 | I18n.load_path = [[locales_dir + '/invalid/syntax.yml']] 26 | I18n.t(:'foo.bar', :default => "baz") 27 | end 28 | end 29 | 30 | test "adding arrays of filenames to the load path does not break locale loading" do 31 | I18n.load_path << Dir[locales_dir + '/*.{rb,yml}'] 32 | assert_equal "baz", I18n.t(:'foo.bar') 33 | end 34 | 35 | test "adding Pathnames to the load path does not break YML file locale loading" do 36 | I18n.load_path << Pathname.new(locales_dir + '/en.yml') 37 | assert_equal "baz", I18n.t(:'foo.bar') 38 | end 39 | 40 | test "adding Pathnames to the load path does not break Ruby file locale loading" do 41 | I18n.load_path << Pathname.new(locales_dir + '/en.rb') 42 | assert_equal "bas", I18n.t(:'fuh.bah') 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/i18n/middleware_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nMiddlewareTest < I18n::TestCase 4 | def setup 5 | super 6 | I18n.default_locale = :fr 7 | @app = DummyRackApp.new 8 | @middleware = I18n::Middleware.new(@app) 9 | end 10 | 11 | test "middleware initializes new config object after request" do 12 | old_i18n_config_object_id = Thread.current[:i18n_config].object_id 13 | @middleware.call({}) 14 | 15 | updated_i18n_config_object_id = Thread.current[:i18n_config].object_id 16 | refute_equal updated_i18n_config_object_id, old_i18n_config_object_id 17 | end 18 | 19 | test "successfully resets i18n locale to default locale by defining new config" do 20 | @middleware.call({}) 21 | 22 | assert_equal :fr, I18n.locale 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/locale/tag/rfc4646_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'test_helper' 3 | 4 | class I18nLocaleTagRfc4646ParserTest < I18n::TestCase 5 | include I18n::Locale 6 | 7 | test "Rfc4646::Parser given a valid tag 'de' returns an array of subtags" do 8 | assert_equal ['de', nil, nil, nil, nil, nil, nil], Tag::Rfc4646::Parser.match('de') 9 | end 10 | 11 | test "Rfc4646::Parser given a valid tag 'de-DE' returns an array of subtags" do 12 | assert_equal ['de', nil, 'DE', nil, nil, nil, nil], Tag::Rfc4646::Parser.match('de-DE') 13 | end 14 | 15 | test "Rfc4646::Parser given a valid lowercase tag 'de-latn-de-variant-x-phonebk' returns an array of subtags" do 16 | assert_equal ['de', 'latn', 'de', 'variant', nil, 'x-phonebk', nil], Tag::Rfc4646::Parser.match('de-latn-de-variant-x-phonebk') 17 | end 18 | 19 | test "Rfc4646::Parser given a valid uppercase tag 'DE-LATN-DE-VARIANT-X-PHONEBK' returns an array of subtags" do 20 | assert_equal ['DE', 'LATN', 'DE', 'VARIANT', nil, 'X-PHONEBK', nil], Tag::Rfc4646::Parser.match('DE-LATN-DE-VARIANT-X-PHONEBK') 21 | end 22 | 23 | test "Rfc4646::Parser given an invalid tag 'a-DE' it returns false" do 24 | assert_equal false, Tag::Rfc4646::Parser.match('a-DE') 25 | end 26 | 27 | test "Rfc4646::Parser given an invalid tag 'de-419-DE' it returns false" do 28 | assert_equal false, Tag::Rfc4646::Parser.match('de-419-DE') 29 | end 30 | end 31 | 32 | # Tag for the locale 'de-Latn-DE-Variant-a-ext-x-phonebk-i-klingon' 33 | 34 | class I18nLocaleTagSubtagsTest < I18n::TestCase 35 | include I18n::Locale 36 | 37 | def setup 38 | super 39 | subtags = %w(de Latn DE variant a-ext x-phonebk i-klingon) 40 | @tag = Tag::Rfc4646.new(*subtags) 41 | end 42 | 43 | test "returns 'de' as the language subtag in lowercase" do 44 | assert_equal 'de', @tag.language 45 | end 46 | 47 | test "returns 'Latn' as the script subtag in titlecase" do 48 | assert_equal 'Latn', @tag.script 49 | end 50 | 51 | test "returns 'DE' as the region subtag in uppercase" do 52 | assert_equal 'DE', @tag.region 53 | end 54 | 55 | test "returns 'variant' as the variant subtag in lowercase" do 56 | assert_equal 'variant', @tag.variant 57 | end 58 | 59 | test "returns 'a-ext' as the extension subtag" do 60 | assert_equal 'a-ext', @tag.extension 61 | end 62 | 63 | test "returns 'x-phonebk' as the privateuse subtag" do 64 | assert_equal 'x-phonebk', @tag.privateuse 65 | end 66 | 67 | test "returns 'i-klingon' as the grandfathered subtag" do 68 | assert_equal 'i-klingon', @tag.grandfathered 69 | end 70 | 71 | test "returns a formatted tag string from #to_s" do 72 | assert_equal 'de-Latn-DE-variant-a-ext-x-phonebk-i-klingon', @tag.to_s 73 | end 74 | 75 | test "returns an array containing the formatted subtags from #to_a" do 76 | assert_equal %w(de Latn DE variant a-ext x-phonebk i-klingon), @tag.to_a 77 | end 78 | end 79 | 80 | # Tag inheritance 81 | 82 | class I18nLocaleTagSubtagsTest < I18n::TestCase 83 | test "#parent returns 'de-Latn-DE-variant-a-ext-x-phonebk' as the parent of 'de-Latn-DE-variant-a-ext-x-phonebk-i-klingon'" do 84 | tag = Tag::Rfc4646.new(*%w(de Latn DE variant a-ext x-phonebk i-klingon)) 85 | assert_equal 'de-Latn-DE-variant-a-ext-x-phonebk', tag.parent.to_s 86 | end 87 | 88 | test "#parent returns 'de-Latn-DE-variant-a-ext' as the parent of 'de-Latn-DE-variant-a-ext-x-phonebk'" do 89 | tag = Tag::Rfc4646.new(*%w(de Latn DE variant a-ext x-phonebk)) 90 | assert_equal 'de-Latn-DE-variant-a-ext', tag.parent.to_s 91 | end 92 | 93 | test "#parent returns 'de-Latn-DE-variant' as the parent of 'de-Latn-DE-variant-a-ext'" do 94 | tag = Tag::Rfc4646.new(*%w(de Latn DE variant a-ext)) 95 | assert_equal 'de-Latn-DE-variant', tag.parent.to_s 96 | end 97 | 98 | test "#parent returns 'de-Latn-DE' as the parent of 'de-Latn-DE-variant'" do 99 | tag = Tag::Rfc4646.new(*%w(de Latn DE variant)) 100 | assert_equal 'de-Latn-DE', tag.parent.to_s 101 | end 102 | 103 | test "#parent returns 'de-Latn' as the parent of 'de-Latn-DE'" do 104 | tag = Tag::Rfc4646.new(*%w(de Latn DE)) 105 | assert_equal 'de-Latn', tag.parent.to_s 106 | end 107 | 108 | test "#parent returns 'de' as the parent of 'de-Latn'" do 109 | tag = Tag::Rfc4646.new(*%w(de Latn)) 110 | assert_equal 'de', tag.parent.to_s 111 | end 112 | 113 | # TODO RFC4647 says: "If no language tag matches the request, the "default" value is returned." 114 | # where should we set the default language? 115 | # test "#parent returns '' as the parent of 'de'" do 116 | # tag = Tag::Rfc4646.new *%w(de) 117 | # assert_equal '', tag.parent.to_s 118 | # end 119 | 120 | test "#parent returns an array of 5 parents for 'de-Latn-DE-variant-a-ext-x-phonebk-i-klingon'" do 121 | parents = %w(de-Latn-DE-variant-a-ext-x-phonebk-i-klingon 122 | de-Latn-DE-variant-a-ext-x-phonebk 123 | de-Latn-DE-variant-a-ext 124 | de-Latn-DE-variant 125 | de-Latn-DE 126 | de-Latn 127 | de) 128 | tag = Tag::Rfc4646.new(*%w(de Latn DE variant a-ext x-phonebk i-klingon)) 129 | assert_equal parents, tag.self_and_parents.map(&:to_s) 130 | end 131 | 132 | test "returns an array of 5 parents for 'de-Latn-DE-variant-a-ext-x-phonebk-i-klingon'" do 133 | parents = %w(de-Latn-DE-variant-a-ext-x-phonebk-i-klingon 134 | de-Latn-DE-variant-a-ext-x-phonebk 135 | de-Latn-DE-variant-a-ext 136 | de-Latn-DE-variant 137 | de-Latn-DE 138 | de-Latn 139 | de) 140 | tag = Tag::Rfc4646.new(*%w(de Latn DE variant a-ext x-phonebk i-klingon)) 141 | assert_equal parents, tag.self_and_parents.map(&:to_s) 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /test/locale/tag/simple_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'test_helper' 3 | 4 | class I18nLocaleTagSimpleTest < I18n::TestCase 5 | include I18n::Locale 6 | 7 | test "returns 'de' as the language subtag in lowercase" do 8 | assert_equal %w(de Latn DE), Tag::Simple.new('de-Latn-DE').subtags 9 | end 10 | 11 | test "returns a formatted tag string from #to_s" do 12 | assert_equal 'de-Latn-DE', Tag::Simple.new('de-Latn-DE').to_s 13 | end 14 | 15 | test "returns an array containing the formatted subtags from #to_a" do 16 | assert_equal %w(de Latn DE), Tag::Simple.new('de-Latn-DE').to_a 17 | end 18 | 19 | # Tag inheritance 20 | 21 | test "#parent returns 'de-Latn' as the parent of 'de-Latn-DE'" do 22 | assert_equal 'de-Latn', Tag::Simple.new('de-Latn-DE').parent.to_s 23 | end 24 | 25 | test "#parent returns 'de' as the parent of 'de-Latn'" do 26 | assert_equal 'de', Tag::Simple.new('de-Latn').parent.to_s 27 | end 28 | 29 | test "#self_and_parents returns an array of 3 tags for 'de-Latn-DE'" do 30 | assert_equal %w(de-Latn-DE de-Latn de), Tag::Simple.new('de-Latn-DE').self_and_parents.map { |tag| tag.to_s} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/run_all.rb: -------------------------------------------------------------------------------- 1 | def bundle_check 2 | `bundle check` == "Resolving dependencies...\nThe Gemfile's dependencies are satisfied\n" 3 | end 4 | 5 | def execute(command) 6 | puts command 7 | system command 8 | end 9 | 10 | gemfiles = %w(Gemfile) + Dir['gemfiles/Gemfile*'].reject { |f| f.end_with?('.lock') } 11 | 12 | results = gemfiles.map do |gemfile| 13 | puts "\nBUNDLE_GEMFILE=#{gemfile}" 14 | ENV['BUNDLE_GEMFILE'] = File.expand_path("../../#{gemfile}", __FILE__) 15 | 16 | execute 'bundle install' unless bundle_check 17 | execute 'bundle exec rake test' 18 | end 19 | 20 | exit results.all? 21 | -------------------------------------------------------------------------------- /test/run_one.rb: -------------------------------------------------------------------------------- 1 | def bundle_check 2 | `bundle check` == "Resolving dependencies...\nThe Gemfile's dependencies are satisfied\n" 3 | end 4 | 5 | def execute(command) 6 | puts command 7 | system command 8 | end 9 | 10 | execute 'bundle install' unless bundle_check 11 | execute "bundle exec ruby -w -I'lib:test' #{ARGV[0]}" 12 | -------------------------------------------------------------------------------- /test/test_data/locales/de.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: version 0.0.1\n" 10 | "POT-Creation-Date: 2009-02-26 19:50+0100\n" 11 | "PO-Revision-Date: 2009-02-18 14:53+0100\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 18 | 19 | # #: app/helpers/translation_helper.rb:3 20 | # msgid "%{relative_time} ago" 21 | # msgstr "vor %{relative_time}" 22 | 23 | #: app/views/cars/show.html.erb:5 24 | msgid "Axis" 25 | msgid_plural "Axis" 26 | msgstr[0] "Achse" 27 | msgstr[1] "Achsen" 28 | 29 | #: app/controllers/cars_controller.rb:47 30 | msgid "Car was successfully created." 31 | msgstr "Auto wurde erfolgreich gespeichert" 32 | 33 | #: app/controllers/cars_controller.rb:64 34 | msgid "Car was successfully updated." 35 | msgstr "Auto wurde erfolgreich aktualisiert" 36 | 37 | #: app/views/cars/show.html.erb:1 locale/model_attributes.rb:3 38 | msgid "Car|Model" 39 | msgstr "Modell" 40 | 41 | #: app/views/cars/show.html.erb:3 locale/model_attributes.rb:4 42 | msgid "Car|Wheels count" 43 | msgstr "Räderzahl" 44 | 45 | msgctxt "New car" 46 | msgid "Wheels count" 47 | msgstr "Räderzahl!" 48 | 49 | #: app/views/cars/show.html.erb:7 50 | msgid "Created" 51 | msgstr "Erstellt" 52 | 53 | #: app/views/cars/show.html.erb:9 54 | msgid "Month" 55 | msgstr "Monat" 56 | 57 | #: locale/model_attributes.rb:2 58 | msgid "car" 59 | msgstr "Auto" 60 | 61 | #: locale/testlog_phrases.rb:2 62 | msgid "this is a dynamic translation which was found thorugh gettext_test_log!" 63 | msgstr "" 64 | "Dies ist eine dynamische Übersetzung, die durch gettext_test_log " 65 | "gefunden wurde!" 66 | 67 | #: app/views/cars/nowhere_really 68 | msgid "Car|wheel" 69 | msgid_plural "Car|wheels" 70 | msgstr[0] "Rad" 71 | msgstr[1] "Räder" 72 | 73 | msgctxt "New car" 74 | msgid "wheel" 75 | msgid_plural "wheels" 76 | msgstr[0] "Rad!" 77 | msgstr[1] "Räder!" 78 | 79 | msgid "On %{count} wheel." 80 | msgid_plural "On %{count} wheels." 81 | msgstr[0] "Auf %{count} Achse." 82 | msgstr[1] "Auf %{count} Achsen." 83 | -------------------------------------------------------------------------------- /test/test_data/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "foo": { 4 | "bar": "baz" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/test_data/locales/en.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | { :en => { :fuh => { :bah => "bas" } } } -------------------------------------------------------------------------------- /test/test_data/locales/en.yaml: -------------------------------------------------------------------------------- 1 | en: 2 | foo: 3 | bar: baz -------------------------------------------------------------------------------- /test/test_data/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | foo: 3 | bar: baz 4 | -------------------------------------------------------------------------------- /test/test_data/locales/fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | animal: 3 | dog: chien 4 | -------------------------------------------------------------------------------- /test/test_data/locales/invalid/empty.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-i18n/i18n/4dddd855039b0c5a0b5b3b2df69783374058a7c9/test/test_data/locales/invalid/empty.yml -------------------------------------------------------------------------------- /test/test_data/locales/invalid/syntax.yml: -------------------------------------------------------------------------------- 1 | en: 2 | foo: foo 3 | bar: 4 | baz: -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'bundler/setup' 3 | require 'i18n' 4 | require 'mocha/minitest' 5 | require 'test_declarative' 6 | 7 | class I18n::TestCase < Minitest::Test 8 | def assert_nothing_raised(*args) 9 | yield 10 | end 11 | 12 | def self.key_value? 13 | defined?(ActiveSupport) 14 | end 15 | 16 | def setup 17 | super 18 | I18n.load_path = nil 19 | I18n.enforce_available_locales = false 20 | end 21 | 22 | def teardown 23 | I18n.locale = nil 24 | I18n.default_locale = nil 25 | I18n.load_path = nil 26 | I18n.available_locales = nil 27 | I18n.backend = nil 28 | I18n.default_separator = nil 29 | I18n.enforce_available_locales = true 30 | I18n.fallbacks = nil if I18n.respond_to?(:fallbacks=) 31 | super 32 | end 33 | 34 | protected 35 | 36 | def translations 37 | I18n.backend.instance_variable_get(:@translations) 38 | end 39 | 40 | def store_translations(locale, data, options = I18n::EMPTY_HASH) 41 | I18n.backend.store_translations(locale, data, options) 42 | end 43 | 44 | def locales_dir 45 | File.dirname(__FILE__) + '/test_data/locales' 46 | end 47 | 48 | def stub_const(klass, constant, new_value) 49 | old_value = klass.const_get(constant) 50 | klass.send(:remove_const, constant) 51 | klass.const_set(constant, new_value) 52 | yield 53 | ensure 54 | klass.send(:remove_const, constant) 55 | klass.const_set(constant, old_value) 56 | end 57 | end 58 | 59 | class DummyRackApp 60 | def call(env) 61 | I18n.locale = :es 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/utils_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nUtilsTest < I18n::TestCase 4 | 5 | test ".deep_symbolize_keys" do 6 | hash = { 'foo' => { 'bar' => { 'baz' => 'bar' } } } 7 | expected = { :foo => { :bar => { :baz => 'bar' } } } 8 | assert_equal expected, I18n::Utils.deep_symbolize_keys(hash) 9 | end 10 | 11 | test "#deep_symbolize_keys with numeric keys" do 12 | hash = { 1 => { 2 => { 3 => 'bar' } } } 13 | expected = { 1 => { 2 => { 3 => 'bar' } } } 14 | assert_equal expected, I18n::Utils.deep_symbolize_keys(hash) 15 | end 16 | 17 | test "#except" do 18 | hash = { :foo => 'bar', :baz => 'bar' } 19 | expected = { :foo => 'bar' } 20 | assert_equal expected, I18n::Utils.except(hash, :baz) 21 | end 22 | 23 | test "#deep_merge!" do 24 | hash = { :foo => { :bar => { :baz => 'bar' } }, :baz => 'bar' } 25 | I18n::Utils.deep_merge!(hash, :foo => { :bar => { :baz => 'foo' } }) 26 | 27 | expected = { :foo => { :bar => { :baz => 'foo' } }, :baz => 'bar' } 28 | assert_equal expected, hash 29 | end 30 | end 31 | --------------------------------------------------------------------------------