Skip to content

Commit 8c4ce3d

Browse files
author
Sven Fuchs
committed
add lambda support to API/Simple backend
1 parent c90e62d commit 8c4ce3d

File tree

3 files changed

+119
-54
lines changed

3 files changed

+119
-54
lines changed

lib/i18n/backend/simple.rb

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module I18n
44
module Backend
55
class Simple
6-
INTERPOLATION_RESERVED_KEYS = %w(scope default)
6+
RESERVED_KEYS = [:scope, :default]
77
MATCH = /(\\\\)?\{\{([^\}]+)\}\}/
88

99
# Accepts a list of paths to translation files. Loads translations from
@@ -23,15 +23,15 @@ def store_translations(locale, data)
2323

2424
def translate(locale, key, options = {})
2525
raise InvalidLocale.new(locale) if locale.nil?
26-
return key.map { |k| translate(locale, k, options) } if key.is_a? Array
26+
return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
2727

28-
reserved = :scope, :default
29-
count, scope, default = options.values_at(:count, *reserved)
30-
default = options.delete(:default)
31-
values = options.reject { |name, value| reserved.include?(name) }
28+
count, scope, default = options.values_at(:count, *RESERVED_KEYS)
29+
values = options.reject { |name, value| RESERVED_KEYS.include?(name) }
3230

