Rails: I18n in mailer layout
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.