diff --git a/lib/i18n/backend.rb b/lib/i18n/backend.rb index a315e8de..821b050f 100644 --- a/lib/i18n/backend.rb +++ b/lib/i18n/backend.rb @@ -3,6 +3,7 @@ module Backend autoload :ActiveRecord, 'i18n/backend/active_record' autoload :Base, 'i18n/backend/base' autoload :Cache, 'i18n/backend/cache' + autoload :Cascade, 'i18n/backend/cascade' autoload :Chain, 'i18n/backend/chain' autoload :Fallbacks, 'i18n/backend/fallbacks' autoload :Gettext, 'i18n/backend/gettext' diff --git a/lib/i18n/backend/cascade.rb b/lib/i18n/backend/cascade.rb new file mode 100644 index 00000000..c2fd8a73 --- /dev/null +++ b/lib/i18n/backend/cascade.rb @@ -0,0 +1,20 @@ +# encoding: utf-8 + +module I18n + @@fallbacks = nil + + module Backend + module Cascade + def lookup(locale, key, scope = [], separator = nil) + return unless key + locale, *scope = I18n.send(:normalize_translation_keys, locale, key, scope, separator) + key = scope.pop + + begin + result = super + return result unless result.nil? + end while scope.pop + end + end + end +end diff --git a/test/cases/backend/cascade_test.rb b/test/cases/backend/cascade_test.rb new file mode 100644 index 00000000..5a29ceea --- /dev/null +++ b/test/cases/backend/cascade_test.rb @@ -0,0 +1,66 @@ +# encoding: utf-8 + +require File.expand_path(File.dirname(__FILE__) + '/../../test_helper') + +class I18nBackendCascadeTest < Test::Unit::TestCase + class Backend + include I18n::Backend::Base + include I18n::Backend::Cascade + end + + def setup + I18n.backend = Backend.new + store_translations(:en, + :foo => 'foo', + :bar => { :baz => 'baz' } + ) + end + + define_method "test: still returns an existing translation as usual" do + assert_equal 'foo', I18n.t(:foo) + assert_equal 'baz', I18n.t(:'bar.baz') + end + + define_method "test: falls back by cutting keys off the end of the scope" do + assert_equal 'foo', I18n.t(:'does_not_exist.foo') + assert_equal 'foo', I18n.t(:'does_not_exist.does_not_exist.foo') + + assert_equal 'baz', I18n.t(:'bar.does_not_exist.baz') + assert_equal 'baz', I18n.t(:'bar.does_not_exist.does_not_exist.baz') + end + + define_method "test: raises I18n::MissingTranslationData exception when no translation was found" do + assert_raises(I18n::MissingTranslationData) { I18n.t(:'foo.does_not_exist', :raise => true) } + assert_raises(I18n::MissingTranslationData) { I18n.t(:'bar.baz.does_not_exist', :raise => true) } + assert_raises(I18n::MissingTranslationData) { I18n.t(:'does_not_exist.bar.baz', :raise => true) } + end + + define_method "test: cascades before evaluating the default" do + assert_equal 'foo', I18n.t(:foo, :scope => :does_not_exist, :default => 'default') + end + + define_method "test: let's us assemble required fallbacks for ActiveRecord validation messages" do + store_translations(:en, + :errors => { + :reply => { + :title => { + :blank => 'blank on reply title' + }, + :taken => 'taken on reply' + }, + :topic => { + :title => { + :format => 'format on topic title' + }, + :length => 'length on topic' + }, + :odd => 'odd on errors' + } + ) + assert_equal 'blank on reply title', I18n.t(:'errors.reply.title.blank', :default => :'errors.topic.title.blank') + assert_equal 'taken on reply', I18n.t(:'errors.reply.title.taken', :default => :'errors.topic.title.taken') + assert_equal 'format on topic title', I18n.t(:'errors.reply.title.format', :default => :'errors.topic.title.format') + assert_equal 'length on topic', I18n.t(:'errors.reply.title.length', :default => :'errors.topic.title.length') + assert_equal 'odd on errors', I18n.t(:'errors.reply.title.odd', :default => :'errors.topic.title.odd') + end +end