6
INTERPOLATION_RESERVED_KEYS = %w(scope default)
7
MATCH = /(\\\\)?\{\{([^\}]+)\}\}/
9
# Accepts a list of paths to translation files. Loads translations from
10
# plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
12
def load_translations(*filenames)
13
filenames.each { |filename| load_file(filename) }
16
# Stores translations for the given locale in memory.
17
# This uses a deep merge for the translations hash, so existing
18
# translations will be overwritten by new ones only at the deepest
20
def store_translations(locale, data)
21
merge_translations(locale, data)
24
def translate(locale, key, options = {})
25
raise InvalidLocale.new(locale) if locale.nil?
26
return key.map { |k| translate(locale, k, options) } if key.is_a? Array
28
reserved = :scope, :default
29
count, scope, default = options.values_at(:count, *reserved)
30
options.delete(:default)
31
values = options.reject { |name, value| reserved.include?(name) }
33
entry = lookup(locale, key, scope)
35
entry = default(locale, default, options)
37
raise(I18n::MissingTranslationData.new(locale, key, options))
40
entry = pluralize(locale, entry, count)
41
entry = interpolate(locale, entry, values)
45
# Acts the same as +strftime+, but returns a localized version of the
46
# formatted date string. Takes a key from the date/time formats
47
# translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
48
def localize(locale, object, format = :default)
49
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
51
type = object.respond_to?(:sec) ? 'time' : 'date'
52
# TODO only translate these if format is a String?
53
formats = translate(locale, :"#{type}.formats")
54
format = formats[format.to_sym] if formats && formats[format.to_sym]
55
# TODO raise exception unless format found?
56
format = format.to_s.dup
58
# TODO only translate these if the format string is actually present
59
# TODO check which format strings are present, then bulk translate then, then replace them
60
format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
61
format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
62
format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
63
format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
64
format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
65
object.strftime(format)
69
@initialized ||= false
72
# Returns an array of locales for which translations are available
74
init_translations unless initialized?
85
load_translations(*I18n.load_path.flatten)
93
# Looks up a translation from the translations hash. Returns nil if
94
# eiher key is nil, or locale, scope or key do not exist as a key in the
95
# nested translations hash. Splits keys or scopes containing dots
96
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
97
# <tt>%w(currency format)</tt>.
98
def lookup(locale, key, scope = [])
100
init_translations unless initialized?
101
keys = I18n.send(:normalize_translation_keys, locale, key, scope)
102
keys.inject(translations) do |result, k|
103
if (x = result[k.to_sym]).nil?
111
# Evaluates a default translation.
112
# If the given default is a String it is used literally. If it is a Symbol
113
# it will be translated with the given options. If it is an Array the first
114
# translation yielded will be returned.
116
# <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
117
# <tt>translate(locale, :foo)</tt> does not yield a result.
118
def default(locale, default, options = {})
120
when String then default
121
when Symbol then translate locale, default, options
122
when Array then default.each do |obj|
123
result = default(locale, obj, options.dup) and return result
126
rescue MissingTranslationData
130
# Picks a translation from an array according to English pluralization
131
# rules. It will pick the first translation if count is not equal to 1
132
# and the second translation if it is equal to 1. Other backends can
133
# implement more flexible or complex pluralization rules.
134
def pluralize(locale, entry, count)
135
return entry unless entry.is_a?(Hash) and count
136
# raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
137
key = :zero if count == 0 && entry.has_key?(:zero)
138
key ||= count == 1 ? :one : :other
139
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
143
# Interpolates values into a given string.
145
# interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
146
# # => "file test.txt opened by {{user}}"
148
# Note that you have to double escape the <tt>\\</tt> when you want to escape
149
# the <tt>{{...}}</tt> key in a string (once for the string and once for the
151
def interpolate(locale, string, values = {})
152
return string unless string.is_a?(String)
154
string.gsub(MATCH) do
155
escaped, pattern, key = $1, $2, $2.to_sym
159
elsif INTERPOLATION_RESERVED_KEYS.include?(pattern)
160
raise ReservedInterpolationKey.new(pattern, string)
161
elsif !values.include?(key)
162
raise MissingInterpolationArgument.new(pattern, string)
169
# Loads a single translations file by delegating to #load_rb or
170
# #load_yml depending on the file extension and directly merges the
171
# data to the existing translations. Raises I18n::UnknownFileType
172
# for all other file extensions.
173
def load_file(filename)
174
type = File.extname(filename).tr('.', '').downcase
175
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
176
data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
177
data.each { |locale, d| merge_translations(locale, d) }
180
# Loads a plain Ruby translations file. eval'ing the file must yield
181
# a Hash containing translation data with locales as toplevel keys.
182
def load_rb(filename)
183
eval(IO.read(filename), binding, filename)
186
# Loads a YAML translations file. The data must have locales as
188
def load_yml(filename)
189
YAML::load(IO.read(filename))
192
# Deep merges the given translations hash with the existing translations
193
# for the given locale
194
def merge_translations(locale, data)
195
locale = locale.to_sym
196
translations[locale] ||= {}
197
data = deep_symbolize_keys(data)
199
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
200
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
201
translations[locale].merge!(data, &merger)
204
# Return a new hash with all keys and nested keys converted to symbols.
205
def deep_symbolize_keys(hash)
206
hash.inject({}) { |result, (key, value)|
207
value = deep_symbolize_keys(value) if value.is_a? Hash
208
result[(key.to_sym rescue key) || key] = value
b'\\ No newline at end of file'