i18n 1.12.0 → 1.14.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/lib/i18n/backend/base.rb +26 -11
- data/lib/i18n/backend/chain.rb +2 -0
- data/lib/i18n/backend/fallbacks.rb +6 -2
- data/lib/i18n/backend/interpolation_compiler.rb +6 -7
- data/lib/i18n/backend/lazy_loadable.rb +1 -1
- data/lib/i18n/backend/pluralization.rb +58 -17
- data/lib/i18n/backend/simple.rb +17 -10
- data/lib/i18n/backend/transliterator.rb +24 -24
- data/lib/i18n/exceptions.rb +12 -4
- data/lib/i18n/interpolate/ruby.rb +15 -1
- data/lib/i18n/locale/fallbacks.rb +13 -3
- data/lib/i18n/middleware.rb +1 -1
- data/lib/i18n/tests/defaults.rb +7 -0
- data/lib/i18n/tests/interpolation.rb +25 -3
- data/lib/i18n/tests/localization/date.rb +6 -1
- data/lib/i18n/tests/localization/date_time.rb +1 -1
- data/lib/i18n/tests/localization/procs.rb +2 -2
- data/lib/i18n/tests/localization/time.rb +1 -1
- data/lib/i18n/tests/lookup.rb +7 -1
- data/lib/i18n/tests/procs.rb +12 -7
- data/lib/i18n/version.rb +1 -1
- data/lib/i18n.rb +50 -7
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edf1a48f0dd110066260f4d93cd2af16e73e9f91845a16126794d4c9af5b664c
|
|
4
|
+
data.tar.gz: 36a488fcbbaefdabb42e1a6f42af534fa36f640de5674d31a92407d6dfd1835d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82897d5266dfb62379930d6f94346dc0e62c14e619995e3933dc6339d0dd6d06d4b491449d1f4eb43128de995e3d9837624e4132b7b506455e0169dece108b7f
|
|
7
|
+
data.tar.gz: 8938b0f8d862e989f7427ccb2299722914bd485af9afff24bf0bda6cd8f438dc750cacd8c85d45292787f2e78438ead7c7533d6c696f5d5b9fc7109535a48ee4
|
data/README.md
CHANGED
|
@@ -13,10 +13,14 @@ Currently maintained by @radar.
|
|
|
13
13
|
|
|
14
14
|
You will most commonly use this library within a Rails app.
|
|
15
15
|
|
|
16
|
+
We support Rails versions from 6.0 and up.
|
|
17
|
+
|
|
16
18
|
[See the Rails Guide](https://guides.rubyonrails.org/i18n.html) for an example of its usage.
|
|
17
19
|
|
|
18
20
|
### Ruby (without Rails)
|
|
19
21
|
|
|
22
|
+
We support Ruby versions from 3.0 and up.
|
|
23
|
+
|
|
20
24
|
If you want to use this library without Rails, you can simply add `i18n` to your `Gemfile`:
|
|
21
25
|
|
|
22
26
|
```ruby
|
|
@@ -26,7 +30,7 @@ gem 'i18n'
|
|
|
26
30
|
Then configure I18n with some translations, and a default locale:
|
|
27
31
|
|
|
28
32
|
```ruby
|
|
29
|
-
I18n.load_path
|
|
33
|
+
I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
|
|
30
34
|
I18n.default_locale = :en # (note that `en` is already the default!)
|
|
31
35
|
```
|
|
32
36
|
|
data/lib/i18n/backend/base.rb
CHANGED
|
@@ -54,19 +54,22 @@ module I18n
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
deep_interpolation = options[:deep_interpolation]
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
skip_interpolation = options[:skip_interpolation]
|
|
58
|
+
values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
|
|
59
|
+
if !skip_interpolation && values && !values.empty?
|
|
59
60
|
entry = if deep_interpolation
|
|
60
61
|
deep_interpolate(locale, entry, values)
|
|
61
62
|
else
|
|
62
63
|
interpolate(locale, entry, values)
|
|
63
64
|
end
|
|
65
|
+
elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
|
|
66
|
+
raise ReservedInterpolationKey.new($1.to_sym, entry)
|
|
64
67
|
end
|
|
65
68
|
entry
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
def exists?(locale, key, options = EMPTY_HASH)
|
|
69
|
-
lookup(locale, key) != nil
|
|
72
|
+
lookup(locale, key, options[:scope]) != nil
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
# Acts the same as +strftime+, but uses a localized version of the
|
|
@@ -123,7 +126,12 @@ module I18n
|
|
|
123
126
|
# first translation that can be resolved. Otherwise it tries to resolve
|
|
124
127
|
# the translation directly.
|
|
125
128
|
def default(locale, object, subject, options = EMPTY_HASH)
|
|
126
|
-
|
|
129
|
+
if options.size == 1 && options.has_key?(:default)
|
|
130
|
+
options = {}
|
|
131
|
+
else
|
|
132
|
+
options = Utils.except(options, :default)
|
|
133
|
+
end
|
|
134
|
+
|
|
127
135
|
case subject
|
|
128
136
|
when Array
|
|
129
137
|
subject.each do |item|
|
|
@@ -144,7 +152,14 @@ module I18n
|
|
|
144
152
|
result = catch(:exception) do
|
|
145
153
|
case subject
|
|
146
154
|
when Symbol
|
|
147
|
-
I18n.translate(
|
|
155
|
+
I18n.translate(
|
|
156
|
+
subject,
|
|
157
|
+
**options.merge(
|
|
158
|
+
:locale => locale,
|
|
159
|
+
:throw => true,
|
|
160
|
+
:skip_interpolation => true
|
|
161
|
+
)
|
|
162
|
+
)
|
|
148
163
|
when Proc
|
|
149
164
|
date_or_time = options.delete(:object) || object
|
|
150
165
|
resolve(locale, object, subject.call(date_or_time, **options))
|
|
@@ -166,7 +181,7 @@ module I18n
|
|
|
166
181
|
# Other backends can implement more flexible or complex pluralization rules.
|
|
167
182
|
def pluralize(locale, entry, count)
|
|
168
183
|
entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
|
|
169
|
-
return entry unless entry.is_a?(Hash) && count
|
|
184
|
+
return entry unless entry.is_a?(Hash) && count
|
|
170
185
|
|
|
171
186
|
key = pluralization_key(entry, count)
|
|
172
187
|
raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
|
|
@@ -181,8 +196,8 @@ module I18n
|
|
|
181
196
|
#
|
|
182
197
|
# if the given subject is an array then:
|
|
183
198
|
# each element of the array is recursively interpolated (until it finds a string)
|
|
184
|
-
# method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
|
|
185
|
-
# # =>
|
|
199
|
+
# method interpolates ["yes, %{user}", ["maybe no, %{user}", "no, %{user}"]], :user => "bartuz"
|
|
200
|
+
# # => ["yes, bartuz", ["maybe no, bartuz", "no, bartuz"]]
|
|
186
201
|
def interpolate(locale, subject, values = EMPTY_HASH)
|
|
187
202
|
return subject if values.empty?
|
|
188
203
|
|
|
@@ -237,7 +252,7 @@ module I18n
|
|
|
237
252
|
# Loads a plain Ruby translations file. eval'ing the file must yield
|
|
238
253
|
# a Hash containing translation data with locales as toplevel keys.
|
|
239
254
|
def load_rb(filename)
|
|
240
|
-
translations = eval(IO.read(filename), binding, filename)
|
|
255
|
+
translations = eval(IO.read(filename), binding, filename.to_s)
|
|
241
256
|
[translations, false]
|
|
242
257
|
end
|
|
243
258
|
|
|
@@ -282,8 +297,8 @@ module I18n
|
|
|
282
297
|
when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
|
|
283
298
|
when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
|
|
284
299
|
when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
|
|
285
|
-
when '%p' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
|
|
286
|
-
when '%P' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
|
|
300
|
+
when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
|
|
301
|
+
when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
|
|
287
302
|
end
|
|
288
303
|
end
|
|
289
304
|
rescue MissingTranslationData => e
|
data/lib/i18n/backend/chain.rb
CHANGED
|
@@ -16,6 +16,8 @@ module I18n
|
|
|
16
16
|
#
|
|
17
17
|
# The implementation assumes that all backends added to the Chain implement
|
|
18
18
|
# a lookup method with the same API as Simple backend does.
|
|
19
|
+
#
|
|
20
|
+
# Fallback translations using the :default option are only used by the last backend of a chain.
|
|
19
21
|
class Chain
|
|
20
22
|
module Implementation
|
|
21
23
|
include Base
|
|
@@ -71,7 +71,11 @@ module I18n
|
|
|
71
71
|
|
|
72
72
|
case subject
|
|
73
73
|
when Symbol
|
|
74
|
-
I18n.translate(subject, **options.merge(
|
|
74
|
+
I18n.translate(subject, **options.merge(
|
|
75
|
+
:locale => options[:fallback_original_locale],
|
|
76
|
+
:throw => true,
|
|
77
|
+
:skip_interpolation => true
|
|
78
|
+
))
|
|
75
79
|
when Proc
|
|
76
80
|
date_or_time = options.delete(:object) || object
|
|
77
81
|
resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
|
|
@@ -95,7 +99,7 @@ module I18n
|
|
|
95
99
|
return super unless options.fetch(:fallback, true)
|
|
96
100
|
I18n.fallbacks[locale].each do |fallback|
|
|
97
101
|
begin
|
|
98
|
-
return true if super(fallback, key)
|
|
102
|
+
return true if super(fallback, key, options)
|
|
99
103
|
rescue I18n::InvalidLocale
|
|
100
104
|
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
|
101
105
|
end
|
|
@@ -21,11 +21,11 @@ module I18n
|
|
|
21
21
|
module Compiler
|
|
22
22
|
extend self
|
|
23
23
|
|
|
24
|
-
TOKENIZER
|
|
25
|
-
INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/
|
|
24
|
+
TOKENIZER = /(%%?\{[^}]+\})/
|
|
26
25
|
|
|
27
26
|
def compile_if_an_interpolation(string)
|
|
28
27
|
if interpolated_str?(string)
|
|
28
|
+
string = +string
|
|
29
29
|
string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
|
|
30
30
|
def i18n_interpolate(v = {})
|
|
31
31
|
"#{compiled_interpolation_body(string)}"
|
|
@@ -37,7 +37,7 @@ module I18n
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def interpolated_str?(str)
|
|
40
|
-
str.kind_of?(::String) && str =~
|
|
40
|
+
str.kind_of?(::String) && str =~ TOKENIZER
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
protected
|
|
@@ -48,13 +48,12 @@ module I18n
|
|
|
48
48
|
|
|
49
49
|
def compiled_interpolation_body(str)
|
|
50
50
|
tokenize(str).map do |token|
|
|
51
|
-
|
|
51
|
+
token.match(TOKENIZER) ? handle_interpolation_token(token) : escape_plain_str(token)
|
|
52
52
|
end.join
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def handle_interpolation_token(
|
|
56
|
-
|
|
57
|
-
escaped ? pattern : compile_interpolation_token(key.to_sym)
|
|
55
|
+
def handle_interpolation_token(token)
|
|
56
|
+
token.start_with?('%%') ? token[1..] : compile_interpolation_token(token[2..-2])
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
def compile_interpolation_token(key)
|
|
@@ -98,7 +98,7 @@ module I18n
|
|
|
98
98
|
# Parse the load path and extract all locales.
|
|
99
99
|
def available_locales
|
|
100
100
|
if lazy_load?
|
|
101
|
-
I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }
|
|
101
|
+
I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }.uniq
|
|
102
102
|
else
|
|
103
103
|
super
|
|
104
104
|
end
|
|
@@ -16,26 +16,57 @@ module I18n
|
|
|
16
16
|
module Pluralization
|
|
17
17
|
# Overwrites the Base backend translate method so that it will check the
|
|
18
18
|
# translation meta data space (:i18n) for a locale specific pluralization
|
|
19
|
-
# rule and use it to pluralize the given entry. I.e
|
|
19
|
+
# rule and use it to pluralize the given entry. I.e., the library expects
|
|
20
20
|
# pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
|
|
21
21
|
#
|
|
22
22
|
# Pluralization rules are expected to respond to #call(count) and
|
|
23
|
-
# return a pluralization key. Valid keys depend on the
|
|
24
|
-
#
|
|
25
|
-
#
|
|
23
|
+
# return a pluralization key. Valid keys depend on the pluralization
|
|
24
|
+
# rules for the locale, as defined in the CLDR.
|
|
25
|
+
# As of v41, 6 locale-specific plural categories are defined:
|
|
26
|
+
# :few, :many, :one, :other, :two, :zero
|
|
26
27
|
#
|
|
27
|
-
# The :
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
28
|
+
# n.b., The :one plural category does not imply the number 1.
|
|
29
|
+
# Instead, :one is a category for any number that behaves like 1 in
|
|
30
|
+
# that locale. For example, in some locales, :one is used for numbers
|
|
31
|
+
# that end in "1" (like 1, 21, 151) but that don't end in
|
|
32
|
+
# 11 (like 11, 111, 10311).
|
|
33
|
+
# Similar notes apply to the :two, and :zero plural categories.
|
|
34
|
+
#
|
|
35
|
+
# If you want to have different strings for the categories of count == 0
|
|
36
|
+
# (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car")
|
|
37
|
+
# use the explicit `"0"` and `"1"` keys.
|
|
38
|
+
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
|
|
31
39
|
def pluralize(locale, entry, count)
|
|
32
40
|
return entry unless entry.is_a?(Hash) && count
|
|
33
41
|
|
|
34
42
|
pluralizer = pluralizer(locale)
|
|
35
43
|
if pluralizer.respond_to?(:call)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
# Deprecation: The use of the `zero` key in this way is incorrect.
|
|
45
|
+
# Users that want a different string for the case of `count == 0` should use the explicit "0" key instead.
|
|
46
|
+
# We keep this incorrect behaviour for now for backwards compatibility until we can remove it.
|
|
47
|
+
# Ref: https://github.com/ruby-i18n/i18n/issues/629
|
|
48
|
+
return entry[:zero] if count == 0 && entry.has_key?(:zero)
|
|
49
|
+
|
|
50
|
+
# "0" and "1" are special cases
|
|
51
|
+
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
|
|
52
|
+
if count == 0 || count == 1
|
|
53
|
+
value = entry[symbolic_count(count)]
|
|
54
|
+
return value if value
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
|
|
58
|
+
# > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
|
|
59
|
+
# > 1. If "x" is numeric, the path falls back to the path with [@count=«the plural rules category for x for that locale»], within that the same locale.
|
|
60
|
+
# > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
|
|
61
|
+
# > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
|
|
62
|
+
# Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
|
|
63
|
+
plural_rule_category = pluralizer.call(count)
|
|
64
|
+
|
|
65
|
+
value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
|
|
66
|
+
entry[plural_rule_category] || entry[:other]
|
|
67
|
+
else
|
|
68
|
+
raise InvalidPluralizationData.new(entry, count, plural_rule_category)
|
|
69
|
+
end
|
|
39
70
|
else
|
|
40
71
|
super
|
|
41
72
|
end
|
|
@@ -43,13 +74,23 @@ module I18n
|
|
|
43
74
|
|
|
44
75
|
protected
|
|
45
76
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
77
|
+
def pluralizers
|
|
78
|
+
@pluralizers ||= {}
|
|
79
|
+
end
|
|
49
80
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
def pluralizer(locale)
|
|
82
|
+
pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Normalizes categories of 0.0 and 1.0
|
|
88
|
+
# and returns the symbolic version
|
|
89
|
+
def symbolic_count(count)
|
|
90
|
+
count = 0 if count == 0
|
|
91
|
+
count = 1 if count == 1
|
|
92
|
+
count.to_s.to_sym
|
|
93
|
+
end
|
|
53
94
|
end
|
|
54
95
|
end
|
|
55
96
|
end
|
data/lib/i18n/backend/simple.rb
CHANGED
|
@@ -10,17 +10,20 @@ module I18n
|
|
|
10
10
|
# The implementation is provided by a Implementation module allowing to easily
|
|
11
11
|
# extend Simple backend's behavior by including modules. E.g.:
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
13
|
+
# module I18n::Backend::Pluralization
|
|
14
|
+
# def pluralize(*args)
|
|
15
|
+
# # extended pluralization logic
|
|
16
|
+
# super
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
|
|
21
21
|
class Simple
|
|
22
22
|
module Implementation
|
|
23
23
|
include Base
|
|
24
|
+
|
|
25
|
+
# Mutex to ensure that concurrent translations loading will be thread-safe
|
|
26
|
+
MUTEX = Mutex.new
|
|
24
27
|
|
|
25
28
|
def initialized?
|
|
26
29
|
@initialized ||= false
|
|
@@ -68,7 +71,11 @@ module I18n
|
|
|
68
71
|
# call `init_translations`
|
|
69
72
|
init_translations if do_init && !initialized?
|
|
70
73
|
|
|
71
|
-
@translations ||= Concurrent::Hash.new
|
|
74
|
+
@translations ||= Concurrent::Hash.new do |h, k|
|
|
75
|
+
MUTEX.synchronize do
|
|
76
|
+
h[k] = Concurrent::Hash.new
|
|
77
|
+
end
|
|
78
|
+
end
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
protected
|
|
@@ -94,7 +101,7 @@ module I18n
|
|
|
94
101
|
return nil unless result.has_key?(_key)
|
|
95
102
|
end
|
|
96
103
|
result = result[_key]
|
|
97
|
-
result = resolve_entry(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
|
|
104
|
+
result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
|
|
98
105
|
result
|
|
99
106
|
end
|
|
100
107
|
end
|
|
@@ -45,30 +45,30 @@ module I18n
|
|
|
45
45
|
"Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
|
|
46
46
|
"Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
|
|
47
47
|
"Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
|
|
48
|
-
"Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG",
|
|
64
|
-
"Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o",
|
|
65
|
-
"œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R",
|
|
66
|
-
"Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s",
|
|
67
|
-
"š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T",
|
|
68
|
-
"Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u",
|
|
69
|
-
"ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W",
|
|
70
|
-
"Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z",
|
|
71
|
-
"Ž"=>"Z", "ž"=>"z"
|
|
48
|
+
"Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "ẞ"=>"SS", "à"=>"a",
|
|
49
|
+
"á"=>"a", "â"=>"a", "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c",
|
|
50
|
+
"è"=>"e", "é"=>"e", "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i",
|
|
51
|
+
"ï"=>"i", "ð"=>"d", "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o",
|
|
52
|
+
"ö"=>"o", "ø"=>"o", "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y",
|
|
53
|
+
"þ"=>"th", "ÿ"=>"y", "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A",
|
|
54
|
+
"ą"=>"a", "Ć"=>"C", "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c",
|
|
55
|
+
"Č"=>"C", "č"=>"c", "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E",
|
|
56
|
+
"ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e",
|
|
57
|
+
"Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G",
|
|
58
|
+
"ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h",
|
|
59
|
+
"Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I",
|
|
60
|
+
"į"=>"i", "İ"=>"I", "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j",
|
|
61
|
+
"Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l",
|
|
62
|
+
"Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N",
|
|
63
|
+
"ń"=>"n", "Ņ"=>"N", "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG",
|
|
64
|
+
"ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o",
|
|
65
|
+
"Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R",
|
|
66
|
+
"ř"=>"r", "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s",
|
|
67
|
+
"Š"=>"S", "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T",
|
|
68
|
+
"ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u",
|
|
69
|
+
"Ů"=>"U", "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W",
|
|
70
|
+
"ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z",
|
|
71
|
+
"ż"=>"z", "Ž"=>"Z", "ž"=>"z"
|
|
72
72
|
}.freeze
|
|
73
73
|
|
|
74
74
|
def initialize(rule = nil)
|
data/lib/i18n/exceptions.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'cgi'
|
|
4
|
-
|
|
5
3
|
module I18n
|
|
6
4
|
class ExceptionHandler
|
|
7
5
|
def call(exception, _locale, _key, _options)
|
|
@@ -47,7 +45,7 @@ module I18n
|
|
|
47
45
|
|
|
48
46
|
class MissingTranslation < ArgumentError
|
|
49
47
|
module Base
|
|
50
|
-
PERMITTED_KEYS = [:scope].freeze
|
|
48
|
+
PERMITTED_KEYS = [:scope, :default].freeze
|
|
51
49
|
|
|
52
50
|
attr_reader :locale, :key, :options
|
|
53
51
|
|
|
@@ -63,8 +61,18 @@ module I18n
|
|
|
63
61
|
end
|
|
64
62
|
|
|
65
63
|
def message
|
|
66
|
-
|
|
64
|
+
if (default = options[:default]).is_a?(Array) && default.any?
|
|
65
|
+
other_options = ([key, *default]).map { |k| normalized_option(k).prepend('- ') }.join("\n")
|
|
66
|
+
"Translation missing. Options considered were:\n#{other_options}"
|
|
67
|
+
else
|
|
68
|
+
"Translation missing: #{keys.join('.')}"
|
|
69
|
+
end
|
|
67
70
|
end
|
|
71
|
+
|
|
72
|
+
def normalized_option(key)
|
|
73
|
+
I18n.normalize_keys(locale, key, options[:scope]).join('.')
|
|
74
|
+
end
|
|
75
|
+
|
|
68
76
|
alias :to_s :message
|
|
69
77
|
|
|
70
78
|
def to_exception
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# heavily based on Masao Mutoh's gettext String interpolation extension
|
|
2
4
|
# http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
|
|
3
5
|
|
|
@@ -10,6 +12,11 @@ module I18n
|
|
|
10
12
|
INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)
|
|
11
13
|
deprecate_constant :INTERPOLATION_PATTERN
|
|
12
14
|
|
|
15
|
+
INTERPOLATION_PATTERNS_CACHE = Hash.new do |hash, patterns|
|
|
16
|
+
hash[patterns] = Regexp.union(patterns)
|
|
17
|
+
end
|
|
18
|
+
private_constant :INTERPOLATION_PATTERNS_CACHE
|
|
19
|
+
|
|
13
20
|
class << self
|
|
14
21
|
# Return String or raises MissingInterpolationArgument exception.
|
|
15
22
|
# Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler.
|
|
@@ -20,7 +27,12 @@ module I18n
|
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
def interpolate_hash(string, values)
|
|
23
|
-
|
|
30
|
+
pattern = INTERPOLATION_PATTERNS_CACHE[config.interpolation_patterns]
|
|
31
|
+
interpolated = false
|
|
32
|
+
|
|
33
|
+
interpolated_string = string.gsub(pattern) do |match|
|
|
34
|
+
interpolated = true
|
|
35
|
+
|
|
24
36
|
if match == '%%'
|
|
25
37
|
'%'
|
|
26
38
|
else
|
|
@@ -34,6 +46,8 @@ module I18n
|
|
|
34
46
|
$3 ? sprintf("%#{$3}", value) : value
|
|
35
47
|
end
|
|
36
48
|
end
|
|
49
|
+
|
|
50
|
+
interpolated ? interpolated_string : string
|
|
37
51
|
end
|
|
38
52
|
end
|
|
39
53
|
end
|
|
@@ -79,14 +79,24 @@ module I18n
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def empty?
|
|
83
|
+
@map.empty? && @defaults.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def inspect
|
|
87
|
+
"#<#{self.class.name} @map=#{@map.inspect} @defaults=#{@defaults.inspect}>"
|
|
88
|
+
end
|
|
89
|
+
|
|
82
90
|
protected
|
|
83
91
|
|
|
84
92
|
def compute(tags, include_defaults = true, exclude = [])
|
|
85
|
-
result =
|
|
93
|
+
result = []
|
|
94
|
+
Array(tags).each do |tag|
|
|
86
95
|
tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude
|
|
87
|
-
|
|
88
|
-
tags
|
|
96
|
+
result += tags
|
|
97
|
+
tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] }
|
|
89
98
|
end
|
|
99
|
+
|
|
90
100
|
result.push(*defaults) if include_defaults
|
|
91
101
|
result.uniq!
|
|
92
102
|
result.compact!
|
data/lib/i18n/middleware.rb
CHANGED
data/lib/i18n/tests/defaults.rb
CHANGED
|
@@ -47,6 +47,13 @@ module I18n
|
|
|
47
47
|
I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' })
|
|
48
48
|
assert_equal 'bar', I18n.t(nil, :default => :'foo|bar', :separator => '|')
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
# Addresses issue: #599
|
|
52
|
+
test "defaults: only interpolates once when resolving defaults" do
|
|
53
|
+
I18n.backend.store_translations(:en, :greeting => 'hey %{name}')
|
|
54
|
+
assert_equal 'hey %{dont_interpolate_me}',
|
|
55
|
+
I18n.t(:does_not_exist, :name => '%{dont_interpolate_me}', default: [:greeting])
|
|
56
|
+
end
|
|
50
57
|
end
|
|
51
58
|
end
|
|
52
59
|
end
|
|
@@ -89,14 +89,14 @@ module I18n
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
test "interpolation: ASCII strings in the backend should be encoded to UTF8 if interpolation options are in UTF8" do
|
|
92
|
-
I18n.backend.store_translations 'en', 'encoding' => ('%{who} let me go'.force_encoding(
|
|
92
|
+
I18n.backend.store_translations 'en', 'encoding' => ('%{who} let me go'.dup.force_encoding(Encoding::US_ASCII))
|
|
93
93
|
result = I18n.t 'encoding', :who => "måmmå miå"
|
|
94
94
|
assert_equal Encoding::UTF_8, result.encoding
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
test "interpolation: UTF8 strings in the backend are still returned as UTF8 with ASCII interpolation" do
|
|
98
98
|
I18n.backend.store_translations 'en', 'encoding' => 'måmmå miå %{what}'
|
|
99
|
-
result = I18n.t 'encoding', :what => 'let me go'.force_encoding(
|
|
99
|
+
result = I18n.t 'encoding', :what => 'let me go'.dup.force_encoding(Encoding::US_ASCII)
|
|
100
100
|
assert_equal Encoding::UTF_8, result.encoding
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -112,6 +112,28 @@ module I18n
|
|
|
112
112
|
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{default}') }
|
|
113
113
|
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{separator}') }
|
|
114
114
|
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:foo => :bar, :default => '%{scope}') }
|
|
115
|
+
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:default => '%{scope}') }
|
|
116
|
+
|
|
117
|
+
I18n.backend.store_translations(:en, :interpolate => 'Hi %{scope}!')
|
|
118
|
+
assert_raises(I18n::ReservedInterpolationKey) { interpolate(:interpolate) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
test "interpolation: it does not raise I18n::ReservedInterpolationKey for escaped variables" do
|
|
122
|
+
assert_nothing_raised do
|
|
123
|
+
assert_equal '%{separator}', interpolate(:foo => :bar, :default => '%%{separator}')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Note: The two interpolations below do not remove the escape character (%) because
|
|
127
|
+
# I18n should not alter the strings when no interpolation parameters are given,
|
|
128
|
+
# see the comment at the top of this file.
|
|
129
|
+
assert_nothing_raised do
|
|
130
|
+
assert_equal '%%{scope}', interpolate(:default => '%%{scope}')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
I18n.backend.store_translations(:en, :interpolate => 'Hi %%{scope}!')
|
|
134
|
+
assert_nothing_raised do
|
|
135
|
+
assert_equal 'Hi %%{scope}!', interpolate(:interpolate)
|
|
136
|
+
end
|
|
115
137
|
end
|
|
116
138
|
|
|
117
139
|
test "interpolation: deep interpolation for default string" do
|
|
@@ -150,7 +172,7 @@ module I18n
|
|
|
150
172
|
end
|
|
151
173
|
|
|
152
174
|
def euc_jp(string)
|
|
153
|
-
string.encode
|
|
175
|
+
string.encode(Encoding::EUC_JP)
|
|
154
176
|
end
|
|
155
177
|
|
|
156
178
|
def interpolate(*args)
|
|
@@ -34,6 +34,11 @@ module I18n
|
|
|
34
34
|
assert_equal 'Sa', I18n.l(@date, :format => '%a', :locale => :de)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
test "localize Date: given an meridian indicator format it returns the correct meridian indicator" do
|
|
38
|
+
assert_equal 'AM', I18n.l(@date, :format => '%p', :locale => :de)
|
|
39
|
+
assert_equal 'am', I18n.l(@date, :format => '%P', :locale => :de)
|
|
40
|
+
end
|
|
41
|
+
|
|
37
42
|
test "localize Date: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do
|
|
38
43
|
assert_equal 'sa'.upcase, I18n.l(@date, :format => '%^a', :locale => :de)
|
|
39
44
|
end
|
|
@@ -59,7 +64,7 @@ module I18n
|
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
test "localize Date: given missing translations it returns the correct error message" do
|
|
62
|
-
assert_equal '
|
|
67
|
+
assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@date, :format => '%b', :locale => :fr)
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
test "localize Date: given an unknown format it does not fail" do
|
|
@@ -60,7 +60,7 @@ module I18n
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
test "localize DateTime: given missing translations it returns the correct error message" do
|
|
63
|
-
assert_equal '
|
|
63
|
+
assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@datetime, :format => '%b', :locale => :fr)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator" do
|
|
@@ -34,7 +34,7 @@ module I18n
|
|
|
34
34
|
test "localize Date: given a format that resolves to a Proc it calls the Proc with the object and extra options" do
|
|
35
35
|
setup_time_proc_translations
|
|
36
36
|
date = ::Date.new(2008, 3, 1)
|
|
37
|
-
assert_equal
|
|
37
|
+
assert_equal %|[Sat, 01 Mar 2008, #{{:foo=>"foo"}}]|, I18n.l(date, :format => :proc, :foo => 'foo', :locale => :ru)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object" do
|
|
@@ -46,7 +46,7 @@ module I18n
|
|
|
46
46
|
test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object and extra options" do
|
|
47
47
|
setup_time_proc_translations
|
|
48
48
|
datetime = ::DateTime.new(2008, 3, 1, 6)
|
|
49
|
-
assert_equal
|
|
49
|
+
assert_equal %|[Sat, 01 Mar 2008 06:00:00 +00:00, #{{:foo=>"foo"}}]|, I18n.l(datetime, :format => :proc, :foo => 'foo', :locale => :ru)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
test "localize Time: given a format that resolves to a Proc it calls the Proc with the object" do
|
|
@@ -61,7 +61,7 @@ module I18n
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
test "localize Time: given missing translations it returns the correct error message" do
|
|
64
|
-
assert_equal '
|
|
64
|
+
assert_equal 'Translation missing: fr.date.abbr_month_names', I18n.l(@time, :format => '%b', :locale => :fr)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
test "localize Time: given a meridian indicator format it returns the correct meridian indicator" do
|
data/lib/i18n/tests/lookup.rb
CHANGED
|
@@ -30,7 +30,7 @@ module I18n
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
test "lookup: given a missing key, no default and no raise option it returns an error message" do
|
|
33
|
-
assert_equal "
|
|
33
|
+
assert_equal "Translation missing: en.missing", I18n.t(:missing)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
test "lookup: given a missing key, no default and the raise option it raises MissingTranslationData" do
|
|
@@ -76,6 +76,12 @@ module I18n
|
|
|
76
76
|
test "lookup: a resulting Hash is not frozen" do
|
|
77
77
|
assert !I18n.t(:hash).frozen?
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
# Addresses issue: #599
|
|
81
|
+
test "lookup: only interpolates once when resolving symbols" do
|
|
82
|
+
I18n.backend.store_translations(:en, foo: :bar, bar: '%{value}')
|
|
83
|
+
assert_equal '%{dont_interpolate_me}', I18n.t(:foo, value: '%{dont_interpolate_me}')
|
|
84
|
+
end
|
|
79
85
|
end
|
|
80
86
|
end
|
|
81
87
|
end
|
data/lib/i18n/tests/procs.rb
CHANGED
|
@@ -5,34 +5,38 @@ module I18n
|
|
|
5
5
|
module Procs
|
|
6
6
|
test "lookup: given a translation is a proc it calls the proc with the key and interpolation values" do
|
|
7
7
|
I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| I18n::Tests::Procs.filter_args(*args) })
|
|
8
|
-
assert_equal
|
|
8
|
+
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(:a_lambda, :foo => 'foo')
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
test "lookup: given a translation is a proc it passes the interpolation values as keyword arguments" do
|
|
12
12
|
I18n.backend.store_translations(:en, :a_lambda => lambda { |key, foo:, **| I18n::Tests::Procs.filter_args(key, foo: foo) })
|
|
13
|
-
assert_equal
|
|
13
|
+
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(:a_lambda, :foo => 'foo')
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
test "defaults: given a default is a Proc it calls it with the key and interpolation values" do
|
|
17
17
|
proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
|
|
18
|
-
assert_equal
|
|
18
|
+
assert_equal %|[nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo')
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
test "defaults: given a default is a key that resolves to a Proc it calls it with the key and interpolation values" do
|
|
22
22
|
the_lambda = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
|
|
23
23
|
I18n.backend.store_translations(:en, :a_lambda => the_lambda)
|
|
24
|
-
assert_equal
|
|
25
|
-
assert_equal
|
|
24
|
+
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => :a_lambda, :foo => 'foo')
|
|
25
|
+
assert_equal %|[:a_lambda, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => [nil, :a_lambda], :foo => 'foo')
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
test "interpolation: given an interpolation value is a lambda it calls it with key and values before interpolating it" do
|
|
29
29
|
proc = lambda { |*args| I18n::Tests::Procs.filter_args(*args) }
|
|
30
|
-
|
|
30
|
+
if RUBY_VERSION < "3.4"
|
|
31
|
+
assert_match %r(\[\{:foo=>#<Proc.*>\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc)
|
|
32
|
+
else
|
|
33
|
+
assert_match %r(\[\{foo: #<Proc.*>\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc)
|
|
34
|
+
end
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
test "interpolation: given a key resolves to a Proc that returns a string then interpolation still works" do
|
|
34
38
|
proc = lambda { |*args| "%{foo}: " + I18n::Tests::Procs.filter_args(*args) }
|
|
35
|
-
assert_equal
|
|
39
|
+
assert_equal %|foo: [nil, #{{:foo=>"foo"}}]|, I18n.t(nil, :default => proc, :foo => 'foo')
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
test "pluralization: given a key resolves to a Proc that returns valid data then pluralization still works" do
|
|
@@ -57,6 +61,7 @@ module I18n
|
|
|
57
61
|
if arg.is_a?(Hash)
|
|
58
62
|
arg.delete(:fallback_in_progress)
|
|
59
63
|
arg.delete(:fallback_original_locale)
|
|
64
|
+
arg.delete(:skip_interpolation)
|
|
60
65
|
end
|
|
61
66
|
arg
|
|
62
67
|
end.inspect
|
data/lib/i18n/version.rb
CHANGED
data/lib/i18n.rb
CHANGED
|
@@ -19,6 +19,7 @@ module I18n
|
|
|
19
19
|
RESERVED_KEYS = %i[
|
|
20
20
|
cascade
|
|
21
21
|
deep_interpolation
|
|
22
|
+
skip_interpolation
|
|
22
23
|
default
|
|
23
24
|
exception_handler
|
|
24
25
|
fallback
|
|
@@ -48,18 +49,19 @@ module I18n
|
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def self.reserved_keys_pattern # :nodoc:
|
|
51
|
-
@reserved_keys_pattern ||=
|
|
52
|
+
@reserved_keys_pattern ||= /(?<!%)%\{(#{RESERVED_KEYS.join("|")})\}/
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
module Base
|
|
55
56
|
# Gets I18n configuration object.
|
|
56
57
|
def config
|
|
57
|
-
Thread.current
|
|
58
|
+
Thread.current.thread_variable_get(:i18n_config) ||
|
|
59
|
+
Thread.current.thread_variable_set(:i18n_config, I18n::Config.new)
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# Sets I18n configuration object.
|
|
61
63
|
def config=(value)
|
|
62
|
-
Thread.current
|
|
64
|
+
Thread.current.thread_variable_set(:i18n_config, value)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
# Write methods which delegates to the configuration object
|
|
@@ -161,7 +163,7 @@ module I18n
|
|
|
161
163
|
# or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
|
|
162
164
|
# I18n.t :foo, :default => [:bar, 'default']
|
|
163
165
|
#
|
|
164
|
-
#
|
|
166
|
+
# <b>BULK LOOKUP</b>
|
|
165
167
|
#
|
|
166
168
|
# This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
|
|
167
169
|
# I18n.t [:foo, :bar]
|
|
@@ -180,7 +182,7 @@ module I18n
|
|
|
180
182
|
# E.g. assuming the key <tt>:salutation</tt> resolves to:
|
|
181
183
|
# lambda { |key, options| options[:gender] == 'm' ? "Mr. #{options[:name]}" : "Mrs. #{options[:name]}" }
|
|
182
184
|
#
|
|
183
|
-
# Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
|
|
185
|
+
# Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith')</tt> will result in "Mrs. Smith".
|
|
184
186
|
#
|
|
185
187
|
# Note that the string returned by lambda will go through string interpolation too,
|
|
186
188
|
# so the following lambda would give the same result:
|
|
@@ -192,7 +194,7 @@ module I18n
|
|
|
192
194
|
# always return the same translations/values per unique combination of argument
|
|
193
195
|
# values.
|
|
194
196
|
#
|
|
195
|
-
#
|
|
197
|
+
# <b>Ruby 2.7+ keyword arguments warning</b>
|
|
196
198
|
#
|
|
197
199
|
# This method uses keyword arguments.
|
|
198
200
|
# There is a breaking change in ruby that produces warning with ruby 2.7 and won't work as expected with ruby 3.0
|
|
@@ -231,11 +233,41 @@ module I18n
|
|
|
231
233
|
end
|
|
232
234
|
alias :t! :translate!
|
|
233
235
|
|
|
236
|
+
# Returns an array of interpolation keys for the given translation key
|
|
237
|
+
#
|
|
238
|
+
# *Examples*
|
|
239
|
+
#
|
|
240
|
+
# Suppose we have the following:
|
|
241
|
+
# I18n.t 'example.zero' == 'Zero interpolations'
|
|
242
|
+
# I18n.t 'example.one' == 'One interpolation %{foo}'
|
|
243
|
+
# I18n.t 'example.two' == 'Two interpolations %{foo} %{bar}'
|
|
244
|
+
# I18n.t 'example.three' == ['One %{foo}', 'Two %{bar}', 'Three %{baz}']
|
|
245
|
+
# I18n.t 'example.one', locale: :other == 'One interpolation %{baz}'
|
|
246
|
+
#
|
|
247
|
+
# Then we can expect the following results:
|
|
248
|
+
# I18n.interpolation_keys('example.zero') #=> []
|
|
249
|
+
# I18n.interpolation_keys('example.one') #=> ['foo']
|
|
250
|
+
# I18n.interpolation_keys('example.two') #=> ['foo', 'bar']
|
|
251
|
+
# I18n.interpolation_keys('example.three') #=> ['foo', 'bar', 'baz']
|
|
252
|
+
# I18n.interpolation_keys('one', scope: 'example', locale: :other) #=> ['baz']
|
|
253
|
+
# I18n.interpolation_keys('does-not-exist') #=> []
|
|
254
|
+
# I18n.interpolation_keys('example') #=> []
|
|
255
|
+
def interpolation_keys(key, **options)
|
|
256
|
+
raise I18n::ArgumentError if !key.is_a?(String) || key.empty?
|
|
257
|
+
|
|
258
|
+
return [] unless exists?(key, **options.slice(:locale, :scope))
|
|
259
|
+
|
|
260
|
+
translation = translate(key, **options.slice(:locale, :scope))
|
|
261
|
+
interpolation_keys_from_translation(translation)
|
|
262
|
+
.flatten.compact
|
|
263
|
+
end
|
|
264
|
+
|
|
234
265
|
# Returns true if a translation exists for a given key, otherwise returns false.
|
|
235
266
|
def exists?(key, _locale = nil, locale: _locale, **options)
|
|
236
267
|
locale ||= config.locale
|
|
237
268
|
raise Disabled.new('exists?') if locale == false
|
|
238
|
-
raise I18n::ArgumentError if key.is_a?(String) && key.empty?
|
|
269
|
+
raise I18n::ArgumentError if (key.is_a?(String) && key.empty?) || key.nil?
|
|
270
|
+
|
|
239
271
|
config.backend.exists?(locale, key, options)
|
|
240
272
|
end
|
|
241
273
|
|
|
@@ -429,6 +461,17 @@ module I18n
|
|
|
429
461
|
keys
|
|
430
462
|
end
|
|
431
463
|
end
|
|
464
|
+
|
|
465
|
+
def interpolation_keys_from_translation(translation)
|
|
466
|
+
case translation
|
|
467
|
+
when ::String
|
|
468
|
+
translation.scan(Regexp.union(I18n.config.interpolation_patterns))
|
|
469
|
+
when ::Array
|
|
470
|
+
translation.map { |element| interpolation_keys_from_translation(element) }
|
|
471
|
+
else
|
|
472
|
+
[]
|
|
473
|
+
end
|
|
474
|
+
end
|
|
432
475
|
end
|
|
433
476
|
|
|
434
477
|
extend Base
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: i18n
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.14.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sven Fuchs
|
|
@@ -13,7 +13,7 @@ authors:
|
|
|
13
13
|
autorequire:
|
|
14
14
|
bindir: bin
|
|
15
15
|
cert_chain: []
|
|
16
|
-
date:
|
|
16
|
+
date: 2025-12-21 00:00:00.000000000 Z
|
|
17
17
|
dependencies:
|
|
18
18
|
- !ruby/object:Gem::Dependency
|
|
19
19
|
name: concurrent-ruby
|
|
@@ -106,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
106
106
|
- !ruby/object:Gem::Version
|
|
107
107
|
version: 1.3.5
|
|
108
108
|
requirements: []
|
|
109
|
-
rubygems_version: 3.
|
|
109
|
+
rubygems_version: 3.5.23
|
|
110
110
|
signing_key:
|
|
111
111
|
specification_version: 4
|
|
112
112
|
summary: New wave Internationalization support for Ruby
|