mirror of
https://github.com/github/rails.git
synced 2026-01-26 23:08:58 -05:00
update i18n gem
This commit is contained in:
@@ -9,14 +9,14 @@ require 'i18n/backend/simple'
|
||||
require 'i18n/exceptions'
|
||||
|
||||
module I18n
|
||||
@@backend = Backend::Simple
|
||||
@@backend = nil
|
||||
@@default_locale = 'en-US'
|
||||
@@exception_handler = :default_exception_handler
|
||||
|
||||
class << self
|
||||
# Returns the current backend. Defaults to +Backend::Simple+.
|
||||
def backend
|
||||
@@backend
|
||||
@@backend ||= Backend::Simple.new
|
||||
end
|
||||
|
||||
# Sets the current backend. Used to set a custom backend.
|
||||
|
||||
@@ -2,194 +2,192 @@ require 'strscan'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
module Simple
|
||||
@@translations = {}
|
||||
|
||||
class << self
|
||||
# Allow client libraries to pass a block that populates the translation
|
||||
# storage. Decoupled for backends like a db backend that persist their
|
||||
# translations, so the backend can decide whether/when to yield or not.
|
||||
def populate(&block)
|
||||
yield
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
reserved = :scope, :default
|
||||
count, scope, default = options.values_at(:count, *reserved)
|
||||
options.delete(:default)
|
||||
values = options.reject{|name, value| reserved.include? name }
|
||||
|
||||
entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
|
||||
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 (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
|
||||
def localize(locale, object, format = :default)
|
||||
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
|
||||
|
||||
type = object.respond_to?(:sec) ? 'time' : 'date'
|
||||
formats = translate(locale, :"#{type}.formats")
|
||||
format = formats[format.to_sym] if formats && formats[format.to_sym]
|
||||
# TODO raise exception unless format found?
|
||||
format = format.to_s.dup
|
||||
|
||||
format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
|
||||
format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
|
||||
format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
|
||||
format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
|
||||
format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
|
||||
object.strftime(format)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# 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. <tt>currency.format</tt> is regarded the same as
|
||||
# <tt>%w(currency format)</tt>.
|
||||
def lookup(locale, key, scope = [])
|
||||
return unless key
|
||||
keys = I18n.send :normalize_translation_keys, locale, key, scope
|
||||
keys.inject(@@translations){|result, k| result[k.to_sym] or return nil }
|
||||
end
|
||||
|
||||
# Evaluates a default translation.
|
||||
# If the given default is a String it is used literally. If it is a Symbol
|
||||
# it will be translated with the given options. If it is an Array the first
|
||||
# translation yielded will be returned.
|
||||
#
|
||||
# <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
|
||||
# <tt>translate(locale, :foo)</tt> does not yield a result.
|
||||
def default(locale, default, options = {})
|
||||
case default
|
||||
when String then default
|
||||
when Symbol then translate locale, default, options
|
||||
when Array then default.each do |obj|
|
||||
result = default(locale, obj, options.dup) and return result
|
||||
end and nil
|
||||
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
|
||||
# raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
|
||||
key = :zero if count == 0 && entry.has_key?(:zero)
|
||||
key ||= count == 1 ? :one : :many
|
||||
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 <tt>\\</tt> when you want to escape
|
||||
# the <tt>{{...}}</tt> key in a string (once for the string and once for the
|
||||
# interpolation).
|
||||
def interpolate(locale, string, values = {})
|
||||
return string if !string.is_a?(String)
|
||||
|
||||
string = string.gsub(/%d/, '{{count}}').gsub(/%s/, '{{value}}')
|
||||
if string.respond_to?(:force_encoding)
|
||||
original_encoding = string.encoding
|
||||
string.force_encoding(Encoding::BINARY)
|
||||
end
|
||||
|
||||
s = StringScanner.new(string)
|
||||
while s.skip_until(/\{\{/)
|
||||
s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
|
||||
start_pos = s.pos - 2
|
||||
key = s.scan_until(/\}\}/)[0..-3]
|
||||
end_pos = s.pos - 1
|
||||
|
||||
raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
|
||||
raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
|
||||
|
||||
s.string[start_pos..end_pos] = values[key.to_sym].to_s
|
||||
s.unscan
|
||||
end
|
||||
|
||||
result = s.string
|
||||
result.force_encoding(original_encoding) if original_encoding
|
||||
result
|
||||
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 do |locale, d|
|
||||
merge_translations locale, d
|
||||
end
|
||||
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
|
||||
class Simple
|
||||
# Allow client libraries to pass a block that populates the translation
|
||||
# storage. Decoupled for backends like a db backend that persist their
|
||||
# translations, so the backend can decide whether/when to yield or not.
|
||||
def populate(&block)
|
||||
yield
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
reserved = :scope, :default
|
||||
count, scope, default = options.values_at(:count, *reserved)
|
||||
options.delete(:default)
|
||||
values = options.reject{|name, value| reserved.include? name }
|
||||
|
||||
entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
|
||||
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 (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
|
||||
def localize(locale, object, format = :default)
|
||||
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
|
||||
|
||||
type = object.respond_to?(:sec) ? 'time' : 'date'
|
||||
formats = translate(locale, :"#{type}.formats")
|
||||
format = formats[format.to_sym] if formats && formats[format.to_sym]
|
||||
# TODO raise exception unless format found?
|
||||
format = format.to_s.dup
|
||||
|
||||
format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
|
||||
format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
|
||||
format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
|
||||
format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
|
||||
format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
|
||||
object.strftime(format)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
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. <tt>currency.format</tt> is regarded the same as
|
||||
# <tt>%w(currency format)</tt>.
|
||||
def lookup(locale, key, scope = [])
|
||||
return unless key
|
||||
keys = I18n.send :normalize_translation_keys, locale, key, scope
|
||||
keys.inject(translations){|result, k| result[k.to_sym] or return nil }
|
||||
end
|
||||
|
||||
# Evaluates a default translation.
|
||||
# If the given default is a String it is used literally. If it is a Symbol
|
||||
# it will be translated with the given options. If it is an Array the first
|
||||
# translation yielded will be returned.
|
||||
#
|
||||
# <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
|
||||
# <tt>translate(locale, :foo)</tt> does not yield a result.
|
||||
def default(locale, default, options = {})
|
||||
case default
|
||||
when String then default
|
||||
when Symbol then translate locale, default, options
|
||||
when Array then default.each do |obj|
|
||||
result = default(locale, obj, options.dup) and return result
|
||||
end and nil
|
||||
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
|
||||
# raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
|
||||
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 <tt>\\</tt> when you want to escape
|
||||
# the <tt>{{...}}</tt> key in a string (once for the string and once for the
|
||||
# interpolation).
|
||||
def interpolate(locale, string, values = {})
|
||||
return string if !string.is_a?(String)
|
||||
|
||||
string = string.gsub(/%d/, '{{count}}').gsub(/%s/, '{{value}}')
|
||||
if string.respond_to?(:force_encoding)
|
||||
original_encoding = string.encoding
|
||||
string.force_encoding(Encoding::BINARY)
|
||||
end
|
||||
s = StringScanner.new(string)
|
||||
|
||||
while s.skip_until(/\{\{/)
|
||||
s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
|
||||
start_pos = s.pos - 2
|
||||
key = s.scan_until(/\}\}/)[0..-3]
|
||||
end_pos = s.pos - 1
|
||||
|
||||
raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
|
||||
raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
|
||||
|
||||
s.string[start_pos..end_pos] = values[key.to_sym].to_s
|
||||
s.unscan
|
||||
end
|
||||
|
||||
result = s.string
|
||||
result.force_encoding(original_encoding) if original_encoding
|
||||
result
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user