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
@@ -0,0 +1,314 @@
# frozen_string_literal: true
require 'yaml'
require 'json'
module I18n
module Backend
module Base
include I18n::Backend::Transliterator
# Accepts a list of paths to translation files. Loads translations from
# plain Ruby (*.rb), YAML files (*.yml), or JSON files (*.json). See #load_rb, #load_yml, and #load_json
# for details.
def load_translations(*filenames)
filenames = I18n.load_path if filenames.empty?
filenames.flatten.each do |filename|
loaded_translations = load_file(filename)
yield filename, loaded_translations if block_given?
end
end
# This method receives a locale, a data hash and options for storing translations.
# Should be implemented
def store_translations(locale, data, options = EMPTY_HASH)
raise NotImplementedError
end
def translate(locale, key, options = EMPTY_HASH)
raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
raise InvalidLocale.new(locale) unless locale
return nil if key.nil? && !options.key?(:default)
entry = lookup(locale, key, options[:scope], options) unless key.nil?
if entry.nil? && options.key?(:default)
entry = default(locale, key, options[:default], options)
else
entry = resolve_entry(locale, key, entry, options)
end
count = options[:count]
if entry.nil? && (subtrees? || !count)
if (options.key?(:default) && !options[:default].nil?) || !options.key?(:default)
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
end
end
entry = entry.dup if entry.is_a?(String)
entry = pluralize(locale, entry, count) if count
if entry.nil? && !subtrees?
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
end
deep_interpolation = options[:deep_interpolation]
skip_interpolation = options[:skip_interpolation]
values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
if !skip_interpolation && values && !values.empty?
entry = if deep_interpolation
deep_interpolate(locale, entry, values)
else
interpolate(locale, entry, values)
end
elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
raise ReservedInterpolationKey.new($1.to_sym, entry)
end
entry
end
def exists?(locale, key, options = EMPTY_HASH)
lookup(locale, key, options[:scope]) != nil
end
# Acts the same as +strftime+, but uses a localized version of the
# format string. Takes a key from the date/time formats translations as
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
def localize(locale, object, format = :default, options = EMPTY_HASH)
if object.nil? && options.include?(:default)
return options[:default]
end
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
if Symbol === format
key = format
type = object.respond_to?(:sec) ? 'time' : 'date'
options = options.merge(:raise => true, :object => object, :locale => locale)
format = I18n.t(:"#{type}.formats.#{key}", **options)
end
format = translate_localization_format(locale, object, format, options)
object.strftime(format)
end
# Returns an array of locales for which translations are available
# ignoring the reserved translation meta data key :i18n.
def available_locales
raise NotImplementedError
end
def reload!
eager_load! if eager_loaded?
end
def eager_load!
@eager_loaded = true
end
protected
def eager_loaded?
@eager_loaded ||= false
end
# The method which actually looks up for the translation in the store.
def lookup(locale, key, scope = [], options = EMPTY_HASH)
raise NotImplementedError
end
def subtrees?
true
end
# Evaluates defaults.
# If given subject is an Array, it walks the array and returns the
# first translation that can be resolved. Otherwise it tries to resolve
# the translation directly.
def default(locale, object, subject, options = EMPTY_HASH)
if options.size == 1 && options.has_key?(:default)
options = {}
else
options = Utils.except(options, :default)
end
case subject
when Array
subject.each do |item|
result = resolve(locale, object, item, options)
return result unless result.nil?
end and nil
else
resolve(locale, object, subject, options)
end
end
# Resolves a translation.
# If the given subject is a Symbol, it will be translated with the
# given options. If it is a Proc then it will be evaluated. All other
# subjects will be returned directly.
def resolve(locale, object, subject, options = EMPTY_HASH)
return subject if options[:resolve] == false
result = catch(:exception) do
case subject
when Symbol
I18n.translate(
subject,
**options.merge(
:locale => locale,
:throw => true,
:skip_interpolation => true
)
)
when Proc
date_or_time = options.delete(:object) || object
resolve(locale, object, subject.call(date_or_time, **options))
else
subject
end
end
result unless result.is_a?(MissingTranslation)
end
alias_method :resolve_entry, :resolve
# Picks a translation from a pluralized mnemonic subkey according to English
# pluralization rules :
# - It will pick the :one subkey if count is equal to 1.
# - It will pick the :other subkey otherwise.
# - It will pick the :zero subkey in the special case where count is
# equal to 0 and there is a :zero subkey present. This behaviour is
# not standard with regards to the CLDR pluralization rules.
# Other backends can implement more flexible or complex pluralization rules.
def pluralize(locale, entry, count)
entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
return entry unless entry.is_a?(Hash) && count
key = pluralization_key(entry, count)
raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
entry[key]
end
# Interpolates values into a given subject.
#
# if the given subject is a string then:
# method interpolates "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
# # => "file test.txt opened by %{user}"
#
# if the given subject is an array then:
# each element of the array is recursively interpolated (until it finds a string)
# method interpolates ["yes, %{user}", ["maybe no, %{user}", "no, %{user}"]], :user => "bartuz"
# # => ["yes, bartuz", ["maybe no, bartuz", "no, bartuz"]]
def interpolate(locale, subject, values = EMPTY_HASH)
return subject if values.empty?
case subject
when ::String then I18n.interpolate(subject, values)
when ::Array then subject.map { |element| interpolate(locale, element, values) }
else
subject
end
end
# Deep interpolation
#
# deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
# ann: 'good', john: 'big'
# #=> { people: { ann: "Ann is good", john: "John is big" } }
def deep_interpolate(locale, data, values = EMPTY_HASH)
return data if values.empty?
case data
when ::String
I18n.interpolate(data, values)
when ::Hash
data.each_with_object({}) do |(k, v), result|
result[k] = deep_interpolate(locale, v, values)
end
when ::Array
data.map do |v|
deep_interpolate(locale, v, values)
end
else
data
end
end
# Loads a single translations file by delegating to #load_rb or
# #load_yml depending on the file extension and directly merges the
# data to the existing translations. Raises I18n::UnknownFileType
# for all other file extensions.
def load_file(filename)
type = File.extname(filename).tr('.', '').downcase
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
data, keys_symbolized = send(:"load_#{type}", filename)
unless data.is_a?(Hash)
raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
end
data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
data
end
# Loads a plain Ruby translations file. eval'ing the file must yield
# a Hash containing translation data with locales as toplevel keys.
def load_rb(filename)
translations = eval(IO.read(filename), binding, filename.to_s)
[translations, false]
end
# Loads a YAML translations file. The data must have locales as
# toplevel keys.
def load_yml(filename)
begin
if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
[YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
else
[YAML.load_file(filename), false]
end
rescue TypeError, ScriptError, StandardError => e
raise InvalidLocaleData.new(filename, e.inspect)
end
end
alias_method :load_yaml, :load_yml
# Loads a JSON translations file. The data must have locales as
# toplevel keys.
def load_json(filename)
begin
# Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
if ::JSON.respond_to?(:load_file)
[::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
else
[::JSON.parse(File.read(filename)), false]
end
rescue TypeError, StandardError => e
raise InvalidLocaleData.new(filename, e.inspect)
end
end
def translate_localization_format(locale, object, format, options)
format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
case match
when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
end
end
rescue MissingTranslationData => e
e.message
end
def pluralization_key(entry, count)
key = :zero if count == 0 && entry.has_key?(:zero)
key ||= count == 1 ? :one : :other
end
end
end
end
@@ -0,0 +1,113 @@
# frozen_string_literal: true
# This module allows you to easily cache all responses from the backend - thus
# speeding up the I18n aspects of your application quite a bit.
#
# To enable caching you can simply include the Cache module to the Simple
# backend - or whatever other backend you are using:
#
# I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
#
# You will also need to set a cache store implementation that you want to use:
#
# I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
#
# You can use any cache implementation you want that provides the same API as
# ActiveSupport::Cache (only the methods #fetch and #write are being used).
#
# The cache_key implementation by default assumes you pass values that return
# a valid key from #hash (see
# https://www.ruby-doc.org/core/classes/Object.html#M000337). However, you can
# configure your own digest method via which responds to #hexdigest (see
# https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/Digest.html):
#
# I18n.cache_key_digest = OpenSSL::Digest::SHA256.new
#
# If you use a lambda as a default value in your translation like this:
#
# I18n.t(:"date.order", :default => lambda {[:month, :day, :year]})
#
# Then you will always have a cache miss, because each time this method
# is called the lambda will have a different hash value. If you know
# the result of the lambda is a constant as in the example above, then
# to cache this you can make the lambda a constant, like this:
#
# DEFAULT_DATE_ORDER = lambda {[:month, :day, :year]}
# ...
# I18n.t(:"date.order", :default => DEFAULT_DATE_ORDER)
#
# If the lambda may result in different values for each call then consider
# also using the Memoize backend.
#
module I18n
class << self
@@cache_store = nil
@@cache_namespace = nil
@@cache_key_digest = nil
def cache_store
@@cache_store
end
def cache_store=(store)
@@cache_store = store
end
def cache_namespace
@@cache_namespace
end
def cache_namespace=(namespace)
@@cache_namespace = namespace
end
def cache_key_digest
@@cache_key_digest
end
def cache_key_digest=(key_digest)
@@cache_key_digest = key_digest
end
def perform_caching?
!cache_store.nil?
end
end
module Backend
# TODO Should the cache be cleared if new translations are stored?
module Cache
def translate(locale, key, options = EMPTY_HASH)
I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
end
protected
def fetch(cache_key, &block)
result = _fetch(cache_key, &block)
throw(:exception, result) if result.is_a?(MissingTranslation)
result = result.dup if result.frozen? rescue result
result
end
def _fetch(cache_key, &block)
result = I18n.cache_store.read(cache_key)
return result unless result.nil?
result = catch(:exception, &block)
I18n.cache_store.write(cache_key, result) unless result.is_a?(Proc)
result
end
def cache_key(locale, key, options)
# This assumes that only simple, native Ruby values are passed to I18n.translate.
"i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{digest_item(options)}"
end
private
def digest_item(key)
I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.to_s.hash
end
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'openssl'
module I18n
module Backend
# Overwrites the Base load_file method to cache loaded file contents.
module CacheFile
# Optionally provide path_roots array to normalize filename paths,
# to make the cached i18n data portable across environments.
attr_accessor :path_roots
protected
# Track loaded translation files in the `i18n.load_file` scope,
# and skip loading the file if its contents are still up-to-date.
def load_file(filename)
initialized = !respond_to?(:initialized?) || initialized?
key = I18n::Backend::Flatten.escape_default_separator(normalized_path(filename))
old_mtime, old_digest = initialized && lookup(:i18n, key, :load_file)
return if (mtime = File.mtime(filename).to_i) == old_mtime ||
(digest = OpenSSL::Digest::SHA256.file(filename).hexdigest) == old_digest
super
store_translations(:i18n, load_file: { key => [mtime, digest] })
end
# Translate absolute filename to relative path for i18n key.
def normalized_path(file)
return file unless path_roots
path = path_roots.find(&file.method(:start_with?)) ||
raise(InvalidLocaleData.new(file, 'outside expected path roots'))
file.sub(path, path_roots.index(path).to_s)
end
end
end
end
@@ -0,0 +1,56 @@
# frozen_string_literal: true
# The Cascade module adds the ability to do cascading lookups to backends that
# are compatible to the Simple backend.
#
# By cascading lookups we mean that for any key that can not be found the
# Cascade module strips one segment off the scope part of the key and then
# tries to look up the key in that scope.
#
# E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then
# the segment :bar will be stripped off the scope part :"foo.bar" and the new
# scope :foo will be used to look up the key :baz. If that does not succeed
# then the remaining scope segment :foo will be omitted, too, and again the
# key :baz will be looked up (now with no scope).
#
# To enable a cascading lookup one passes the :cascade option:
#
# I18n.t(:'foo.bar.baz', :cascade => true)
#
# This will return the first translation found for :"foo.bar.baz", :"foo.baz"
# or :baz in this order.
#
# The cascading lookup takes precedence over resolving any given defaults.
# I.e. defaults will kick in after the cascading lookups haven't succeeded.
#
# This behavior is useful for libraries like ActiveRecord validations where
# the library wants to give users a bunch of more or less fine-grained options
# of scopes for a particular key.
#
# Thanks to Clemens Kofler for the initial idea and implementation! See
# http://github.com/clemens/i18n-cascading-backend
module I18n
module Backend
module Cascade
def lookup(locale, key, scope = [], options = EMPTY_HASH)
return super unless cascade = options[:cascade]
cascade = { :step => 1 } unless cascade.is_a?(Hash)
step = cascade[:step] || 1
offset = cascade[:offset] || 1
separator = options[:separator] || I18n.default_separator
skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true
scope = I18n.normalize_keys(nil, key, scope, separator)
key = (scope.slice!(-offset, offset) || []).join(separator)
begin
result = super
return result unless result.nil?
scope = scope.dup
end while (!scope.empty? || !skip_root) && scope.slice!(-step, step)
end
end
end
end
@@ -0,0 +1,130 @@
# frozen_string_literal: true
module I18n
module Backend
# Backend that chains multiple other backends and checks each of them when
# a translation needs to be looked up. This is useful when you want to use
# standard translations with a Simple backend but store custom application
# translations in a database or other backends.
#
# To use the Chain backend instantiate it and set it to the I18n module.
# You can add chained backends through the initializer or backends
# accessor:
#
# # preserves the existing Simple backend set to I18n.backend
# I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
#
# The implementation assumes that all backends added to the Chain implement
# a lookup method with the same API as Simple backend does.
#
# Fallback translations using the :default option are only used by the last backend of a chain.
class Chain
module Implementation
include Base
attr_accessor :backends
def initialize(*backends)
self.backends = backends
end
def initialized?
backends.all? do |backend|
backend.instance_eval do
return false unless initialized?
end
end
true
end
def reload!
backends.each { |backend| backend.reload! }
end
def eager_load!
backends.each { |backend| backend.eager_load! }
end
def store_translations(locale, data, options = EMPTY_HASH)
backends.first.store_translations(locale, data, options)
end
def available_locales
backends.map { |backend| backend.available_locales }.flatten.uniq
end
def translate(locale, key, default_options = EMPTY_HASH)
namespace = nil
options = Utils.except(default_options, :default)
backends.each do |backend|
catch(:exception) do
options = default_options if backend == backends.last
translation = backend.translate(locale, key, options)
if namespace_lookup?(translation, options)
namespace = _deep_merge(translation, namespace || {})
elsif !translation.nil? || (options.key?(:default) && options[:default].nil?)
return translation
end
end
end
return namespace if namespace
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
end
def exists?(locale, key, options = EMPTY_HASH)
backends.any? do |backend|
backend.exists?(locale, key, options)
end
end
def localize(locale, object, format = :default, options = EMPTY_HASH)
backends.each do |backend|
catch(:exception) do
result = backend.localize(locale, object, format, options) and return result
end
end
throw(:exception, I18n::MissingTranslation.new(locale, format, options))
end
protected
def init_translations
backends.each do |backend|
backend.send(:init_translations)
end
end
def translations
backends.reverse.each_with_object({}) do |backend, memo|
partial_translations = backend.instance_eval do
init_translations unless initialized?
translations
end
Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a }
end
end
def namespace_lookup?(result, options)
result.is_a?(Hash) && !options.has_key?(:count)
end
private
# This is approximately what gets used in ActiveSupport.
# However since we are not guaranteed to run in an ActiveSupport context
# it is wise to have our own copy. We underscore it
# to not pollute the namespace of the including class.
def _deep_merge(hash, other_hash)
copy = hash.dup
other_hash.each_pair do |k,v|
value_from_other = hash[k]
copy[k] = value_from_other.is_a?(Hash) && v.is_a?(Hash) ? _deep_merge(value_from_other, v) : v
end
copy
end
end
include Implementation
end
end
end
@@ -0,0 +1,119 @@
# frozen_string_literal: true
# I18n locale fallbacks are useful when you want your application to use
# translations from other locales when translations for the current locale are
# missing. E.g. you might want to use :en translations when translations in
# your applications main locale :de are missing.
#
# To enable locale fallbacks you can simply include the Fallbacks module to
# the Simple backend - or whatever other backend you are using:
#
# I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
module I18n
@@fallbacks = nil
class << self
# Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
def fallbacks
@@fallbacks ||= I18n::Locale::Fallbacks.new
Thread.current[:i18n_fallbacks] || @@fallbacks
end
# Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
def fallbacks=(fallbacks)
@@fallbacks = fallbacks.is_a?(Array) ? I18n::Locale::Fallbacks.new(fallbacks) : fallbacks
Thread.current[:i18n_fallbacks] = @@fallbacks
end
end
module Backend
module Fallbacks
# Overwrites the Base backend translate method so that it will try each
# locale given by I18n.fallbacks for the given locale. E.g. for the
# locale :"de-DE" it might try the locales :"de-DE", :de and :en
# (depends on the fallbacks implementation) until it finds a result with
# the given options. If it does not find any result for any of the
# locales it will then throw MissingTranslation as usual.
#
# The default option takes precedence over fallback locales only when
# it's a Symbol. When the default contains a String, Proc or Hash
# it is evaluated last after all the fallback locales have been tried.
def translate(locale, key, options = EMPTY_HASH)
return super unless options.fetch(:fallback, true)
return super if options[:fallback_in_progress]
default = extract_non_symbol_default!(options) if options[:default]
fallback_options = options.merge(:fallback_in_progress => true, fallback_original_locale: locale)
I18n.fallbacks[locale].each do |fallback|
begin
catch(:exception) do
result = super(fallback, key, fallback_options)
unless result.nil?
on_fallback(locale, fallback, key, options) if locale.to_s != fallback.to_s
return result
end
end
rescue I18n::InvalidLocale
# we do nothing when the locale is invalid, as this is a fallback anyways.
end
end
return if options.key?(:default) && options[:default].nil?
return super(locale, nil, options.merge(:default => default)) if default
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
end
def resolve_entry(locale, object, subject, options = EMPTY_HASH)
return subject if options[:resolve] == false
result = catch(:exception) do
options.delete(:fallback_in_progress) if options.key?(:fallback_in_progress)
case subject
when Symbol
I18n.translate(subject, **options.merge(
:locale => options[:fallback_original_locale],
:throw => true,
:skip_interpolation => true
))
when Proc
date_or_time = options.delete(:object) || object
resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
else
subject
end
end
result unless result.is_a?(MissingTranslation)
end
def extract_non_symbol_default!(options)
defaults = [options[:default]].flatten
first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
if first_non_symbol_default
options[:default] = defaults[0, defaults.index(first_non_symbol_default)]
end
return first_non_symbol_default
end
def exists?(locale, key, options = EMPTY_HASH)
return super unless options.fetch(:fallback, true)
I18n.fallbacks[locale].each do |fallback|
begin
return true if super(fallback, key, options)
rescue I18n::InvalidLocale
# we do nothing when the locale is invalid, as this is a fallback anyways.
end
end
false
end
private
# Overwrite on_fallback to add specified logic when the fallback succeeds.
def on_fallback(_original_locale, _fallback_locale, _key, _options)
nil
end
end
end
end
@@ -0,0 +1,118 @@
# frozen_string_literal: true
module I18n
module Backend
# This module contains several helpers to assist flattening translations.
# You may want to flatten translations for:
#
# 1) speed up lookups, as in the Memoize backend;
# 2) In case you want to store translations in a data store, as in ActiveRecord backend;
#
# You can check both backends above for some examples.
# This module also keeps all links in a hash so they can be properly resolved when flattened.
module Flatten
SEPARATOR_ESCAPE_CHAR = "\001"
FLATTEN_SEPARATOR = "."
# normalize_keys the flatten way. This method is significantly faster
# and creates way less objects than the one at I18n.normalize_keys.
# It also handles escaping the translation keys.
def self.normalize_flat_keys(locale, key, scope, separator)
keys = [scope, key]
keys.flatten!
keys.compact!
separator ||= I18n.default_separator
if separator != FLATTEN_SEPARATOR
from_str = "#{FLATTEN_SEPARATOR}#{separator}"
to_str = "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}"
keys.map! { |k| k.to_s.tr from_str, to_str }
end
keys.join(".")
end
# Receives a string and escape the default separator.
def self.escape_default_separator(key) #:nodoc:
key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
end
# Shortcut to I18n::Backend::Flatten.normalize_flat_keys
# and then resolve_links.
def normalize_flat_keys(locale, key, scope, separator)
key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator)
resolve_link(locale, key)
end
# Store flattened links.
def links
@links ||= I18n.new_double_nested_cache
end
# Flatten keys for nested Hashes by chaining up keys:
#
# >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind
# => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }
#
def flatten_keys(hash, escape, prev_key=nil, &block)
hash.each_pair do |key, value|
key = escape_default_separator(key) if escape
curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
yield curr_key, value
flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash)
end
end
# Receives a hash of translations (where the key is a locale and
# the value is another hash) and return a hash with all
# translations flattened.
#
# Nested hashes are included in the flattened hash just if subtree
# is true and Symbols are automatically stored as links.
def flatten_translations(locale, data, escape, subtree)
hash = {}
flatten_keys(data, escape) do |key, value|
if value.is_a?(Hash)
hash[key] = value if subtree
else
store_link(locale, key, value) if value.is_a?(Symbol)
hash[key] = value
end
end
hash
end
protected
def store_link(locale, key, link)
links[locale.to_sym][key.to_s] = link.to_s
end
def resolve_link(locale, key)
key, locale = key.to_s, locale.to_sym
links = self.links[locale]
if links.key?(key)
links[key]
elsif link = find_link(locale, key)
store_link(locale, key, key.gsub(*link))
else
key
end
end
def find_link(locale, key) #:nodoc:
links[locale].each_pair do |from, to|
return [from, to] if key[0, from.length] == from
end && nil
end
def escape_default_separator(key) #:nodoc:
I18n::Backend::Flatten.escape_default_separator(key)
end
end
end
end
@@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'i18n/gettext'
require 'i18n/gettext/po_parser'
module I18n
module Backend
# Experimental support for using Gettext po files to store translations.
#
# To use this you can simply include the module to the Simple backend - or
# whatever other backend you are using.
#
# I18n::Backend::Simple.include(I18n::Backend::Gettext)
#
# Now you should be able to include your Gettext translation (*.po) files to
# the +I18n.load_path+ so they're loaded to the backend and you can use them as
# usual:
#
# I18n.load_path += Dir["path/to/locales/*.po"]
#
# Following the Gettext convention this implementation expects that your
# translation files are named by their locales. E.g. the file en.po would
# contain the translations for the English locale.
#
# To translate text <b>you must use</b> one of the translate methods provided by
# I18n::Gettext::Helpers.
#
# include I18n::Gettext::Helpers
# puts _("some string")
#
# Without it strings containing periods (".") will not be translated.
module Gettext
class PoData < Hash
def set_comment(msgid_or_sym, comment)
# ignore
end
end
protected
def load_po(filename)
locale = ::File.basename(filename, '.po').to_sym
data = normalize(locale, parse(filename))
[{ locale => data }, false]
end
def parse(filename)
GetText::PoParser.new.parse(::File.read(filename), PoData.new)
end
def normalize(locale, data)
data.inject({}) do |result, (key, value)|
unless key.nil? || key.empty?
key = key.gsub(I18n::Gettext::CONTEXT_SEPARATOR, '|')
key, value = normalize_pluralization(locale, key, value) if key.index("\000")
parts = key.split('|').reverse
normalized = parts.inject({}) do |_normalized, part|
{ part => _normalized.empty? ? value : _normalized }
end
Utils.deep_merge!(result, normalized)
end
result
end
end
def normalize_pluralization(locale, key, value)
# FIXME po_parser includes \000 chars that can not be turned into Symbols
key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first
keys = I18n::Gettext.plural_keys(locale)
values = value.split("\000")
raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect} on #{locale} locale for msgid #{key.inspect} with values #{values.inspect}" if values.size != keys.size
result = {}
values.each_with_index { |_value, ix| result[keys[ix]] = _value }
[key, result]
end
end
end
end
@@ -0,0 +1,121 @@
# frozen_string_literal: true
# The InterpolationCompiler module contains optimizations that can tremendously
# speed up the interpolation process on the Simple backend.
#
# It works by defining a pre-compiled method on stored translation Strings that
# already bring all the knowledge about contained interpolation variables etc.
# so that the actual recurring interpolation will be very fast.
#
# To enable pre-compiled interpolations you can simply include the
# InterpolationCompiler module to the Simple backend:
#
# I18n::Backend::Simple.include(I18n::Backend::InterpolationCompiler)
#
# Note that InterpolationCompiler does not yield meaningful results and consequently
# should not be used with Ruby 1.9 (YARV) but improves performance everywhere else
# (jRuby, Rubinius).
module I18n
module Backend
module InterpolationCompiler
module Compiler
extend self
TOKENIZER = /(%%?\{[^}]+\})/
def compile_if_an_interpolation(string)
if interpolated_str?(string)
string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
def i18n_interpolate(v = {})
"#{compiled_interpolation_body(string)}"
end
RUBY_EVAL
end
string
end
def interpolated_str?(str)
str.kind_of?(::String) && str =~ TOKENIZER
end
protected
# tokenize("foo %{bar} baz %%{buz}") # => ["foo ", "%{bar}", " baz ", "%%{buz}"]
def tokenize(str)
str.split(TOKENIZER)
end
def compiled_interpolation_body(str)
tokenize(str).map do |token|
token.match(TOKENIZER) ? handle_interpolation_token(token) : escape_plain_str(token)
end.join
end
def handle_interpolation_token(token)
token.start_with?('%%') ? token[1..] : compile_interpolation_token(token[2..-2])
end
def compile_interpolation_token(key)
"\#{#{interpolate_or_raise_missing(key)}}"
end
def interpolate_or_raise_missing(key)
escaped_key = escape_key_sym(key)
RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key)
end
def interpolate_key(key)
[direct_key(key), nil_key(key), missing_key(key)].join('||')
end
def direct_key(key)
"((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
end
def nil_key(key)
"(v.has_key?(#{key}) && '')"
end
def missing_key(key)
"I18n.config.missing_interpolation_argument_handler.call(#{key}, v, self)"
end
def reserved_key(key)
"raise(ReservedInterpolationKey.new(#{key}, self))"
end
def escape_plain_str(str)
str.gsub(/"|\\|#/) {|x| "\\#{x}"}
end
def escape_key_sym(key)
# rely on Ruby to do all the hard work :)
key.to_sym.inspect
end
end
def interpolate(locale, string, values)
if string.respond_to?(:i18n_interpolate)
string.i18n_interpolate(values)
elsif values
super
else
string
end
end
def store_translations(locale, data, options = EMPTY_HASH)
compile_all_strings_in(data)
super
end
protected
def compile_all_strings_in(data)
data.each_value do |value|
Compiler.compile_if_an_interpolation(value)
compile_all_strings_in(value) if value.kind_of?(Hash)
end
end
end
end
end
@@ -0,0 +1,204 @@
# frozen_string_literal: true
require 'i18n/backend/base'
module I18n
begin
require 'oj'
class JSON
class << self
def encode(value)
Oj::Rails.encode(value)
end
def decode(value)
Oj.load(value)
end
end
end
rescue LoadError
require 'active_support/json'
JSON = ActiveSupport::JSON
end
module Backend
# This is a basic backend for key value stores. It receives on
# initialization the store, which should respond to three methods:
#
# * store#[](key) - Used to get a value
# * store#[]=(key, value) - Used to set a value
# * store#keys - Used to get all keys
#
# Since these stores only supports string, all values are converted
# to JSON before being stored, allowing it to also store booleans,
# hashes and arrays. However, this store does not support Procs.
#
# As the ActiveRecord backend, Symbols are just supported when loading
# translations from the filesystem or through explicit store translations.
#
# Also, avoid calling I18n.available_locales since it's a somehow
# expensive operation in most stores.
#
# == Example
#
# To setup I18n to use TokyoCabinet in memory is quite straightforward:
#
# require 'rufus/tokyo/cabinet' # gem install rufus-tokyo
# I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
#
# == Performance
#
# You may make this backend even faster by including the Memoize module.
# However, notice that you should properly clear the cache if you change
# values directly in the key-store.
#
# == Subtrees
#
# In most backends, you are allowed to retrieve part of a translation tree:
#
# I18n.backend.store_translations :en, :foo => { :bar => :baz }
# I18n.t "foo" #=> { :bar => :baz }
#
# This backend supports this feature by default, but it slows down the storage
# of new data considerably and makes hard to delete entries. That said, you are
# allowed to disable the storage of subtrees on initialization:
#
# I18n::Backend::KeyValue.new(@store, false)
#
# This is useful if you are using a KeyValue backend chained to a Simple backend.
class KeyValue
module Implementation
attr_accessor :store
include Base, Flatten
def initialize(store, subtrees=true)
@store, @subtrees = store, subtrees
end
def initialized?
!@store.nil?
end
def store_translations(locale, data, options = EMPTY_HASH)
escape = options.fetch(:escape, true)
flatten_translations(locale, data, escape, @subtrees).each do |key, value|
key = "#{locale}.#{key}"
case value
when Hash
if @subtrees && (old_value = @store[key])
old_value = JSON.decode(old_value)
value = Utils.deep_merge!(Utils.deep_symbolize_keys(old_value), value) if old_value.is_a?(Hash)
end
when Proc
raise "Key-value stores cannot handle procs"
end
@store[key] = JSON.encode(value) unless value.is_a?(Symbol)
end
end
def available_locales
locales = @store.keys.map { |k| k =~ /\./; $` }
locales.uniq!
locales.compact!
locales.map! { |k| k.to_sym }
locales
end
protected
# Queries the translations from the key-value store and converts
# them into a hash such as the one returned from loading the
# haml files
def translations
@translations = Utils.deep_symbolize_keys(@store.keys.clone.map do |main_key|
main_value = JSON.decode(@store[main_key])
main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
{key.to_sym => value}
end
end.inject{|hash, elem| Utils.deep_merge!(hash, elem)})
end
def init_translations
# NO OP
# This call made also inside Simple Backend and accessed by
# other plugins like I18n-js and babilu and
# to use it along with the Chain backend we need to
# provide a uniform API even for protected methods :S
end
def subtrees?
@subtrees
end
def lookup(locale, key, scope = [], options = EMPTY_HASH)
key = normalize_flat_keys(locale, key, scope, options[:separator])
value = @store["#{locale}.#{key}"]
value = JSON.decode(value) if value
if value.is_a?(Hash)
Utils.deep_symbolize_keys(value)
elsif !value.nil?
value
elsif !@subtrees
SubtreeProxy.new("#{locale}.#{key}", @store)
end
end
def pluralize(locale, entry, count)
if subtrees?
super
else
return entry unless entry.is_a?(Hash)
key = pluralization_key(entry, count)
entry[key]
end
end
end
class SubtreeProxy
def initialize(master_key, store)
@master_key = master_key
@store = store
@subtree = nil
end
def has_key?(key)
@subtree && @subtree.has_key?(key) || self[key]
end
def [](key)
unless @subtree && value = @subtree[key]
value = @store["#{@master_key}.#{key}"]
if value
value = JSON.decode(value)
(@subtree ||= {})[key] = value
end
end
value
end
def is_a?(klass)
Hash == klass || super
end
alias :kind_of? :is_a?
def instance_of?(klass)
Hash == klass || super
end
def nil?
@subtree.nil?
end
def inspect
@subtree.inspect
end
end
include Implementation
end
end
end
@@ -0,0 +1,184 @@
# frozen_string_literal: true
module I18n
module Backend
# Backend that lazy loads translations based on the current locale. This
# implementation avoids loading all translations up front. Instead, it only
# loads the translations that belong to the current locale. This offers a
# performance incentive in local development and test environments for
# applications with many translations for many different locales. It's
# particularly useful when the application only refers to a single locales'
# translations at a time (ex. A Rails workload). The implementation
# identifies which translation files from the load path belong to the
# current locale by pattern matching against their path name.
#
# Specifically, a translation file is considered to belong to a locale if:
# a) the filename is in the I18n load path
# b) the filename ends in a supported extension (ie. .yml, .json, .po, .rb)
# c) the filename starts with the locale identifier
# d) the locale identifier and optional proceeding text is separated by an underscore, ie. "_".
#
# Examples:
# Valid files that will be selected by this backend:
#
# "files/locales/en_translation.yml" (Selected for locale "en")
# "files/locales/fr.po" (Selected for locale "fr")
#
# Invalid files that won't be selected by this backend:
#
# "files/locales/translation-file"
# "files/locales/en-translation.unsupported"
# "files/locales/french/translation.yml"
# "files/locales/fr/translation.yml"
#
# The implementation uses this assumption to defer the loading of
# translation files until the current locale actually requires them.
#
# The backend has two working modes: lazy_load and eager_load.
#
# Note: This backend should only be enabled in test environments!
# When the mode is set to false, the backend behaves exactly like the
# Simple backend, with an additional check that the paths being loaded
# abide by the format. If paths can't be matched to the format, an error is raised.
#
# You can configure lazy loaded backends through the initializer or backends
# accessor:
#
# # In test environments
#
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: true)
#
# # In other environments, such as production and CI
#
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: false) # default
#
class LocaleExtractor
class << self
def locale_from_path(path)
name = File.basename(path, ".*")
locale = name.split("_").first
locale.to_sym unless locale.nil?
end
end
end
class LazyLoadable < Simple
def initialize(lazy_load: false)
@lazy_load = lazy_load
end
# Returns whether the current locale is initialized.
def initialized?
if lazy_load?
initialized_locales[I18n.locale]
else
super
end
end
# Clean up translations and uninitialize all locales.
def reload!
if lazy_load?
@initialized_locales = nil
@translations = nil
else
super
end
end
# Eager loading is not supported in the lazy context.
def eager_load!
if lazy_load?
raise UnsupportedMethod.new(__method__, self.class, "Cannot eager load translations because backend was configured with lazy_load: true.")
else
super
end
end
# Parse the load path and extract all locales.
def available_locales
if lazy_load?
I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }.uniq
else
super
end
end
def lookup(locale, key, scope = [], options = EMPTY_HASH)
if lazy_load?
I18n.with_locale(locale) do
super
end
else
super
end
end
protected
# Load translations from files that belong to the current locale.
def init_translations
file_errors = if lazy_load?
initialized_locales[I18n.locale] = true
load_translations_and_collect_file_errors(filenames_for_current_locale)
else
@initialized = true
load_translations_and_collect_file_errors(I18n.load_path)
end
raise InvalidFilenames.new(file_errors) unless file_errors.empty?
end
def initialized_locales
@initialized_locales ||= Hash.new(false)
end
private
def lazy_load?
@lazy_load
end
class FilenameIncorrect < StandardError
def initialize(file, expected_locale, unexpected_locales)
super "#{file} can only load translations for \"#{expected_locale}\". Found translations for: #{unexpected_locales}."
end
end
# Loads each file supplied and asserts that the file only loads
# translations as expected by the name. The method returns a list of
# errors corresponding to offending files.
def load_translations_and_collect_file_errors(files)
errors = []
load_translations(files) do |file, loaded_translations|
assert_file_named_correctly!(file, loaded_translations)
rescue FilenameIncorrect => e
errors << e
end
errors
end
# Select all files from I18n load path that belong to current locale.
# These files must start with the locale identifier (ie. "en", "pt-BR"),
# followed by an "_" demarcation to separate proceeding text.
def filenames_for_current_locale
I18n.load_path.flatten.select do |path|
LocaleExtractor.locale_from_path(path) == I18n.locale
end
end
# Checks if a filename is named in correspondence to the translations it loaded.
# The locale extracted from the path must be the single locale loaded in the translations.
def assert_file_named_correctly!(file, translations)
loaded_locales = translations.keys.map(&:to_sym)
expected_locale = LocaleExtractor.locale_from_path(file)
unexpected_locales = loaded_locales.reject { |locale| locale == expected_locale }
raise FilenameIncorrect.new(file, expected_locale, unexpected_locales) unless unexpected_locales.empty?
end
end
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
# Memoize module simply memoizes the values returned by lookup using
# a flat hash and can tremendously speed up the lookup process in a backend.
#
# To enable it you can simply include the Memoize module to your backend:
#
# I18n::Backend::Simple.include(I18n::Backend::Memoize)
#
# Notice that it's the responsibility of the backend to define whenever the
# cache should be cleaned.
module I18n
module Backend
module Memoize
def available_locales
@memoized_locales ||= super
end
def store_translations(locale, data, options = EMPTY_HASH)
reset_memoizations!(locale)
super
end
def reload!
reset_memoizations!
super
end
def eager_load!
memoized_lookup
available_locales
super
end
protected
def lookup(locale, key, scope = nil, options = EMPTY_HASH)
flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
key, scope, options[:separator]).to_sym
flat_hash = memoized_lookup[locale.to_sym]
flat_hash.key?(flat_key) ? flat_hash[flat_key] : (flat_hash[flat_key] = super)
end
def memoized_lookup
@memoized_lookup ||= I18n.new_double_nested_cache
end
def reset_memoizations!(locale=nil)
@memoized_locales = nil
(locale ? memoized_lookup[locale.to_sym] : memoized_lookup).clear
end
end
end
end
@@ -0,0 +1,71 @@
# frozen_string_literal: true
# I18n translation metadata is useful when you want to access information
# about how a translation was looked up, pluralized or interpolated in
# your application.
#
# msg = I18n.t(:message, :default => 'Hi!', :scope => :foo)
# msg.translation_metadata
# # => { :key => :message, :scope => :foo, :default => 'Hi!' }
#
# If a :count option was passed to #translate it will be set to the metadata.
# Likewise, if any interpolation variables were passed they will also be set.
#
# To enable translation metadata you can simply include the Metadata module
# into the Simple backend class - or whatever other backend you are using:
#
# I18n::Backend::Simple.include(I18n::Backend::Metadata)
#
module I18n
module Backend
module Metadata
class << self
def included(base)
Object.class_eval do
def translation_metadata
unless self.frozen?
@translation_metadata ||= {}
else
{}
end
end
def translation_metadata=(translation_metadata)
@translation_metadata = translation_metadata unless self.frozen?
end
end unless Object.method_defined?(:translation_metadata)
end
end
def translate(locale, key, options = EMPTY_HASH)
metadata = {
:locale => locale,
:key => key,
:scope => options[:scope],
:default => options[:default],
:separator => options[:separator],
:values => options.reject { |name, _value| RESERVED_KEYS.include?(name) }
}
with_metadata(metadata) { super }
end
def interpolate(locale, entry, values = EMPTY_HASH)
metadata = entry.translation_metadata.merge(:original => entry)
with_metadata(metadata) { super }
end
def pluralize(locale, entry, count)
with_metadata(:count => count) { super }
end
protected
def with_metadata(metadata, &block)
result = yield
result.translation_metadata = result.translation_metadata.merge(metadata) if result
result
end
end
end
end
@@ -0,0 +1,96 @@
# frozen_string_literal: true
# I18n Pluralization are useful when you want your application to
# customize pluralization rules.
#
# To enable locale specific pluralizations you can simply include the
# Pluralization module to the Simple backend - or whatever other backend you
# are using.
#
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
#
# You also need to make sure to provide pluralization algorithms to the
# backend, i.e. include them to your I18n.load_path accordingly.
module I18n
module Backend
module Pluralization
# Overwrites the Base backend translate method so that it will check the
# translation meta data space (:i18n) for a locale specific pluralization
# rule and use it to pluralize the given entry. I.e., the library expects
# pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
#
# Pluralization rules are expected to respond to #call(count) and
# return a pluralization key. Valid keys depend on the pluralization
# rules for the locale, as defined in the CLDR.
# As of v41, 6 locale-specific plural categories are defined:
# :few, :many, :one, :other, :two, :zero
#
# n.b., The :one plural category does not imply the number 1.
# Instead, :one is a category for any number that behaves like 1 in
# that locale. For example, in some locales, :one is used for numbers
# that end in "1" (like 1, 21, 151) but that don't end in
# 11 (like 11, 111, 10311).
# Similar notes apply to the :two, and :zero plural categories.
#
# If you want to have different strings for the categories of count == 0
# (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car")
# use the explicit `"0"` and `"1"` keys.
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
def pluralize(locale, entry, count)
return entry unless entry.is_a?(Hash) && count
pluralizer = pluralizer(locale)
if pluralizer.respond_to?(:call)
# Deprecation: The use of the `zero` key in this way is incorrect.
# Users that want a different string for the case of `count == 0` should use the explicit "0" key instead.
# We keep this incorrect behaviour for now for backwards compatibility until we can remove it.
# Ref: https://github.com/ruby-i18n/i18n/issues/629
return entry[:zero] if count == 0 && entry.has_key?(:zero)
# "0" and "1" are special cases
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
if count == 0 || count == 1
value = entry[symbolic_count(count)]
return value if value
end
# Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
# > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
# > 1. If "x" is numeric, the path falls back to the path with [@count=«the plural rules category for x for that locale»], within that the same locale.
# > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
# > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
# Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
plural_rule_category = pluralizer.call(count)
value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
entry[plural_rule_category] || entry[:other]
else
raise InvalidPluralizationData.new(entry, count, plural_rule_category)
end
else
super
end
end
protected
def pluralizers
@pluralizers ||= {}
end
def pluralizer(locale)
pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
end
private
# Normalizes categories of 0.0 and 1.0
# and returns the symbolic version
def symbolic_count(count)
count = 0 if count == 0
count = 1 if count == 1
count.to_s.to_sym
end
end
end
end
@@ -0,0 +1,113 @@
# frozen_string_literal: true
require 'i18n/backend/base'
module I18n
module Backend
# A simple backend that reads translations from YAML files and stores them in
# an in-memory hash. Relies on the Base backend.
#
# The implementation is provided by a Implementation module allowing to easily
# extend Simple backend's behavior by including modules. E.g.:
#
# module I18n::Backend::Pluralization
# def pluralize(*args)
# # extended pluralization logic
# super
# end
# end
#
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
class Simple
module Implementation
include Base
# Mutex to ensure that concurrent translations loading will be thread-safe
MUTEX = Mutex.new
def initialized?
@initialized ||= false
end
# Stores translations for the given locale in memory.
# This uses a deep merge for the translations hash, so existing
# translations will be overwritten by new ones only at the deepest
# level of the hash.
def store_translations(locale, data, options = EMPTY_HASH)
if I18n.enforce_available_locales &&
I18n.available_locales_initialized? &&
!I18n.locale_available?(locale)
return data
end
locale = locale.to_sym
translations[locale] ||= Concurrent::Hash.new
data = Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false)
Utils.deep_merge!(translations[locale], data)
end
# Get available locales from the translations hash
def available_locales
init_translations unless initialized?
translations.inject([]) do |locales, (locale, data)|
locales << locale unless data.size <= 1 && (data.empty? || data.has_key?(:i18n))
locales
end
end
# Clean up translations hash and set initialized to false on reload!
def reload!
@initialized = false
@translations = nil
super
end
def eager_load!
init_translations unless initialized?
super
end
def translations(do_init: false)
# To avoid returning empty translations,
# call `init_translations`
init_translations if do_init && !initialized?
@translations ||= Concurrent::Hash.new do |h, k|
MUTEX.synchronize do
h[k] = Concurrent::Hash.new
end
end
end
protected
def init_translations
load_translations
@initialized = true
end
# Looks up a translation from the translations hash. Returns nil if
# either key is nil, or locale, scope or key do not exist as a key in the
# nested translations hash. Splits keys or scopes containing dots
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
# <tt>%w(currency format)</tt>.
def lookup(locale, key, scope = [], options = EMPTY_HASH)
init_translations unless initialized?
keys = I18n.normalize_keys(locale, key, scope, options[:separator])
keys.inject(translations) do |result, _key|
return nil unless result.is_a?(Hash)
unless result.has_key?(_key)
_key = _key.to_s.to_sym
return nil unless result.has_key?(_key)
end
result = result[_key]
result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
result
end
end
end
include Implementation
end
end
end
@@ -0,0 +1,108 @@
# encoding: utf-8
# frozen_string_literal: true
module I18n
module Backend
module Transliterator
DEFAULT_REPLACEMENT_CHAR = "?"
# Given a locale and a UTF-8 string, return the locale's ASCII
# approximation for the string.
def transliterate(locale, string, replacement = nil)
@transliterators ||= {}
@transliterators[locale] ||= Transliterator.get I18n.t(:'i18n.transliterate.rule',
:locale => locale, :resolve => false, :default => {})
@transliterators[locale].transliterate(string, replacement)
end
# Get a transliterator instance.
def self.get(rule = nil)
if !rule || rule.kind_of?(Hash)
HashTransliterator.new(rule)
elsif rule.kind_of? Proc
ProcTransliterator.new(rule)
else
raise I18n::ArgumentError, "Transliteration rule must be a proc or a hash."
end
end
# A transliterator which accepts a Proc as its transliteration rule.
class ProcTransliterator
def initialize(rule)
@rule = rule
end
def transliterate(string, replacement = nil)
@rule.call(string)
end
end
# A transliterator which accepts a Hash of characters as its translation
# rule.
class HashTransliterator
DEFAULT_APPROXIMATIONS = {
"À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE",
"Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
"Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
"Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
"Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "ẞ"=>"SS", "à"=>"a",
"á"=>"a", "â"=>"a", "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c",
"è"=>"e", "é"=>"e", "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i",
"ï"=>"i", "ð"=>"d", "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o",
"ö"=>"o", "ø"=>"o", "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y",
"þ"=>"th", "ÿ"=>"y", "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A",
"ą"=>"a", "Ć"=>"C", "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c",
"Č"=>"C", "č"=>"c", "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E",
"ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e",
"Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G",
"ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h",
"Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I",
"į"=>"i", "İ"=>"I", "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j",
"Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l",
"Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N",
"ń"=>"n", "Ņ"=>"N", "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG",
"ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o",
"Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R",
"ř"=>"r", "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s",
"Š"=>"S", "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T",
"ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u",
"Ů"=>"U", "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W",
"ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z",
"ż"=>"z", "Ž"=>"Z", "ž"=>"z"
}.freeze
def initialize(rule = nil)
@rule = rule
add_default_approximations
add rule if rule
end
def transliterate(string, replacement = nil)
replacement ||= DEFAULT_REPLACEMENT_CHAR
string.gsub(/[^\x00-\x7f]/u) do |char|
approximations[char] || replacement
end
end
private
def approximations
@approximations ||= {}
end
def add_default_approximations
DEFAULT_APPROXIMATIONS.each do |key, value|
approximations[key] = value
end
end
# Add transliteration rules to the approximations hash.
def add(hash)
hash.each do |key, value|
approximations[key.to_s] = value.to_s
end
end
end
end
end
end