require 'yaml'
module I18n
module Backend
class Base
RESERVED_KEYS = [:scope, :default, :separator]
INTERPOLATION_SYNTAX_PATTERN = /(\\\\)?\{\{([^\}]+)\}\}/
# Accepts a list of paths to translation files. Loads translations from
# plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
# for details.
def load_translations(*filenames)
filenames.each { |filename| load_file(filename) }
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)
merge_translations(locale, data)
end
def translate(locale, key, options = {})
raise InvalidLocale.new(locale) if locale.nil?
return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
count, scope, default, separator = options.values_at(:count, *RESERVED_KEYS)
values = options.reject { |name, value| RESERVED_KEYS.include?(name) }
entry = lookup(locale, key, scope, separator)
entry = entry.nil? ? default(locale, key, default, options) : resolve(locale, key, entry, options)
raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
entry = pluralize(locale, entry, count)
entry = interpolate(locale, entry, values)
entry
end
# Acts the same as +strftime+, but returns a localized version of the
# formatted date string. Takes a key from the date/time formats
# translations as a format argument (e.g., :short in :'date.formats').
def localize(locale, object, format = :default, options={})
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
if Symbol === format
type = object.respond_to?(:sec) ? 'time' : 'date'
format = lookup(locale, :"#{type}.formats.#{format}")
end
format = resolve(locale, object, format, options.merge(:raise => true))
# TODO check which format strings are present, then bulk translate them, then replace them
format.gsub!(/%a/, translate(locale, :"date.abbr_day_names", :format => format)[object.wday]) if format.include?('%a')
format.gsub!(/%A/, translate(locale, :"date.day_names", :format => format)[object.wday]) if format.include?('%A')
format.gsub!(/%b/, translate(locale, :"date.abbr_month_names", :format => format)[object.mon]) if format.include?('%b')
format.gsub!(/%B/, translate(locale, :"date.month_names", :format => format)[object.mon]) if format.include?('%B')
format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}", :format => format)) if format.include?('%p') && object.respond_to?(:hour)
object.strftime(format)
end
def initialized?
@initialized ||= false
end
# Returns an array of locales for which translations are available
# ignoring the reserved translation meta data key :i18n.
def available_locales
init_translations unless initialized?
translations.inject([]) do |locales, (locale, data)|
locales << locale unless data.keys.tap { |keys| keys.delete(:i18n) }.empty?
locales
end
end
def reload!
@initialized = false
@translations = nil
end
protected
def init_translations
load_translations(*I18n.load_path.flatten)
@initialized = true
end
def translations
@translations ||= {}
end
# Looks up a translation from the translations hash. Returns nil if
# eiher 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. currency.format is regarded the same as
# %w(currency format).
def lookup(locale, key, scope = [], separator = nil)
return unless key
init_translations unless initialized?
keys = I18n.send(:normalize_translation_keys, locale, key, scope, separator)
keys.inject(translations) do |result, k|
if (x = result[k.to_sym]).nil?
return nil
else
x
end
end
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 = {})
options = options.dup.reject { |key, value| key == :default }
case subject
when Array
subject.each do |subject|
result = resolve(locale, object, subject, options) and return result
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 = {})
case subject
when Symbol
translate(locale, subject, options)
when Proc
resolve(locale, object, subject.call(object, options), options = {})
else
subject
end
rescue MissingTranslationData
nil
end
# Picks a translation from an array according to English pluralization
# rules. It will pick the first translation if count is not equal to 1
# and the second translation if it is equal to 1. Other backends can
# implement more flexible or complex pluralization rules.
def pluralize(locale, entry, count)
return entry unless entry.is_a?(Hash) and count
key = :zero if count == 0 && entry.has_key?(:zero)
key ||= count == 1 ? :one : :other
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
entry[key]
end
# Interpolates values into a given string.
#
# interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
# # => "file test.txt opened by {{user}}"
#
# Note that you have to double escape the \\ when you want to escape
# the {{...}} key in a string (once for the string and once for the
# interpolation).
def interpolate(locale, string, values = {})
return string unless string.is_a?(String) && !values.empty?
string.gsub(INTERPOLATION_SYNTAX_PATTERN) do
escaped, key = $1, $2.to_sym
if escaped
key
elsif RESERVED_KEYS.include?(key)
raise ReservedInterpolationKey.new(key, string)
else
"%{#{key}}"
end
end % values
rescue KeyError => e
raise MissingInterpolationArgument.new(values, string)
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}")
data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
data.each { |locale, d| merge_translations(locale, d) }
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)
eval(IO.read(filename), binding, filename)
end
# Loads a YAML translations file. The data must have locales as
# toplevel keys.
def load_yml(filename)
YAML::load(IO.read(filename))
end
# Deep merges the given translations hash with the existing translations
# for the given locale
def merge_translations(locale, data)
locale = locale.to_sym
translations[locale] ||= {}
data = deep_symbolize_keys(data)
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
translations[locale].merge!(data, &merger)
end
# Return a new hash with all keys and nested keys converted to symbols.
def deep_symbolize_keys(hash)
hash.inject({}) { |result, (key, value)|
value = deep_symbolize_keys(value) if value.is_a?(Hash)
result[(key.to_sym rescue key) || key] = value
result
}
end
# Flatten the given array once
def flatten_once(array)
result = []
for element in array # a little faster than each
result.push(*element)
end
result
end
end
end
end