Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+507
View File
@@ -0,0 +1,507 @@
# 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
if Fiber.respond_to?(:[])
current = Fiber[:i18n_config] || self.config = I18n::Config.new
if current.respond_to?(:owned_by?) && !current.owned_by?(Fiber.current)
current = current.dup
Fiber[:i18n_config] = current
end
current
else
Thread.current.thread_variable_get(:i18n_config) ||
Thread.current.thread_variable_set(:i18n_config, I18n::Config.new)
end
end
# Gets a mutable I18n configuration object.
def writable_config
current = config
return current unless current.frozen?
current = current.dup
if Fiber.respond_to?(:[])
Fiber[:i18n_config] = current
else
Thread.current.thread_variable_set(:i18n_config, current)
end
current
end
# Sets I18n configuration object.
def config=(value)
if Fiber.respond_to?(:[])
Fiber[:i18n_config] = value
value.owner = Fiber.current if value.respond_to?(:owner=) && !value.frozen?
else
Thread.current.thread_variable_set(:i18n_config, value)
end
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)
writable_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
+22
View File
@@ -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,127 @@
# 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
if Fiber.respond_to?(:[])
Fiber[:i18n_fallbacks] || @@fallbacks
else
Thread.current[:i18n_fallbacks] || @@fallbacks
end
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
if Fiber.respond_to?(:[])
Fiber[:i18n_fallbacks] = @@fallbacks
else
Thread.current[:i18n_fallbacks] = @@fallbacks
end
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,122 @@
# 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 = +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)
MUTEX.synchronize { 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,109 @@
# 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", "Ǫ"=>"O", "ǫ"=>"o", "Ǭ"=>"O",
"ǭ"=>"o"
}.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 == :none ? 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
+199
View File
@@ -0,0 +1,199 @@
# 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
def initialize
@owner = Fiber.current
end
def owned_by?(fiber)
@owner == fiber
end
def owner=(fiber)
@owner = fiber
end
def initialize_copy(other)
@owner = Fiber.current
end
# Returns a copied configuration with the provided attributes set.
def with(**attrs)
dup.tap do |copy|
attrs.each do |name, value|
copy.public_send("#{name}=", value)
end
end
end
# Sets this configuration as the current one for the active execution context.
# The stored configuration is frozen to avoid sharing mutable state between fibers.
def set!
self.owner = Fiber.current unless frozen?
freeze
I18n.config = self
self
end
# Sets the current locale pseudo-globally, i.e. in the Thread.current or Fiber local 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
+155
View File
@@ -0,0 +1,155 @@
# frozen_string_literal: true
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
+28
View File
@@ -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
+8
View File
@@ -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,21 @@
# frozen_string_literal: true
module I18n
class Middleware
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
ensure
if Fiber.respond_to?(:[])
Fiber[:i18n_config] = I18n::Config.new
else
Thread.current.thread_variable_set(:i18n_config, I18n::Config.new)
end
end
end
end
+14
View File
@@ -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'.dup.force_encoding(Encoding::US_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'.dup.force_encoding(Encoding::US_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
+55
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module I18n
VERSION = "1.15.2"
end