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