DEV Community

Zee
Zee

Posted on

Surprising behavior with Rails I18n Lazy Lookups and Partials

Rails i18n supports lazy lookup so that if you have a locales file shaped like this:

en:
 newsletter_cta:
   title: "Stay Up To Date With The Latest Happenings"

You can reference the value in newsletter_cta.title via t('.title') when in app/views/_newsletter_cta.html.erb

In many Rails applications, I extract partials to encapsulate components so any HTML/CSS oriented team member doesn't have to learn how to write helper methods when writing components; and so we don't wind up with copy-pasted code that would necessitate massive pain when upgrading or shifting the underlying UI framework.

Here's an example of a bootstrap section:

# app/views/application/_section.html.erb
<section class="section-wrap">
  <div class="container">
    <%= yield %>
  </div>
</section>

Which I can then use in other views, like this:

# app/views/_newsletter_cta.html.erb
<%= render "section" do %>
  <h2><%= t('newsletter_cta.title') %></h2>
  <%= render "cta-button", text: t('newsletter_cta.title') %> 
<% end %>

This works fine, so long as don't rely on lazy lookup and I hardcode the full lookup string for the translation.

However, as soon as I use lazy lookups the translation is not found.

# app/views/_newsletter_cta.html.erb 
<%= render "section" do %>
  <h2><%= t('.title') %></h2>
  <%= render "cta-button", text: t('.title') %> 
<% end %>

Tracing through to the translate method in ActionView::Helpers::TranslationHelper we see that it relies on a method named scope_key_by_partial which in turn, relies on an instance variable @virtual_path.

Do any of y'all know of a way to preserve the virtual path while using partials with blocks? Is there a gem somewhere that changes this behavior? I'm a bit surprised no one else has stumbled into this.

Top comments (4)

Collapse
 
yourivdlans profile image
Youri van der Lans

I've also been running into this issue. But because I use the i18n-tasks gem to keep my localisation file in check I've chosen to write out the absolute path for a translation when using them within render blocks.

Basically what happens is as soon as you put a relative translation in a render block the path gets changed. In your example it would be: "application.section.title".

One way to approach this could be to pass the i18n scope of the view to the partial and use that when translating a key.

Something like:

<%= render "section", i18n_scope: @virtual_path.gsub(%r{/_?}, ".") do %>
  <h2><%= t('title', scope: i18n_scope) %></h2>
<% end %>

Note the removed dot before "title" indicating this is not a relative lookup.

Collapse
 
zspencer profile image
Zee

I think landing on "Use absolute paths at all times with I18n" is great advice, especially since the lookup inference pattern is pretty unique to Rails.

Collapse
 
joshantbrown profile image
Josh Brown

When you call render "section" what it seems actually happens is it treats the partial as a template, this changes the scope of execution and therefore changes where those translations are executed.

Instead you can explicitly render it as a partial by passing it as render partial: "section". However, partials don't allow you to pass blocks to them, instead you'll need to capture the content into a variable and pass it as a local:

<% content = capture do %>
  <!-- Your complex HTML/ERB code here -->
<% end %>

<%= render partial: "shared/section", locals: { content: content } %>
Enter fullscreen mode Exit fullscreen mode

Putting these in a helper tidies things up in the views:

module ApplicationHelper
  def section(&block)
    content = capture(&block)
    render partial: "shared/section", locals: { content: content }
  end
end
Enter fullscreen mode Exit fullscreen mode

Then in your view you can do:

<%= section do %>
  <h2><%= t('.title') %></h2>
<% end %>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
krebbl profile image
Marcus Krejpowicz

In case somebody is looking for a workaround, here it is:

module ApplicationHelper
...
def render(*args, &block)
  if block
    vpath = instance_variable_get(:@virtual_path)
    super(*args) do |*kwargs|
      original_path = instance_variable_get(:@virtual_path)
      instance_variable_set(:@virtual_path, vpath)
      ret = capture(*kwargs, &block)
      instance_variable_set(:@virtual_path, original_path)
      ret
    end
  else
    super(*args, &block)
  end
end
...
end
Enter fullscreen mode Exit fullscreen mode