1 # encoding: utf-8
2
3 # Authors:: Sven Fuchs (http://www.artweb-design.de),
4 # Joshua Harvey (http://www.workingwithrails.com/person/759-joshua-harvey),
5 # Stephan Soller (http://www.arkanis-development.de/),
6 # Saimon Moore (http://saimonmoore.net),
7 # Matt Aimonetti (http://railsontherun.com/)
8 # Copyright:: Copyright (c) 2008 The Ruby i18n Team
9 # License:: MIT
10 require 'i18n/exceptions'
11 require 'i18n/core_ext/string/interpolate'
12
13 module I18n
14 autoload :Backend, 'i18n/backend'
15 autoload :Config, 'i18n/config'
16 autoload :Gettext, 'i18n/gettext'
17 autoload :Locale, 'i18n/locale'
18
19 class << self
20 # Gets I18n configuration object.
21 def config
22 Thread.current[:i18n_config] ||= I18n::Config.new
23 end
24
25 # Sets I18n configuration object.
26 def config=(value)
27 Thread.current[:i18n_config] = value
28 end
29
30 # Write methods which delegates to the configuration object
31 %w(locale backend default_locale available_locales default_separator
32 exception_handler load_path).each do |method|
33 module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1
34 def #{method}
35 config.#{method}
36 end
37
38 def #{method}=(value)
39 config.#{method} = (value)
40 end
41 DELEGATORS
42 end
43
44 # Tells the backend to reload translations. Used in situations like the
45 # Rails development environment. Backends can implement whatever strategy
46 # is useful.
47 def reload!
48 config.backend.reload!
49 end
50
51 # Translates, pluralizes and interpolates a given key using a given locale,
52 # scope, and default, as well as interpolation values.
53 #
54 # *LOOKUP*
55 #
56 # Translation data is organized as a nested hash using the upper-level keys
57 # as namespaces. <em>E.g.</em>, ActionView ships with the translation:
58 # <tt>:date => {:formats => {:short => "%b %d"}}</tt>.
59 #
60 # Translations can be looked up at any level of this hash using the key argument
61 # and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt>
62 # returns the whole translations hash <tt>{:formats => {:short => "%b %d"}}</tt>.
63 #
64 # Key can be either a single key or a dot-separated key (both Strings and Symbols
65 # work). <em>E.g.</em>, the short format can be looked up using both:
66 # I18n.t 'date.formats.short'
67 # I18n.t :'date.formats.short'
68 #
69 # Scope can be either a single key, a dot-separated key or an array of keys
70 # or dot-separated keys. Keys and scopes can be combined freely. So these
71 # examples will all look up the same short date format:
72 # I18n.t 'date.formats.short'
73 # I18n.t 'formats.short', :scope => 'date'
74 # I18n.t 'short', :scope => 'date.formats'
75 # I18n.t 'short', :scope => %w(date formats)
76 #
77 # *INTERPOLATION*
78 #
79 # Translations can contain interpolation variables which will be replaced by
80 # values passed to #translate as part of the options hash, with the keys matching
81 # the interpolation variable names.
82 #
83 # <em>E.g.</em>, with a translation <tt>:foo => "foo %{bar}"</tt> the option
84 # value for the key +bar+ will be interpolated into the translation:
85 # I18n.t :foo, :bar => 'baz' # => 'foo baz'
86 #
87 # *PLURALIZATION*
88 #
89 # Translation data can contain pluralized translations. Pluralized translations
90 # are arrays of singluar/plural versions of translations like <tt>['Foo', 'Foos']</tt>.
91 #
92 # Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English
93 # pluralization rules. Other algorithms can be supported by custom backends.
94 #
95 # This returns the singular version of a pluralized translation:
96 # I18n.t :foo, :count => 1 # => 'Foo'
97 #
98 # These both return the plural version of a pluralized translation:
99 # I18n.t :foo, :count => 0 # => 'Foos'
100 # I18n.t :foo, :count => 2 # => 'Foos'
101 #
102 # The <tt>:count</tt> option can be used both for pluralization and interpolation.
103 # <em>E.g.</em>, with the translation
104 # <tt>:foo => ['%{count} foo', '%{count} foos']</tt>, count will
105 # be interpolated to the pluralized translation:
106 # I18n.t :foo, :count => 1 # => '1 foo'
107 #
108 # *DEFAULTS*
109 #
110 # This returns the translation for <tt>:foo</tt> or <tt>default</tt> if no translation was found:
111 # I18n.t :foo, :default => 'default'
112 #
113 # This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no
114 # translation for <tt>:foo</tt> was found:
115 # I18n.t :foo, :default => :bar
116 #
117 # Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt>
118 # or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
119 # I18n.t :foo, :default => [:bar, 'default']
120 #
121 # *BULK LOOKUP*
122 #
123 # This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
124 # I18n.t [:foo, :bar]
125 #
126 # Can be used with dot-separated nested keys:
127 # I18n.t [:'baz.foo', :'baz.bar']
128 #
129 # Which is the same as using a scope option:
130 # I18n.t [:foo, :bar], :scope => :baz
131 #
132 # *LAMBDAS*
133 #
134 # Both translations and defaults can be given as Ruby lambdas. Lambdas will be
135 # called and passed the key and options.
136 #
137 # E.g. assuming the key <tt>:salutation</tt> resolves to:
138 # lambda { |key, options| options[:gender] == 'm' ? "Mr. %{options[:name]}" : "Mrs. %{options[:name]}" }
139 #
140 # Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
141 #
142 # It is recommended to use/implement lambdas in an "idempotent" way. E.g. when
143 # a cache layer is put in front of I18n.translate it will generate a cache key
144 # from the argument values passed to #translate. Therefor your lambdas should
145 # always return the same translations/values per unique combination of argument
146 # values.
147 def translate(*args)
148 options = args.pop if args.last.is_a?(Hash)
149 key = args.shift
150 locale = options && options.delete(:locale) || config.locale
151 raises = options && options.delete(:raise)
152 config.backend.translate(locale, key, options || {})
153 rescue I18n::ArgumentError => exception
154 raise exception if raises
155 handle_exception(exception, locale, key, options)
156 end
157 alias :t :translate
158
159 def translate!(key, options = {})
160 translate(key, options.merge( :raise => true ))
161 end
162 alias :t! :translate!
163
164 # Transliterates UTF-8 characters to ASCII. By default this method will
165 # transliterate only Latin strings to an ASCII approximation:
166 #
167 # I18n.transliterate("Ærøskøbing")
168 # # => "AEroskobing"
169 #
170 # I18n.transliterate("日本語")
171 # # => "???"
172 #
173 # It's also possible to add support for per-locale transliterations. I18n
174 # expects transliteration rules to be stored at
175 # <tt>i18n.transliterate.rule</tt>.
176 #
177 # Transliteration rules can either be a Hash or a Proc. Procs must accept a
178 # single string argument. Hash rules inherit the default transliteration
179 # rules, while Procs do not.
180 #
181 # *Examples*
182 #
183 # Setting a Hash in <locale>.yml:
184 #
185 # i18n:
186 # transliterate:
187 # rule:
188 # ü: "ue"
189 # ö: "oe"
190 #
191 # Setting a Hash using Ruby:
192 #
193 # store_translations(:de, :i18n => {
194 # :transliterate => {
195 # :rule => {
196 # "ü" => "ue",
197 # "ö" => "oe"
198 # }
199 # }
200 # )
201 #
202 # Setting a Proc:
203 #
204 # translit = lambda {|string| MyTransliterator.transliterate(string) }
205 # store_translations(:xx, :i18n => {:transliterate => {:rule => translit})
206 #
207 # Transliterating strings:
208 #
209 # I18n.locale = :en
210 # I18n.transliterate("Jürgen") # => "Jurgen"
211 # I18n.locale = :de
212 # I18n.transliterate("Jürgen") # => "Juergen"
213 # I18n.transliterate("Jürgen", :locale => :en) # => "Jurgen"
214 # I18n.transliterate("Jürgen", :locale => :de) # => "Juergen"
215 def transliterate(*args)
216 options = args.pop if args.last.is_a?(Hash)
217 key = args.shift
218 locale = options && options.delete(:locale) || config.locale
219 raises = options && options.delete(:raise)
220 replacement = options && options.delete(:replacement)
221 config.backend.transliterate(locale, key, replacement)
222 rescue I18n::ArgumentError => exception
223 raise exception if raises
224 handle_exception(exception, locale, key, options)
225 end
226
227 # Localizes certain objects, such as dates and numbers to local formatting.
228 def localize(object, options = {})
229 locale = options.delete(:locale) || config.locale
230 format = options.delete(:format) || :default
231 config.backend.localize(locale, object, format, options)
232 end
233 alias :l :localize
234
235 # Executes block with given I18n.locale set.
236 def with_locale(tmp_locale = nil)
237 if tmp_locale
238 current_locale = self.locale
239 self.locale = tmp_locale
240 end
241 yield
242 ensure
243 self.locale = current_locale if tmp_locale
244 end
245
246
247 # Merges the given locale, key and scope into a single array of keys.
248 # Splits keys that contain dots into multiple keys. Makes sure all
249 # keys are Symbols.
250 def normalize_keys(locale, key, scope, separator = nil)
251 separator ||= I18n.default_separator
252
253 keys = []
254 keys.concat normalize_key(locale, separator)
255 keys.concat normalize_key(scope, separator)
256 keys.concat normalize_key(key, separator)
257 keys
258 end
259
260 # making these private until Ruby 1.9.2 can send to protected methods again
261 # see http://redmine.ruby-lang.org/repositories/revision/ruby-19?rev=24280
262 private
263
264 # Handles exceptions raised in the backend. All exceptions except for
265 # MissingTranslationData exceptions are re-raised. When a MissingTranslationData
266 # was caught and the option :raise is not set the handler returns an error
267 # message string containing the key/scope.
268 def default_exception_handler(exception, locale, key, options)
269 return exception.message if MissingTranslationData === exception
270 raise exception
271 end
272
273 # Any exceptions thrown in translate will be sent to the @@exception_handler
274 # which can be a Symbol, a Proc or any other Object.
275 #
276 # If exception_handler is a Symbol then it will simply be sent to I18n as
277 # a method call. A Proc will simply be called. In any other case the
278 # method #call will be called on the exception_handler object.
279 #
280 # Examples:
281 #
282 # I18n.exception_handler = :default_exception_handler # this is the default
283 # I18n.default_exception_handler(exception, locale, key, options) # will be called like this
284 #
285 # I18n.exception_handler = lambda { |*args| ... } # a lambda
286 # I18n.exception_handler.call(exception, locale, key, options) # will be called like this
287 #
288 # I18n.exception_handler = I18nExceptionHandler.new # an object
289 # I18n.exception_handler.call(exception, locale, key, options) # will be called like this
290 def handle_exception(exception, locale, key, options)
291 case config.exception_handler
292 when Symbol
293 send(config.exception_handler, exception, locale, key, options)
294 else
295 config.exception_handler.call(exception, locale, key, options)
296 end
297 end
298
299 # Deprecated. Will raise a warning in future versions and then finally be
300 # removed. Use I18n.normalize_keys instead.
301 def normalize_translation_keys(locale, key, scope, separator = nil)
302 normalize_keys(locale, key, scope, separator)
303 end
304
305 def normalize_key(key, separator)
306 normalized_key_cache[separator][key] ||=
307 case key
308 when Array
309 key.map { |k| normalize_key(k, separator) }.flatten
310 else
311 keys = key.to_s.split(separator)
312 keys.delete('')
313 keys.map!{ |k| k.to_sym }
314 keys
315 end
316 end
317
318 def normalized_key_cache
319 @normalized_key_cache ||= Hash.new { |h,k| h[k] = {} }
320 end
321 end
322 end