-
-
Notifications
You must be signed in to change notification settings - Fork 411
/
i18n.rb
477 lines (437 loc) · 16.7 KB
/
i18n.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# frozen_string_literal: true
require 'concurrent/map'
require 'concurrent/hash'
require 'i18n/version'
require 'i18n/utils'
require 'i18n/exceptions'
require 'i18n/interpolate/ruby'
module I18n
autoload :Backend, 'i18n/backend'
autoload :Config, 'i18n/config'
autoload :Gettext, 'i18n/gettext'
autoload :Locale, 'i18n/locale'
autoload :Tests, 'i18n/tests'
autoload :Middleware, 'i18n/middleware'
RESERVED_KEYS = %i[
cascade
deep_interpolation
skip_interpolation
default
exception_handler
fallback
fallback_in_progress
fallback_original_locale
format
object
raise
resolve
scope
separator
throw
]
EMPTY_HASH = {}.freeze
def self.new_double_nested_cache # :nodoc:
Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
end
# Marks a key as reserved. Reserved keys are used internally,
# and can't also be used for interpolation. If you are using any
# extra keys as I18n options, you should call I18n.reserve_key
# before any I18n.translate (etc) calls are made.
def self.reserve_key(key)
RESERVED_KEYS << key.to_sym
@reserved_keys_pattern = nil
end
def self.reserved_keys_pattern # :nodoc:
@reserved_keys_pattern ||= /(?<!%)%\{(#{RESERVED_KEYS.join("|")})\}/
end
module Base
# Gets I18n configuration object.
def config
Thread.current[:i18n_config] ||= I18n::Config.new
end
# Sets I18n configuration object.
def config=(value)
Thread.current[:i18n_config] = value
end
# Write methods which delegates to the configuration object
%w(locale backend default_locale available_locales default_separator
exception_handler load_path enforce_available_locales).each do |method|
module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1
def #{method}
config.#{method}
end
def #{method}=(value)
config.#{method} = (value)
end
DELEGATORS
end
# Tells the backend to reload translations. Used in situations like the
# Rails development environment. Backends can implement whatever strategy
# is useful.
def reload!
config.clear_available_locales_set
config.backend.reload!
end
# Tells the backend to load translations now. Used in situations like the
# Rails production environment. Backends can implement whatever strategy
# is useful.
def eager_load!
config.backend.eager_load!
end
# Translates, pluralizes and interpolates a given key using a given locale,
# scope, and default, as well as interpolation values.
#
# *LOOKUP*
#
# Translation data is organized as a nested hash using the upper-level keys
# as namespaces. <em>E.g.</em>, ActionView ships with the translation:
# <tt>:date => {:formats => {:short => "%b %d"}}</tt>.
#
# Translations can be looked up at any level of this hash using the key argument
# and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt>
# returns the whole translations hash <tt>{:formats => {:short => "%b %d"}}</tt>.
#
# Key can be either a single key or a dot-separated key (both Strings and Symbols
# work). <em>E.g.</em>, the short format can be looked up using both:
# I18n.t 'date.formats.short'
# I18n.t :'date.formats.short'
#
# Scope can be either a single key, a dot-separated key or an array of keys
# or dot-separated keys. Keys and scopes can be combined freely. So these
# examples will all look up the same short date format:
# I18n.t 'date.formats.short'
# I18n.t 'formats.short', :scope => 'date'
# I18n.t 'short', :scope => 'date.formats'
# I18n.t 'short', :scope => %w(date formats)
#
# *INTERPOLATION*
#
# Translations can contain interpolation variables which will be replaced by
# values passed to #translate as part of the options hash, with the keys matching
# the interpolation variable names.
#
# <em>E.g.</em>, with a translation <tt>:foo => "foo %{bar}"</tt> the option
# value for the key +bar+ will be interpolated into the translation:
# I18n.t :foo, :bar => 'baz' # => 'foo baz'
#
# *PLURALIZATION*
#
# Translation data can contain pluralized translations. Pluralized translations
# are arrays of singular/plural versions of translations like <tt>['Foo', 'Foos']</tt>.
#
# Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English
# pluralization rules. Other algorithms can be supported by custom backends.
#
# This returns the singular version of a pluralized translation:
# I18n.t :foo, :count => 1 # => 'Foo'
#
# These both return the plural version of a pluralized translation:
# I18n.t :foo, :count => 0 # => 'Foos'
# I18n.t :foo, :count => 2 # => 'Foos'
#
# The <tt>:count</tt> option can be used both for pluralization and interpolation.
# <em>E.g.</em>, with the translation
# <tt>:foo => ['%{count} foo', '%{count} foos']</tt>, count will
# be interpolated to the pluralized translation:
# I18n.t :foo, :count => 1 # => '1 foo'
#
# *DEFAULTS*
#
# This returns the translation for <tt>:foo</tt> or <tt>default</tt> if no translation was found:
# I18n.t :foo, :default => 'default'
#
# This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no
# translation for <tt>:foo</tt> was found:
# I18n.t :foo, :default => :bar
#
# Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt>
# or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
# I18n.t :foo, :default => [:bar, 'default']
#
# <b>BULK LOOKUP</b>
#
# This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
# I18n.t [:foo, :bar]
#
# Can be used with dot-separated nested keys:
# I18n.t [:'baz.foo', :'baz.bar']
#
# Which is the same as using a scope option:
# I18n.t [:foo, :bar], :scope => :baz
#
# *LAMBDAS*
#
# Both translations and defaults can be given as Ruby lambdas. Lambdas will be
# called and passed the key and options.
#
# E.g. assuming the key <tt>:salutation</tt> resolves to:
# lambda { |key, options| options[:gender] == 'm' ? "Mr. #{options[:name]}" : "Mrs. #{options[:name]}" }
#
# Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith')</tt> will result in "Mrs. Smith".
#
# Note that the string returned by lambda will go through string interpolation too,
# so the following lambda would give the same result:
# lambda { |key, options| options[:gender] == 'm' ? "Mr. %{name}" : "Mrs. %{name}" }
#
# It is recommended to use/implement lambdas in an "idempotent" way. E.g. when
# a cache layer is put in front of I18n.translate it will generate a cache key
# from the argument values passed to #translate. Therefore your lambdas should
# always return the same translations/values per unique combination of argument
# values.
#
# <b>Ruby 2.7+ keyword arguments warning</b>
#
# This method uses keyword arguments.
# There is a breaking change in ruby that produces warning with ruby 2.7 and won't work as expected with ruby 3.0
# The "hash" parameter must be passed as keyword argument.
#
# Good:
# I18n.t(:salutation, :gender => 'w', :name => 'Smith')
# I18n.t(:salutation, **{ :gender => 'w', :name => 'Smith' })
# I18n.t(:salutation, **any_hash)
#
# Bad:
# I18n.t(:salutation, { :gender => 'w', :name => 'Smith' })
# I18n.t(:salutation, any_hash)
#
def translate(key = nil, throw: false, raise: false, locale: nil, **options) # TODO deprecate :raise
locale ||= config.locale
raise Disabled.new('t') if locale == false
enforce_available_locales!(locale)
backend = config.backend
if key.is_a?(Array)
key.map do |k|
translate_key(k, throw, raise, locale, backend, options)
end
else
translate_key(key, throw, raise, locale, backend, options)
end
end
alias :t :translate
# Wrapper for <tt>translate</tt> that adds <tt>:raise => true</tt>. With
# this option, if no translation is found, it will raise <tt>I18n::MissingTranslationData</tt>
def translate!(key, **options)
translate(key, **options, raise: true)
end
alias :t! :translate!
# Returns an array of interpolation keys for the given translation key
#
# *Examples*
#
# Suppose we have the following:
# I18n.t 'example.zero' == 'Zero interpolations'
# I18n.t 'example.one' == 'One interpolation %{foo}'
# I18n.t 'example.two' == 'Two interpolations %{foo} %{bar}'
# I18n.t 'example.three' == ['One %{foo}', 'Two %{bar}', 'Three %{baz}']
# I18n.t 'example.one', locale: :other == 'One interpolation %{baz}'
#
# Then we can expect the following results:
# I18n.interpolation_keys('example.zero') #=> []
# I18n.interpolation_keys('example.one') #=> ['foo']
# I18n.interpolation_keys('example.two') #=> ['foo', 'bar']
# I18n.interpolation_keys('example.three') #=> ['foo', 'bar', 'baz']
# I18n.interpolation_keys('one', scope: 'example', locale: :other) #=> ['baz']
# I18n.interpolation_keys('does-not-exist') #=> []
# I18n.interpolation_keys('example') #=> []
def interpolation_keys(key, **options)
raise I18n::ArgumentError if !key.is_a?(String) || key.empty?
return [] unless exists?(key, **options.slice(:locale, :scope))
translation = translate(key, **options.slice(:locale, :scope))
interpolation_keys_from_translation(translation)
.flatten.compact
end
# Returns true if a translation exists for a given key, otherwise returns false.
def exists?(key, _locale = nil, locale: _locale, **options)
locale ||= config.locale
raise Disabled.new('exists?') if locale == false
raise I18n::ArgumentError if (key.is_a?(String) && key.empty?) || key.nil?
config.backend.exists?(locale, key, options)
end
# Transliterates UTF-8 characters to ASCII. By default this method will
# transliterate only Latin strings to an ASCII approximation:
#
# I18n.transliterate("Ærøskøbing")
# # => "AEroskobing"
#
# I18n.transliterate("日本語")
# # => "???"
#
# It's also possible to add support for per-locale transliterations. I18n
# expects transliteration rules to be stored at
# <tt>i18n.transliterate.rule</tt>.
#
# Transliteration rules can either be a Hash or a Proc. Procs must accept a
# single string argument. Hash rules inherit the default transliteration
# rules, while Procs do not.
#
# *Examples*
#
# Setting a Hash in <locale>.yml:
#
# i18n:
# transliterate:
# rule:
# ü: "ue"
# ö: "oe"
#
# Setting a Hash using Ruby:
#
# store_translations(:de, i18n: {
# transliterate: {
# rule: {
# 'ü' => 'ue',
# 'ö' => 'oe'
# }
# }
# })
#
# Setting a Proc:
#
# translit = lambda {|string| MyTransliterator.transliterate(string) }
# store_translations(:xx, :i18n => {:transliterate => {:rule => translit})
#
# Transliterating strings:
#
# I18n.locale = :en
# I18n.transliterate("Jürgen") # => "Jurgen"
# I18n.locale = :de
# I18n.transliterate("Jürgen") # => "Juergen"
# I18n.transliterate("Jürgen", :locale => :en) # => "Jurgen"
# I18n.transliterate("Jürgen", :locale => :de) # => "Juergen"
def transliterate(key, throw: false, raise: false, locale: nil, replacement: nil, **options)
locale ||= config.locale
raise Disabled.new('transliterate') if locale == false
enforce_available_locales!(locale)
config.backend.transliterate(locale, key, replacement)
rescue I18n::ArgumentError => exception
handle_exception((throw && :throw || raise && :raise), exception, locale, key, options)
end
# Localizes certain objects, such as dates and numbers to local formatting.
def localize(object, locale: nil, format: nil, **options)
locale ||= config.locale
raise Disabled.new('l') if locale == false
enforce_available_locales!(locale)
format ||= :default
config.backend.localize(locale, object, format, options)
end
alias :l :localize
# Executes block with given I18n.locale set.
def with_locale(tmp_locale = nil)
if tmp_locale == nil
yield
else
current_locale = self.locale
self.locale = tmp_locale
begin
yield
ensure
self.locale = current_locale
end
end
end
# Merges the given locale, key and scope into a single array of keys.
# Splits keys that contain dots into multiple keys. Makes sure all
# keys are Symbols.
def normalize_keys(locale, key, scope, separator = nil)
separator ||= I18n.default_separator
[
*normalize_key(locale, separator),
*normalize_key(scope, separator),
*normalize_key(key, separator)
]
end
# Returns true when the passed locale, which can be either a String or a
# Symbol, is in the list of available locales. Returns false otherwise.
def locale_available?(locale)
I18n.config.available_locales_set.include?(locale)
end
# Raises an InvalidLocale exception when the passed locale is not available.
def enforce_available_locales!(locale)
if locale != false && config.enforce_available_locales
raise I18n::InvalidLocale.new(locale) if !locale_available?(locale)
end
end
def available_locales_initialized?
config.available_locales_initialized?
end
private
def translate_key(key, throw, raise, locale, backend, options)
result = catch(:exception) do
backend.translate(locale, key, options)
end
if result.is_a?(MissingTranslation)
handle_exception((throw && :throw || raise && :raise), result, locale, key, options)
else
result
end
end
# Any exceptions thrown in translate will be sent to the @@exception_handler
# which can be a Symbol, a Proc or any other Object unless they're forced to
# be raised or thrown (MissingTranslation).
#
# If exception_handler is a Symbol then it will simply be sent to I18n as
# a method call. A Proc will simply be called. In any other case the
# method #call will be called on the exception_handler object.
#
# Examples:
#
# I18n.exception_handler = :custom_exception_handler # this is the default
# I18n.custom_exception_handler(exception, locale, key, options) # will be called like this
#
# I18n.exception_handler = lambda { |*args| ... } # a lambda
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
#
# I18n.exception_handler = I18nExceptionHandler.new # an object
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
def handle_exception(handling, exception, locale, key, options)
case handling
when :raise
raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
when :throw
throw :exception, exception
else
case handler = options[:exception_handler] || config.exception_handler
when Symbol
send(handler, exception, locale, key, options)
else
handler.call(exception, locale, key, options)
end
end
end
@@normalized_key_cache = I18n.new_double_nested_cache
def normalize_key(key, separator)
@@normalized_key_cache[separator][key] ||=
case key
when Array
key.flat_map { |k| normalize_key(k, separator) }
else
keys = key.to_s.split(separator)
keys.delete('')
keys.map! do |k|
case k
when /\A[-+]?([1-9]\d*|0)\z/ # integer
k.to_i
when 'true'
true
when 'false'
false
else
k.to_sym
end
end
keys
end
end
def interpolation_keys_from_translation(translation)
case translation
when ::String
translation.scan(Regexp.union(I18n.config.interpolation_patterns))
when ::Array
translation.map { |element| interpolation_keys_from_translation(element) }
else
[]
end
end
end
extend Base
end