Rails: I18n in mailer layout

Youssef Chaker
Bear & Giraffe
Published in
3 min readMar 26, 2019

Recently I had a simple task to do in a Ruby on Rails application. The style of the emails needed to show the same subject as a heading in the body of the email. Simple task, right? Sure, until you actually try it and realize that there is not a built in way to do it. It is still a quick fix, so let’s go through it.

The first instinct is to add a line like this to views/layouts/mailer.html.erb :

<h1><%= I18n.t('.subject') %></h1>

What this is supposed to do is to get the subject at the relative scope to where the code is being executed. Typically that would map to the file you’re calling it from and get you the correct scope. So if you had this line in views/custom_mailer/notification.html.erb it will look for this scope: customer_mailer.noticiation.subject in your yml file. The problem in this case is that the call to the translation is done from a layout file, and it will not pick up the scope of the final view it will be used in. So the result of our code above would be something like this: Missing translation: :subject.

So what do we do? What we will need to supply is a scope based on the mailer class and the action. Can we get those somehow? My quick instinct is to go look at the ActionMailer guides and see what we can come up with: https://api.rubyonrails.org/classes/ActionMailer/Base.html

After a quick glance, we see a method called default_i18n_subject that might be helpful. Luckily it has both things we need i18n and subject. How about them apples! This is what the code inside of that method looks like (click on “Show” to see the source):

def default_i18n_subject(interpolations = {}) # :doc:
mailer_scope = self.class.mailer_name.tr("/", ".")
I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
end

Interesting. So that gives us a lead. The method takes in a param called interpolations which is probably a way to override some options to the t method. Let’s ignore that. What we care about is this: scope: [mailer_scope, action_name]. We can see that mailer_scope is defined right above. action_name however looks like a helper method that might or might not be available to us in the view. Let’s try it and see what happens.

<% mailer_scope = self.class.mailer_name.tr("/", ".") %>
<h1><%= I18n.t(:subject, scope: [mailer_scope, action_name]) %></h1>

Well, that doesn’t work. Why is that? hmmmm…
`self.class` in the original code referred to the mailer class that inherits from ActionMailer::Base whereas in the view it would refer to something else, which means mailer_name would not be available as a method to call on it. We need another way to get the mailer name while inside of the view.

After a bit of googling I realized that mailers act the same way as controllers, which means that action_name will be available to us as a view helper BUT for some reason we do not get the controller_name helper 🤔. No worries, we can extract that using the following code:

<% mailer_scope = self.controller
.class
.name
.underscore
.tr('/', '.')
%>
<h1>
<%= I18n.t(:subject, scope: [ mailer_scope, action_name ]) %>
</h1>

There you have it, now you’re able to use scoped internationalization from inside of the mailer layout. Was that useful? What similar tricks have you used in the past? Please share in the comments.

--

--