diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 89f984bc7f7..513290275b7 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
require "action_view/helpers/tag_helper"
-require "active_support/core_ext/string/access"
-require "i18n/exceptions"
+require "active_support/core_ext/symbol/starts_ends_with"
module ActionView
# = Action View Translation Helpers
@@ -69,78 +68,43 @@ module ActionView
# resolved against.
#
def translate(key, **options)
- unless options[:default].nil?
- remaining_defaults = Array.wrap(options.delete(:default)).compact
- options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
+
+ alternatives = if options.key?(:default)
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
end
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
- if options[:raise] == false
- raise_error = false
- i18n_raise = false
- else
- raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
- i18n_raise = true
- end
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
+ default = MISSING_TRANSLATION
- fully_resolved_key = scope_key_by_partial(key)
-
- if html_safe_translation_key?(key)
- html_safe_options = options.dup
-
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
- unless name == :count && value.is_a?(Numeric)
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
- end
+ translation = while key
+ if alternatives.blank? && !options[:raise].nil?
+ default = NO_DEFAULT # let I18n handle missing translation
end
- html_safe_options[:default] = MISSING_TRANSLATION unless html_safe_options[:default].blank?
+ key = scope_key_by_partial(key)
+ first_key ||= key
- translation = I18n.translate(fully_resolved_key, **html_safe_options.merge(raise: i18n_raise))
-
- if translation.equal?(MISSING_TRANSLATION)
- translated_text = options[:default].first
- elsif translation.respond_to?(:map)
- translated_text = translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
+ if html_safe_translation_key?(key)
+ html_safe_options ||= html_escape_translation_options(options)
+ translated = I18n.translate(key, **html_safe_options, default: default)
+ break html_safe_translation(translated) unless translated.equal?(MISSING_TRANSLATION)
else
- translated_text = translation.respond_to?(:html_safe) ? translation.html_safe : translation
- end
- else
- translated_text = I18n.translate(fully_resolved_key, **options.merge(raise: i18n_raise))
- end
-
- if block_given?
- yield(translated_text, fully_resolved_key)
- else
- translated_text
- end
- rescue I18n::MissingTranslationData => e
- if remaining_defaults.present?
- translate remaining_defaults.shift, **options.merge(default: remaining_defaults)
- else
- raise e if raise_error
-
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
- title = +"translation missing: #{keys.join('.')}"
-
- interpolations = options.except(:default, :scope)
-
- if interpolations.any?
- title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
+ translated = I18n.translate(key, **options, default: default)
+ break translated unless translated.equal?(MISSING_TRANSLATION)
end
- return title unless ActionView::Base.debug_missing_translation
+ break alternatives.first if alternatives.present? && !alternatives.first.is_a?(Symbol)
- translated_fallback = content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
-
- if block_given?
- yield(translated_fallback, scope_key_by_partial(key))
- else
- translated_fallback
- end
+ key = alternatives&.shift
end
+
+ if key.nil?
+ translation = missing_translation(first_key, options)
+ key = first_key
+ end
+
+ block_given? ? yield(translation, key) : translation
end
alias :t :translate
@@ -157,13 +121,19 @@ module ActionView
MISSING_TRANSLATION = Object.new
private_constant :MISSING_TRANSLATION
+ NO_DEFAULT = [].freeze
+ private_constant :NO_DEFAULT
+
+ def self.i18n_option?(name)
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
+ end
+
def scope_key_by_partial(key)
- stringified_key = key.to_s
- if stringified_key.start_with?(".")
+ if key.start_with?(".")
if @current_template&.virtual_path
@_scope_key_by_partial_cache ||= {}
@_scope_key_by_partial_cache[@current_template.virtual_path] ||= @current_template.virtual_path.gsub(%r{/_?}, ".")
- "#{@_scope_key_by_partial_cache[@current_template.virtual_path]}#{stringified_key}"
+ "#{@_scope_key_by_partial_cache[@current_template.virtual_path]}#{key}"
else
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
end
@@ -172,8 +142,47 @@ module ActionView
end
end
+ def html_escape_translation_options(options)
+ return options if options.empty?
+ html_safe_options = options.dup
+
+ options.each do |name, value|
+ unless TranslationHelper.i18n_option?(name) || (name == :count && value.is_a?(Numeric))
+ html_safe_options[name] = ERB::Util.html_escape(value.to_s)
+ end
+ end
+
+ html_safe_options
+ end
+
def html_safe_translation_key?(key)
- /(?:_|\b)html\z/.match?(key.to_s)
+ /(?:_|\b)html\z/.match?(key)
+ end
+
+ def html_safe_translation(translation)
+ if translation.respond_to?(:map)
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
+ else
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
+ end
+ end
+
+ def missing_translation(key, options)
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
+
+ title = +"translation missing: #{keys.join(".")}"
+
+ options.each do |name, value|
+ unless name == :scope
+ title << ", " << name.to_s << ": " << ERB::Util.html_escape(value)
+ end
+ end
+
+ if ActionView::Base.debug_missing_translation
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
+ else
+ title
+ end
end
end
end
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index 9b0b2e3faa6..a563378055e 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -49,9 +49,18 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_delegates_setting_to_i18n
- assert_called_with(I18n, :translate, [:foo, locale: "en", raise: true], returns: "") do
+ matcher_called = false
+ matcher = ->(key, options) do
+ matcher_called = true
+ assert_equal :foo, key
+ assert_equal "en", options[:locale]
+ end
+
+ I18n.stub(:translate, matcher) do
translate :foo, locale: "en"
end
+
+ assert matcher_called
end
def test_delegates_localize_to_i18n
@@ -102,6 +111,15 @@ class TranslationHelperTest < ActiveSupport::TestCase
ActionView::Base.raise_on_missing_translations = false
end
+ def test_raise_arg_overrides_raise_config_option
+ ActionView::Base.raise_on_missing_translations = true
+
+ expected = "translation missing: en.translations.missing"
+ assert_equal expected, translate(:"translations.missing", raise: false)
+ ensure
+ ActionView::Base.raise_on_missing_translations = false
+ end
+
def test_raises_missing_translation_message_with_raise_option
assert_raise(I18n::MissingTranslationData) do
translate(:"translations.missing", raise: true)
@@ -164,6 +182,16 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_equal expected, view.render(template: "translations/templates/missing_yield_block").strip
end
+ def test_missing_translation_scoped_by_partial_yield_block_without_debug_wrapper
+ old_debug_missing_translation = ActionView::Base.debug_missing_translation
+ ActionView::Base.debug_missing_translation = false
+
+ expected = "translations.templates.missing_yield_block.missing: translation missing: en.translations.templates.missing_yield_block.missing"
+ assert_equal expected, view.render(template: "translations/templates/missing_yield_block").strip
+ ensure
+ ActionView::Base.debug_missing_translation = old_debug_missing_translation
+ end
+
def test_missing_translation_with_default_scoped_by_partial_yield_block
expected = "translations.templates.missing_with_default_yield_block.missing: Default"
assert_equal expected, view.render(template: "translations/templates/missing_with_default_yield_block").strip
@@ -210,15 +238,26 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
end
+ def test_translate_with_default_and_raise_false
+ translation = translate(:"translations.missing", default: :"translations.foo", raise: false)
+ assert_equal "Foo", translation
+ end
+
def test_translate_with_default_named_html
translation = translate(:'translations.missing', default: :'translations.hello_html')
assert_equal "Hello World", translation
assert_equal true, translation.html_safe?
end
+ def test_translate_with_default_named_html_and_raise_false
+ translation = translate(:"translations.missing", default: :"translations.hello_html", raise: false)
+ assert_equal "Hello World", translation
+ assert_predicate translation, :html_safe?
+ end
+
def test_translate_with_missing_default
- translation = translate(:'translations.missing', default: :'translations.missing_html')
- expected = 'Missing Html'
+ translation = translate(:"translations.missing", default: :also_missing)
+ expected = 'Missing'
assert_equal expected, translation
assert_equal true, translation.html_safe?
end
@@ -229,6 +268,12 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
end
+ def test_translate_with_html_key_and_missing_default_and_raise_option
+ assert_raise(I18n::MissingTranslationData) do
+ translate(:"translations.missing_html", default: :"translations.missing_html", raise: true)
+ end
+ end
+
def test_translate_with_two_defaults_named_html
translation = translate(:'translations.missing', default: [:'translations.missing_html', :'translations.hello_html'])
assert_equal "Hello World", translation
@@ -289,6 +334,32 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_nil translation
end
+ def test_translate_bulk_lookup
+ translations = translate([:"translations.foo", :"translations.foo"])
+ assert_equal ["Foo", "Foo"], translations
+ end
+
+ def test_translate_bulk_lookup_with_default
+ translations = translate([:"translations.missing", :"translations.missing"], default: :"translations.foo")
+ assert_equal ["Foo", "Foo"], translations
+ end
+
+ def test_translate_bulk_lookup_html
+ translations = translate([:"translations.html", :"translations.hello_html"])
+ assert_equal ["Hello World", "Hello World"], translations
+ translations.each do |translation|
+ assert_predicate translation, :html_safe?
+ end
+ end
+
+ def test_translate_bulk_lookup_html_with_default
+ translations = translate([:"translations.missing", :"translations.missing"], default: :"translations.html")
+ assert_equal ["Hello World", "Hello World"], translations
+ translations.each do |translation|
+ assert_predicate translation, :html_safe?
+ end
+ end
+
def test_translate_does_not_change_options
options = {}
if RUBY_VERSION >= "2.7"