Here’s a classic example: a _form.html.erb partial that is used in both new.html.erb and edit.html.erb contains a <%= content_tag(:h1, t(“.title”)) %>. I’d like this to be translated as follows:
mymodel:
new:
title: "New MyModel"
edit:
title: "Edit MyModel"
But instead the i18n key ends up being mymodel.form.title for both new and edit. This is regardless of whether the form is rendered explicitly as a partial:
What’s the most elegant way to ensure that lazy i18n keys inside partials always resolve to include the template name (either as mymodel.new.title or mymodel.new.form.title) ?
The relevant code appears to be in actionview-n.n.n/lib/action_view/helpers/translation_helper.rb:
def scope_key_by_partial(key)
if key&.start_with?(".")
if @virtual_path
@_scope_key_by_partial_cache ||= {}
@_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
"#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
else
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
end
else
key
end
end
def page_key
# Used for CSS classes, and in custom i18n "tt" method
page_key = []
page_key << controller_namespace if controller_namespace.present?
page_key << controller.controller_name
# "create" and "update" have the same page_keys as "new" and "edit"
page_key << controller.action_name.sub("create", "new").sub("update", "edit")
end
def tt(key, **options)
# This method uses page_key as scope for lazy i18n keys, allowing overrides by namespace
if key&.start_with?(".")
keys = [page_key]
# Add a second set of keys without the namespace, if present
keys << keys[0].drop(1) if keys[0].count > 2
# Convert all keys to symbols and add the supplied key(s) to the end of each set
keys.map!{|a| a.map {|b| b.to_sym } + key.split(".").delete_if(&:empty?).map {|c| c.to_sym }}
# Default to nil for keys that can't be found (unless told otherwise)
matches = I18n.t(keys, **options.reverse_merge({default: nil}))
# Add the result from the standard t() method to the matches
matches << t(key, **options)
# Return the first non-empty match
matches.delete_if(&:nil?).first
else
t(key, **options)
end
end
I already had the page_key method, which avoids having different keys for “edit” and “update”, and “new” and “create”, while adding the controller namespace (if any). I use this to generate CSS classes etc. The new tt method uses this page_key to look for translations; first in the controller namespace (if any), then outside it, before finally falling back to the standard translate method. Subtle differences between I18n.t and plain translate is the reason I’m using both methods here; plain translate does not handle an array of keys the same way that I18n.t does.
Edit: I had a bug in the tt method that caused it to fail if the supplied key was deeper than one level - fixed above. I’ve also updated it to only handle lazy keys (starting with a “.”) since that’s the intended use - regular i18n keys are just passed to plain translate.
Normally I would put the title in the parent page, e.g. edit.html.erb or new.html.erb in which case the i18n lazy lookup works exactly as desired by default. If you need to pass a value into the form partial, why not just pass it they way you would any other value?
<%= render form, title: t.(‘.title’)) %>
Having the form partial lazy load translations from its own namespace is good because it means you don’t have to pollute the namespace of every page that includes that partial with keys used only by the form.
And if you need to pass a value from the form partial up to the parent page, that’s what yield … provide is for.