This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2008 The Ruby I18n team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Ruby I18n
|
||||
|
||||
[](https://badge.fury.io/rb/i18n)
|
||||
[](https://github.com/ruby-i18n/i18n/actions?query=workflow%3ARuby)
|
||||
|
||||
Ruby internationalization and localization (i18n) solution.
|
||||
|
||||
Currently maintained by @radar.
|
||||
|
||||
## Usage
|
||||
|
||||
### Rails
|
||||
|
||||
You will most commonly use this library within a Rails app.
|
||||
|
||||
We support Rails versions from 6.0 and up.
|
||||
|
||||
[See the Rails Guide](https://guides.rubyonrails.org/i18n.html) for an example of its usage.
|
||||
|
||||
### Ruby (without Rails)
|
||||
|
||||
We support Ruby versions from 3.0 and up.
|
||||
|
||||
If you want to use this library without Rails, you can simply add `i18n` to your `Gemfile`:
|
||||
|
||||
```ruby
|
||||
gem 'i18n'
|
||||
```
|
||||
|
||||
Then configure I18n with some translations, and a default locale:
|
||||
|
||||
```ruby
|
||||
I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
|
||||
I18n.default_locale = :en # (note that `en` is already the default!)
|
||||
```
|
||||
|
||||
A simple translation file in your project might live at `config/locales/en.yml` and look like:
|
||||
|
||||
```yml
|
||||
en:
|
||||
test: "This is a test"
|
||||
```
|
||||
|
||||
You can then access this translation by doing:
|
||||
|
||||
```ruby
|
||||
I18n.t(:test)
|
||||
```
|
||||
|
||||
You can switch locales in your project by setting `I18n.locale` to a different value:
|
||||
|
||||
```ruby
|
||||
I18n.locale = :de
|
||||
I18n.t(:test) # => "Dies ist ein Test"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
* Translation and localization
|
||||
* Interpolation of values to translations
|
||||
* Pluralization (CLDR compatible)
|
||||
* Customizable transliteration to ASCII
|
||||
* Flexible defaults
|
||||
* Bulk lookup
|
||||
* Lambdas as translation data
|
||||
* Custom key/scope separator
|
||||
* Custom exception handlers
|
||||
* Extensible architecture with a swappable backend
|
||||
|
||||
## Pluggable Features
|
||||
|
||||
* Cache
|
||||
* Pluralization: lambda pluralizers stored as translation data
|
||||
* Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation)
|
||||
* [Gettext support](https://github.com/ruby-i18n/i18n/wiki/Gettext)
|
||||
* Translation metadata
|
||||
|
||||
## Alternative Backend
|
||||
|
||||
* Chain
|
||||
* ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs)
|
||||
* KeyValue (uses active_support/json and cannot store procs)
|
||||
|
||||
For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/ruby-i18n/i18n/wiki/Resources).
|
||||
|
||||
## Tests
|
||||
|
||||
You can run tests both with
|
||||
|
||||
* `rake test` or just `rake`
|
||||
* run any test file directly, e.g. `ruby -Ilib:test test/api/simple_test.rb`
|
||||
|
||||
You can run all tests against all Gemfiles with
|
||||
|
||||
* `ruby test/run_all.rb`
|
||||
|
||||
The structure of the test suite is a bit unusual as it uses modules to reuse
|
||||
particular tests in different test cases.
|
||||
|
||||
The reason for this is that we need to enforce the I18n API across various
|
||||
combinations of extensions. E.g. the Simple backend alone needs to support
|
||||
the same API as any combination of feature and/or optimization modules included
|
||||
to the Simple backend. We test this by reusing the same API definition (implemented
|
||||
as test methods) in test cases with different setups.
|
||||
|
||||
You can find the test cases that enforce the API in test/api. And you can find
|
||||
the API definition test methods in test/api/tests.
|
||||
|
||||
All other test cases (e.g. as defined in test/backend, test/core_ext) etc.
|
||||
follow the usual test setup and should be easy to grok.
|
||||
|
||||
## More Documentation
|
||||
|
||||
Additional documentation can be found here: https://github.com/ruby-i18n/i18n/wiki
|
||||
|
||||
## Contributors
|
||||
|
||||
* @radar
|
||||
* @carlosantoniodasilva
|
||||
* @josevalim
|
||||
* @knapo
|
||||
* @tigrish
|
||||
* [and many more](https://github.com/ruby-i18n/i18n/graphs/contributors)
|
||||
|
||||
## License
|
||||
|
||||
MIT License. See the included MIT-LICENSE file.
|
||||
@@ -0,0 +1,477 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'concurrent/map'
|
||||
require 'concurrent/hash'
|
||||
|
||||
require 'i18n/version'
|
||||
require 'i18n/utils'
|
||||
require 'i18n/exceptions'
|
||||
require 'i18n/interpolate/ruby'
|
||||
|
||||
module I18n
|
||||
autoload :Backend, 'i18n/backend'
|
||||
autoload :Config, 'i18n/config'
|
||||
autoload :Gettext, 'i18n/gettext'
|
||||
autoload :Locale, 'i18n/locale'
|
||||
autoload :Tests, 'i18n/tests'
|
||||
autoload :Middleware, 'i18n/middleware'
|
||||
|
||||
RESERVED_KEYS = %i[
|
||||
cascade
|
||||
deep_interpolation
|
||||
skip_interpolation
|
||||
default
|
||||
exception_handler
|
||||
fallback
|
||||
fallback_in_progress
|
||||
fallback_original_locale
|
||||
format
|
||||
object
|
||||
raise
|
||||
resolve
|
||||
scope
|
||||
separator
|
||||
throw
|
||||
]
|
||||
EMPTY_HASH = {}.freeze
|
||||
|
||||
def self.new_double_nested_cache # :nodoc:
|
||||
Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
|
||||
end
|
||||
|
||||
# Marks a key as reserved. Reserved keys are used internally,
|
||||
# and can't also be used for interpolation. If you are using any
|
||||
# extra keys as I18n options, you should call I18n.reserve_key
|
||||
# before any I18n.translate (etc) calls are made.
|
||||
def self.reserve_key(key)
|
||||
RESERVED_KEYS << key.to_sym
|
||||
@reserved_keys_pattern = nil
|
||||
end
|
||||
|
||||
def self.reserved_keys_pattern # :nodoc:
|
||||
@reserved_keys_pattern ||= /(?<!%)%\{(#{RESERVED_KEYS.join("|")})\}/
|
||||
end
|
||||
|
||||
module Base
|
||||
# Gets I18n configuration object.
|
||||
def config
|
||||
Thread.current[:i18n_config] ||= I18n::Config.new
|
||||
end
|
||||
|
||||
# Sets I18n configuration object.
|
||||
def config=(value)
|
||||
Thread.current[:i18n_config] = value
|
||||
end
|
||||
|
||||
# Write methods which delegates to the configuration object
|
||||
%w(locale backend default_locale available_locales default_separator
|
||||
exception_handler load_path enforce_available_locales).each do |method|
|
||||
module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1
|
||||
def #{method}
|
||||
config.#{method}
|
||||
end
|
||||
|
||||
def #{method}=(value)
|
||||
config.#{method} = (value)
|
||||
end
|
||||
DELEGATORS
|
||||
end
|
||||
|
||||
# Tells the backend to reload translations. Used in situations like the
|
||||
# Rails development environment. Backends can implement whatever strategy
|
||||
# is useful.
|
||||
def reload!
|
||||
config.clear_available_locales_set
|
||||
config.backend.reload!
|
||||
end
|
||||
|
||||
# Tells the backend to load translations now. Used in situations like the
|
||||
# Rails production environment. Backends can implement whatever strategy
|
||||
# is useful.
|
||||
def eager_load!
|
||||
config.backend.eager_load!
|
||||
end
|
||||
|
||||
# Translates, pluralizes and interpolates a given key using a given locale,
|
||||
# scope, and default, as well as interpolation values.
|
||||
#
|
||||
# *LOOKUP*
|
||||
#
|
||||
# Translation data is organized as a nested hash using the upper-level keys
|
||||
# as namespaces. <em>E.g.</em>, ActionView ships with the translation:
|
||||
# <tt>:date => {:formats => {:short => "%b %d"}}</tt>.
|
||||
#
|
||||
# Translations can be looked up at any level of this hash using the key argument
|
||||
# and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt>
|
||||
# returns the whole translations hash <tt>{:formats => {:short => "%b %d"}}</tt>.
|
||||
#
|
||||
# Key can be either a single key or a dot-separated key (both Strings and Symbols
|
||||
# work). <em>E.g.</em>, the short format can be looked up using both:
|
||||
# I18n.t 'date.formats.short'
|
||||
# I18n.t :'date.formats.short'
|
||||
#
|
||||
# Scope can be either a single key, a dot-separated key or an array of keys
|
||||
# or dot-separated keys. Keys and scopes can be combined freely. So these
|
||||
# examples will all look up the same short date format:
|
||||
# I18n.t 'date.formats.short'
|
||||
# I18n.t 'formats.short', :scope => 'date'
|
||||
# I18n.t 'short', :scope => 'date.formats'
|
||||
# I18n.t 'short', :scope => %w(date formats)
|
||||
#
|
||||
# *INTERPOLATION*
|
||||
#
|
||||
# Translations can contain interpolation variables which will be replaced by
|
||||
# values passed to #translate as part of the options hash, with the keys matching
|
||||
# the interpolation variable names.
|
||||
#
|
||||
# <em>E.g.</em>, with a translation <tt>:foo => "foo %{bar}"</tt> the option
|
||||
# value for the key +bar+ will be interpolated into the translation:
|
||||
# I18n.t :foo, :bar => 'baz' # => 'foo baz'
|
||||
#
|
||||
# *PLURALIZATION*
|
||||
#
|
||||
# Translation data can contain pluralized translations. Pluralized translations
|
||||
# are arrays of singular/plural versions of translations like <tt>['Foo', 'Foos']</tt>.
|
||||
#
|
||||
# Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English
|
||||
# pluralization rules. Other algorithms can be supported by custom backends.
|
||||
#
|
||||
# This returns the singular version of a pluralized translation:
|
||||
# I18n.t :foo, :count => 1 # => 'Foo'
|
||||
#
|
||||
# These both return the plural version of a pluralized translation:
|
||||
# I18n.t :foo, :count => 0 # => 'Foos'
|
||||
# I18n.t :foo, :count => 2 # => 'Foos'
|
||||
#
|
||||
# The <tt>:count</tt> option can be used both for pluralization and interpolation.
|
||||
# <em>E.g.</em>, with the translation
|
||||
# <tt>:foo => ['%{count} foo', '%{count} foos']</tt>, count will
|
||||
# be interpolated to the pluralized translation:
|
||||
# I18n.t :foo, :count => 1 # => '1 foo'
|
||||
#
|
||||
# *DEFAULTS*
|
||||
#
|
||||
# This returns the translation for <tt>:foo</tt> or <tt>default</tt> if no translation was found:
|
||||
# I18n.t :foo, :default => 'default'
|
||||
#
|
||||
# This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no
|
||||
# translation for <tt>:foo</tt> was found:
|
||||
# I18n.t :foo, :default => :bar
|
||||
#
|
||||
# Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt>
|
||||
# or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
|
||||
# I18n.t :foo, :default => [:bar, 'default']
|
||||
#
|
||||
# <b>BULK LOOKUP</b>
|
||||
#
|
||||
# This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
|
||||
# I18n.t [:foo, :bar]
|
||||
#
|
||||
# Can be used with dot-separated nested keys:
|
||||
# I18n.t [:'baz.foo', :'baz.bar']
|
||||
#
|
||||
# Which is the same as using a scope option:
|
||||
# I18n.t [:foo, :bar], :scope => :baz
|
||||
#
|
||||
# *LAMBDAS*
|
||||
#
|
||||
# Both translations and defaults can be given as Ruby lambdas. Lambdas will be
|
||||
# called and passed the key and options.
|
||||
#
|
||||
# E.g. assuming the key <tt>:salutation</tt> resolves to:
|
||||
# lambda { |key, options| options[:gender] == 'm' ? "Mr. #{options[:name]}" : "Mrs. #{options[:name]}" }
|
||||
#
|
||||
# Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith')</tt> will result in "Mrs. Smith".
|
||||
#
|
||||
# Note that the string returned by lambda will go through string interpolation too,
|
||||
# so the following lambda would give the same result:
|
||||
# lambda { |key, options| options[:gender] == 'm' ? "Mr. %{name}" : "Mrs. %{name}" }
|
||||
#
|
||||
# It is recommended to use/implement lambdas in an "idempotent" way. E.g. when
|
||||
# a cache layer is put in front of I18n.translate it will generate a cache key
|
||||
# from the argument values passed to #translate. Therefore your lambdas should
|
||||
# always return the same translations/values per unique combination of argument
|
||||
# values.
|
||||
#
|
||||
# <b>Ruby 2.7+ keyword arguments warning</b>
|
||||
#
|
||||
# This method uses keyword arguments.
|
||||
# There is a breaking change in ruby that produces warning with ruby 2.7 and won't work as expected with ruby 3.0
|
||||
# The "hash" parameter must be passed as keyword argument.
|
||||
#
|
||||
# Good:
|
||||
# I18n.t(:salutation, :gender => 'w', :name => 'Smith')
|
||||
# I18n.t(:salutation, **{ :gender => 'w', :name => 'Smith' })
|
||||
# I18n.t(:salutation, **any_hash)
|
||||
#
|
||||
# Bad:
|
||||
# I18n.t(:salutation, { :gender => 'w', :name => 'Smith' })
|
||||
# I18n.t(:salutation, any_hash)
|
||||
#
|
||||
def translate(key = nil, throw: false, raise: false, locale: nil, **options) # TODO deprecate :raise
|
||||
locale ||= config.locale
|
||||
raise Disabled.new('t') if locale == false
|
||||
enforce_available_locales!(locale)
|
||||
|
||||
backend = config.backend
|
||||
|
||||
if key.is_a?(Array)
|
||||
key.map do |k|
|
||||
translate_key(k, throw, raise, locale, backend, options)
|
||||
end
|
||||
else
|
||||
translate_key(key, throw, raise, locale, backend, options)
|
||||
end
|
||||
end
|
||||
alias :t :translate
|
||||
|
||||
# Wrapper for <tt>translate</tt> that adds <tt>:raise => true</tt>. With
|
||||
# this option, if no translation is found, it will raise <tt>I18n::MissingTranslationData</tt>
|
||||
def translate!(key, **options)
|
||||
translate(key, **options, raise: true)
|
||||
end
|
||||
alias :t! :translate!
|
||||
|
||||
# Returns an array of interpolation keys for the given translation key
|
||||
#
|
||||
# *Examples*
|
||||
#
|
||||
# Suppose we have the following:
|
||||
# I18n.t 'example.zero' == 'Zero interpolations'
|
||||
# I18n.t 'example.one' == 'One interpolation %{foo}'
|
||||
# I18n.t 'example.two' == 'Two interpolations %{foo} %{bar}'
|
||||
# I18n.t 'example.three' == ['One %{foo}', 'Two %{bar}', 'Three %{baz}']
|
||||
# I18n.t 'example.one', locale: :other == 'One interpolation %{baz}'
|
||||
#
|
||||
# Then we can expect the following results:
|
||||
# I18n.interpolation_keys('example.zero') #=> []
|
||||
# I18n.interpolation_keys('example.one') #=> ['foo']
|
||||
# I18n.interpolation_keys('example.two') #=> ['foo', 'bar']
|
||||
# I18n.interpolation_keys('example.three') #=> ['foo', 'bar', 'baz']
|
||||
# I18n.interpolation_keys('one', scope: 'example', locale: :other) #=> ['baz']
|
||||
# I18n.interpolation_keys('does-not-exist') #=> []
|
||||
# I18n.interpolation_keys('example') #=> []
|
||||
def interpolation_keys(key, **options)
|
||||
raise I18n::ArgumentError if !key.is_a?(String) || key.empty?
|
||||
|
||||
return [] unless exists?(key, **options.slice(:locale, :scope))
|
||||
|
||||
translation = translate(key, **options.slice(:locale, :scope))
|
||||
interpolation_keys_from_translation(translation)
|
||||
.flatten.compact
|
||||
end
|
||||
|
||||
# Returns true if a translation exists for a given key, otherwise returns false.
|
||||
def exists?(key, _locale = nil, locale: _locale, **options)
|
||||
locale ||= config.locale
|
||||
raise Disabled.new('exists?') if locale == false
|
||||
raise I18n::ArgumentError if (key.is_a?(String) && key.empty?) || key.nil?
|
||||
|
||||
config.backend.exists?(locale, key, options)
|
||||
end
|
||||
|
||||
# Transliterates UTF-8 characters to ASCII. By default this method will
|
||||
# transliterate only Latin strings to an ASCII approximation:
|
||||
#
|
||||
# I18n.transliterate("Ærøskøbing")
|
||||
# # => "AEroskobing"
|
||||
#
|
||||
# I18n.transliterate("日本語")
|
||||
# # => "???"
|
||||
#
|
||||
# It's also possible to add support for per-locale transliterations. I18n
|
||||
# expects transliteration rules to be stored at
|
||||
# <tt>i18n.transliterate.rule</tt>.
|
||||
#
|
||||
# Transliteration rules can either be a Hash or a Proc. Procs must accept a
|
||||
# single string argument. Hash rules inherit the default transliteration
|
||||
# rules, while Procs do not.
|
||||
#
|
||||
# *Examples*
|
||||
#
|
||||
# Setting a Hash in <locale>.yml:
|
||||
#
|
||||
# i18n:
|
||||
# transliterate:
|
||||
# rule:
|
||||
# ü: "ue"
|
||||
# ö: "oe"
|
||||
#
|
||||
# Setting a Hash using Ruby:
|
||||
#
|
||||
# store_translations(:de, i18n: {
|
||||
# transliterate: {
|
||||
# rule: {
|
||||
# 'ü' => 'ue',
|
||||
# 'ö' => 'oe'
|
||||
# }
|
||||
# }
|
||||
# })
|
||||
#
|
||||
# Setting a Proc:
|
||||
#
|
||||
# translit = lambda {|string| MyTransliterator.transliterate(string) }
|
||||
# store_translations(:xx, :i18n => {:transliterate => {:rule => translit})
|
||||
#
|
||||
# Transliterating strings:
|
||||
#
|
||||
# I18n.locale = :en
|
||||
# I18n.transliterate("Jürgen") # => "Jurgen"
|
||||
# I18n.locale = :de
|
||||
# I18n.transliterate("Jürgen") # => "Juergen"
|
||||
# I18n.transliterate("Jürgen", :locale => :en) # => "Jurgen"
|
||||
# I18n.transliterate("Jürgen", :locale => :de) # => "Juergen"
|
||||
def transliterate(key, throw: false, raise: false, locale: nil, replacement: nil, **options)
|
||||
locale ||= config.locale
|
||||
raise Disabled.new('transliterate') if locale == false
|
||||
enforce_available_locales!(locale)
|
||||
|
||||
config.backend.transliterate(locale, key, replacement)
|
||||
rescue I18n::ArgumentError => exception
|
||||
handle_exception((throw && :throw || raise && :raise), exception, locale, key, options)
|
||||
end
|
||||
|
||||
# Localizes certain objects, such as dates and numbers to local formatting.
|
||||
def localize(object, locale: nil, format: nil, **options)
|
||||
locale ||= config.locale
|
||||
raise Disabled.new('l') if locale == false
|
||||
enforce_available_locales!(locale)
|
||||
|
||||
format ||= :default
|
||||
config.backend.localize(locale, object, format, options)
|
||||
end
|
||||
alias :l :localize
|
||||
|
||||
# Executes block with given I18n.locale set.
|
||||
def with_locale(tmp_locale = nil)
|
||||
if tmp_locale == nil
|
||||
yield
|
||||
else
|
||||
current_locale = self.locale
|
||||
self.locale = tmp_locale
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
self.locale = current_locale
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Merges the given locale, key and scope into a single array of keys.
|
||||
# Splits keys that contain dots into multiple keys. Makes sure all
|
||||
# keys are Symbols.
|
||||
def normalize_keys(locale, key, scope, separator = nil)
|
||||
separator ||= I18n.default_separator
|
||||
|
||||
[
|
||||
*normalize_key(locale, separator),
|
||||
*normalize_key(scope, separator),
|
||||
*normalize_key(key, separator)
|
||||
]
|
||||
end
|
||||
|
||||
# Returns true when the passed locale, which can be either a String or a
|
||||
# Symbol, is in the list of available locales. Returns false otherwise.
|
||||
def locale_available?(locale)
|
||||
I18n.config.available_locales_set.include?(locale)
|
||||
end
|
||||
|
||||
# Raises an InvalidLocale exception when the passed locale is not available.
|
||||
def enforce_available_locales!(locale)
|
||||
if locale != false && config.enforce_available_locales
|
||||
raise I18n::InvalidLocale.new(locale) if !locale_available?(locale)
|
||||
end
|
||||
end
|
||||
|
||||
def available_locales_initialized?
|
||||
config.available_locales_initialized?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def translate_key(key, throw, raise, locale, backend, options)
|
||||
result = catch(:exception) do
|
||||
backend.translate(locale, key, options)
|
||||
end
|
||||
|
||||
if result.is_a?(MissingTranslation)
|
||||
handle_exception((throw && :throw || raise && :raise), result, locale, key, options)
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
# Any exceptions thrown in translate will be sent to the @@exception_handler
|
||||
# which can be a Symbol, a Proc or any other Object unless they're forced to
|
||||
# be raised or thrown (MissingTranslation).
|
||||
#
|
||||
# If exception_handler is a Symbol then it will simply be sent to I18n as
|
||||
# a method call. A Proc will simply be called. In any other case the
|
||||
# method #call will be called on the exception_handler object.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# I18n.exception_handler = :custom_exception_handler # this is the default
|
||||
# I18n.custom_exception_handler(exception, locale, key, options) # will be called like this
|
||||
#
|
||||
# I18n.exception_handler = lambda { |*args| ... } # a lambda
|
||||
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
|
||||
#
|
||||
# I18n.exception_handler = I18nExceptionHandler.new # an object
|
||||
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
|
||||
def handle_exception(handling, exception, locale, key, options)
|
||||
case handling
|
||||
when :raise
|
||||
raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
|
||||
when :throw
|
||||
throw :exception, exception
|
||||
else
|
||||
case handler = options[:exception_handler] || config.exception_handler
|
||||
when Symbol
|
||||
send(handler, exception, locale, key, options)
|
||||
else
|
||||
handler.call(exception, locale, key, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@normalized_key_cache = I18n.new_double_nested_cache
|
||||
|
||||
def normalize_key(key, separator)
|
||||
@@normalized_key_cache[separator][key] ||=
|
||||
case key
|
||||
when Array
|
||||
key.flat_map { |k| normalize_key(k, separator) }
|
||||
else
|
||||
keys = key.to_s.split(separator)
|
||||
keys.delete('')
|
||||
keys.map! do |k|
|
||||
case k
|
||||
when /\A[-+]?([1-9]\d*|0)\z/ # integer
|
||||
k.to_i
|
||||
when 'true'
|
||||
true
|
||||
when 'false'
|
||||
false
|
||||
else
|
||||
k.to_sym
|
||||
end
|
||||
end
|
||||
keys
|
||||
end
|
||||
end
|
||||
|
||||
def interpolation_keys_from_translation(translation)
|
||||
case translation
|
||||
when ::String
|
||||
translation.scan(Regexp.union(I18n.config.interpolation_patterns))
|
||||
when ::Array
|
||||
translation.map { |element| interpolation_keys_from_translation(element) }
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
extend Base
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
autoload :Base, 'i18n/backend/base'
|
||||
autoload :Cache, 'i18n/backend/cache'
|
||||
autoload :CacheFile, 'i18n/backend/cache_file'
|
||||
autoload :Cascade, 'i18n/backend/cascade'
|
||||
autoload :Chain, 'i18n/backend/chain'
|
||||
autoload :Fallbacks, 'i18n/backend/fallbacks'
|
||||
autoload :Flatten, 'i18n/backend/flatten'
|
||||
autoload :Gettext, 'i18n/backend/gettext'
|
||||
autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
|
||||
autoload :KeyValue, 'i18n/backend/key_value'
|
||||
autoload :LazyLoadable, 'i18n/backend/lazy_loadable'
|
||||
autoload :Memoize, 'i18n/backend/memoize'
|
||||
autoload :Metadata, 'i18n/backend/metadata'
|
||||
autoload :Pluralization, 'i18n/backend/pluralization'
|
||||
autoload :Simple, 'i18n/backend/simple'
|
||||
autoload :Transliterator, 'i18n/backend/transliterator'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,314 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
require 'json'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
module Base
|
||||
include I18n::Backend::Transliterator
|
||||
|
||||
# Accepts a list of paths to translation files. Loads translations from
|
||||
# plain Ruby (*.rb), YAML files (*.yml), or JSON files (*.json). See #load_rb, #load_yml, and #load_json
|
||||
# for details.
|
||||
def load_translations(*filenames)
|
||||
filenames = I18n.load_path if filenames.empty?
|
||||
filenames.flatten.each do |filename|
|
||||
loaded_translations = load_file(filename)
|
||||
yield filename, loaded_translations if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
# This method receives a locale, a data hash and options for storing translations.
|
||||
# Should be implemented
|
||||
def store_translations(locale, data, options = EMPTY_HASH)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def translate(locale, key, options = EMPTY_HASH)
|
||||
raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
|
||||
raise InvalidLocale.new(locale) unless locale
|
||||
return nil if key.nil? && !options.key?(:default)
|
||||
|
||||
entry = lookup(locale, key, options[:scope], options) unless key.nil?
|
||||
|
||||
if entry.nil? && options.key?(:default)
|
||||
entry = default(locale, key, options[:default], options)
|
||||
else
|
||||
entry = resolve_entry(locale, key, entry, options)
|
||||
end
|
||||
|
||||
count = options[:count]
|
||||
|
||||
if entry.nil? && (subtrees? || !count)
|
||||
if (options.key?(:default) && !options[:default].nil?) || !options.key?(:default)
|
||||
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
||||
end
|
||||
end
|
||||
|
||||
entry = entry.dup if entry.is_a?(String)
|
||||
entry = pluralize(locale, entry, count) if count
|
||||
|
||||
if entry.nil? && !subtrees?
|
||||
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
||||
end
|
||||
|
||||
deep_interpolation = options[:deep_interpolation]
|
||||
skip_interpolation = options[:skip_interpolation]
|
||||
values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
|
||||
if !skip_interpolation && values && !values.empty?
|
||||
entry = if deep_interpolation
|
||||
deep_interpolate(locale, entry, values)
|
||||
else
|
||||
interpolate(locale, entry, values)
|
||||
end
|
||||
elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
|
||||
raise ReservedInterpolationKey.new($1.to_sym, entry)
|
||||
end
|
||||
entry
|
||||
end
|
||||
|
||||
def exists?(locale, key, options = EMPTY_HASH)
|
||||
lookup(locale, key, options[:scope]) != nil
|
||||
end
|
||||
|
||||
# Acts the same as +strftime+, but uses a localized version of the
|
||||
# format string. Takes a key from the date/time formats translations as
|
||||
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
|
||||
def localize(locale, object, format = :default, options = EMPTY_HASH)
|
||||
if object.nil? && options.include?(:default)
|
||||
return options[:default]
|
||||
end
|
||||
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
|
||||
|
||||
if Symbol === format
|
||||
key = format
|
||||
type = object.respond_to?(:sec) ? 'time' : 'date'
|
||||
options = options.merge(:raise => true, :object => object, :locale => locale)
|
||||
format = I18n.t(:"#{type}.formats.#{key}", **options)
|
||||
end
|
||||
|
||||
format = translate_localization_format(locale, object, format, options)
|
||||
object.strftime(format)
|
||||
end
|
||||
|
||||
# Returns an array of locales for which translations are available
|
||||
# ignoring the reserved translation meta data key :i18n.
|
||||
def available_locales
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def reload!
|
||||
eager_load! if eager_loaded?
|
||||
end
|
||||
|
||||
def eager_load!
|
||||
@eager_loaded = true
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def eager_loaded?
|
||||
@eager_loaded ||= false
|
||||
end
|
||||
|
||||
# The method which actually looks up for the translation in the store.
|
||||
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def subtrees?
|
||||
true
|
||||
end
|
||||
|
||||
# Evaluates defaults.
|
||||
# If given subject is an Array, it walks the array and returns the
|
||||
# first translation that can be resolved. Otherwise it tries to resolve
|
||||
# the translation directly.
|
||||
def default(locale, object, subject, options = EMPTY_HASH)
|
||||
if options.size == 1 && options.has_key?(:default)
|
||||
options = {}
|
||||
else
|
||||
options = Utils.except(options, :default)
|
||||
end
|
||||
|
||||
case subject
|
||||
when Array
|
||||
subject.each do |item|
|
||||
result = resolve(locale, object, item, options)
|
||||
return result unless result.nil?
|
||||
end and nil
|
||||
else
|
||||
resolve(locale, object, subject, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Resolves a translation.
|
||||
# If the given subject is a Symbol, it will be translated with the
|
||||
# given options. If it is a Proc then it will be evaluated. All other
|
||||
# subjects will be returned directly.
|
||||
def resolve(locale, object, subject, options = EMPTY_HASH)
|
||||
return subject if options[:resolve] == false
|
||||
result = catch(:exception) do
|
||||
case subject
|
||||
when Symbol
|
||||
I18n.translate(
|
||||
subject,
|
||||
**options.merge(
|
||||
:locale => locale,
|
||||
:throw => true,
|
||||
:skip_interpolation => true
|
||||
)
|
||||
)
|
||||
when Proc
|
||||
date_or_time = options.delete(:object) || object
|
||||
resolve(locale, object, subject.call(date_or_time, **options))
|
||||
else
|
||||
subject
|
||||
end
|
||||
end
|
||||
result unless result.is_a?(MissingTranslation)
|
||||
end
|
||||
alias_method :resolve_entry, :resolve
|
||||
|
||||
# Picks a translation from a pluralized mnemonic subkey according to English
|
||||
# pluralization rules :
|
||||
# - It will pick the :one subkey if count is equal to 1.
|
||||
# - It will pick the :other subkey otherwise.
|
||||
# - It will pick the :zero subkey in the special case where count is
|
||||
# equal to 0 and there is a :zero subkey present. This behaviour is
|
||||
# not standard with regards to the CLDR pluralization rules.
|
||||
# Other backends can implement more flexible or complex pluralization rules.
|
||||
def pluralize(locale, entry, count)
|
||||
entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
|
||||
return entry unless entry.is_a?(Hash) && count
|
||||
|
||||
key = pluralization_key(entry, count)
|
||||
raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
|
||||
entry[key]
|
||||
end
|
||||
|
||||
# Interpolates values into a given subject.
|
||||
#
|
||||
# if the given subject is a string then:
|
||||
# method interpolates "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
|
||||
# # => "file test.txt opened by %{user}"
|
||||
#
|
||||
# if the given subject is an array then:
|
||||
# each element of the array is recursively interpolated (until it finds a string)
|
||||
# method interpolates ["yes, %{user}", ["maybe no, %{user}", "no, %{user}"]], :user => "bartuz"
|
||||
# # => ["yes, bartuz", ["maybe no, bartuz", "no, bartuz"]]
|
||||
def interpolate(locale, subject, values = EMPTY_HASH)
|
||||
return subject if values.empty?
|
||||
|
||||
case subject
|
||||
when ::String then I18n.interpolate(subject, values)
|
||||
when ::Array then subject.map { |element| interpolate(locale, element, values) }
|
||||
else
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
# Deep interpolation
|
||||
#
|
||||
# deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
|
||||
# ann: 'good', john: 'big'
|
||||
# #=> { people: { ann: "Ann is good", john: "John is big" } }
|
||||
def deep_interpolate(locale, data, values = EMPTY_HASH)
|
||||
return data if values.empty?
|
||||
|
||||
case data
|
||||
when ::String
|
||||
I18n.interpolate(data, values)
|
||||
when ::Hash
|
||||
data.each_with_object({}) do |(k, v), result|
|
||||
result[k] = deep_interpolate(locale, v, values)
|
||||
end
|
||||
when ::Array
|
||||
data.map do |v|
|
||||
deep_interpolate(locale, v, values)
|
||||
end
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
# Loads a single translations file by delegating to #load_rb or
|
||||
# #load_yml depending on the file extension and directly merges the
|
||||
# data to the existing translations. Raises I18n::UnknownFileType
|
||||
# for all other file extensions.
|
||||
def load_file(filename)
|
||||
type = File.extname(filename).tr('.', '').downcase
|
||||
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
|
||||
data, keys_symbolized = send(:"load_#{type}", filename)
|
||||
unless data.is_a?(Hash)
|
||||
raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
|
||||
end
|
||||
data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
# Loads a plain Ruby translations file. eval'ing the file must yield
|
||||
# a Hash containing translation data with locales as toplevel keys.
|
||||
def load_rb(filename)
|
||||
translations = eval(IO.read(filename), binding, filename.to_s)
|
||||
[translations, false]
|
||||
end
|
||||
|
||||
# Loads a YAML translations file. The data must have locales as
|
||||
# toplevel keys.
|
||||
def load_yml(filename)
|
||||
begin
|
||||
if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
|
||||
[YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
|
||||
else
|
||||
[YAML.load_file(filename), false]
|
||||
end
|
||||
rescue TypeError, ScriptError, StandardError => e
|
||||
raise InvalidLocaleData.new(filename, e.inspect)
|
||||
end
|
||||
end
|
||||
alias_method :load_yaml, :load_yml
|
||||
|
||||
# Loads a JSON translations file. The data must have locales as
|
||||
# toplevel keys.
|
||||
def load_json(filename)
|
||||
begin
|
||||
# Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
|
||||
if ::JSON.respond_to?(:load_file)
|
||||
[::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
|
||||
else
|
||||
[::JSON.parse(File.read(filename)), false]
|
||||
end
|
||||
rescue TypeError, StandardError => e
|
||||
raise InvalidLocaleData.new(filename, e.inspect)
|
||||
end
|
||||
end
|
||||
|
||||
def translate_localization_format(locale, object, format, options)
|
||||
format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
|
||||
case match
|
||||
when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
|
||||
when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
|
||||
when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
|
||||
when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
|
||||
when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
|
||||
when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
|
||||
when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
|
||||
when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
|
||||
when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
|
||||
when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
|
||||
end
|
||||
end
|
||||
rescue MissingTranslationData => e
|
||||
e.message
|
||||
end
|
||||
|
||||
def pluralization_key(entry, count)
|
||||
key = :zero if count == 0 && entry.has_key?(:zero)
|
||||
key ||= count == 1 ? :one : :other
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This module allows you to easily cache all responses from the backend - thus
|
||||
# speeding up the I18n aspects of your application quite a bit.
|
||||
#
|
||||
# To enable caching you can simply include the Cache module to the Simple
|
||||
# backend - or whatever other backend you are using:
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
|
||||
#
|
||||
# You will also need to set a cache store implementation that you want to use:
|
||||
#
|
||||
# I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
|
||||
#
|
||||
# You can use any cache implementation you want that provides the same API as
|
||||
# ActiveSupport::Cache (only the methods #fetch and #write are being used).
|
||||
#
|
||||
# The cache_key implementation by default assumes you pass values that return
|
||||
# a valid key from #hash (see
|
||||
# https://www.ruby-doc.org/core/classes/Object.html#M000337). However, you can
|
||||
# configure your own digest method via which responds to #hexdigest (see
|
||||
# https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/Digest.html):
|
||||
#
|
||||
# I18n.cache_key_digest = OpenSSL::Digest::SHA256.new
|
||||
#
|
||||
# If you use a lambda as a default value in your translation like this:
|
||||
#
|
||||
# I18n.t(:"date.order", :default => lambda {[:month, :day, :year]})
|
||||
#
|
||||
# Then you will always have a cache miss, because each time this method
|
||||
# is called the lambda will have a different hash value. If you know
|
||||
# the result of the lambda is a constant as in the example above, then
|
||||
# to cache this you can make the lambda a constant, like this:
|
||||
#
|
||||
# DEFAULT_DATE_ORDER = lambda {[:month, :day, :year]}
|
||||
# ...
|
||||
# I18n.t(:"date.order", :default => DEFAULT_DATE_ORDER)
|
||||
#
|
||||
# If the lambda may result in different values for each call then consider
|
||||
# also using the Memoize backend.
|
||||
#
|
||||
module I18n
|
||||
class << self
|
||||
@@cache_store = nil
|
||||
@@cache_namespace = nil
|
||||
@@cache_key_digest = nil
|
||||
|
||||
def cache_store
|
||||
@@cache_store
|
||||
end
|
||||
|
||||
def cache_store=(store)
|
||||
@@cache_store = store
|
||||
end
|
||||
|
||||
def cache_namespace
|
||||
@@cache_namespace
|
||||
end
|
||||
|
||||
def cache_namespace=(namespace)
|
||||
@@cache_namespace = namespace
|
||||
end
|
||||
|
||||
def cache_key_digest
|
||||
@@cache_key_digest
|
||||
end
|
||||
|
||||
def cache_key_digest=(key_digest)
|
||||
@@cache_key_digest = key_digest
|
||||
end
|
||||
|
||||
def perform_caching?
|
||||
!cache_store.nil?
|
||||
end
|
||||
end
|
||||
|
||||
module Backend
|
||||
# TODO Should the cache be cleared if new translations are stored?
|
||||
module Cache
|
||||
def translate(locale, key, options = EMPTY_HASH)
|
||||
I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def fetch(cache_key, &block)
|
||||
result = _fetch(cache_key, &block)
|
||||
throw(:exception, result) if result.is_a?(MissingTranslation)
|
||||
result = result.dup if result.frozen? rescue result
|
||||
result
|
||||
end
|
||||
|
||||
def _fetch(cache_key, &block)
|
||||
result = I18n.cache_store.read(cache_key)
|
||||
return result unless result.nil?
|
||||
result = catch(:exception, &block)
|
||||
I18n.cache_store.write(cache_key, result) unless result.is_a?(Proc)
|
||||
result
|
||||
end
|
||||
|
||||
def cache_key(locale, key, options)
|
||||
# This assumes that only simple, native Ruby values are passed to I18n.translate.
|
||||
"i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{digest_item(options)}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def digest_item(key)
|
||||
I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.to_s.hash
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'openssl'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# Overwrites the Base load_file method to cache loaded file contents.
|
||||
module CacheFile
|
||||
# Optionally provide path_roots array to normalize filename paths,
|
||||
# to make the cached i18n data portable across environments.
|
||||
attr_accessor :path_roots
|
||||
|
||||
protected
|
||||
|
||||
# Track loaded translation files in the `i18n.load_file` scope,
|
||||
# and skip loading the file if its contents are still up-to-date.
|
||||
def load_file(filename)
|
||||
initialized = !respond_to?(:initialized?) || initialized?
|
||||
key = I18n::Backend::Flatten.escape_default_separator(normalized_path(filename))
|
||||
old_mtime, old_digest = initialized && lookup(:i18n, key, :load_file)
|
||||
return if (mtime = File.mtime(filename).to_i) == old_mtime ||
|
||||
(digest = OpenSSL::Digest::SHA256.file(filename).hexdigest) == old_digest
|
||||
super
|
||||
store_translations(:i18n, load_file: { key => [mtime, digest] })
|
||||
end
|
||||
|
||||
# Translate absolute filename to relative path for i18n key.
|
||||
def normalized_path(file)
|
||||
return file unless path_roots
|
||||
path = path_roots.find(&file.method(:start_with?)) ||
|
||||
raise(InvalidLocaleData.new(file, 'outside expected path roots'))
|
||||
file.sub(path, path_roots.index(path).to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# The Cascade module adds the ability to do cascading lookups to backends that
|
||||
# are compatible to the Simple backend.
|
||||
#
|
||||
# By cascading lookups we mean that for any key that can not be found the
|
||||
# Cascade module strips one segment off the scope part of the key and then
|
||||
# tries to look up the key in that scope.
|
||||
#
|
||||
# E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then
|
||||
# the segment :bar will be stripped off the scope part :"foo.bar" and the new
|
||||
# scope :foo will be used to look up the key :baz. If that does not succeed
|
||||
# then the remaining scope segment :foo will be omitted, too, and again the
|
||||
# key :baz will be looked up (now with no scope).
|
||||
#
|
||||
# To enable a cascading lookup one passes the :cascade option:
|
||||
#
|
||||
# I18n.t(:'foo.bar.baz', :cascade => true)
|
||||
#
|
||||
# This will return the first translation found for :"foo.bar.baz", :"foo.baz"
|
||||
# or :baz in this order.
|
||||
#
|
||||
# The cascading lookup takes precedence over resolving any given defaults.
|
||||
# I.e. defaults will kick in after the cascading lookups haven't succeeded.
|
||||
#
|
||||
# This behavior is useful for libraries like ActiveRecord validations where
|
||||
# the library wants to give users a bunch of more or less fine-grained options
|
||||
# of scopes for a particular key.
|
||||
#
|
||||
# Thanks to Clemens Kofler for the initial idea and implementation! See
|
||||
# http://github.com/clemens/i18n-cascading-backend
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
module Cascade
|
||||
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
||||
return super unless cascade = options[:cascade]
|
||||
|
||||
cascade = { :step => 1 } unless cascade.is_a?(Hash)
|
||||
step = cascade[:step] || 1
|
||||
offset = cascade[:offset] || 1
|
||||
separator = options[:separator] || I18n.default_separator
|
||||
skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true
|
||||
|
||||
scope = I18n.normalize_keys(nil, key, scope, separator)
|
||||
key = (scope.slice!(-offset, offset) || []).join(separator)
|
||||
|
||||
begin
|
||||
result = super
|
||||
return result unless result.nil?
|
||||
scope = scope.dup
|
||||
end while (!scope.empty? || !skip_root) && scope.slice!(-step, step)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# Backend that chains multiple other backends and checks each of them when
|
||||
# a translation needs to be looked up. This is useful when you want to use
|
||||
# standard translations with a Simple backend but store custom application
|
||||
# translations in a database or other backends.
|
||||
#
|
||||
# To use the Chain backend instantiate it and set it to the I18n module.
|
||||
# You can add chained backends through the initializer or backends
|
||||
# accessor:
|
||||
#
|
||||
# # preserves the existing Simple backend set to I18n.backend
|
||||
# I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
|
||||
#
|
||||
# The implementation assumes that all backends added to the Chain implement
|
||||
# a lookup method with the same API as Simple backend does.
|
||||
#
|
||||
# Fallback translations using the :default option are only used by the last backend of a chain.
|
||||
class Chain
|
||||
module Implementation
|
||||
include Base
|
||||
|
||||
attr_accessor :backends
|
||||
|
||||
def initialize(*backends)
|
||||
self.backends = backends
|
||||
end
|
||||
|
||||
def initialized?
|
||||
backends.all? do |backend|
|
||||
backend.instance_eval do
|
||||
return false unless initialized?
|
||||
end
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def reload!
|
||||
backends.each { |backend| backend.reload! }
|
||||
end
|
||||
|
||||
def eager_load!
|
||||
backends.each { |backend| backend.eager_load! }
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = EMPTY_HASH)
|
||||
backends.first.store_translations(locale, data, options)
|
||||
end
|
||||
|
||||
def available_locales
|
||||
backends.map { |backend| backend.available_locales }.flatten.uniq
|
||||
end
|
||||
|
||||
def translate(locale, key, default_options = EMPTY_HASH)
|
||||
namespace = nil
|
||||
options = Utils.except(default_options, :default)
|
||||
|
||||
backends.each do |backend|
|
||||
catch(:exception) do
|
||||
options = default_options if backend == backends.last
|
||||
translation = backend.translate(locale, key, options)
|
||||
if namespace_lookup?(translation, options)
|
||||
namespace = _deep_merge(translation, namespace || {})
|
||||
elsif !translation.nil? || (options.key?(:default) && options[:default].nil?)
|
||||
return translation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return namespace if namespace
|
||||
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
||||
end
|
||||
|
||||
def exists?(locale, key, options = EMPTY_HASH)
|
||||
backends.any? do |backend|
|
||||
backend.exists?(locale, key, options)
|
||||
end
|
||||
end
|
||||
|
||||
def localize(locale, object, format = :default, options = EMPTY_HASH)
|
||||
backends.each do |backend|
|
||||
catch(:exception) do
|
||||
result = backend.localize(locale, object, format, options) and return result
|
||||
end
|
||||
end
|
||||
throw(:exception, I18n::MissingTranslation.new(locale, format, options))
|
||||
end
|
||||
|
||||
protected
|
||||
def init_translations
|
||||
backends.each do |backend|
|
||||
backend.send(:init_translations)
|
||||
end
|
||||
end
|
||||
|
||||
def translations
|
||||
backends.reverse.each_with_object({}) do |backend, memo|
|
||||
partial_translations = backend.instance_eval do
|
||||
init_translations unless initialized?
|
||||
translations
|
||||
end
|
||||
Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a }
|
||||
end
|
||||
end
|
||||
|
||||
def namespace_lookup?(result, options)
|
||||
result.is_a?(Hash) && !options.has_key?(:count)
|
||||
end
|
||||
|
||||
private
|
||||
# This is approximately what gets used in ActiveSupport.
|
||||
# However since we are not guaranteed to run in an ActiveSupport context
|
||||
# it is wise to have our own copy. We underscore it
|
||||
# to not pollute the namespace of the including class.
|
||||
def _deep_merge(hash, other_hash)
|
||||
copy = hash.dup
|
||||
other_hash.each_pair do |k,v|
|
||||
value_from_other = hash[k]
|
||||
copy[k] = value_from_other.is_a?(Hash) && v.is_a?(Hash) ? _deep_merge(value_from_other, v) : v
|
||||
end
|
||||
copy
|
||||
end
|
||||
end
|
||||
|
||||
include Implementation
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,119 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# I18n locale fallbacks are useful when you want your application to use
|
||||
# translations from other locales when translations for the current locale are
|
||||
# missing. E.g. you might want to use :en translations when translations in
|
||||
# your applications main locale :de are missing.
|
||||
#
|
||||
# To enable locale fallbacks you can simply include the Fallbacks module to
|
||||
# the Simple backend - or whatever other backend you are using:
|
||||
#
|
||||
# I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
|
||||
module I18n
|
||||
@@fallbacks = nil
|
||||
|
||||
class << self
|
||||
# Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
|
||||
def fallbacks
|
||||
@@fallbacks ||= I18n::Locale::Fallbacks.new
|
||||
Thread.current[:i18n_fallbacks] || @@fallbacks
|
||||
end
|
||||
|
||||
# Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
|
||||
def fallbacks=(fallbacks)
|
||||
@@fallbacks = fallbacks.is_a?(Array) ? I18n::Locale::Fallbacks.new(fallbacks) : fallbacks
|
||||
Thread.current[:i18n_fallbacks] = @@fallbacks
|
||||
end
|
||||
end
|
||||
|
||||
module Backend
|
||||
module Fallbacks
|
||||
# Overwrites the Base backend translate method so that it will try each
|
||||
# locale given by I18n.fallbacks for the given locale. E.g. for the
|
||||
# locale :"de-DE" it might try the locales :"de-DE", :de and :en
|
||||
# (depends on the fallbacks implementation) until it finds a result with
|
||||
# the given options. If it does not find any result for any of the
|
||||
# locales it will then throw MissingTranslation as usual.
|
||||
#
|
||||
# The default option takes precedence over fallback locales only when
|
||||
# it's a Symbol. When the default contains a String, Proc or Hash
|
||||
# it is evaluated last after all the fallback locales have been tried.
|
||||
def translate(locale, key, options = EMPTY_HASH)
|
||||
return super unless options.fetch(:fallback, true)
|
||||
return super if options[:fallback_in_progress]
|
||||
default = extract_non_symbol_default!(options) if options[:default]
|
||||
|
||||
fallback_options = options.merge(:fallback_in_progress => true, fallback_original_locale: locale)
|
||||
I18n.fallbacks[locale].each do |fallback|
|
||||
begin
|
||||
catch(:exception) do
|
||||
result = super(fallback, key, fallback_options)
|
||||
unless result.nil?
|
||||
on_fallback(locale, fallback, key, options) if locale.to_s != fallback.to_s
|
||||
return result
|
||||
end
|
||||
end
|
||||
rescue I18n::InvalidLocale
|
||||
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
||||
end
|
||||
end
|
||||
|
||||
return if options.key?(:default) && options[:default].nil?
|
||||
|
||||
return super(locale, nil, options.merge(:default => default)) if default
|
||||
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
||||
end
|
||||
|
||||
def resolve_entry(locale, object, subject, options = EMPTY_HASH)
|
||||
return subject if options[:resolve] == false
|
||||
result = catch(:exception) do
|
||||
options.delete(:fallback_in_progress) if options.key?(:fallback_in_progress)
|
||||
|
||||
case subject
|
||||
when Symbol
|
||||
I18n.translate(subject, **options.merge(
|
||||
:locale => options[:fallback_original_locale],
|
||||
:throw => true,
|
||||
:skip_interpolation => true
|
||||
))
|
||||
when Proc
|
||||
date_or_time = options.delete(:object) || object
|
||||
resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
|
||||
else
|
||||
subject
|
||||
end
|
||||
end
|
||||
result unless result.is_a?(MissingTranslation)
|
||||
end
|
||||
|
||||
def extract_non_symbol_default!(options)
|
||||
defaults = [options[:default]].flatten
|
||||
first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
|
||||
if first_non_symbol_default
|
||||
options[:default] = defaults[0, defaults.index(first_non_symbol_default)]
|
||||
end
|
||||
return first_non_symbol_default
|
||||
end
|
||||
|
||||
def exists?(locale, key, options = EMPTY_HASH)
|
||||
return super unless options.fetch(:fallback, true)
|
||||
I18n.fallbacks[locale].each do |fallback|
|
||||
begin
|
||||
return true if super(fallback, key, options)
|
||||
rescue I18n::InvalidLocale
|
||||
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Overwrite on_fallback to add specified logic when the fallback succeeds.
|
||||
def on_fallback(_original_locale, _fallback_locale, _key, _options)
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# This module contains several helpers to assist flattening translations.
|
||||
# You may want to flatten translations for:
|
||||
#
|
||||
# 1) speed up lookups, as in the Memoize backend;
|
||||
# 2) In case you want to store translations in a data store, as in ActiveRecord backend;
|
||||
#
|
||||
# You can check both backends above for some examples.
|
||||
# This module also keeps all links in a hash so they can be properly resolved when flattened.
|
||||
module Flatten
|
||||
SEPARATOR_ESCAPE_CHAR = "\001"
|
||||
FLATTEN_SEPARATOR = "."
|
||||
|
||||
# normalize_keys the flatten way. This method is significantly faster
|
||||
# and creates way less objects than the one at I18n.normalize_keys.
|
||||
# It also handles escaping the translation keys.
|
||||
def self.normalize_flat_keys(locale, key, scope, separator)
|
||||
keys = [scope, key]
|
||||
keys.flatten!
|
||||
keys.compact!
|
||||
|
||||
separator ||= I18n.default_separator
|
||||
|
||||
if separator != FLATTEN_SEPARATOR
|
||||
from_str = "#{FLATTEN_SEPARATOR}#{separator}"
|
||||
to_str = "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}"
|
||||
|
||||
keys.map! { |k| k.to_s.tr from_str, to_str }
|
||||
end
|
||||
|
||||
keys.join(".")
|
||||
end
|
||||
|
||||
# Receives a string and escape the default separator.
|
||||
def self.escape_default_separator(key) #:nodoc:
|
||||
key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
|
||||
end
|
||||
|
||||
# Shortcut to I18n::Backend::Flatten.normalize_flat_keys
|
||||
# and then resolve_links.
|
||||
def normalize_flat_keys(locale, key, scope, separator)
|
||||
key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator)
|
||||
resolve_link(locale, key)
|
||||
end
|
||||
|
||||
# Store flattened links.
|
||||
def links
|
||||
@links ||= I18n.new_double_nested_cache
|
||||
end
|
||||
|
||||
# Flatten keys for nested Hashes by chaining up keys:
|
||||
#
|
||||
# >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind
|
||||
# => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }
|
||||
#
|
||||
def flatten_keys(hash, escape, prev_key=nil, &block)
|
||||
hash.each_pair do |key, value|
|
||||
key = escape_default_separator(key) if escape
|
||||
curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
|
||||
yield curr_key, value
|
||||
flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
# Receives a hash of translations (where the key is a locale and
|
||||
# the value is another hash) and return a hash with all
|
||||
# translations flattened.
|
||||
#
|
||||
# Nested hashes are included in the flattened hash just if subtree
|
||||
# is true and Symbols are automatically stored as links.
|
||||
def flatten_translations(locale, data, escape, subtree)
|
||||
hash = {}
|
||||
flatten_keys(data, escape) do |key, value|
|
||||
if value.is_a?(Hash)
|
||||
hash[key] = value if subtree
|
||||
else
|
||||
store_link(locale, key, value) if value.is_a?(Symbol)
|
||||
hash[key] = value
|
||||
end
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def store_link(locale, key, link)
|
||||
links[locale.to_sym][key.to_s] = link.to_s
|
||||
end
|
||||
|
||||
def resolve_link(locale, key)
|
||||
key, locale = key.to_s, locale.to_sym
|
||||
links = self.links[locale]
|
||||
|
||||
if links.key?(key)
|
||||
links[key]
|
||||
elsif link = find_link(locale, key)
|
||||
store_link(locale, key, key.gsub(*link))
|
||||
else
|
||||
key
|
||||
end
|
||||
end
|
||||
|
||||
def find_link(locale, key) #:nodoc:
|
||||
links[locale].each_pair do |from, to|
|
||||
return [from, to] if key[0, from.length] == from
|
||||
end && nil
|
||||
end
|
||||
|
||||
def escape_default_separator(key) #:nodoc:
|
||||
I18n::Backend::Flatten.escape_default_separator(key)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'i18n/gettext'
|
||||
require 'i18n/gettext/po_parser'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# Experimental support for using Gettext po files to store translations.
|
||||
#
|
||||
# To use this you can simply include the module to the Simple backend - or
|
||||
# whatever other backend you are using.
|
||||
#
|
||||
# I18n::Backend::Simple.include(I18n::Backend::Gettext)
|
||||
#
|
||||
# Now you should be able to include your Gettext translation (*.po) files to
|
||||
# the +I18n.load_path+ so they're loaded to the backend and you can use them as
|
||||
# usual:
|
||||
#
|
||||
# I18n.load_path += Dir["path/to/locales/*.po"]
|
||||
#
|
||||
# Following the Gettext convention this implementation expects that your
|
||||
# translation files are named by their locales. E.g. the file en.po would
|
||||
# contain the translations for the English locale.
|
||||
#
|
||||
# To translate text <b>you must use</b> one of the translate methods provided by
|
||||
# I18n::Gettext::Helpers.
|
||||
#
|
||||
# include I18n::Gettext::Helpers
|
||||
# puts _("some string")
|
||||
#
|
||||
# Without it strings containing periods (".") will not be translated.
|
||||
|
||||
module Gettext
|
||||
class PoData < Hash
|
||||
def set_comment(msgid_or_sym, comment)
|
||||
# ignore
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def load_po(filename)
|
||||
locale = ::File.basename(filename, '.po').to_sym
|
||||
data = normalize(locale, parse(filename))
|
||||
[{ locale => data }, false]
|
||||
end
|
||||
|
||||
def parse(filename)
|
||||
GetText::PoParser.new.parse(::File.read(filename), PoData.new)
|
||||
end
|
||||
|
||||
def normalize(locale, data)
|
||||
data.inject({}) do |result, (key, value)|
|
||||
unless key.nil? || key.empty?
|
||||
key = key.gsub(I18n::Gettext::CONTEXT_SEPARATOR, '|')
|
||||
key, value = normalize_pluralization(locale, key, value) if key.index("\000")
|
||||
|
||||
parts = key.split('|').reverse
|
||||
normalized = parts.inject({}) do |_normalized, part|
|
||||
{ part => _normalized.empty? ? value : _normalized }
|
||||
end
|
||||
|
||||
Utils.deep_merge!(result, normalized)
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_pluralization(locale, key, value)
|
||||
# FIXME po_parser includes \000 chars that can not be turned into Symbols
|
||||
key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first
|
||||
|
||||
keys = I18n::Gettext.plural_keys(locale)
|
||||
values = value.split("\000")
|
||||
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
|
||||
|
||||
result = {}
|
||||
values.each_with_index { |_value, ix| result[keys[ix]] = _value }
|
||||
[key, result]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,121 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# The InterpolationCompiler module contains optimizations that can tremendously
|
||||
# speed up the interpolation process on the Simple backend.
|
||||
#
|
||||
# It works by defining a pre-compiled method on stored translation Strings that
|
||||
# already bring all the knowledge about contained interpolation variables etc.
|
||||
# so that the actual recurring interpolation will be very fast.
|
||||
#
|
||||
# To enable pre-compiled interpolations you can simply include the
|
||||
# InterpolationCompiler module to the Simple backend:
|
||||
#
|
||||
# I18n::Backend::Simple.include(I18n::Backend::InterpolationCompiler)
|
||||
#
|
||||
# Note that InterpolationCompiler does not yield meaningful results and consequently
|
||||
# should not be used with Ruby 1.9 (YARV) but improves performance everywhere else
|
||||
# (jRuby, Rubinius).
|
||||
module I18n
|
||||
module Backend
|
||||
module InterpolationCompiler
|
||||
module Compiler
|
||||
extend self
|
||||
|
||||
TOKENIZER = /(%%?\{[^}]+\})/
|
||||
|
||||
def compile_if_an_interpolation(string)
|
||||
if interpolated_str?(string)
|
||||
string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
|
||||
def i18n_interpolate(v = {})
|
||||
"#{compiled_interpolation_body(string)}"
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
|
||||
string
|
||||
end
|
||||
|
||||
def interpolated_str?(str)
|
||||
str.kind_of?(::String) && str =~ TOKENIZER
|
||||
end
|
||||
|
||||
protected
|
||||
# tokenize("foo %{bar} baz %%{buz}") # => ["foo ", "%{bar}", " baz ", "%%{buz}"]
|
||||
def tokenize(str)
|
||||
str.split(TOKENIZER)
|
||||
end
|
||||
|
||||
def compiled_interpolation_body(str)
|
||||
tokenize(str).map do |token|
|
||||
token.match(TOKENIZER) ? handle_interpolation_token(token) : escape_plain_str(token)
|
||||
end.join
|
||||
end
|
||||
|
||||
def handle_interpolation_token(token)
|
||||
token.start_with?('%%') ? token[1..] : compile_interpolation_token(token[2..-2])
|
||||
end
|
||||
|
||||
def compile_interpolation_token(key)
|
||||
"\#{#{interpolate_or_raise_missing(key)}}"
|
||||
end
|
||||
|
||||
def interpolate_or_raise_missing(key)
|
||||
escaped_key = escape_key_sym(key)
|
||||
RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key)
|
||||
end
|
||||
|
||||
def interpolate_key(key)
|
||||
[direct_key(key), nil_key(key), missing_key(key)].join('||')
|
||||
end
|
||||
|
||||
def direct_key(key)
|
||||
"((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
|
||||
end
|
||||
|
||||
def nil_key(key)
|
||||
"(v.has_key?(#{key}) && '')"
|
||||
end
|
||||
|
||||
def missing_key(key)
|
||||
"I18n.config.missing_interpolation_argument_handler.call(#{key}, v, self)"
|
||||
end
|
||||
|
||||
def reserved_key(key)
|
||||
"raise(ReservedInterpolationKey.new(#{key}, self))"
|
||||
end
|
||||
|
||||
def escape_plain_str(str)
|
||||
str.gsub(/"|\\|#/) {|x| "\\#{x}"}
|
||||
end
|
||||
|
||||
def escape_key_sym(key)
|
||||
# rely on Ruby to do all the hard work :)
|
||||
key.to_sym.inspect
|
||||
end
|
||||
end
|
||||
|
||||
def interpolate(locale, string, values)
|
||||
if string.respond_to?(:i18n_interpolate)
|
||||
string.i18n_interpolate(values)
|
||||
elsif values
|
||||
super
|
||||
else
|
||||
string
|
||||
end
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = EMPTY_HASH)
|
||||
compile_all_strings_in(data)
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
def compile_all_strings_in(data)
|
||||
data.each_value do |value|
|
||||
Compiler.compile_if_an_interpolation(value)
|
||||
compile_all_strings_in(value) if value.kind_of?(Hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,204 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'i18n/backend/base'
|
||||
|
||||
module I18n
|
||||
|
||||
begin
|
||||
require 'oj'
|
||||
class JSON
|
||||
class << self
|
||||
def encode(value)
|
||||
Oj::Rails.encode(value)
|
||||
end
|
||||
def decode(value)
|
||||
Oj.load(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue LoadError
|
||||
require 'active_support/json'
|
||||
JSON = ActiveSupport::JSON
|
||||
end
|
||||
|
||||
module Backend
|
||||
# This is a basic backend for key value stores. It receives on
|
||||
# initialization the store, which should respond to three methods:
|
||||
#
|
||||
# * store#[](key) - Used to get a value
|
||||
# * store#[]=(key, value) - Used to set a value
|
||||
# * store#keys - Used to get all keys
|
||||
#
|
||||
# Since these stores only supports string, all values are converted
|
||||
# to JSON before being stored, allowing it to also store booleans,
|
||||
# hashes and arrays. However, this store does not support Procs.
|
||||
#
|
||||
# As the ActiveRecord backend, Symbols are just supported when loading
|
||||
# translations from the filesystem or through explicit store translations.
|
||||
#
|
||||
# Also, avoid calling I18n.available_locales since it's a somehow
|
||||
# expensive operation in most stores.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# To setup I18n to use TokyoCabinet in memory is quite straightforward:
|
||||
#
|
||||
# require 'rufus/tokyo/cabinet' # gem install rufus-tokyo
|
||||
# I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
|
||||
#
|
||||
# == Performance
|
||||
#
|
||||
# You may make this backend even faster by including the Memoize module.
|
||||
# However, notice that you should properly clear the cache if you change
|
||||
# values directly in the key-store.
|
||||
#
|
||||
# == Subtrees
|
||||
#
|
||||
# In most backends, you are allowed to retrieve part of a translation tree:
|
||||
#
|
||||
# I18n.backend.store_translations :en, :foo => { :bar => :baz }
|
||||
# I18n.t "foo" #=> { :bar => :baz }
|
||||
#
|
||||
# This backend supports this feature by default, but it slows down the storage
|
||||
# of new data considerably and makes hard to delete entries. That said, you are
|
||||
# allowed to disable the storage of subtrees on initialization:
|
||||
#
|
||||
# I18n::Backend::KeyValue.new(@store, false)
|
||||
#
|
||||
# This is useful if you are using a KeyValue backend chained to a Simple backend.
|
||||
class KeyValue
|
||||
module Implementation
|
||||
attr_accessor :store
|
||||
|
||||
include Base, Flatten
|
||||
|
||||
def initialize(store, subtrees=true)
|
||||
@store, @subtrees = store, subtrees
|
||||
end
|
||||
|
||||
def initialized?
|
||||
!@store.nil?
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = EMPTY_HASH)
|
||||
escape = options.fetch(:escape, true)
|
||||
flatten_translations(locale, data, escape, @subtrees).each do |key, value|
|
||||
key = "#{locale}.#{key}"
|
||||
|
||||
case value
|
||||
when Hash
|
||||
if @subtrees && (old_value = @store[key])
|
||||
old_value = JSON.decode(old_value)
|
||||
value = Utils.deep_merge!(Utils.deep_symbolize_keys(old_value), value) if old_value.is_a?(Hash)
|
||||
end
|
||||
when Proc
|
||||
raise "Key-value stores cannot handle procs"
|
||||
end
|
||||
|
||||
@store[key] = JSON.encode(value) unless value.is_a?(Symbol)
|
||||
end
|
||||
end
|
||||
|
||||
def available_locales
|
||||
locales = @store.keys.map { |k| k =~ /\./; $` }
|
||||
locales.uniq!
|
||||
locales.compact!
|
||||
locales.map! { |k| k.to_sym }
|
||||
locales
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Queries the translations from the key-value store and converts
|
||||
# them into a hash such as the one returned from loading the
|
||||
# haml files
|
||||
def translations
|
||||
@translations = Utils.deep_symbolize_keys(@store.keys.clone.map do |main_key|
|
||||
main_value = JSON.decode(@store[main_key])
|
||||
main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
|
||||
{key.to_sym => value}
|
||||
end
|
||||
end.inject{|hash, elem| Utils.deep_merge!(hash, elem)})
|
||||
end
|
||||
|
||||
def init_translations
|
||||
# NO OP
|
||||
# This call made also inside Simple Backend and accessed by
|
||||
# other plugins like I18n-js and babilu and
|
||||
# to use it along with the Chain backend we need to
|
||||
# provide a uniform API even for protected methods :S
|
||||
end
|
||||
|
||||
def subtrees?
|
||||
@subtrees
|
||||
end
|
||||
|
||||
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
||||
key = normalize_flat_keys(locale, key, scope, options[:separator])
|
||||
value = @store["#{locale}.#{key}"]
|
||||
value = JSON.decode(value) if value
|
||||
|
||||
if value.is_a?(Hash)
|
||||
Utils.deep_symbolize_keys(value)
|
||||
elsif !value.nil?
|
||||
value
|
||||
elsif !@subtrees
|
||||
SubtreeProxy.new("#{locale}.#{key}", @store)
|
||||
end
|
||||
end
|
||||
|
||||
def pluralize(locale, entry, count)
|
||||
if subtrees?
|
||||
super
|
||||
else
|
||||
return entry unless entry.is_a?(Hash)
|
||||
key = pluralization_key(entry, count)
|
||||
entry[key]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SubtreeProxy
|
||||
def initialize(master_key, store)
|
||||
@master_key = master_key
|
||||
@store = store
|
||||
@subtree = nil
|
||||
end
|
||||
|
||||
def has_key?(key)
|
||||
@subtree && @subtree.has_key?(key) || self[key]
|
||||
end
|
||||
|
||||
def [](key)
|
||||
unless @subtree && value = @subtree[key]
|
||||
value = @store["#{@master_key}.#{key}"]
|
||||
if value
|
||||
value = JSON.decode(value)
|
||||
(@subtree ||= {})[key] = value
|
||||
end
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def is_a?(klass)
|
||||
Hash == klass || super
|
||||
end
|
||||
alias :kind_of? :is_a?
|
||||
|
||||
def instance_of?(klass)
|
||||
Hash == klass || super
|
||||
end
|
||||
|
||||
def nil?
|
||||
@subtree.nil?
|
||||
end
|
||||
|
||||
def inspect
|
||||
@subtree.inspect
|
||||
end
|
||||
end
|
||||
|
||||
include Implementation
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,184 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# Backend that lazy loads translations based on the current locale. This
|
||||
# implementation avoids loading all translations up front. Instead, it only
|
||||
# loads the translations that belong to the current locale. This offers a
|
||||
# performance incentive in local development and test environments for
|
||||
# applications with many translations for many different locales. It's
|
||||
# particularly useful when the application only refers to a single locales'
|
||||
# translations at a time (ex. A Rails workload). The implementation
|
||||
# identifies which translation files from the load path belong to the
|
||||
# current locale by pattern matching against their path name.
|
||||
#
|
||||
# Specifically, a translation file is considered to belong to a locale if:
|
||||
# a) the filename is in the I18n load path
|
||||
# b) the filename ends in a supported extension (ie. .yml, .json, .po, .rb)
|
||||
# c) the filename starts with the locale identifier
|
||||
# d) the locale identifier and optional proceeding text is separated by an underscore, ie. "_".
|
||||
#
|
||||
# Examples:
|
||||
# Valid files that will be selected by this backend:
|
||||
#
|
||||
# "files/locales/en_translation.yml" (Selected for locale "en")
|
||||
# "files/locales/fr.po" (Selected for locale "fr")
|
||||
#
|
||||
# Invalid files that won't be selected by this backend:
|
||||
#
|
||||
# "files/locales/translation-file"
|
||||
# "files/locales/en-translation.unsupported"
|
||||
# "files/locales/french/translation.yml"
|
||||
# "files/locales/fr/translation.yml"
|
||||
#
|
||||
# The implementation uses this assumption to defer the loading of
|
||||
# translation files until the current locale actually requires them.
|
||||
#
|
||||
# The backend has two working modes: lazy_load and eager_load.
|
||||
#
|
||||
# Note: This backend should only be enabled in test environments!
|
||||
# When the mode is set to false, the backend behaves exactly like the
|
||||
# Simple backend, with an additional check that the paths being loaded
|
||||
# abide by the format. If paths can't be matched to the format, an error is raised.
|
||||
#
|
||||
# You can configure lazy loaded backends through the initializer or backends
|
||||
# accessor:
|
||||
#
|
||||
# # In test environments
|
||||
#
|
||||
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: true)
|
||||
#
|
||||
# # In other environments, such as production and CI
|
||||
#
|
||||
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: false) # default
|
||||
#
|
||||
class LocaleExtractor
|
||||
class << self
|
||||
def locale_from_path(path)
|
||||
name = File.basename(path, ".*")
|
||||
locale = name.split("_").first
|
||||
locale.to_sym unless locale.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class LazyLoadable < Simple
|
||||
def initialize(lazy_load: false)
|
||||
@lazy_load = lazy_load
|
||||
end
|
||||
|
||||
# Returns whether the current locale is initialized.
|
||||
def initialized?
|
||||
if lazy_load?
|
||||
initialized_locales[I18n.locale]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Clean up translations and uninitialize all locales.
|
||||
def reload!
|
||||
if lazy_load?
|
||||
@initialized_locales = nil
|
||||
@translations = nil
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Eager loading is not supported in the lazy context.
|
||||
def eager_load!
|
||||
if lazy_load?
|
||||
raise UnsupportedMethod.new(__method__, self.class, "Cannot eager load translations because backend was configured with lazy_load: true.")
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Parse the load path and extract all locales.
|
||||
def available_locales
|
||||
if lazy_load?
|
||||
I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }.uniq
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
||||
if lazy_load?
|
||||
I18n.with_locale(locale) do
|
||||
super
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
|
||||
# Load translations from files that belong to the current locale.
|
||||
def init_translations
|
||||
file_errors = if lazy_load?
|
||||
initialized_locales[I18n.locale] = true
|
||||
load_translations_and_collect_file_errors(filenames_for_current_locale)
|
||||
else
|
||||
@initialized = true
|
||||
load_translations_and_collect_file_errors(I18n.load_path)
|
||||
end
|
||||
|
||||
raise InvalidFilenames.new(file_errors) unless file_errors.empty?
|
||||
end
|
||||
|
||||
def initialized_locales
|
||||
@initialized_locales ||= Hash.new(false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lazy_load?
|
||||
@lazy_load
|
||||
end
|
||||
|
||||
class FilenameIncorrect < StandardError
|
||||
def initialize(file, expected_locale, unexpected_locales)
|
||||
super "#{file} can only load translations for \"#{expected_locale}\". Found translations for: #{unexpected_locales}."
|
||||
end
|
||||
end
|
||||
|
||||
# Loads each file supplied and asserts that the file only loads
|
||||
# translations as expected by the name. The method returns a list of
|
||||
# errors corresponding to offending files.
|
||||
def load_translations_and_collect_file_errors(files)
|
||||
errors = []
|
||||
|
||||
load_translations(files) do |file, loaded_translations|
|
||||
assert_file_named_correctly!(file, loaded_translations)
|
||||
rescue FilenameIncorrect => e
|
||||
errors << e
|
||||
end
|
||||
|
||||
errors
|
||||
end
|
||||
|
||||
# Select all files from I18n load path that belong to current locale.
|
||||
# These files must start with the locale identifier (ie. "en", "pt-BR"),
|
||||
# followed by an "_" demarcation to separate proceeding text.
|
||||
def filenames_for_current_locale
|
||||
I18n.load_path.flatten.select do |path|
|
||||
LocaleExtractor.locale_from_path(path) == I18n.locale
|
||||
end
|
||||
end
|
||||
|
||||
# Checks if a filename is named in correspondence to the translations it loaded.
|
||||
# The locale extracted from the path must be the single locale loaded in the translations.
|
||||
def assert_file_named_correctly!(file, translations)
|
||||
loaded_locales = translations.keys.map(&:to_sym)
|
||||
expected_locale = LocaleExtractor.locale_from_path(file)
|
||||
unexpected_locales = loaded_locales.reject { |locale| locale == expected_locale }
|
||||
|
||||
raise FilenameIncorrect.new(file, expected_locale, unexpected_locales) unless unexpected_locales.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Memoize module simply memoizes the values returned by lookup using
|
||||
# a flat hash and can tremendously speed up the lookup process in a backend.
|
||||
#
|
||||
# To enable it you can simply include the Memoize module to your backend:
|
||||
#
|
||||
# I18n::Backend::Simple.include(I18n::Backend::Memoize)
|
||||
#
|
||||
# Notice that it's the responsibility of the backend to define whenever the
|
||||
# cache should be cleaned.
|
||||
module I18n
|
||||
module Backend
|
||||
module Memoize
|
||||
def available_locales
|
||||
@memoized_locales ||= super
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = EMPTY_HASH)
|
||||
reset_memoizations!(locale)
|
||||
super
|
||||
end
|
||||
|
||||
def reload!
|
||||
reset_memoizations!
|
||||
super
|
||||
end
|
||||
|
||||
def eager_load!
|
||||
memoized_lookup
|
||||
available_locales
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def lookup(locale, key, scope = nil, options = EMPTY_HASH)
|
||||
flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
|
||||
key, scope, options[:separator]).to_sym
|
||||
flat_hash = memoized_lookup[locale.to_sym]
|
||||
flat_hash.key?(flat_key) ? flat_hash[flat_key] : (flat_hash[flat_key] = super)
|
||||
end
|
||||
|
||||
def memoized_lookup
|
||||
@memoized_lookup ||= I18n.new_double_nested_cache
|
||||
end
|
||||
|
||||
def reset_memoizations!(locale=nil)
|
||||
@memoized_locales = nil
|
||||
(locale ? memoized_lookup[locale.to_sym] : memoized_lookup).clear
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# I18n translation metadata is useful when you want to access information
|
||||
# about how a translation was looked up, pluralized or interpolated in
|
||||
# your application.
|
||||
#
|
||||
# msg = I18n.t(:message, :default => 'Hi!', :scope => :foo)
|
||||
# msg.translation_metadata
|
||||
# # => { :key => :message, :scope => :foo, :default => 'Hi!' }
|
||||
#
|
||||
# If a :count option was passed to #translate it will be set to the metadata.
|
||||
# Likewise, if any interpolation variables were passed they will also be set.
|
||||
#
|
||||
# To enable translation metadata you can simply include the Metadata module
|
||||
# into the Simple backend class - or whatever other backend you are using:
|
||||
#
|
||||
# I18n::Backend::Simple.include(I18n::Backend::Metadata)
|
||||
#
|
||||
module I18n
|
||||
module Backend
|
||||
module Metadata
|
||||
class << self
|
||||
def included(base)
|
||||
Object.class_eval do
|
||||
def translation_metadata
|
||||
unless self.frozen?
|
||||
@translation_metadata ||= {}
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def translation_metadata=(translation_metadata)
|
||||
@translation_metadata = translation_metadata unless self.frozen?
|
||||
end
|
||||
end unless Object.method_defined?(:translation_metadata)
|
||||
end
|
||||
end
|
||||
|
||||
def translate(locale, key, options = EMPTY_HASH)
|
||||
metadata = {
|
||||
:locale => locale,
|
||||
:key => key,
|
||||
:scope => options[:scope],
|
||||
:default => options[:default],
|
||||
:separator => options[:separator],
|
||||
:values => options.reject { |name, _value| RESERVED_KEYS.include?(name) }
|
||||
}
|
||||
with_metadata(metadata) { super }
|
||||
end
|
||||
|
||||
def interpolate(locale, entry, values = EMPTY_HASH)
|
||||
metadata = entry.translation_metadata.merge(:original => entry)
|
||||
with_metadata(metadata) { super }
|
||||
end
|
||||
|
||||
def pluralize(locale, entry, count)
|
||||
with_metadata(:count => count) { super }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def with_metadata(metadata, &block)
|
||||
result = yield
|
||||
result.translation_metadata = result.translation_metadata.merge(metadata) if result
|
||||
result
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# I18n Pluralization are useful when you want your application to
|
||||
# customize pluralization rules.
|
||||
#
|
||||
# To enable locale specific pluralizations you can simply include the
|
||||
# Pluralization module to the Simple backend - or whatever other backend you
|
||||
# are using.
|
||||
#
|
||||
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
|
||||
#
|
||||
# You also need to make sure to provide pluralization algorithms to the
|
||||
# backend, i.e. include them to your I18n.load_path accordingly.
|
||||
module I18n
|
||||
module Backend
|
||||
module Pluralization
|
||||
# Overwrites the Base backend translate method so that it will check the
|
||||
# translation meta data space (:i18n) for a locale specific pluralization
|
||||
# rule and use it to pluralize the given entry. I.e., the library expects
|
||||
# pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
|
||||
#
|
||||
# Pluralization rules are expected to respond to #call(count) and
|
||||
# return a pluralization key. Valid keys depend on the pluralization
|
||||
# rules for the locale, as defined in the CLDR.
|
||||
# As of v41, 6 locale-specific plural categories are defined:
|
||||
# :few, :many, :one, :other, :two, :zero
|
||||
#
|
||||
# n.b., The :one plural category does not imply the number 1.
|
||||
# Instead, :one is a category for any number that behaves like 1 in
|
||||
# that locale. For example, in some locales, :one is used for numbers
|
||||
# that end in "1" (like 1, 21, 151) but that don't end in
|
||||
# 11 (like 11, 111, 10311).
|
||||
# Similar notes apply to the :two, and :zero plural categories.
|
||||
#
|
||||
# If you want to have different strings for the categories of count == 0
|
||||
# (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car")
|
||||
# use the explicit `"0"` and `"1"` keys.
|
||||
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
|
||||
def pluralize(locale, entry, count)
|
||||
return entry unless entry.is_a?(Hash) && count
|
||||
|
||||
pluralizer = pluralizer(locale)
|
||||
if pluralizer.respond_to?(:call)
|
||||
# Deprecation: The use of the `zero` key in this way is incorrect.
|
||||
# Users that want a different string for the case of `count == 0` should use the explicit "0" key instead.
|
||||
# We keep this incorrect behaviour for now for backwards compatibility until we can remove it.
|
||||
# Ref: https://github.com/ruby-i18n/i18n/issues/629
|
||||
return entry[:zero] if count == 0 && entry.has_key?(:zero)
|
||||
|
||||
# "0" and "1" are special cases
|
||||
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
|
||||
if count == 0 || count == 1
|
||||
value = entry[symbolic_count(count)]
|
||||
return value if value
|
||||
end
|
||||
|
||||
# Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
|
||||
# > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
|
||||
# > 1. If "x" is numeric, the path falls back to the path with [@count=«the plural rules category for x for that locale»], within that the same locale.
|
||||
# > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
|
||||
# > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
|
||||
# Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
|
||||
plural_rule_category = pluralizer.call(count)
|
||||
|
||||
value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
|
||||
entry[plural_rule_category] || entry[:other]
|
||||
else
|
||||
raise InvalidPluralizationData.new(entry, count, plural_rule_category)
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def pluralizers
|
||||
@pluralizers ||= {}
|
||||
end
|
||||
|
||||
def pluralizer(locale)
|
||||
pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Normalizes categories of 0.0 and 1.0
|
||||
# and returns the symbolic version
|
||||
def symbolic_count(count)
|
||||
count = 0 if count == 0
|
||||
count = 1 if count == 1
|
||||
count.to_s.to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'i18n/backend/base'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# A simple backend that reads translations from YAML files and stores them in
|
||||
# an in-memory hash. Relies on the Base backend.
|
||||
#
|
||||
# The implementation is provided by a Implementation module allowing to easily
|
||||
# extend Simple backend's behavior by including modules. E.g.:
|
||||
#
|
||||
# module I18n::Backend::Pluralization
|
||||
# def pluralize(*args)
|
||||
# # extended pluralization logic
|
||||
# super
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
|
||||
class Simple
|
||||
module Implementation
|
||||
include Base
|
||||
|
||||
# Mutex to ensure that concurrent translations loading will be thread-safe
|
||||
MUTEX = Mutex.new
|
||||
|
||||
def initialized?
|
||||
@initialized ||= false
|
||||
end
|
||||
|
||||
# Stores translations for the given locale in memory.
|
||||
# This uses a deep merge for the translations hash, so existing
|
||||
# translations will be overwritten by new ones only at the deepest
|
||||
# level of the hash.
|
||||
def store_translations(locale, data, options = EMPTY_HASH)
|
||||
if I18n.enforce_available_locales &&
|
||||
I18n.available_locales_initialized? &&
|
||||
!I18n.locale_available?(locale)
|
||||
return data
|
||||
end
|
||||
locale = locale.to_sym
|
||||
translations[locale] ||= Concurrent::Hash.new
|
||||
data = Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false)
|
||||
Utils.deep_merge!(translations[locale], data)
|
||||
end
|
||||
|
||||
# Get available locales from the translations hash
|
||||
def available_locales
|
||||
init_translations unless initialized?
|
||||
translations.inject([]) do |locales, (locale, data)|
|
||||
locales << locale unless data.size <= 1 && (data.empty? || data.has_key?(:i18n))
|
||||
locales
|
||||
end
|
||||
end
|
||||
|
||||
# Clean up translations hash and set initialized to false on reload!
|
||||
def reload!
|
||||
@initialized = false
|
||||
@translations = nil
|
||||
super
|
||||
end
|
||||
|
||||
def eager_load!
|
||||
init_translations unless initialized?
|
||||
super
|
||||
end
|
||||
|
||||
def translations(do_init: false)
|
||||
# To avoid returning empty translations,
|
||||
# call `init_translations`
|
||||
init_translations if do_init && !initialized?
|
||||
|
||||
@translations ||= Concurrent::Hash.new do |h, k|
|
||||
MUTEX.synchronize do
|
||||
h[k] = Concurrent::Hash.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def init_translations
|
||||
load_translations
|
||||
@initialized = true
|
||||
end
|
||||
|
||||
# Looks up a translation from the translations hash. Returns nil if
|
||||
# either key is nil, or locale, scope or key do not exist as a key in the
|
||||
# nested translations hash. Splits keys or scopes containing dots
|
||||
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
|
||||
# <tt>%w(currency format)</tt>.
|
||||
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
||||
init_translations unless initialized?
|
||||
keys = I18n.normalize_keys(locale, key, scope, options[:separator])
|
||||
|
||||
keys.inject(translations) do |result, _key|
|
||||
return nil unless result.is_a?(Hash)
|
||||
unless result.has_key?(_key)
|
||||
_key = _key.to_s.to_sym
|
||||
return nil unless result.has_key?(_key)
|
||||
end
|
||||
result = result[_key]
|
||||
result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include Implementation
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
module Transliterator
|
||||
DEFAULT_REPLACEMENT_CHAR = "?"
|
||||
|
||||
# Given a locale and a UTF-8 string, return the locale's ASCII
|
||||
# approximation for the string.
|
||||
def transliterate(locale, string, replacement = nil)
|
||||
@transliterators ||= {}
|
||||
@transliterators[locale] ||= Transliterator.get I18n.t(:'i18n.transliterate.rule',
|
||||
:locale => locale, :resolve => false, :default => {})
|
||||
@transliterators[locale].transliterate(string, replacement)
|
||||
end
|
||||
|
||||
# Get a transliterator instance.
|
||||
def self.get(rule = nil)
|
||||
if !rule || rule.kind_of?(Hash)
|
||||
HashTransliterator.new(rule)
|
||||
elsif rule.kind_of? Proc
|
||||
ProcTransliterator.new(rule)
|
||||
else
|
||||
raise I18n::ArgumentError, "Transliteration rule must be a proc or a hash."
|
||||
end
|
||||
end
|
||||
|
||||
# A transliterator which accepts a Proc as its transliteration rule.
|
||||
class ProcTransliterator
|
||||
def initialize(rule)
|
||||
@rule = rule
|
||||
end
|
||||
|
||||
def transliterate(string, replacement = nil)
|
||||
@rule.call(string)
|
||||
end
|
||||
end
|
||||
|
||||
# A transliterator which accepts a Hash of characters as its translation
|
||||
# rule.
|
||||
class HashTransliterator
|
||||
DEFAULT_APPROXIMATIONS = {
|
||||
"À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE",
|
||||
"Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
|
||||
"Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
|
||||
"Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
|
||||
"Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "ẞ"=>"SS", "à"=>"a",
|
||||
"á"=>"a", "â"=>"a", "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c",
|
||||
"è"=>"e", "é"=>"e", "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i",
|
||||
"ï"=>"i", "ð"=>"d", "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o",
|
||||
"ö"=>"o", "ø"=>"o", "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y",
|
||||
"þ"=>"th", "ÿ"=>"y", "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A",
|
||||
"ą"=>"a", "Ć"=>"C", "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c",
|
||||
"Č"=>"C", "č"=>"c", "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E",
|
||||
"ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e",
|
||||
"Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G",
|
||||
"ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h",
|
||||
"Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I",
|
||||
"į"=>"i", "İ"=>"I", "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j",
|
||||
"Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l",
|
||||
"Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N",
|
||||
"ń"=>"n", "Ņ"=>"N", "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG",
|
||||
"ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o",
|
||||
"Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R",
|
||||
"ř"=>"r", "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s",
|
||||
"Š"=>"S", "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T",
|
||||
"ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u",
|
||||
"Ů"=>"U", "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W",
|
||||
"ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z",
|
||||
"ż"=>"z", "Ž"=>"Z", "ž"=>"z"
|
||||
}.freeze
|
||||
|
||||
def initialize(rule = nil)
|
||||
@rule = rule
|
||||
add_default_approximations
|
||||
add rule if rule
|
||||
end
|
||||
|
||||
def transliterate(string, replacement = nil)
|
||||
replacement ||= DEFAULT_REPLACEMENT_CHAR
|
||||
string.gsub(/[^\x00-\x7f]/u) do |char|
|
||||
approximations[char] || replacement
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def approximations
|
||||
@approximations ||= {}
|
||||
end
|
||||
|
||||
def add_default_approximations
|
||||
DEFAULT_APPROXIMATIONS.each do |key, value|
|
||||
approximations[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
# Add transliteration rules to the approximations hash.
|
||||
def add(hash)
|
||||
hash.each do |key, value|
|
||||
approximations[key.to_s] = value.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,165 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module I18n
|
||||
class Config
|
||||
# The only configuration value that is not global and scoped to thread is :locale.
|
||||
# It defaults to the default_locale.
|
||||
def locale
|
||||
defined?(@locale) && @locale != nil ? @locale : default_locale
|
||||
end
|
||||
|
||||
# Sets the current locale pseudo-globally, i.e. in the Thread.current hash.
|
||||
def locale=(locale)
|
||||
I18n.enforce_available_locales!(locale)
|
||||
@locale = locale && locale.to_sym
|
||||
end
|
||||
|
||||
# Returns the current backend. Defaults to +Backend::Simple+.
|
||||
def backend
|
||||
@@backend ||= Backend::Simple.new
|
||||
end
|
||||
|
||||
# Sets the current backend. Used to set a custom backend.
|
||||
def backend=(backend)
|
||||
@@backend = backend
|
||||
end
|
||||
|
||||
# Returns the current default locale. Defaults to :'en'
|
||||
def default_locale
|
||||
@@default_locale ||= :en
|
||||
end
|
||||
|
||||
# Sets the current default locale. Used to set a custom default locale.
|
||||
def default_locale=(locale)
|
||||
I18n.enforce_available_locales!(locale)
|
||||
@@default_locale = locale && locale.to_sym
|
||||
end
|
||||
|
||||
# Returns an array of locales for which translations are available.
|
||||
# Unless you explicitly set these through I18n.available_locales=
|
||||
# the call will be delegated to the backend.
|
||||
def available_locales
|
||||
@@available_locales ||= nil
|
||||
@@available_locales || backend.available_locales
|
||||
end
|
||||
|
||||
# Caches the available locales list as both strings and symbols in a Set, so
|
||||
# that we can have faster lookups to do the available locales enforce check.
|
||||
def available_locales_set #:nodoc:
|
||||
@@available_locales_set ||= available_locales.inject(Set.new) do |set, locale|
|
||||
set << locale.to_s << locale.to_sym
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the available locales.
|
||||
def available_locales=(locales)
|
||||
@@available_locales = Array(locales).map { |locale| locale.to_sym }
|
||||
@@available_locales = nil if @@available_locales.empty?
|
||||
@@available_locales_set = nil
|
||||
end
|
||||
|
||||
# Returns true if the available_locales have been initialized
|
||||
def available_locales_initialized?
|
||||
( !!defined?(@@available_locales) && !!@@available_locales )
|
||||
end
|
||||
|
||||
# Clears the available locales set so it can be recomputed again after I18n
|
||||
# gets reloaded.
|
||||
def clear_available_locales_set #:nodoc:
|
||||
@@available_locales_set = nil
|
||||
end
|
||||
|
||||
# Returns the current default scope separator. Defaults to '.'
|
||||
def default_separator
|
||||
@@default_separator ||= '.'
|
||||
end
|
||||
|
||||
# Sets the current default scope separator.
|
||||
def default_separator=(separator)
|
||||
@@default_separator = separator
|
||||
end
|
||||
|
||||
# Returns the current exception handler. Defaults to an instance of
|
||||
# I18n::ExceptionHandler.
|
||||
def exception_handler
|
||||
@@exception_handler ||= ExceptionHandler.new
|
||||
end
|
||||
|
||||
# Sets the exception handler.
|
||||
def exception_handler=(exception_handler)
|
||||
@@exception_handler = exception_handler
|
||||
end
|
||||
|
||||
# Returns the current handler for situations when interpolation argument
|
||||
# is missing. MissingInterpolationArgument will be raised by default.
|
||||
def missing_interpolation_argument_handler
|
||||
@@missing_interpolation_argument_handler ||= lambda do |missing_key, provided_hash, string|
|
||||
raise MissingInterpolationArgument.new(missing_key, provided_hash, string)
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the missing interpolation argument handler. It can be any
|
||||
# object that responds to #call. The arguments that will be passed to #call
|
||||
# are the same as for MissingInterpolationArgument initializer. Use +Proc.new+
|
||||
# if you don't care about arity.
|
||||
#
|
||||
# == Example:
|
||||
# You can suppress raising an exception and return string instead:
|
||||
#
|
||||
# I18n.config.missing_interpolation_argument_handler = Proc.new do |key|
|
||||
# "#{key} is missing"
|
||||
# end
|
||||
def missing_interpolation_argument_handler=(exception_handler)
|
||||
@@missing_interpolation_argument_handler = exception_handler
|
||||
end
|
||||
|
||||
# Allow clients to register paths providing translation data sources. The
|
||||
# backend defines acceptable sources.
|
||||
#
|
||||
# E.g. the provided SimpleBackend accepts a list of paths to translation
|
||||
# files which are either named *.rb and contain plain Ruby Hashes or are
|
||||
# named *.yml and contain YAML data. So for the SimpleBackend clients may
|
||||
# register translation files like this:
|
||||
# I18n.load_path << 'path/to/locale/en.yml'
|
||||
def load_path
|
||||
@@load_path ||= []
|
||||
end
|
||||
|
||||
# Sets the load path instance. Custom implementations are expected to
|
||||
# behave like a Ruby Array.
|
||||
def load_path=(load_path)
|
||||
@@load_path = load_path
|
||||
@@available_locales_set = nil
|
||||
backend.reload!
|
||||
end
|
||||
|
||||
# Whether or not to verify if locales are in the list of available locales.
|
||||
# Defaults to true.
|
||||
@@enforce_available_locales = true
|
||||
def enforce_available_locales
|
||||
@@enforce_available_locales
|
||||
end
|
||||
|
||||
def enforce_available_locales=(enforce_available_locales)
|
||||
@@enforce_available_locales = enforce_available_locales
|
||||
end
|
||||
|
||||
# Returns the current interpolation patterns. Defaults to
|
||||
# I18n::DEFAULT_INTERPOLATION_PATTERNS.
|
||||
def interpolation_patterns
|
||||
@@interpolation_patterns ||= I18n::DEFAULT_INTERPOLATION_PATTERNS.dup
|
||||
end
|
||||
|
||||
# Sets the current interpolation patterns. Used to set a interpolation
|
||||
# patterns.
|
||||
#
|
||||
# E.g. using {{}} as a placeholder like "{{hello}}, world!":
|
||||
#
|
||||
# I18n.config.interpolation_patterns << /\{\{(\w+)\}\}/
|
||||
def interpolation_patterns=(interpolation_patterns)
|
||||
@@interpolation_patterns = interpolation_patterns
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,157 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
|
||||
module I18n
|
||||
class ExceptionHandler
|
||||
def call(exception, _locale, _key, _options)
|
||||
if exception.is_a?(MissingTranslation)
|
||||
exception.message
|
||||
else
|
||||
raise exception
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ArgumentError < ::ArgumentError; end
|
||||
|
||||
class Disabled < ArgumentError
|
||||
def initialize(method)
|
||||
super(<<~MESSAGE)
|
||||
I18n.#{method} is currently disabled, likely because your application is still in its loading phase.
|
||||
|
||||
This method is meant to display text in the user locale, so calling it before the user locale has
|
||||
been set is likely to display text from the wrong locale to some users.
|
||||
|
||||
If you have a legitimate reason to access i18n data outside of the user flow, you can do so by passing
|
||||
the desired locale explicitly with the `locale` argument, e.g. `I18n.#{method}(..., locale: :en)`
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidLocale < ArgumentError
|
||||
attr_reader :locale
|
||||
def initialize(locale)
|
||||
@locale = locale
|
||||
super "#{locale.inspect} is not a valid locale"
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidLocaleData < ArgumentError
|
||||
attr_reader :filename
|
||||
def initialize(filename, exception_message)
|
||||
@filename, @exception_message = filename, exception_message
|
||||
super "can not load translations from #{filename}: #{exception_message}"
|
||||
end
|
||||
end
|
||||
|
||||
class MissingTranslation < ArgumentError
|
||||
module Base
|
||||
PERMITTED_KEYS = [:scope, :default].freeze
|
||||
|
||||
attr_reader :locale, :key, :options
|
||||
|
||||
def initialize(locale, key, options = EMPTY_HASH)
|
||||
@key, @locale, @options = key, locale, options.slice(*PERMITTED_KEYS)
|
||||
options.each { |k, v| self.options[k] = v.inspect if v.is_a?(Proc) }
|
||||
end
|
||||
|
||||
def keys
|
||||
@keys ||= I18n.normalize_keys(locale, key, options[:scope]).tap do |keys|
|
||||
keys << 'no key' if keys.size < 2
|
||||
end
|
||||
end
|
||||
|
||||
def message
|
||||
if (default = options[:default]).is_a?(Array) && default.any?
|
||||
other_options = ([key, *default]).map { |k| normalized_option(k).prepend('- ') }.join("\n")
|
||||
"Translation missing. Options considered were:\n#{other_options}"
|
||||
else
|
||||
"Translation missing: #{keys.join('.')}"
|
||||
end
|
||||
end
|
||||
|
||||
def normalized_option(key)
|
||||
I18n.normalize_keys(locale, key, options[:scope]).join('.')
|
||||
end
|
||||
|
||||
alias :to_s :message
|
||||
|
||||
def to_exception
|
||||
MissingTranslationData.new(locale, key, options)
|
||||
end
|
||||
end
|
||||
|
||||
include Base
|
||||
end
|
||||
|
||||
class MissingTranslationData < ArgumentError
|
||||
include MissingTranslation::Base
|
||||
end
|
||||
|
||||
class InvalidPluralizationData < ArgumentError
|
||||
attr_reader :entry, :count, :key
|
||||
def initialize(entry, count, key)
|
||||
@entry, @count, @key = entry, count, key
|
||||
super "translation data #{entry.inspect} can not be used with :count => #{count}. key '#{key}' is missing."
|
||||
end
|
||||
end
|
||||
|
||||
class MissingInterpolationArgument < ArgumentError
|
||||
attr_reader :key, :values, :string
|
||||
def initialize(key, values, string)
|
||||
@key, @values, @string = key, values, string
|
||||
super "missing interpolation argument #{key.inspect} in #{string.inspect} (#{values.inspect} given)"
|
||||
end
|
||||
end
|
||||
|
||||
class ReservedInterpolationKey < ArgumentError
|
||||
attr_reader :key, :string
|
||||
def initialize(key, string)
|
||||
@key, @string = key, string
|
||||
super "reserved key #{key.inspect} used in #{string.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
class UnknownFileType < ArgumentError
|
||||
attr_reader :type, :filename
|
||||
def initialize(type, filename)
|
||||
@type, @filename = type, filename
|
||||
super "can not load translations from #{filename}, the file type #{type} is not known"
|
||||
end
|
||||
end
|
||||
|
||||
class UnsupportedMethod < ArgumentError
|
||||
attr_reader :method, :backend_klass, :msg
|
||||
def initialize(method, backend_klass, msg)
|
||||
@method = method
|
||||
@backend_klass = backend_klass
|
||||
@msg = msg
|
||||
super "#{backend_klass} does not support the ##{method} method. #{msg}"
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidFilenames < ArgumentError
|
||||
NUMBER_OF_ERRORS_SHOWN = 20
|
||||
def initialize(file_errors)
|
||||
super <<~MSG
|
||||
Found #{file_errors.count} error(s).
|
||||
The first #{[file_errors.count, NUMBER_OF_ERRORS_SHOWN].min} error(s):
|
||||
#{file_errors.map(&:message).first(NUMBER_OF_ERRORS_SHOWN).join("\n")}
|
||||
|
||||
To use the LazyLoadable backend:
|
||||
1. Filenames must start with the locale.
|
||||
2. An underscore must separate the locale with any optional text that follows.
|
||||
3. The file must only contain translation data for the single locale.
|
||||
|
||||
Example:
|
||||
"/config/locales/fr.yml" which contains:
|
||||
```yml
|
||||
fr:
|
||||
dog:
|
||||
chien
|
||||
```
|
||||
MSG
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Gettext
|
||||
PLURAL_SEPARATOR = "\001"
|
||||
CONTEXT_SEPARATOR = "\004"
|
||||
|
||||
autoload :Helpers, 'i18n/gettext/helpers'
|
||||
|
||||
@@plural_keys = { :en => [:one, :other] }
|
||||
|
||||
class << self
|
||||
# returns an array of plural keys for the given locale or the whole hash
|
||||
# of locale mappings to plural keys so that we can convert from gettext's
|
||||
# integer-index based style
|
||||
# TODO move this information to the pluralization module
|
||||
def plural_keys(*args)
|
||||
args.empty? ? @@plural_keys : @@plural_keys[args.first] || @@plural_keys[:en]
|
||||
end
|
||||
|
||||
def extract_scope(msgid, separator)
|
||||
scope = msgid.to_s.split(separator)
|
||||
msgid = scope.pop
|
||||
[scope, msgid]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'i18n/gettext'
|
||||
|
||||
module I18n
|
||||
module Gettext
|
||||
# Implements classical Gettext style accessors. To use this include the
|
||||
# module to the global namespace or wherever you want to use it.
|
||||
#
|
||||
# include I18n::Gettext::Helpers
|
||||
module Helpers
|
||||
# Makes dynamic translation messages readable for the gettext parser.
|
||||
# <tt>_(fruit)</tt> cannot be understood by the gettext parser. To help the parser find all your translations,
|
||||
# you can add <tt>fruit = N_("Apple")</tt> which does not translate, but tells the parser: "Apple" needs translation.
|
||||
# * msgid: the message id.
|
||||
# * Returns: msgid.
|
||||
def N_(msgsid)
|
||||
msgsid
|
||||
end
|
||||
|
||||
def gettext(msgid, options = EMPTY_HASH)
|
||||
I18n.t(msgid, **{:default => msgid, :separator => '|'}.merge(options))
|
||||
end
|
||||
alias _ gettext
|
||||
|
||||
def sgettext(msgid, separator = '|')
|
||||
scope, msgid = I18n::Gettext.extract_scope(msgid, separator)
|
||||
I18n.t(msgid, :scope => scope, :default => msgid, :separator => separator)
|
||||
end
|
||||
alias s_ sgettext
|
||||
|
||||
def pgettext(msgctxt, msgid)
|
||||
separator = I18n::Gettext::CONTEXT_SEPARATOR
|
||||
sgettext([msgctxt, msgid].join(separator), separator)
|
||||
end
|
||||
alias p_ pgettext
|
||||
|
||||
def ngettext(msgid, msgid_plural, n = 1)
|
||||
nsgettext(msgid, msgid_plural, n)
|
||||
end
|
||||
alias n_ ngettext
|
||||
|
||||
# Method signatures:
|
||||
# nsgettext('Fruits|apple', 'apples', 2)
|
||||
# nsgettext(['Fruits|apple', 'apples'], 2)
|
||||
def nsgettext(msgid, msgid_plural, n = 1, separator = '|')
|
||||
if msgid.is_a?(Array)
|
||||
msgid, msgid_plural, n, separator = msgid[0], msgid[1], msgid_plural, n
|
||||
separator = '|' unless separator.is_a?(::String)
|
||||
end
|
||||
|
||||
scope, msgid = I18n::Gettext.extract_scope(msgid, separator)
|
||||
default = { :one => msgid, :other => msgid_plural }
|
||||
I18n.t(msgid, :default => default, :count => n, :scope => scope, :separator => separator)
|
||||
end
|
||||
alias ns_ nsgettext
|
||||
|
||||
# Method signatures:
|
||||
# npgettext('Fruits', 'apple', 'apples', 2)
|
||||
# npgettext('Fruits', ['apple', 'apples'], 2)
|
||||
def npgettext(msgctxt, msgid, msgid_plural, n = 1)
|
||||
separator = I18n::Gettext::CONTEXT_SEPARATOR
|
||||
|
||||
if msgid.is_a?(Array)
|
||||
msgid_plural, msgid, n = msgid[1], [msgctxt, msgid[0]].join(separator), msgid_plural
|
||||
else
|
||||
msgid = [msgctxt, msgid].join(separator)
|
||||
end
|
||||
|
||||
nsgettext(msgid, msgid_plural, n, separator)
|
||||
end
|
||||
alias np_ npgettext
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,329 @@
|
||||
=begin
|
||||
poparser.rb - Generate a .mo
|
||||
|
||||
Copyright (C) 2003-2009 Masao Mutoh <mutoh at highway.ne.jp>
|
||||
|
||||
You may redistribute it and/or modify it under the same
|
||||
license terms as Ruby.
|
||||
=end
|
||||
|
||||
#MODIFIED
|
||||
# removed include GetText etc
|
||||
# added stub translation method _(x)
|
||||
require 'racc/parser'
|
||||
|
||||
module GetText
|
||||
|
||||
class PoParser < Racc::Parser
|
||||
|
||||
def _(x)
|
||||
x
|
||||
end
|
||||
|
||||
module_eval <<'..end src/poparser.ry modeval..id7a99570e05', 'src/poparser.ry', 108
|
||||
def unescape(orig)
|
||||
ret = orig.gsub(/\\n/, "\n")
|
||||
ret.gsub!(/\\t/, "\t")
|
||||
ret.gsub!(/\\r/, "\r")
|
||||
ret.gsub!(/\\"/, "\"")
|
||||
ret
|
||||
end
|
||||
|
||||
def parse(str, data, ignore_fuzzy = true)
|
||||
@comments = []
|
||||
@data = data
|
||||
@fuzzy = false
|
||||
@msgctxt = ""
|
||||
$ignore_fuzzy = ignore_fuzzy
|
||||
|
||||
str.strip!
|
||||
@q = []
|
||||
until str.empty? do
|
||||
case str
|
||||
when /\A\s+/
|
||||
str = $'
|
||||
when /\Amsgctxt/
|
||||
@q.push [:MSGCTXT, $&]
|
||||
str = $'
|
||||
when /\Amsgid_plural/
|
||||
@q.push [:MSGID_PLURAL, $&]
|
||||
str = $'
|
||||
when /\Amsgid/
|
||||
@q.push [:MSGID, $&]
|
||||
str = $'
|
||||
when /\Amsgstr/
|
||||
@q.push [:MSGSTR, $&]
|
||||
str = $'
|
||||
when /\A\[(\d+)\]/
|
||||
@q.push [:PLURAL_NUM, $1]
|
||||
str = $'
|
||||
when /\A\#~(.*)/
|
||||
$stderr.print _("Warning: obsolete msgid exists.\n")
|
||||
$stderr.print " #{$&}\n"
|
||||
@q.push [:COMMENT, $&]
|
||||
str = $'
|
||||
when /\A\#(.*)/
|
||||
@q.push [:COMMENT, $&]
|
||||
str = $'
|
||||
when /\A\"(.*)\"/
|
||||
@q.push [:STRING, $1]
|
||||
str = $'
|
||||
else
|
||||
#c = str[0,1]
|
||||
#@q.push [:STRING, c]
|
||||
str = str[1..-1]
|
||||
end
|
||||
end
|
||||
@q.push [false, '$end']
|
||||
if $DEBUG
|
||||
@q.each do |a,b|
|
||||
puts "[#{a}, #{b}]"
|
||||
end
|
||||
end
|
||||
@yydebug = true if $DEBUG
|
||||
do_parse
|
||||
|
||||
if @comments.size > 0
|
||||
@data.set_comment(:last, @comments.join("\n"))
|
||||
end
|
||||
@data
|
||||
end
|
||||
|
||||
def next_token
|
||||
@q.shift
|
||||
end
|
||||
|
||||
def on_message(msgid, msgstr)
|
||||
if msgstr.size > 0
|
||||
@data[msgid] = msgstr
|
||||
@data.set_comment(msgid, @comments.join("\n"))
|
||||
end
|
||||
@comments.clear
|
||||
@msgctxt = ""
|
||||
end
|
||||
|
||||
def on_comment(comment)
|
||||
@fuzzy = true if (/fuzzy/ =~ comment)
|
||||
@comments << comment
|
||||
end
|
||||
|
||||
|
||||
..end src/poparser.ry modeval..id7a99570e05
|
||||
|
||||
##### racc 1.4.5 generates ###
|
||||
|
||||
racc_reduce_table = [
|
||||
0, 0, :racc_error,
|
||||
0, 10, :_reduce_none,
|
||||
2, 10, :_reduce_none,
|
||||
2, 10, :_reduce_none,
|
||||
2, 10, :_reduce_none,
|
||||
2, 12, :_reduce_5,
|
||||
1, 13, :_reduce_none,
|
||||
1, 13, :_reduce_none,
|
||||
4, 15, :_reduce_8,
|
||||
5, 16, :_reduce_9,
|
||||
2, 17, :_reduce_10,
|
||||
1, 17, :_reduce_none,
|
||||
3, 18, :_reduce_12,
|
||||
1, 11, :_reduce_13,
|
||||
2, 14, :_reduce_14,
|
||||
1, 14, :_reduce_15 ]
|
||||
|
||||
racc_reduce_n = 16
|
||||
|
||||
racc_shift_n = 26
|
||||
|
||||
racc_action_table = [
|
||||
3, 13, 5, 7, 9, 15, 16, 17, 20, 17,
|
||||
13, 17, 13, 13, 11, 17, 23, 20, 13, 17 ]
|
||||
|
||||
racc_action_check = [
|
||||
1, 16, 1, 1, 1, 12, 12, 12, 18, 18,
|
||||
7, 14, 15, 9, 3, 19, 20, 21, 23, 25 ]
|
||||
|
||||
racc_action_pointer = [
|
||||
nil, 0, nil, 14, nil, nil, nil, 3, nil, 6,
|
||||
nil, nil, 0, nil, 4, 5, -6, nil, 2, 8,
|
||||
8, 11, nil, 11, nil, 12 ]
|
||||
|
||||
racc_action_default = [
|
||||
-1, -16, -2, -16, -3, -13, -4, -16, -6, -16,
|
||||
-7, 26, -16, -15, -5, -16, -16, -14, -16, -8,
|
||||
-16, -9, -11, -16, -10, -12 ]
|
||||
|
||||
racc_goto_table = [
|
||||
12, 22, 14, 4, 24, 6, 2, 8, 18, 19,
|
||||
10, 21, 1, nil, nil, nil, 25 ]
|
||||
|
||||
racc_goto_check = [
|
||||
5, 9, 5, 3, 9, 4, 2, 6, 5, 5,
|
||||
7, 8, 1, nil, nil, nil, 5 ]
|
||||
|
||||
racc_goto_pointer = [
|
||||
nil, 12, 5, 2, 4, -7, 6, 9, -7, -17 ]
|
||||
|
||||
racc_goto_default = [
|
||||
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil ]
|
||||
|
||||
racc_token_table = {
|
||||
false => 0,
|
||||
Object.new => 1,
|
||||
:COMMENT => 2,
|
||||
:MSGID => 3,
|
||||
:MSGCTXT => 4,
|
||||
:MSGID_PLURAL => 5,
|
||||
:MSGSTR => 6,
|
||||
:STRING => 7,
|
||||
:PLURAL_NUM => 8 }
|
||||
|
||||
racc_use_result_var = true
|
||||
|
||||
racc_nt_base = 9
|
||||
|
||||
Racc_arg = [
|
||||
racc_action_table,
|
||||
racc_action_check,
|
||||
racc_action_default,
|
||||
racc_action_pointer,
|
||||
racc_goto_table,
|
||||
racc_goto_check,
|
||||
racc_goto_default,
|
||||
racc_goto_pointer,
|
||||
racc_nt_base,
|
||||
racc_reduce_table,
|
||||
racc_token_table,
|
||||
racc_shift_n,
|
||||
racc_reduce_n,
|
||||
racc_use_result_var ]
|
||||
|
||||
Racc_token_to_s_table = [
|
||||
'$end',
|
||||
'error',
|
||||
'COMMENT',
|
||||
'MSGID',
|
||||
'MSGCTXT',
|
||||
'MSGID_PLURAL',
|
||||
'MSGSTR',
|
||||
'STRING',
|
||||
'PLURAL_NUM',
|
||||
'$start',
|
||||
'msgfmt',
|
||||
'comment',
|
||||
'msgctxt',
|
||||
'message',
|
||||
'string_list',
|
||||
'single_message',
|
||||
'plural_message',
|
||||
'msgstr_plural',
|
||||
'msgstr_plural_line']
|
||||
|
||||
Racc_debug_parser = true
|
||||
|
||||
##### racc system variables end #####
|
||||
|
||||
# reduce 0 omitted
|
||||
|
||||
# reduce 1 omitted
|
||||
|
||||
# reduce 2 omitted
|
||||
|
||||
# reduce 3 omitted
|
||||
|
||||
# reduce 4 omitted
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 25
|
||||
def _reduce_5( val, _values, result )
|
||||
@msgctxt = unescape(val[1]) + "\004"
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
# reduce 6 omitted
|
||||
|
||||
# reduce 7 omitted
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 48
|
||||
def _reduce_8( val, _values, result )
|
||||
if @fuzzy and $ignore_fuzzy
|
||||
if val[1] != ""
|
||||
$stderr.print _("Warning: fuzzy message was ignored.\n")
|
||||
$stderr.print " msgid '#{val[1]}'\n"
|
||||
else
|
||||
on_message('', unescape(val[3]))
|
||||
end
|
||||
@fuzzy = false
|
||||
else
|
||||
on_message(@msgctxt + unescape(val[1]), unescape(val[3]))
|
||||
end
|
||||
result = ""
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 65
|
||||
def _reduce_9( val, _values, result )
|
||||
if @fuzzy and $ignore_fuzzy
|
||||
if val[1] != ""
|
||||
$stderr.print _("Warning: fuzzy message was ignored.\n")
|
||||
$stderr.print "msgid = '#{val[1]}\n"
|
||||
else
|
||||
on_message('', unescape(val[3]))
|
||||
end
|
||||
@fuzzy = false
|
||||
else
|
||||
on_message(@msgctxt + unescape(val[1]) + "\000" + unescape(val[3]), unescape(val[4]))
|
||||
end
|
||||
result = ""
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 76
|
||||
def _reduce_10( val, _values, result )
|
||||
if val[0].size > 0
|
||||
result = val[0] + "\000" + val[1]
|
||||
else
|
||||
result = ""
|
||||
end
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
# reduce 11 omitted
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 84
|
||||
def _reduce_12( val, _values, result )
|
||||
result = val[2]
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 91
|
||||
def _reduce_13( val, _values, result )
|
||||
on_comment(val[0])
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 99
|
||||
def _reduce_14( val, _values, result )
|
||||
result = val.delete_if{|item| item == ""}.join
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval <<'.,.,', 'src/poparser.ry', 103
|
||||
def _reduce_15( val, _values, result )
|
||||
result = val[0]
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
def _reduce_none( val, _values, result )
|
||||
result
|
||||
end
|
||||
|
||||
end # class PoParser
|
||||
|
||||
end # module GetText
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# heavily based on Masao Mutoh's gettext String interpolation extension
|
||||
# http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
|
||||
|
||||
module I18n
|
||||
DEFAULT_INTERPOLATION_PATTERNS = [
|
||||
/%%/,
|
||||
/%\{([\w|]+)\}/, # matches placeholders like "%{foo} or %{foo|word}"
|
||||
/%<(\w+)>([^\d]*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
|
||||
].freeze
|
||||
INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)
|
||||
deprecate_constant :INTERPOLATION_PATTERN
|
||||
|
||||
INTERPOLATION_PATTERNS_CACHE = Hash.new do |hash, patterns|
|
||||
hash[patterns] = Regexp.union(patterns)
|
||||
end
|
||||
private_constant :INTERPOLATION_PATTERNS_CACHE
|
||||
|
||||
class << self
|
||||
# Return String or raises MissingInterpolationArgument exception.
|
||||
# Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler.
|
||||
def interpolate(string, values)
|
||||
raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ I18n.reserved_keys_pattern
|
||||
raise ArgumentError.new('Interpolation values must be a Hash.') unless values.kind_of?(Hash)
|
||||
interpolate_hash(string, values)
|
||||
end
|
||||
|
||||
def interpolate_hash(string, values)
|
||||
pattern = INTERPOLATION_PATTERNS_CACHE[config.interpolation_patterns]
|
||||
interpolated = false
|
||||
|
||||
interpolated_string = string.gsub(pattern) do |match|
|
||||
interpolated = true
|
||||
|
||||
if match == '%%'
|
||||
'%'
|
||||
else
|
||||
key = ($1 || $2 || match.tr("%{}", "")).to_sym
|
||||
value = if values.key?(key)
|
||||
values[key]
|
||||
else
|
||||
config.missing_interpolation_argument_handler.call(key, values, string)
|
||||
end
|
||||
value = value.call(values) if value.respond_to?(:call)
|
||||
$3 ? sprintf("%#{$3}", value) : value
|
||||
end
|
||||
end
|
||||
|
||||
interpolated ? interpolated_string : string
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Locale
|
||||
autoload :Fallbacks, 'i18n/locale/fallbacks'
|
||||
autoload :Tag, 'i18n/locale/tag'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
# Locale Fallbacks
|
||||
#
|
||||
# Extends the I18n module to hold a fallbacks instance which is set to an
|
||||
# instance of I18n::Locale::Fallbacks by default but can be swapped with a
|
||||
# different implementation.
|
||||
#
|
||||
# Locale fallbacks will compute a number of fallback locales for a given locale.
|
||||
# For example:
|
||||
#
|
||||
# <pre><code>
|
||||
# I18n.fallbacks[:"es-MX"] # => [:"es-MX", :es, :en] </code></pre>
|
||||
#
|
||||
# Locale fallbacks always fall back to
|
||||
#
|
||||
# * all parent locales of a given locale (e.g. :es for :"es-MX") first,
|
||||
# * the current default locales and all of their parents second
|
||||
#
|
||||
# The default locales are set to [] by default but can be set to something else.
|
||||
#
|
||||
# One can additionally add any number of additional fallback locales manually.
|
||||
# These will be added before the default locales to the fallback chain. For
|
||||
# example:
|
||||
#
|
||||
# # using a custom locale as default fallback locale
|
||||
#
|
||||
# I18n.fallbacks = I18n::Locale::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de)
|
||||
# I18n.fallbacks[:"de-AT"] # => [:"de-AT", :de, :"en-GB", :en]
|
||||
# I18n.fallbacks[:"de-CH"] # => [:"de-CH", :de, :"en-GB", :en]
|
||||
#
|
||||
# # mapping fallbacks to an existing instance
|
||||
#
|
||||
# # people speaking Catalan also speak Spanish as spoken in Spain
|
||||
# fallbacks = I18n.fallbacks
|
||||
# fallbacks.map(:ca => :"es-ES")
|
||||
# fallbacks[:ca] # => [:ca, :"es-ES", :es, :"en-US", :en]
|
||||
#
|
||||
# # people speaking Arabian as spoken in Palestine also speak Hebrew as spoken in Israel
|
||||
# fallbacks.map(:"ar-PS" => :"he-IL")
|
||||
# fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en]
|
||||
# fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en]
|
||||
#
|
||||
# # people speaking Sami as spoken in Finland also speak Swedish and Finnish as spoken in Finland
|
||||
# fallbacks.map(:sms => [:"se-FI", :"fi-FI"])
|
||||
# fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en]
|
||||
|
||||
module I18n
|
||||
module Locale
|
||||
class Fallbacks < Hash
|
||||
def initialize(*mappings)
|
||||
@map = {}
|
||||
map(mappings.pop) if mappings.last.is_a?(Hash)
|
||||
self.defaults = mappings.empty? ? [] : mappings
|
||||
end
|
||||
|
||||
def defaults=(defaults)
|
||||
@defaults = defaults.flat_map { |default| compute(default, false) }
|
||||
end
|
||||
attr_reader :defaults
|
||||
|
||||
def [](locale)
|
||||
raise InvalidLocale.new(locale) if locale.nil?
|
||||
raise Disabled.new('fallback#[]') if locale == false
|
||||
locale = locale.to_sym
|
||||
super || store(locale, compute(locale))
|
||||
end
|
||||
|
||||
def map(*args, &block)
|
||||
if args.count == 1 && !block_given?
|
||||
mappings = args.first
|
||||
mappings.each do |from, to|
|
||||
from, to = from.to_sym, Array(to)
|
||||
to.each do |_to|
|
||||
@map[from] ||= []
|
||||
@map[from] << _to.to_sym
|
||||
end
|
||||
end
|
||||
else
|
||||
@map.map(*args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def empty?
|
||||
@map.empty? && @defaults.empty?
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class.name} @map=#{@map.inspect} @defaults=#{@defaults.inspect}>"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def compute(tags, include_defaults = true, exclude = [])
|
||||
result = []
|
||||
Array(tags).each do |tag|
|
||||
tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude
|
||||
result += tags
|
||||
tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] }
|
||||
end
|
||||
|
||||
result.push(*defaults) if include_defaults
|
||||
result.uniq!
|
||||
result.compact!
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Locale
|
||||
module Tag
|
||||
autoload :Parents, 'i18n/locale/tag/parents'
|
||||
autoload :Rfc4646, 'i18n/locale/tag/rfc4646'
|
||||
autoload :Simple, 'i18n/locale/tag/simple'
|
||||
|
||||
class << self
|
||||
# Returns the current locale tag implementation. Defaults to +I18n::Locale::Tag::Simple+.
|
||||
def implementation
|
||||
@@implementation ||= Simple
|
||||
end
|
||||
|
||||
# Sets the current locale tag implementation. Use this to set a different locale tag implementation.
|
||||
def implementation=(implementation)
|
||||
@@implementation = implementation
|
||||
end
|
||||
|
||||
# Factory method for locale tags. Delegates to the current locale tag implementation.
|
||||
def tag(tag)
|
||||
implementation.tag(tag)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
module I18n
|
||||
module Locale
|
||||
module Tag
|
||||
module Parents
|
||||
def parent
|
||||
@parent ||=
|
||||
begin
|
||||
segs = to_a
|
||||
segs.compact!
|
||||
segs.length > 1 ? self.class.tag(*segs[0..(segs.length - 2)].join('-')) : nil
|
||||
end
|
||||
end
|
||||
|
||||
def self_and_parents
|
||||
@self_and_parents ||= [self].concat parents
|
||||
end
|
||||
|
||||
def parents
|
||||
@parents ||= parent ? [parent].concat(parent.parents) : []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
# RFC 4646/47 compliant Locale tag implementation that parses locale tags to
|
||||
# subtags such as language, script, region, variant etc.
|
||||
#
|
||||
# For more information see by http://en.wikipedia.org/wiki/IETF_language_tag
|
||||
#
|
||||
# Rfc4646::Parser does not implement grandfathered tags.
|
||||
|
||||
module I18n
|
||||
module Locale
|
||||
module Tag
|
||||
RFC4646_SUBTAGS = [ :language, :script, :region, :variant, :extension, :privateuse, :grandfathered ]
|
||||
RFC4646_FORMATS = { :language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase }
|
||||
|
||||
class Rfc4646 < Struct.new(*RFC4646_SUBTAGS)
|
||||
class << self
|
||||
# Parses the given tag and returns a Tag instance if it is valid.
|
||||
# Returns false if the given tag is not valid according to RFC 4646.
|
||||
def tag(tag)
|
||||
matches = parser.match(tag)
|
||||
new(*matches) if matches
|
||||
end
|
||||
|
||||
def parser
|
||||
@@parser ||= Rfc4646::Parser
|
||||
end
|
||||
|
||||
def parser=(parser)
|
||||
@@parser = parser
|
||||
end
|
||||
end
|
||||
|
||||
include Parents
|
||||
|
||||
RFC4646_FORMATS.each do |name, format|
|
||||
define_method(name) { self[name].send(format) unless self[name].nil? }
|
||||
end
|
||||
|
||||
def to_sym
|
||||
to_s.to_sym
|
||||
end
|
||||
|
||||
def to_s
|
||||
@tag ||= to_a.compact.join("-")
|
||||
end
|
||||
|
||||
def to_a
|
||||
members.collect { |attr| self.send(attr) }
|
||||
end
|
||||
|
||||
module Parser
|
||||
PATTERN = %r{\A(?:
|
||||
([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language
|
||||
(?:-([a-z]{4}))? # script
|
||||
(?:-([a-z]{2}|\d{3}))? # region
|
||||
(?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant
|
||||
(?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension
|
||||
(?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag
|
||||
(x(?:-[0-9a-z]{1,8})+)| # privateuse tag
|
||||
/* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered
|
||||
)\z}xi
|
||||
|
||||
class << self
|
||||
def match(tag)
|
||||
c = PATTERN.match(tag.to_s).captures
|
||||
c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here?
|
||||
rescue
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# Simple Locale tag implementation that computes subtags by simply splitting
|
||||
# the locale tag at '-' occurrences.
|
||||
module I18n
|
||||
module Locale
|
||||
module Tag
|
||||
class Simple
|
||||
class << self
|
||||
def tag(tag)
|
||||
new(tag)
|
||||
end
|
||||
end
|
||||
|
||||
include Parents
|
||||
|
||||
attr_reader :tag
|
||||
|
||||
def initialize(*tag)
|
||||
@tag = tag.join('-').to_sym
|
||||
end
|
||||
|
||||
def subtags
|
||||
@subtags = tag.to_s.split('-').map!(&:to_s)
|
||||
end
|
||||
|
||||
def to_sym
|
||||
tag
|
||||
end
|
||||
|
||||
def to_s
|
||||
tag.to_s
|
||||
end
|
||||
|
||||
def to_a
|
||||
subtags
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
class Middleware
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
ensure
|
||||
Thread.current[:i18n_config] = I18n::Config.new
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
autoload :Basics, 'i18n/tests/basics'
|
||||
autoload :Defaults, 'i18n/tests/defaults'
|
||||
autoload :Interpolation, 'i18n/tests/interpolation'
|
||||
autoload :Link, 'i18n/tests/link'
|
||||
autoload :Localization, 'i18n/tests/localization'
|
||||
autoload :Lookup, 'i18n/tests/lookup'
|
||||
autoload :Pluralization, 'i18n/tests/pluralization'
|
||||
autoload :Procs, 'i18n/tests/procs'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
module I18n
|
||||
module Tests
|
||||
module Basics
|
||||
def teardown
|
||||
I18n.available_locales = nil
|
||||
end
|
||||
|
||||
test "available_locales returns the available_locales produced by the backend, by default" do
|
||||
I18n.backend.store_translations('de', :foo => 'bar')
|
||||
I18n.backend.store_translations('en', :foo => 'foo')
|
||||
|
||||
assert_equal I18n.available_locales, I18n.backend.available_locales
|
||||
end
|
||||
|
||||
test "available_locales can be set to something else independently from the actual locale data" do
|
||||
I18n.backend.store_translations('de', :foo => 'bar')
|
||||
I18n.backend.store_translations('en', :foo => 'foo')
|
||||
|
||||
I18n.available_locales = :foo
|
||||
assert_equal [:foo], I18n.available_locales
|
||||
|
||||
I18n.available_locales = [:foo, 'bar']
|
||||
assert_equal [:foo, :bar], I18n.available_locales
|
||||
|
||||
I18n.available_locales = nil
|
||||
assert_equal I18n.available_locales, I18n.backend.available_locales
|
||||
end
|
||||
|
||||
test "available_locales memoizes when set explicitly" do
|
||||
I18n.backend.expects(:available_locales).never
|
||||
I18n.available_locales = [:foo]
|
||||
I18n.backend.store_translations('de', :bar => 'baz')
|
||||
I18n.reload!
|
||||
assert_equal [:foo], I18n.available_locales
|
||||
end
|
||||
|
||||
test "available_locales delegates to the backend when not set explicitly" do
|
||||
original_available_locales_value = I18n.backend.available_locales
|
||||
I18n.backend.expects(:available_locales).returns(original_available_locales_value).twice
|
||||
assert_equal I18n.backend.available_locales, I18n.available_locales
|
||||
end
|
||||
|
||||
test "exists? is implemented by the backend" do
|
||||
I18n.backend.store_translations(:foo, :bar => 'baz')
|
||||
assert I18n.exists?(:bar, :foo)
|
||||
end
|
||||
|
||||
test "storing a nil value as a translation removes it from the available locale data" do
|
||||
I18n.backend.store_translations(:en, :to_be_deleted => 'bar')
|
||||
assert_equal 'bar', I18n.t(:to_be_deleted, :default => 'baz')
|
||||
|
||||
I18n.cache_store.clear if I18n.respond_to?(:cache_store) && I18n.cache_store
|
||||
I18n.backend.store_translations(:en, :to_be_deleted => nil)
|
||||
assert_equal 'baz', I18n.t(:to_be_deleted, :default => 'baz')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Defaults
|
||||
def setup
|
||||
super
|
||||
I18n.backend.store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' })
|
||||
end
|
||||
|
||||
test "defaults: given nil as a key it returns the given default" do
|
||||
assert_equal 'default', I18n.t(nil, :default => 'default')
|
||||
end
|
||||
|
||||
test "defaults: given a symbol as a default it translates the symbol" do
|
||||
assert_equal 'bar', I18n.t(nil, :default => :'foo.bar')
|
||||
end
|
||||
|
||||
test "defaults: given a symbol as a default and a scope it stays inside the scope when looking up the symbol" do
|
||||
assert_equal 'bar', I18n.t(:missing, :default => :bar, :scope => :foo)
|
||||
end
|
||||
|
||||
test "defaults: given an array as a default it returns the first match" do
|
||||
assert_equal 'bar', I18n.t(:does_not_exist, :default => [:does_not_exist_2, :'foo.bar'])
|
||||
end
|
||||
|
||||
test "defaults: given an array as a default with false it returns false" do
|
||||
assert_equal false, I18n.t(:does_not_exist, :default => [false])
|
||||
end
|
||||
|
||||
test "defaults: given false it returns false" do
|
||||
assert_equal false, I18n.t(:does_not_exist, :default => false)
|
||||
end
|
||||
|
||||
test "defaults: given nil it returns nil" do
|
||||
assert_nil I18n.t(:does_not_exist, :default => nil)
|
||||
end
|
||||
|
||||
test "defaults: given an array of missing keys it raises a MissingTranslationData exception" do
|
||||
assert_raises I18n::MissingTranslationData do
|
||||
I18n.t(:does_not_exist, :default => [:does_not_exist_2, :does_not_exist_3], :raise => true)
|
||||
end
|
||||
end
|
||||
|
||||
test "defaults: using a custom scope separator" do
|
||||
# data must have been stored using the custom separator when using the ActiveRecord backend
|
||||
I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' })
|
||||
assert_equal 'bar', I18n.t(nil, :default => :'foo|bar', :separator => '|')
|
||||
end
|
||||
|
||||
# Addresses issue: #599
|
||||
test "defaults: only interpolates once when resolving defaults" do
|
||||
I18n.backend.store_translations(:en, :greeting => 'hey %{name}')
|
||||
assert_equal 'hey %{dont_interpolate_me}',
|
||||
I18n.t(:does_not_exist, :name => '%{dont_interpolate_me}', default: [:greeting])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,185 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Interpolation
|
||||
# If no interpolation parameter is not given, I18n should not alter the string.
|
||||
# This behavior is due to three reasons:
|
||||
#
|
||||
# * Checking interpolation keys in all strings hits performance, badly;
|
||||
#
|
||||
# * This allows us to retrieve untouched values through I18n. For example
|
||||
# I could have a middleware that returns I18n lookup results in JSON
|
||||
# to be processed through Javascript. Leaving the keys untouched allows
|
||||
# the interpolation to happen at the javascript level;
|
||||
#
|
||||
# * Security concerns: if I allow users to translate a web site, they can
|
||||
# insert %{} in messages causing the I18n lookup to fail in every request.
|
||||
#
|
||||
test "interpolation: given no values it does not alter the string" do
|
||||
assert_equal 'Hi %{name}!', interpolate(:default => 'Hi %{name}!')
|
||||
end
|
||||
|
||||
test "interpolation: given values it interpolates them into the string" do
|
||||
assert_equal 'Hi David!', interpolate(:default => 'Hi %{name}!', :name => 'David')
|
||||
end
|
||||
|
||||
test "interpolation: works with a pipe" do
|
||||
assert_equal 'Hi david!', interpolate(:default => 'Hi %{name|lowercase}!', :'name|lowercase' => 'david')
|
||||
end
|
||||
|
||||
test "interpolation: given a nil value it still interpolates it into the string" do
|
||||
assert_equal 'Hi !', interpolate(:default => 'Hi %{name}!', :name => nil)
|
||||
end
|
||||
|
||||
test "interpolation: given a lambda as a value it calls it if the string contains the key" do
|
||||
assert_equal 'Hi David!', interpolate(:default => 'Hi %{name}!', :name => lambda { |*args| 'David' })
|
||||
end
|
||||
|
||||
test "interpolation: given a lambda as a value it does not call it if the string does not contain the key" do
|
||||
assert_nothing_raised { interpolate(:default => 'Hi!', :name => lambda { |*args| raise 'fail' }) }
|
||||
end
|
||||
|
||||
test "interpolation: given values but missing a key it raises I18n::MissingInterpolationArgument" do
|
||||
assert_raises(I18n::MissingInterpolationArgument) do
|
||||
interpolate(:default => '%{foo}', :bar => 'bar')
|
||||
end
|
||||
end
|
||||
|
||||
test "interpolation: it does not raise I18n::MissingInterpolationArgument for escaped variables" do
|
||||
assert_nothing_raised do
|
||||
assert_equal 'Barr %{foo}', interpolate(:default => '%{bar} %%{foo}', :bar => 'Barr')
|
||||
end
|
||||
end
|
||||
|
||||
test "interpolation: it does not change the original, stored translation string" do
|
||||
I18n.backend.store_translations(:en, :interpolate => 'Hi %{name}!')
|
||||
assert_equal 'Hi David!', interpolate(:interpolate, :name => 'David')
|
||||
assert_equal 'Hi Yehuda!', interpolate(:interpolate, :name => 'Yehuda')
|
||||
end
|
||||
|
||||
test "interpolation: given an array interpolates each element" do
|
||||
I18n.backend.store_translations(:en, :array_interpolate => ['Hi', 'Mr. %{name}', 'or sir %{name}'])
|
||||
assert_equal ['Hi', 'Mr. Bartuz', 'or sir Bartuz'], interpolate(:array_interpolate, :name => 'Bartuz')
|
||||
end
|
||||
|
||||
test "interpolation: given the translation is in utf-8 it still works" do
|
||||
assert_equal 'Häi David!', interpolate(:default => 'Häi %{name}!', :name => 'David')
|
||||
end
|
||||
|
||||
test "interpolation: given the value is in utf-8 it still works" do
|
||||
assert_equal 'Hi ゆきひろ!', interpolate(:default => 'Hi %{name}!', :name => 'ゆきひろ')
|
||||
end
|
||||
|
||||
test "interpolation: given the translation and the value are in utf-8 it still works" do
|
||||
assert_equal 'こんにちは、ゆきひろさん!', interpolate(:default => 'こんにちは、%{name}さん!', :name => 'ゆきひろ')
|
||||
end
|
||||
|
||||
if Object.const_defined?(:Encoding)
|
||||
test "interpolation: given a euc-jp translation and a utf-8 value it raises Encoding::CompatibilityError" do
|
||||
assert_raises(Encoding::CompatibilityError) do
|
||||
interpolate(:default => euc_jp('こんにちは、%{name}さん!'), :name => 'ゆきひろ')
|
||||
end
|
||||
end
|
||||
|
||||
test "interpolation: given a utf-8 translation and a euc-jp value it raises Encoding::CompatibilityError" do
|
||||
assert_raises(Encoding::CompatibilityError) do
|
||||
interpolate(:default => 'こんにちは、%{name}さん!', :name => euc_jp('ゆきひろ'))
|
||||
end
|
||||
end
|
||||
|
||||
test "interpolation: ASCII strings in the backend should be encoded to UTF8 if interpolation options are in UTF8" do
|
||||
I18n.backend.store_translations 'en', 'encoding' => ('%{who} let me go'.force_encoding("ASCII"))
|
||||
result = I18n.t 'encoding', :who => "måmmå miå"
|
||||
assert_equal Encoding::UTF_8, result.encoding
|
||||
end
|
||||
|
||||
test "interpolation: UTF8 strings in the backend are still returned as UTF8 with ASCII interpolation" do
|
||||
I18n.backend.store_translations 'en', 'encoding' => 'måmmå miå %{what}'
|
||||
result = I18n.t 'encoding', :what => 'let me go'.force_encoding("ASCII")
|
||||
assert_equal Encoding::UTF_8, result.encoding
|
||||
end
|
||||
|
||||
test "interpolation: UTF8 strings in the backend are still returned as UTF8 even with numbers interpolation" do
|
||||
I18n.backend.store_translations 'en', 'encoding' => '%{count} times: måmmå miå'
|
||||
result = I18n.t 'encoding', :count => 3
|
||||
assert_equal Encoding::UTF_8, result.encoding
|
||||
end
|
||||
end
|
||||
|
||||
test "interpolation: given a translations containing a reserved key it raises I18n::ReservedInterpolationKey" do
|
||||
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{exception_handler}') }
|
||||
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{default}') }
|
||||
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{separator}') }
|
||||
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{scope}') }
|
||||
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:default => '%{scope}') }
|
||||
|
||||
I18n.backend.store_translations(:en, :interpolate => 'Hi %{scope}!')
|
||||
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:interpolate) }
|
||||
end
|
||||
|
||||
test "interpolation: it does not raise I18n::ReservedInterpolationKey for escaped variables" do
|
||||
assert_nothing_raised do
|
||||
assert_equal '%{separator}', interpolate(:foo => :bar, :default => '%%{separator}')
|
||||
end
|
||||
|
||||
# Note: The two interpolations below do not remove the escape character (%) because
|
||||
# I18n should not alter the strings when no interpolation parameters are given,
|
||||
# see the comment at the top of this file.
|
||||
assert_nothing_raised do
|
||||
assert_equal '%%{scope}', interpolate(:default => '%%{scope}')
|
||||
end
|
||||
|
||||
I18n.backend.store_translations(:en, :interpolate => 'Hi %%{scope}!')
|
||||
assert_nothing_raised do
|
||||
assert_equal 'Hi %%{scope}!', interpolate(:interpolate)
|
||||
end
|
||||
end
|
||||
|
||||
test "interpolation: deep interpolation for default string" do
|
||||
assert_equal 'Hi %{name}!', interpolate(:default => 'Hi %{name}!', :deep_interpolation => true)
|
||||
end
|
||||
|
||||
test "interpolation: deep interpolation for interpolated string" do
|
||||
assert_equal 'Hi Ann!', interpolate(:default => 'Hi %{name}!', :name => 'Ann', :deep_interpolation => true)
|
||||
end
|
||||
|
||||
test "interpolation: deep interpolation for Hash" do
|
||||
people = { :people => { :ann => 'Ann is %{ann}', :john => 'John is %{john}' } }
|
||||
interpolated_people = { :people => { :ann => 'Ann is good', :john => 'John is big' } }
|
||||
assert_equal interpolated_people, interpolate(:default => people, :ann => 'good', :john => 'big', :deep_interpolation => true)
|
||||
end
|
||||
|
||||
test "interpolation: deep interpolation for Array" do
|
||||
people = { :people => ['Ann is %{ann}', 'John is %{john}'] }
|
||||
interpolated_people = { :people => ['Ann is good', 'John is big'] }
|
||||
assert_equal interpolated_people, interpolate(:default => people, :ann => 'good', :john => 'big', :deep_interpolation => true)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def capture(stream)
|
||||
begin
|
||||
stream = stream.to_s
|
||||
eval "$#{stream} = StringIO.new"
|
||||
yield
|
||||
result = eval("$#{stream}").string
|
||||
ensure
|
||||
eval("$#{stream} = #{stream.upcase}")
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def euc_jp(string)
|
||||
string.encode!(Encoding::EUC_JP)
|
||||
end
|
||||
|
||||
def interpolate(*args)
|
||||
options = args.last.is_a?(Hash) ? args.pop : {}
|
||||
key = args.pop
|
||||
I18n.backend.translate('en', key, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Link
|
||||
test "linked lookup: if a key resolves to a symbol it looks up the symbol" do
|
||||
I18n.backend.store_translations 'en', {
|
||||
:link => :linked,
|
||||
:linked => 'linked'
|
||||
}
|
||||
assert_equal 'linked', I18n.backend.translate('en', :link)
|
||||
end
|
||||
|
||||
test "linked lookup: if a key resolves to a dot-separated symbol it looks up the symbol" do
|
||||
I18n.backend.store_translations 'en', {
|
||||
:link => :"foo.linked",
|
||||
:foo => { :linked => 'linked' }
|
||||
}
|
||||
assert_equal('linked', I18n.backend.translate('en', :link))
|
||||
end
|
||||
|
||||
test "linked lookup: if a dot-separated key resolves to a symbol it looks up the symbol" do
|
||||
I18n.backend.store_translations 'en', {
|
||||
:foo => { :link => :linked },
|
||||
:linked => 'linked'
|
||||
}
|
||||
assert_equal('linked', I18n.backend.translate('en', :'foo.link'))
|
||||
end
|
||||
|
||||
test "linked lookup: if a dot-separated key resolves to a dot-separated symbol it looks up the symbol" do
|
||||
I18n.backend.store_translations 'en', {
|
||||
:foo => { :link => :"bar.linked" },
|
||||
:bar => { :linked => 'linked' }
|
||||
}
|
||||
assert_equal('linked', I18n.backend.translate('en', :'foo.link'))
|
||||
end
|
||||
|
||||
test "linked lookup: links always refer to the absolute key" do
|
||||
I18n.backend.store_translations 'en', {
|
||||
:foo => { :link => :linked, :linked => 'linked in foo' },
|
||||
:linked => 'linked absolutely'
|
||||
}
|
||||
assert_equal 'linked absolutely', I18n.backend.translate('en', :link, :scope => :foo)
|
||||
end
|
||||
|
||||
test "linked lookup: a link can resolve to a namespace in the middle of a dot-separated key" do
|
||||
I18n.backend.store_translations 'en', {
|
||||
:activemodel => { :errors => { :messages => { :blank => "can't be blank" } } },
|
||||
:activerecord => { :errors => { :messages => :"activemodel.errors.messages" } }
|
||||
}
|
||||
assert_equal "can't be blank", I18n.t(:"activerecord.errors.messages.blank")
|
||||
assert_equal "can't be blank", I18n.t(:"activerecord.errors.messages.blank")
|
||||
end
|
||||
|
||||
test "linked lookup: a link can resolve with option :count" do
|
||||
I18n.backend.store_translations 'en', {
|
||||
:counter => :counted,
|
||||
:counted => { :foo => { :one => "one", :other => "other" }, :bar => "bar" }
|
||||
}
|
||||
assert_equal "one", I18n.t(:'counter.foo', count: 1)
|
||||
assert_equal "other", I18n.t(:'counter.foo', count: 2)
|
||||
assert_equal "bar", I18n.t(:'counter.bar', count: 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module I18n
|
||||
module Tests
|
||||
module Localization
|
||||
autoload :Date, 'i18n/tests/localization/date'
|
||||
autoload :DateTime, 'i18n/tests/localization/date_time'
|
||||
autoload :Time, 'i18n/tests/localization/time'
|
||||
autoload :Procs, 'i18n/tests/localization/procs'
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
include I18n::Tests::Localization::Date
|
||||
include I18n::Tests::Localization::DateTime
|
||||
include I18n::Tests::Localization::Procs
|
||||
include I18n::Tests::Localization::Time
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,122 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Localization
|
||||
module Date
|
||||
def setup
|
||||
super
|
||||
setup_date_translations
|
||||
@date = ::Date.new(2008, 3, 1)
|
||||
end
|
||||
|
||||
test "localize Date: given the short format it uses it" do
|
||||
assert_equal '01. Mär', I18n.l(@date, :format => :short, :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given the long format it uses it" do
|
||||
assert_equal '01. März 2008', I18n.l(@date, :format => :long, :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given the default format it uses it" do
|
||||
assert_equal '01.03.2008', I18n.l(@date, :format => :default, :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given a day name format it returns the correct day name" do
|
||||
assert_equal 'Samstag', I18n.l(@date, :format => '%A', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given a uppercased day name format it returns the correct day name in upcase" do
|
||||
assert_equal 'samstag'.upcase, I18n.l(@date, :format => '%^A', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given an abbreviated day name format it returns the correct abbreviated day name" do
|
||||
assert_equal 'Sa', I18n.l(@date, :format => '%a', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given an meridian indicator format it returns the correct meridian indicator" do
|
||||
assert_equal 'AM', I18n.l(@date, :format => '%p', :locale => :de)
|
||||
assert_equal 'am', I18n.l(@date, :format => '%P', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do
|
||||
assert_equal 'sa'.upcase, I18n.l(@date, :format => '%^a', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given a month name format it returns the correct month name" do
|
||||
assert_equal 'März', I18n.l(@date, :format => '%B', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given a uppercased month name format it returns the correct month name in upcase" do
|
||||
assert_equal 'märz'.upcase, I18n.l(@date, :format => '%^B', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given an abbreviated month name format it returns the correct abbreviated month name" do
|
||||
assert_equal 'Mär', I18n.l(@date, :format => '%b', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given an abbreviated and uppercased month name format it returns the correct abbreviated month name in upcase" do
|
||||
assert_equal 'mär'.upcase, I18n.l(@date, :format => '%^b', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given a date format with the month name upcased it returns the correct value" do
|
||||
assert_equal '1. FEBRUAR 2008', I18n.l(::Date.new(2008, 2, 1), :format => "%-d. %^B %Y", :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Date: given missing translations it returns the correct error message" do
|
||||
assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@date, :format => '%b', :locale => :fr)
|
||||
end
|
||||
|
||||
test "localize Date: given an unknown format it does not fail" do
|
||||
assert_nothing_raised { I18n.l(@date, :format => '%x') }
|
||||
end
|
||||
|
||||
test "localize Date: does not modify the options hash" do
|
||||
options = { :format => '%b', :locale => :de }
|
||||
assert_equal 'Mär', I18n.l(@date, **options)
|
||||
assert_equal({ :format => '%b', :locale => :de }, options)
|
||||
assert_nothing_raised { I18n.l(@date, **options.freeze) }
|
||||
end
|
||||
|
||||
test "localize Date: given nil with default value it returns default" do
|
||||
assert_equal 'default', I18n.l(nil, :default => 'default')
|
||||
end
|
||||
|
||||
test "localize Date: given nil it raises I18n::ArgumentError" do
|
||||
assert_raises(I18n::ArgumentError) { I18n.l(nil) }
|
||||
end
|
||||
|
||||
test "localize Date: given a plain Object it raises I18n::ArgumentError" do
|
||||
assert_raises(I18n::ArgumentError) { I18n.l(Object.new) }
|
||||
end
|
||||
|
||||
test "localize Date: given a format is missing it raises I18n::MissingTranslationData" do
|
||||
assert_raises(I18n::MissingTranslationData) { I18n.l(@date, :format => :missing) }
|
||||
end
|
||||
|
||||
test "localize Date: it does not alter the format string" do
|
||||
assert_equal '01. Februar 2009', I18n.l(::Date.parse('2009-02-01'), :format => :long, :locale => :de)
|
||||
assert_equal '01. Oktober 2009', I18n.l(::Date.parse('2009-10-01'), :format => :long, :locale => :de)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def setup_date_translations
|
||||
I18n.backend.store_translations :de, {
|
||||
:date => {
|
||||
:formats => {
|
||||
:default => "%d.%m.%Y",
|
||||
:short => "%d. %b",
|
||||
:long => "%d. %B %Y",
|
||||
},
|
||||
:day_names => %w(Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag),
|
||||
:abbr_day_names => %w(So Mo Di Mi Do Fr Sa),
|
||||
:month_names => %w(Januar Februar März April Mai Juni Juli August September Oktober November Dezember).unshift(nil),
|
||||
:abbr_month_names => %w(Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez).unshift(nil)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,103 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Localization
|
||||
module DateTime
|
||||
def setup
|
||||
super
|
||||
setup_datetime_translations
|
||||
@datetime = ::DateTime.new(2008, 3, 1, 6)
|
||||
@other_datetime = ::DateTime.new(2008, 3, 1, 18)
|
||||
end
|
||||
|
||||
test "localize DateTime: given the short format it uses it" do
|
||||
assert_equal '01. Mär 06:00', I18n.l(@datetime, :format => :short, :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given the long format it uses it" do
|
||||
assert_equal '01. März 2008 06:00', I18n.l(@datetime, :format => :long, :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given the default format it uses it" do
|
||||
assert_equal 'Sa, 01. Mär 2008 06:00:00 +0000', I18n.l(@datetime, :format => :default, :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a day name format it returns the correct day name" do
|
||||
assert_equal 'Samstag', I18n.l(@datetime, :format => '%A', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a uppercased day name format it returns the correct day name in upcase" do
|
||||
assert_equal 'samstag'.upcase, I18n.l(@datetime, :format => '%^A', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given an abbreviated day name format it returns the correct abbreviated day name" do
|
||||
assert_equal 'Sa', I18n.l(@datetime, :format => '%a', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do
|
||||
assert_equal 'sa'.upcase, I18n.l(@datetime, :format => '%^a', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a month name format it returns the correct month name" do
|
||||
assert_equal 'März', I18n.l(@datetime, :format => '%B', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a uppercased month name format it returns the correct month name in upcase" do
|
||||
assert_equal 'märz'.upcase, I18n.l(@datetime, :format => '%^B', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given an abbreviated month name format it returns the correct abbreviated month name" do
|
||||
assert_equal 'Mär', I18n.l(@datetime, :format => '%b', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given an abbreviated and uppercased month name format it returns the correct abbreviated month name in upcase" do
|
||||
assert_equal 'mär'.upcase, I18n.l(@datetime, :format => '%^b', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a date format with the month name upcased it returns the correct value" do
|
||||
assert_equal '1. FEBRUAR 2008', I18n.l(::DateTime.new(2008, 2, 1, 6), :format => "%-d. %^B %Y", :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given missing translations it returns the correct error message" do
|
||||
assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@datetime, :format => '%b', :locale => :fr)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator" do
|
||||
assert_equal 'AM', I18n.l(@datetime, :format => '%p', :locale => :de)
|
||||
assert_equal 'PM', I18n.l(@other_datetime, :format => '%p', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator in downcase" do
|
||||
assert_equal 'am', I18n.l(@datetime, :format => '%P', :locale => :de)
|
||||
assert_equal 'pm', I18n.l(@other_datetime, :format => '%P', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize DateTime: given an unknown format it does not fail" do
|
||||
assert_nothing_raised { I18n.l(@datetime, :format => '%x') }
|
||||
end
|
||||
|
||||
test "localize DateTime: given a format is missing it raises I18n::MissingTranslationData" do
|
||||
assert_raises(I18n::MissingTranslationData) { I18n.l(@datetime, :format => :missing) }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def setup_datetime_translations
|
||||
# time translations might have been set up in Tests::Api::Localization::Time
|
||||
I18n.backend.store_translations :de, {
|
||||
:time => {
|
||||
:formats => {
|
||||
:default => "%a, %d. %b %Y %H:%M:%S %z",
|
||||
:short => "%d. %b %H:%M",
|
||||
:long => "%d. %B %Y %H:%M"
|
||||
},
|
||||
:am => 'am',
|
||||
:pm => 'pm'
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Localization
|
||||
module Procs
|
||||
test "localize: using day names from lambdas" do
|
||||
setup_time_proc_translations
|
||||
time = ::Time.utc(2008, 3, 1, 6, 0)
|
||||
assert_match(/Суббота/, I18n.l(time, :format => "%A, %d %B", :locale => :ru))
|
||||
assert_match(/суббота/, I18n.l(time, :format => "%d %B (%A)", :locale => :ru))
|
||||
end
|
||||
|
||||
test "localize: using month names from lambdas" do
|
||||
setup_time_proc_translations
|
||||
time = ::Time.utc(2008, 3, 1, 6, 0)
|
||||
assert_match(/марта/, I18n.l(time, :format => "%d %B %Y", :locale => :ru))
|
||||
assert_match(/Март /, I18n.l(time, :format => "%B %Y", :locale => :ru))
|
||||
end
|
||||
|
||||
test "localize: using abbreviated day names from lambdas" do
|
||||
setup_time_proc_translations
|
||||
time = ::Time.utc(2008, 3, 1, 6, 0)
|
||||
assert_match(/марта/, I18n.l(time, :format => "%d %b %Y", :locale => :ru))
|
||||
assert_match(/март /, I18n.l(time, :format => "%b %Y", :locale => :ru))
|
||||
end
|
||||
|
||||
test "localize Date: given a format that resolves to a Proc it calls the Proc with the object" do
|
||||
setup_time_proc_translations
|
||||
date = ::Date.new(2008, 3, 1)
|
||||
assert_equal '[Sat, 01 Mar 2008, {}]', I18n.l(date, :format => :proc, :locale => :ru)
|
||||
end
|
||||
|
||||
test "localize Date: given a format that resolves to a Proc it calls the Proc with the object and extra options" do
|
||||
setup_time_proc_translations
|
||||
date = ::Date.new(2008, 3, 1)
|
||||
assert_equal %|[Sat, 01 Mar 2008, #{{:foo=>"foo"}}]|, I18n.l(date, :format => :proc, :foo => 'foo', :locale => :ru)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object" do
|
||||
setup_time_proc_translations
|
||||
datetime = ::DateTime.new(2008, 3, 1, 6)
|
||||
assert_equal '[Sat, 01 Mar 2008 06:00:00 +00:00, {}]', I18n.l(datetime, :format => :proc, :locale => :ru)
|
||||
end
|
||||
|
||||
test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object and extra options" do
|
||||
setup_time_proc_translations
|
||||
datetime = ::DateTime.new(2008, 3, 1, 6)
|
||||
assert_equal %|[Sat, 01 Mar 2008 06:00:00 +00:00, #{{:foo=>"foo"}}]|, I18n.l(datetime, :format => :proc, :foo => 'foo', :locale => :ru)
|
||||
end
|
||||
|
||||
test "localize Time: given a format that resolves to a Proc it calls the Proc with the object" do
|
||||
setup_time_proc_translations
|
||||
time = ::Time.utc(2008, 3, 1, 6, 0)
|
||||
assert_equal I18n::Tests::Localization::Procs.inspect_args([time], {}), I18n.l(time, :format => :proc, :locale => :ru)
|
||||
end
|
||||
|
||||
test "localize Time: given a format that resolves to a Proc it calls the Proc with the object and extra options" do
|
||||
setup_time_proc_translations
|
||||
time = ::Time.utc(2008, 3, 1, 6, 0)
|
||||
options = { :foo => 'foo' }
|
||||
assert_equal I18n::Tests::Localization::Procs.inspect_args([time], options), I18n.l(time, **options.merge(:format => :proc, :locale => :ru))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def self.inspect_args(args, kwargs)
|
||||
args << kwargs
|
||||
args = args.map do |arg|
|
||||
case arg
|
||||
when ::Time, ::DateTime
|
||||
arg.strftime('%a, %d %b %Y %H:%M:%S %Z').sub('+0000', '+00:00')
|
||||
when ::Date
|
||||
arg.strftime('%a, %d %b %Y')
|
||||
when Hash
|
||||
arg.delete(:fallback_in_progress)
|
||||
arg.delete(:fallback_original_locale)
|
||||
arg.inspect
|
||||
else
|
||||
arg.inspect
|
||||
end
|
||||
end
|
||||
"[#{args.join(', ')}]"
|
||||
end
|
||||
|
||||
def setup_time_proc_translations
|
||||
I18n.backend.store_translations :ru, {
|
||||
:time => {
|
||||
:formats => {
|
||||
:proc => lambda { |*args, **kwargs| I18n::Tests::Localization::Procs.inspect_args(args, kwargs) }
|
||||
}
|
||||
},
|
||||
:date => {
|
||||
:formats => {
|
||||
:proc => lambda { |*args, **kwargs| I18n::Tests::Localization::Procs.inspect_args(args, kwargs) }
|
||||
},
|
||||
:'day_names' => lambda { |key, options|
|
||||
(options[:format] =~ /^%A/) ?
|
||||
%w(Воскресенье Понедельник Вторник Среда Четверг Пятница Суббота) :
|
||||
%w(воскресенье понедельник вторник среда четверг пятница суббота)
|
||||
},
|
||||
:'month_names' => lambda { |key, options|
|
||||
(options[:format] =~ /(%d|%e)(\s*)?(%B)/) ?
|
||||
%w(января февраля марта апреля мая июня июля августа сентября октября ноября декабря).unshift(nil) :
|
||||
%w(Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь).unshift(nil)
|
||||
},
|
||||
:'abbr_month_names' => lambda { |key, options|
|
||||
(options[:format] =~ /(%d|%e)(\s*)(%b)/) ?
|
||||
%w(янв. февр. марта апр. мая июня июля авг. сент. окт. нояб. дек.).unshift(nil) :
|
||||
%w(янв. февр. март апр. май июнь июль авг. сент. окт. нояб. дек.).unshift(nil)
|
||||
},
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,103 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Localization
|
||||
module Time
|
||||
def setup
|
||||
super
|
||||
setup_time_translations
|
||||
@time = ::Time.utc(2008, 3, 1, 6, 0)
|
||||
@other_time = ::Time.utc(2008, 3, 1, 18, 0)
|
||||
end
|
||||
|
||||
test "localize Time: given the short format it uses it" do
|
||||
assert_equal '01. Mär 06:00', I18n.l(@time, :format => :short, :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given the long format it uses it" do
|
||||
assert_equal '01. März 2008 06:00', I18n.l(@time, :format => :long, :locale => :de)
|
||||
end
|
||||
|
||||
# TODO Seems to break on Windows because ENV['TZ'] is ignored. What's a better way to do this?
|
||||
# def test_localize_given_the_default_format_it_uses_it
|
||||
# assert_equal 'Sa, 01. Mar 2008 06:00:00 +0000', I18n.l(@time, :format => :default, :locale => :de)
|
||||
# end
|
||||
|
||||
test "localize Time: given a day name format it returns the correct day name" do
|
||||
assert_equal 'Samstag', I18n.l(@time, :format => '%A', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given a uppercased day name format it returns the correct day name in upcase" do
|
||||
assert_equal 'samstag'.upcase, I18n.l(@time, :format => '%^A', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given an abbreviated day name format it returns the correct abbreviated day name" do
|
||||
assert_equal 'Sa', I18n.l(@time, :format => '%a', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do
|
||||
assert_equal 'sa'.upcase, I18n.l(@time, :format => '%^a', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given a month name format it returns the correct month name" do
|
||||
assert_equal 'März', I18n.l(@time, :format => '%B', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given a uppercased month name format it returns the correct month name in upcase" do
|
||||
assert_equal 'märz'.upcase, I18n.l(@time, :format => '%^B', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given an abbreviated month name format it returns the correct abbreviated month name" do
|
||||
assert_equal 'Mär', I18n.l(@time, :format => '%b', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given an abbreviated and uppercased month name format it returns the correct abbreviated month name in upcase" do
|
||||
assert_equal 'mär'.upcase, I18n.l(@time, :format => '%^b', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given a date format with the month name upcased it returns the correct value" do
|
||||
assert_equal '1. FEBRUAR 2008', I18n.l(::Time.utc(2008, 2, 1, 6, 0), :format => "%-d. %^B %Y", :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given missing translations it returns the correct error message" do
|
||||
assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@time, :format => '%b', :locale => :fr)
|
||||
end
|
||||
|
||||
test "localize Time: given a meridian indicator format it returns the correct meridian indicator" do
|
||||
assert_equal 'AM', I18n.l(@time, :format => '%p', :locale => :de)
|
||||
assert_equal 'PM', I18n.l(@other_time, :format => '%p', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given a meridian indicator format it returns the correct meridian indicator in upcase" do
|
||||
assert_equal 'am', I18n.l(@time, :format => '%P', :locale => :de)
|
||||
assert_equal 'pm', I18n.l(@other_time, :format => '%P', :locale => :de)
|
||||
end
|
||||
|
||||
test "localize Time: given an unknown format it does not fail" do
|
||||
assert_nothing_raised { I18n.l(@time, :format => '%x') }
|
||||
end
|
||||
|
||||
test "localize Time: given a format is missing it raises I18n::MissingTranslationData" do
|
||||
assert_raises(I18n::MissingTranslationData) { I18n.l(@time, :format => :missing) }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def setup_time_translations
|
||||
I18n.backend.store_translations :de, {
|
||||
:time => {
|
||||
:formats => {
|
||||
:default => "%a, %d. %b %Y %H:%M:%S %z",
|
||||
:short => "%d. %b %H:%M",
|
||||
:long => "%d. %B %Y %H:%M",
|
||||
},
|
||||
:am => 'am',
|
||||
:pm => 'pm'
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Lookup
|
||||
def setup
|
||||
super
|
||||
I18n.backend.store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' }, :falsy => false, :truthy => true,
|
||||
:string => "a", :array => %w(a b c), :hash => { "a" => "b" })
|
||||
end
|
||||
|
||||
test "lookup: it returns a string" do
|
||||
assert_equal("a", I18n.t(:string))
|
||||
end
|
||||
|
||||
test "lookup: it returns hash" do
|
||||
assert_equal({ :a => "b" }, I18n.t(:hash))
|
||||
end
|
||||
|
||||
test "lookup: it returns an array" do
|
||||
assert_equal(%w(a b c), I18n.t(:array))
|
||||
end
|
||||
|
||||
test "lookup: it returns a native true" do
|
||||
assert I18n.t(:truthy) === true
|
||||
end
|
||||
|
||||
test "lookup: it returns a native false" do
|
||||
assert I18n.t(:falsy) === false
|
||||
end
|
||||
|
||||
test "lookup: given a missing key, no default and no raise option it returns an error message" do
|
||||
assert_equal "Translation missing: en.missing", I18n.t(:missing)
|
||||
end
|
||||
|
||||
test "lookup: given a missing key, no default and the raise option it raises MissingTranslationData" do
|
||||
assert_raises(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) }
|
||||
end
|
||||
|
||||
test "lookup: does not raise an exception if no translation data is present for the given locale" do
|
||||
assert_nothing_raised { I18n.t(:foo, :locale => :xx) }
|
||||
end
|
||||
|
||||
test "lookup: does not modify the options hash" do
|
||||
options = {}
|
||||
assert_equal "a", I18n.t(:string, **options)
|
||||
assert_equal({}, options)
|
||||
assert_nothing_raised { I18n.t(:string, **options.freeze) }
|
||||
end
|
||||
|
||||
test "lookup: given an array of keys it translates all of them" do
|
||||
assert_equal %w(bar baz), I18n.t([:bar, :baz], :scope => [:foo])
|
||||
end
|
||||
|
||||
test "lookup: using a custom scope separator" do
|
||||
# data must have been stored using the custom separator when using the ActiveRecord backend
|
||||
I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' })
|
||||
assert_equal 'bar', I18n.t('foo|bar', :separator => '|')
|
||||
end
|
||||
|
||||
# In fact it probably *should* fail but Rails currently relies on using the default locale instead.
|
||||
# So we'll stick to this for now until we get it fixed in Rails.
|
||||
test "lookup: given nil as a locale it does not raise but use the default locale" do
|
||||
# assert_raises(I18n::InvalidLocale) { I18n.t(:bar, :locale => nil) }
|
||||
assert_nothing_raised { I18n.t(:bar, :locale => nil) }
|
||||
end
|
||||
|
||||
test "lookup: a resulting String is not frozen" do
|
||||
assert !I18n.t(:string).frozen?
|
||||
end
|
||||
|
||||
test "lookup: a resulting Array is not frozen" do
|
||||
assert !I18n.t(:array).frozen?
|
||||
end
|
||||
|
||||
test "lookup: a resulting Hash is not frozen" do
|
||||
assert !I18n.t(:hash).frozen?
|
||||
end
|
||||
|
||||
# Addresses issue: #599
|
||||
test "lookup: only interpolates once when resolving symbols" do
|
||||
I18n.backend.store_translations(:en, foo: :bar, bar: '%{value}')
|
||||
assert_equal '%{dont_interpolate_me}', I18n.t(:foo, value: '%{dont_interpolate_me}')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Pluralization
|
||||
test "pluralization: given 0 it returns the :zero translation if it is defined" do
|
||||
assert_equal 'zero', I18n.t(:default => { :zero => 'zero' }, :count => 0)
|
||||
end
|
||||
|
||||
test "pluralization: given 0 it returns the :other translation if :zero is not defined" do
|
||||
assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 0)
|
||||
end
|
||||
|
||||
test "pluralization: given 1 it returns the singular translation" do
|
||||
assert_equal 'bar', I18n.t(:default => { :one => 'bar' }, :count => 1)
|
||||
end
|
||||
|
||||
test "pluralization: given 2 it returns the :other translation" do
|
||||
assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 2)
|
||||
end
|
||||
|
||||
test "pluralization: given 3 it returns the :other translation" do
|
||||
assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 3)
|
||||
end
|
||||
|
||||
test "pluralization: given nil it returns the whole entry" do
|
||||
assert_equal({ :one => 'bar' }, I18n.t(:default => { :one => 'bar' }, :count => nil))
|
||||
end
|
||||
|
||||
test "pluralization: given incomplete pluralization data it raises I18n::InvalidPluralizationData" do
|
||||
assert_raises(I18n::InvalidPluralizationData) { I18n.t(:default => { :one => 'bar' }, :count => 2) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Tests
|
||||
module Procs
|
||||
test "lookup: given a translation is a proc it calls the proc with the key and interpolation values" do
|
||||
I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| I18n::Tests::Procs.filter_args(*args) })
|
||||
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(:a_lambda, :foo => 'foo')
|
||||
end
|
||||
|
||||
test "lookup: given a translation is a proc it passes the interpolation values as keyword arguments" do
|
||||
I18n.backend.store_translations(:en, :a_lambda => lambda { |key, foo:, **| I18n::Tests::Procs.filter_args(key, foo: foo) })
|
||||
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(:a_lambda, :foo => 'foo')
|
||||
end
|
||||
|
||||
test "defaults: given a default is a Proc it calls it with the key and interpolation values" do
|
||||
proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
|
||||
assert_equal %|[nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo')
|
||||
end
|
||||
|
||||
test "defaults: given a default is a key that resolves to a Proc it calls it with the key and interpolation values" do
|
||||
the_lambda = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
|
||||
I18n.backend.store_translations(:en, :a_lambda => the_lambda)
|
||||
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => :a_lambda, :foo => 'foo')
|
||||
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => [nil, :a_lambda], :foo => 'foo')
|
||||
end
|
||||
|
||||
test "interpolation: given an interpolation value is a lambda it calls it with key and values before interpolating it" do
|
||||
proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
|
||||
if RUBY_VERSION < "3.4"
|
||||
assert_match %r(\[\{:foo=>#<Proc.*>\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc)
|
||||
else
|
||||
assert_match %r(\[\{foo: #<Proc.*>\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc)
|
||||
end
|
||||
end
|
||||
|
||||
test "interpolation: given a key resolves to a Proc that returns a string then interpolation still works" do
|
||||
proc = lambda { |*args| "%{foo}: " + I18n::Tests::Procs.filter_args(*args) }
|
||||
assert_equal %|foo: [nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo')
|
||||
end
|
||||
|
||||
test "pluralization: given a key resolves to a Proc that returns valid data then pluralization still works" do
|
||||
proc = lambda { |*args| { :zero => 'zero', :one => 'one', :other => 'other' } }
|
||||
assert_equal 'zero', I18n.t(:default => proc, :count => 0)
|
||||
assert_equal 'one', I18n.t(:default => proc, :count => 1)
|
||||
assert_equal 'other', I18n.t(:default => proc, :count => 2)
|
||||
end
|
||||
|
||||
test "lookup: given the option :resolve => false was passed it does not resolve proc translations" do
|
||||
I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| I18n::Tests::Procs.filter_args(*args) })
|
||||
assert_equal Proc, I18n.t(:a_lambda, :resolve => false).class
|
||||
end
|
||||
|
||||
test "lookup: given the option :resolve => false was passed it does not resolve proc default" do
|
||||
assert_equal Proc, I18n.t(nil, :default => lambda { |*args| I18n::Tests::Procs.filter_args(*args) }, :resolve => false).class
|
||||
end
|
||||
|
||||
|
||||
def self.filter_args(*args)
|
||||
args.map do |arg|
|
||||
if arg.is_a?(Hash)
|
||||
arg.delete(:fallback_in_progress)
|
||||
arg.delete(:fallback_original_locale)
|
||||
arg.delete(:skip_interpolation)
|
||||
end
|
||||
arg
|
||||
end.inspect
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
module Utils
|
||||
class << self
|
||||
if Hash.method_defined?(:except)
|
||||
def except(hash, *keys)
|
||||
hash.except(*keys)
|
||||
end
|
||||
else
|
||||
def except(hash, *keys)
|
||||
hash = hash.dup
|
||||
keys.each { |k| hash.delete(k) }
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
def deep_merge(hash, other_hash, &block)
|
||||
deep_merge!(hash.dup, other_hash, &block)
|
||||
end
|
||||
|
||||
def deep_merge!(hash, other_hash, &block)
|
||||
hash.merge!(other_hash) do |key, this_val, other_val|
|
||||
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
||||
deep_merge(this_val, other_val, &block)
|
||||
elsif block_given?
|
||||
yield key, this_val, other_val
|
||||
else
|
||||
other_val
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def deep_symbolize_keys(hash)
|
||||
hash.each_with_object({}) do |(key, value), result|
|
||||
result[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize_keys_in_object(value)
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def deep_symbolize_keys_in_object(value)
|
||||
case value
|
||||
when Hash
|
||||
deep_symbolize_keys(value)
|
||||
when Array
|
||||
value.map { |e| deep_symbolize_keys_in_object(e) }
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module I18n
|
||||
VERSION = "1.14.7"
|
||||
end
|
||||
Reference in New Issue
Block a user