Skip to content


Subversion checkout URL

You can clone with
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

155 lines (133 sloc) 7.104 kB
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)
# 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)
def translate(key, locale, options = {})
raise if locale.nil?
return{|key| translate key, locale, options } if key.is_a? Array
reserved = :scope, :default
count, scope, default = options.values_at(:count, *reserved)
values = options.reject{|name, value| reserved.include? name }
entry = lookup(locale, key, scope) || default(locale, default, options) || raise(, locale, options))
entry = pluralize entry, count
entry = interpolate entry, values
# 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(object, locale = nil, 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(:"#{type}.formats", locale)
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(:"date.abbr_day_names", locale)[object.wday])
format.gsub!(/%A/, translate(:"date.day_names", locale)[object.wday])
format.gsub!(/%b/, translate(:"date.abbr_month_names", locale)[object.mon])
format.gsub!(/%B/, translate(:"date.month_names", locale)[object.mon])
format.gsub!(/%p/, translate(:"time.#{object.hour < 12 ? :am : :pm}", locale)) if object.respond_to? :hour
# 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, key| result[key.to_sym] or return nil }
# 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 default, locale, options
when Array then default.each do |obj|
result = default(locale, obj, options.dup) and return result
rescue MissingTranslationData
# 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(entry, count)
return entry unless entry.is_a?(Array) and count
raise, count) unless entry.size == 2
entry[count == 1 ? 0 : 1]
# 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(string, values = {})
return string if !string.is_a?(String)
map = {'%d' => '{{count}}', '%s' => '{{value}}'} # TODO deprecate this?
string.gsub!(/#{map.keys.join('|')}/){|key| map[key]}
s = string.dup
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, string) if %w(scope default).include?(key)
raise, string) unless values.has_key? key.to_sym
s.string[start_pos..end_pos] = values[key.to_sym].to_s
# 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
merger = proc{|key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
@@translations[locale].merge! data, &merger
# 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
Jump to Line
Something went wrong with that request. Please try again.