Skip to content

Commit

Permalink
add lambda support to API/Simple backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven Fuchs committed May 1, 2009
1 parent c90e62d commit 8c4ce3d
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 54 deletions.
82 changes: 52 additions & 30 deletions lib/i18n/backend/simple.rb
Expand Up @@ -3,7 +3,7 @@
module I18n
module Backend
class Simple
INTERPOLATION_RESERVED_KEYS = %w(scope default)
RESERVED_KEYS = [:scope, :default]
MATCH = /(\\\\)?\{\{([^\}]+)\}\}/

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

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

reserved = :scope, :default
count, scope, default = options.values_at(:count, *reserved)
default = options.delete(:default)
values = options.reject { |name, value| reserved.include?(name) }
count, scope, default = options.values_at(:count, *RESERVED_KEYS)
values = options.reject { |name, value| RESERVED_KEYS.include?(name) }

entry = lookup(locale, key, scope) || resolve(locale, key, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
entry = entry.call(values) if entry.is_a? Proc
entry = lookup(locale, key, scope)
entry = entry.nil? ? default(locale, key, default, options) : resolve(locale, key, entry, options)

raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
entry = pluralize(locale, entry, count)
entry = interpolate(locale, entry, values)
entry
Expand All @@ -47,16 +47,16 @@ def localize(locale, object, format = :default, options={})
type = object.respond_to?(:sec) ? 'time' : 'date'
format = lookup(locale, :"#{type}.formats.#{format}")
end

format = resolve(locale, object, format, options.merge(:raise => true))
format = format.to_s.dup

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

Expand Down Expand Up @@ -103,21 +103,34 @@ def lookup(locale, key, scope = [])
end
end

# Evaluates defaults.
# If given subject is an Array, it walks the array and returns the
# first translation that can be resolved. Otherwise it tries to resolve
# the translation directly.
def default(locale, object, subject, options = {})
options = options.dup.reject { |key, value| key == :default }
case subject
when Array
subject.each do |subject|
result = resolve(locale, object, subject, options) and return result
end and nil
else
resolve(locale, object, subject, options)
end
end

# Resolves a translation.
# If the given default is a String it is used literally. If it is a Symbol
# it will be translated with the given options. If it is an Array the first
# translation yielded will be returned. If it is a Proc then it is evaluated.
#
# <em>I.e.</em>, <tt>resolve(locale, [:foo, 'default'])</tt> will return +default+ if
# <tt>translate(locale, :foo)</tt> does not yield a result.
# If the given subject is a Symbol, it will be translated with the
# given options. If it is a Proc then it will be evaluated. All other
# subjects will be returned directly.
def resolve(locale, object, subject, options = {})
case subject
when String then subject
when Symbol then translate locale, subject, options
when Proc then subject.call object, options
when Array then subject.each do |subject|
result = resolve(locale, object, subject, options.dup) and return result
end and nil
when Symbol
translate(locale, subject, options)
when Proc
subject.call(object, options)
else
subject
end
rescue MissingTranslationData
nil
Expand All @@ -129,7 +142,7 @@ def resolve(locale, object, subject, options = {})
# implement more flexible or complex pluralization rules.
def pluralize(locale, entry, count)
return entry unless entry.is_a?(Hash) and count
# raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)

key = :zero if count == 0 && entry.has_key?(:zero)
key ||= count == 1 ? :one : :other
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
Expand All @@ -148,14 +161,14 @@ def interpolate(locale, string, values = {})
return string unless string.is_a?(String)

string.gsub(MATCH) do
escaped, pattern, key = $1, $2, $2.to_sym
escaped, key = $1, $2.to_sym

if escaped
pattern
elsif INTERPOLATION_RESERVED_KEYS.include?(pattern)
raise ReservedInterpolationKey.new(pattern, string)
key
elsif RESERVED_KEYS.include?(key)
raise ReservedInterpolationKey.new(key, string)
elsif !values.include?(key)
raise MissingInterpolationArgument.new(pattern, string)
raise MissingInterpolationArgument.new(key, string)
else
values[key].to_s
end
Expand Down Expand Up @@ -200,11 +213,20 @@ def merge_translations(locale, data)
# Return a new hash with all keys and nested keys converted to symbols.
def deep_symbolize_keys(hash)
hash.inject({}) { |result, (key, value)|
value = deep_symbolize_keys(value) if value.is_a? Hash
value = deep_symbolize_keys(value) if value.is_a?(Hash)
result[(key.to_sym rescue key) || key] = value
result
}
end

# Flatten the given array once
def flatten_once(array)
result = []
for element in array # a little faster than each
result.push(*element)
end
result
end
end
end
end
6 changes: 3 additions & 3 deletions test/i18n_exceptions_test.rb
Expand Up @@ -50,7 +50,7 @@ def test_invalid_pluralization_data_message
def test_missing_interpolation_argument_stores_key_and_string
force_missing_interpolation_argument
rescue I18n::ArgumentError => e
assert_equal 'bar', e.key
assert_equal :bar, e.key
assert_equal "{{bar}}", e.string
end

Expand All @@ -63,14 +63,14 @@ def test_missing_interpolation_argument_message
def test_reserved_interpolation_key_stores_key_and_string
force_reserved_interpolation_key
rescue I18n::ArgumentError => e
assert_equal 'scope', e.key
assert_equal :scope, e.key
assert_equal "{{scope}}", e.string
end

def test_reserved_interpolation_key_message
force_reserved_interpolation_key
rescue I18n::ArgumentError => e
assert_equal 'reserved key "scope" used in "{{scope}}"', e.message
assert_equal 'reserved key :scope used in "{{scope}}"', e.message
end

private
Expand Down
85 changes: 64 additions & 21 deletions test/simple_backend_test.rb
Expand Up @@ -12,7 +12,7 @@ module I18nSimpleBackendTestSetup
def setup_backend
# backend_reset_translations!
@backend = I18n::Backend::Simple.new
@backend.store_translations 'en', :foo => {:bar => 'bar', :baz => 'baz'}
@backend.store_translations :en, :foo => {:bar => 'bar', :baz => 'baz'}
@locale_dir = File.dirname(__FILE__) + '/locale'
end
alias :setup :setup_backend
Expand All @@ -27,7 +27,7 @@ def backend_get_translations
end

def add_datetime_translations
@backend.store_translations :'de', {
@backend.store_translations :de, {
:date => {
:formats => {
:default => "%d.%m.%Y",
Expand Down Expand Up @@ -155,7 +155,11 @@ def test_given_no_keys_it_returns_the_default
end

def test_translate_given_a_symbol_as_a_default_translates_the_symbol
assert_equal 'bar', @backend.translate('en', nil, :scope => [:foo], :default => :bar)
assert_equal 'bar', @backend.translate('en', nil, :default => :'foo.bar')
end

def test_translate_default_with_scope_stays_in_scope_when_looking_up_the_symbol
assert_equal 'bar', @backend.translate('en', :does_not_exist, :default => :bar, :scope => :foo)
end

def test_translate_given_an_array_as_default_uses_the_first_match
Expand Down Expand Up @@ -196,7 +200,7 @@ def test_translate_with_a_bogus_key_and_no_default_raises_missing_translation_da
end
end

class I18nSimpleBackendTranslateLambdaTest < Test::Unit::TestCase
class I18nSimpleBackendLambdaTest < Test::Unit::TestCase
include I18nSimpleBackendTestSetup

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

def test_translate_with_proc_as_default
expected = 'result from lambda'
assert_equal expected, @backend.translate(:en, :'does not exist', :default => lambda { expected })
end

private
def setup_proc_translation
@backend.store_translations 'en', {
:a_lambda => lambda { |attributes|
attributes.keys.sort_by(&:to_s).collect { |key| "#{key}=#{attributes[key]}"}.join(', ')
},
:lambda_for_pluralize => lambda { |attributes| attributes },
:lambda_for_interpolate => lambda { |attributes|
"{{#{attributes.keys.sort_by(&:to_s).join('}} {{')}}}"
def setup_proc_translation
@backend.store_translations 'en', {
:a_lambda => lambda { |key, values|
values.keys.sort_by(&:to_s).collect { |key| "#{key}=#{values[key]}"}.join(', ')
},
:lambda_for_pluralize => lambda { |key, values| values },
:lambda_for_interpolate => lambda { |key, values|
"{{#{values.keys.sort_by(&:to_s).join('}} {{')}}}"
}
}
}
end
end
end

class I18nSimpleBackendLookupTest < Test::Unit::TestCase
Expand All @@ -250,6 +259,40 @@ def test_lookup_given_nested_keys_looks_up_a_nested_hash_value
end
end

class I18nSimpleBackendTranslateLinkedTest < Test::Unit::TestCase
def setup
@backend = I18n::Backend::Simple.new
@backend.store_translations 'en', {
:foo => 'foo',
:bar => { :baz => 'baz', :link_to_baz => :baz, :link_to_buz => :'boz.buz' },
:boz => { :buz => 'buz' },
:link_to_foo => :foo,
:link_to_bar => :bar,
:link_to_baz => :'bar.baz'
}
end

def test_translate_calls_translate_if_resolves_to_a_symbol
assert_equal 'foo', @backend.translate('en', :link_to_foo)
end

def test_translate_calls_translate_if_resolves_to_a_symbol2
assert_equal('baz', @backend.translate('en', :link_to_baz))
end

def test_translate_calls_translate_if_resolves_to_a_symbol3
assert @backend.translate('en', :link_to_bar).key?(:baz)
end

def test_translate_calls_translate_if_resolves_to_a_symbol_with_scope_1
assert_equal('baz', @backend.translate('en', :link_to_baz, :scope => :bar))
end

def test_translate_calls_translate_if_resolves_to_a_symbol_with_scope_1
assert_equal('buz', @backend.translate('en', :'bar.link_to_buz'))
end
end

class I18nSimpleBackendPluralizeTest < Test::Unit::TestCase
include I18nSimpleBackendTestSetup

Expand Down Expand Up @@ -347,9 +390,9 @@ def test_interpolate_given_an_empty_values_hash_raises_missing_interpolation_arg
def test_interpolate_given_a_string_containing_a_reserved_key_raises_reserved_interpolation_key
assert_raises(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) }
end

private

def euc_jp(string)
string.encode!(Encoding::EUC_JP)
end
Expand Down Expand Up @@ -613,27 +656,27 @@ def test_adding_arrays_of_filenames_to_load_path_do_not_break_locale_loading

class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase
include I18nSimpleBackendTestSetup

def setup
@backend = I18n::Backend::Simple.new
I18n.load_path = [File.dirname(__FILE__) + '/locale/en.yml']
assert_nil backend_get_translations
@backend.send :init_translations
end

def teardown
I18n.load_path = []
end

def test_setup
assert_not_nil backend_get_translations
end

def test_reload_translations_unloads_translations
@backend.reload!
assert_nil backend_get_translations
end

def test_reload_translations_uninitializes_translations
@backend.reload!
assert_equal @backend.initialized?, false
Expand Down

0 comments on commit 8c4ce3d

Please sign in to comment.