Skip to content


Subversion checkout URL

You can clone with
Download ZIP
Fetching contributors…
Cannot retrieve contributors at this time
194 lines (167 sloc) 8.42 KB
require 'strscan'
module I18n
module Backend
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)
# 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 }
# 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(locale, key, options = {})
raise if locale.nil?
return{|k| translate locale, k, 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(, key, options))
entry = pluralize locale, entry, count
entry = interpolate locale, 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(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
def translations
@translations ||= {}
# 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 }
# 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
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(locale, entry, count)
return entry unless entry.is_a?(Hash) and count
# raise, count) unless entry.is_a?(Hash)
key = :zero if count == 0 && entry.has_key?(:zero)
key ||= count == 1 ? :one : :other
raise, count) unless entry.has_key?(key)
# 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
s =
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
result = s.string
result.force_encoding(original_encoding) if original_encoding
# 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, 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 }
# 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, binding, filename
# Loads a YAML translations file. The data must have locales as
# toplevel keys.
def load_yml(filename)
# 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.