33-
entry = lookup(locale, key, scope) || resolve(locale, key, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
34-
entry = entry.call(values) if entry.is_a? Proc
31+
entry = lookup(locale, key, scope)
32+
entry = entry.nil? ? default(locale, key, default, options) : resolve(locale, key, entry, options)
33+
34+
raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
3535
entry = pluralize(locale, entry, count)
3636
entry = interpolate(locale, entry, values)
3737
entry
@@ -47,16 +47,16 @@ def localize(locale, object, format = :default, options={})
4747
type = object.respond_to?(:sec) ? 'time' : 'date'
4848
format = lookup(locale, :"#{type}.formats.#{format}")
4949
end
50+
5051
format = resolve(locale, object, format, options.merge(:raise => true))
51-
format = format.to_s.dup
5252

5353
# TODO only translate these if the format string is actually present
54-
# TODO check which format strings are present, then bulk translate then, then replace them
54+
# TODO check which format strings are present, then bulk translate them, then replace them
5555
format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
5656
format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
5757
format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
5858
format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
59-
format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
59+
format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to?(:hour)
6060
object.strftime(format)
6161
end
6262

@@ -103,21 +103,34 @@ def lookup(locale, key, scope = [])
103103
end
104104
end
105105

106+
# Evaluates defaults.
107+
# If given subject is an Array, it walks the array and returns the
108+
# first translation that can be resolved. Otherwise it tries to resolve
109+
# the translation directly.
110+
def default(locale, object, subject, options = {})
111+
options = options.dup.reject { |key, value| key == :default }
112+
case subject
113+
when Array
114+
subject.each do |subject|
115+
result = resolve(locale, object, subject, options) and return result
116+
end and nil
117+
else
118+
resolve(locale, object, subject, options)
119+
end
120+
end
121+
106122
# Resolves a translation.
107-
# If the given default is a String it is used literally. If it is a Symbol
108-
# it will be translated with the given options. If it is an Array the first
109-
# translation yielded will be returned. If it is a Proc then it is evaluated.
110-
#
111-
# <em>I.e.</em>, <tt>resolve(locale, [:foo, 'default'])</tt> will return +default+ if
112-
# <tt>translate(locale, :foo)</tt> does not yield a result.
123+
# If the given subject is a Symbol, it will be translated with the
124+
# given options. If it is a Proc then it will be evaluated. All other
125+
# subjects will be returned directly.
113126
def resolve(locale, object, subject, options = {})
114127
case subject
115-
when String then subject
116-
when Symbol then translate locale, subject, options
117-
when Proc then subject.call object, options
118-
when Array then subject.each do |subject|
119-
result = resolve(locale, object, subject, options.dup) and return result
120-
end and nil
128+
when Symbol
129+
translate(locale, subject, options)
130+
when Proc
131+
subject.call(object, options)
132+
else
133+
subject
121134
end
122135
rescue MissingTranslationData
123136
nil
@@ -129,7 +142,7 @@ def resolve(locale, object, subject, options = {})
129142
# implement more flexible or complex pluralization rules.
130143
def pluralize(locale, entry, count)
131144
return entry unless entry.is_a?(Hash) and count
132-
# raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
145+
133146
key = :zero if count == 0 && entry.has_key?(:zero)
134147
key ||= count == 1 ? :one : :other
135148
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
@@ -148,14 +161,14 @@ def interpolate(locale, string, values = {})
148161
return string unless string.is_a?(String)
149162

150163
string.gsub(MATCH) do
151-
escaped, pattern, key = $1, $2, $2.to_sym
164+
escaped, key = $1, $2.to_sym
152165

153166
if escaped
154-
pattern
155-
elsif INTERPOLATION_RESERVED_KEYS.include?(pattern)
156-
raise ReservedInterpolationKey.new(pattern, string)
167+
key
168+
elsif RESERVED_KEYS.include?(key)
169+
raise ReservedInterpolationKey.new(key, string)
157170
elsif !values.include?(key)
158-
raise MissingInterpolationArgument.new(pattern, string)
171+
raise MissingInterpolationArgument.new(key, string)
159172
else
160173
values[key].to_s
161174
end
@@ -200,11 +213,20 @@ def merge_translations(locale, data)
200213
# Return a new hash with all keys and nested keys converted to symbols.
201214
def deep_symbolize_keys(hash)
202215
hash.inject({}) { |result, (key, value)|
203-
value = deep_symbolize_keys(value) if value.is_a? Hash
216+
value = deep_symbolize_keys(value) if value.is_a?(Hash)
204217
result[(key.to_sym rescue key) || key] = value
205218
result
206219
}
207220
end
221+
222+
# Flatten the given array once
223+
def flatten_once(array)
224+
result = []
225+
for element in array # a little faster than each
226+
result.push(*element)
227+
end
228+
result
229+
end
208230
end
209231
end
210232
end

test/i18n_exceptions_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_invalid_pluralization_data_message
5050
def test_missing_interpolation_argument_stores_key_and_string
5151
force_missing_interpolation_argument
5252
rescue I18n::ArgumentError => e
53-
assert_equal 'bar', e.key
53+
assert_equal :bar, e.key
5454
assert_equal "{{bar}}", e.string
5555
end
5656

@@ -63,14 +63,14 @@ def test_missing_interpolation_argument_message
6363
def test_reserved_interpolation_key_stores_key_and_string
6464
force_reserved_interpolation_key
6565
rescue I18n::ArgumentError => e
66-
assert_equal 'scope', e.key
66+
assert_equal :scope, e.key
6767
assert_equal "{{scope}}", e.string
6868
end
6969

7070
def test_reserved_interpolation_key_message
7171
force_reserved_interpolation_key
7272
rescue I18n::ArgumentError => e
73-
assert_equal 'reserved key "scope" used in "{{scope}}"', e.message
73+
assert_equal 'reserved key :scope used in "{{scope}}"', e.message
7474
end
7575

7676
private

test/simple_backend_test.rb

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module I18nSimpleBackendTestSetup
1212
def setup_backend
1313
# backend_reset_translations!
1414
@backend = I18n::Backend::Simple.new
15-
@backend.store_translations 'en', :foo => {:bar => 'bar', :baz => 'baz'}
15+
@backend.store_translations :en, :foo => {:bar => 'bar', :baz => 'baz'}
1616
@locale_dir = File.dirname(__FILE__) + '/locale'
1717
end
1818
alias :setup :setup_backend
@@ -27,7 +27,7 @@ def backend_get_translations
2727
end
2828

2929
def add_datetime_translations
30-
@backend.store_translations :'de', {
30+
@backend.store_translations :de, {
3131
:date => {
3232
:formats => {
3333
:default => "%d.%m.%Y",
@@ -155,7 +155,11 @@ def test_given_no_keys_it_returns_the_default
155155
end
156156

157157
def test_translate_given_a_symbol_as_a_default_translates_the_symbol
158-
assert_equal 'bar', @backend.translate('en', nil, :scope => [:foo], :default => :bar)
158+
assert_equal 'bar', @backend.translate('en', nil, :default => :'foo.bar')
159+
end
160+
161+
def test_translate_default_with_scope_stays_in_scope_when_looking_up_the_symbol
162+
assert_equal 'bar', @backend.translate('en', :does_not_exist, :default => :bar, :scope => :foo)
159163
end
160164

161165
def test_translate_given_an_array_as_default_uses_the_first_match
@@ -196,7 +200,7 @@ def test_translate_with_a_bogus_key_and_no_default_raises_missing_translation_da
196200
end
197201
end
198202

199-
class I18nSimpleBackendTranslateLambdaTest < Test::Unit::TestCase
203+
class I18nSimpleBackendLambdaTest < Test::Unit::TestCase
200204
include I18nSimpleBackendTestSetup
201205

202206
def test_translate_simple_proc
@@ -223,18 +227,23 @@ def test_translate_proc_with_interpolate
223227
assert_equal 'bar baz foo', @backend.translate('en', :lambda_for_interpolate, :foo => 'foo', :bar => 'bar', :baz => 'baz')
224228
end
225229

230+
def test_translate_with_proc_as_default
231+
expected = 'result from lambda'
232+
assert_equal expected, @backend.translate(:en, :'does not exist', :default => lambda { expected })
233+
end
234+
226235
private
227-
def setup_proc_translation
228-
@backend.store_translations 'en', {
229-
:a_lambda => lambda { |attributes|
230-
attributes.keys.sort_by(&:to_s).collect { |key| "#{key}=#{attributes[key]}"}.join(', ')
231-
},
232-
:lambda_for_pluralize => lambda { |attributes| attributes },
233-
:lambda_for_interpolate => lambda { |attributes|
234-
"{{#{attributes.keys.sort_by(&:to_s).join('}} {{')}}}"
236+
def setup_proc_translation
237+
@backend.store_translations 'en', {
238+
:a_lambda => lambda { |key, values|
239+
values.keys.sort_by(&:to_s).collect { |key| "#{key}=#{values[key]}"}.join(', ')
240+
},
241+
:lambda_for_pluralize => lambda { |key, values| values },
242+
:lambda_for_interpolate => lambda { |key, values|
243+
"{{#{values.keys.sort_by(&:to_s).join('}} {{')}}}"
244+
}
235245
}
236-
}
237-
end
246+
end
238247
end
239248

240249
class I18nSimpleBackendLookupTest < Test::Unit::TestCase
@@ -250,6 +259,40 @@ def test_lookup_given_nested_keys_looks_up_a_nested_hash_value
250259
end
251260
end
252261

262+
class I18nSimpleBackendTranslateLinkedTest < Test::Unit::TestCase
263+
def setup
264+
@backend = I18n::Backend::Simple.new
265+
@backend.store_translations 'en', {
266+
:foo => 'foo',
267+
:bar => { :baz => 'baz', :link_to_baz => :baz, :link_to_buz => :'boz.buz' },
268+
:boz => { :buz => 'buz' },
269+
:link_to_foo => :foo,
270+
:link_to_bar => :bar,
271+
:link_to_baz => :'bar.baz'
272+
}
273+
end
274+
275+
def test_translate_calls_translate_if_resolves_to_a_symbol
276+
assert_equal 'foo', @backend.translate('en', :link_to_foo)
277+
end
278+
279+
def test_translate_calls_translate_if_resolves_to_a_symbol2
280+
assert_equal('baz', @backend.translate('en', :link_to_baz))
281+
end
282+
283+
def test_translate_calls_translate_if_resolves_to_a_symbol3
284+
assert @backend.translate('en', :link_to_bar).key?(:baz)
285+
end
286+
287+
def test_translate_calls_translate_if_resolves_to_a_symbol_with_scope_1
288+
assert_equal('baz', @backend.translate('en', :link_to_baz, :scope => :bar))
289+
end
290+
291+
def test_translate_calls_translate_if_resolves_to_a_symbol_with_scope_1
292+
assert_equal('buz', @backend.translate('en', :'bar.link_to_buz'))
293+
end
294+
end
295+
253296
class I18nSimpleBackendPluralizeTest < Test::Unit::TestCase
254297
include I18nSimpleBackendTestSetup
255298

@@ -347,9 +390,9 @@ def test_interpolate_given_an_empty_values_hash_raises_missing_interpolation_arg
347390
def test_interpolate_given_a_string_containing_a_reserved_key_raises_reserved_interpolation_key
348391
assert_raises(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) }
349392
end
350-
393+
351394
private
352-
395+
353396
def euc_jp(string)
354397
string.encode!(Encoding::EUC_JP)
355398
end
@@ -613,27 +656,27 @@ def test_adding_arrays_of_filenames_to_load_path_do_not_break_locale_loading
613656

614657
class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase
615658
include I18nSimpleBackendTestSetup
616-
659+
617660
def setup
618661
@backend = I18n::Backend::Simple.new
619662
I18n.load_path = [File.dirname(__FILE__) + '/locale/en.yml']
620663
assert_nil backend_get_translations
621664
@backend.send :init_translations
622665
end
623-
666+
624667
def teardown
625668
I18n.load_path = []
626669
end
627-
670+
628671
def test_setup
629672
assert_not_nil backend_get_translations
630673
end
631-
674+
632675
def test_reload_translations_unloads_translations
633676
@backend.reload!
634677
assert_nil backend_get_translations
635678
end
636-
679+
637680
def test_reload_translations_uninitializes_translations
638681
@backend.reload!
639682
assert_equal @backend.initialized?, false

0 commit comments

Comments
 (0)