Sometimes, the best blog post comes from a need to scratch your own itch. And today's blog post is an example of that!
You probably know Devise—a popular authentication solution for Rails applications. When working with Devise, I found that getting a translation up and running was more complex than I had anticipated. Past me wished that there was a checklist that could help guide me through the process.
So here it is - for future me and you - the ultimate Devise Internationalization Checklist!
When (Not) to Use the devise-i18n Gem?
Devise's policy is to not make the strings in the Devise views translatable. All the strings are hard coded. Kudos to the creators of the devise-i18n gem, which makes all the Devise templates translatable and also provides community-sourced translations. For international apps, where you need multi-language support, the devise-i18n gem is the easiest solution.
However, the devise-i18n gem explicitly chooses to follow the Devise text as closely as possible. But the text that Devise provides isn't very consistent to start with. And since literal translation will never generate fluent language, I'm constantly tweaking translations (and copying files between projects). That is an ongoing struggle because it's hard to get all the translations consistent when looking at one string or one view at a time.
As a result, the quality of the translations of the i18n-devise gem doesn't reach the standards required for my monolingual, Dutch-only projects.
Another disadvantage of using the gem arrises with customized views. Even when it's only to do with styling, every update of the devise-i18n gem that touches any changes that Devise made to the views, will mean you have to fix things. You will either need to regenerate the views and apply the customizations or you will have to copy/paste the changes into your views. This problem is not solved with the approach in the checklist below, by the way, but it's also not more difficult to solve without the devise-i18n gem. 🤷♀
🇳🇱 A Dutch treat
I decided to create one nice Dutch translation file for Devise, and never have to worry about it again. Sounds like a treat? You can find the new and improved translation file on GitHub. See step 3 in the checklist. The rationale for each translation decision is explained in the file.
The Ultimate Checklist
Now that we're flying solo with the :nl translation, let's get started with of all the details and start checking boxes.
Note that this assumes that you have an app ready, with Devise added and installed with a User model, and the Devise views generated. If you're coding along: this is the starting point. And note that nl
in *.nl.yml
can be replaced by the language of your choice, and that User and :user can be replaced by the model name you use in Devise.
Step One: Set locale
Add :nl
to the available locales (if you don't have a strong opinion on the matter, start with putting it in application.rb
for now). Now you can set it as default.
1# application.rb
2config.i18n.available_locales = [:nl, :en]
3config.i18n.default_locale = :nl
There are better, perhaps cleaner options to set the locale than adding it to application.rb
. Check the Rails i18n guide after reading this post to choose the option that suits you best.
Step Two: Add Rails translation
Add the Rails :nl translation file to take care of the strings in the interface that are not Devise specific (especially the Rails validation error messages).
Step Three: The Devise keys
Translate the Devise keys to :nl
: add the New and Improved Dutch Translation! (Link above 🇳🇱)
Step Four: Create views.*.yml files
Add Dutch and English .yml
files for the views, views.*.yml
. There's an example of a finished views.nl.yml file in the repo, reflecting steps 4, 6 and 7.
1#example views.nl.yml
2nl:
3 devise:
4 # to be filled in later with Devise scopes:
5 # registrations:
6 # new:
7 # forget_password: Wachtwoord vergeten?
Step Five: The Active Record attributes
Now, add translations for the labels of the Active Record attributes, like email and password. Translate just the ones that the user and the admin user see, for each of the modules that you implemented. No need to translate token names and such (as the i18n gem does). Here's what I consider useful 'translatables':
1# In: active_record.nl.yml
2# translating the labels that map to User attributes
3nl:
4 activerecord:
5 attributes:
6 user:
7 current_password: Huidig wachtwoord
8 email: e-mailadres
9 password: wachtwoord
10 password_confirmation: Wachtwoord bevestigen
11 remember_me: Ingelogd blijven?
12 models:
13 user: Gebruiker
Note that the :email
and :password
values are downcased. That's because of the tweaks to the Rails error messages in step 7.
For now, put the keys in active_record.*.yml
.
Step Six: Check every Devise view
Next, all the Devise-specific templates and strings need to be i18n-ed with the corresponding translations. Doing it yourself is an annoying job. Before you dive into it, see if one of these shortcuts work for you:
You could use the 118n-devise gem for the views, but you still have to copy the keys from the gem's
.yml
files into your project's.yml
files. The gem interleaves them indevise.*.yml
, but we don't want to overwrite our nice new Dutch translation. And if you have styled the Devise views, you'll need to copy the styling too. Yugh.If you already have i18n-ed projects, you could copy the view files. (Same disadvantages with this as in the previous option.)
Use the repo for examples of a few views that I used in my latest project.
Or solve it once and for all: head down, create a set of files that you can use from now on, exactly the way you want them to be. The examples in the previous step are my own first version; they may help you kick-start your own. Check out the mini-checklist below for all the requirements.
Mini Checklist for the Devise Forms
- Strings that are not Active Record strings need to be 118n-ed and have (properly scoped) keys and translations (stored in, for instance, a
views.*.yml
file—see example). - Take care that the Dutch translations, when adding your own, are consistent with the new
devise.nl.yml
. - Ignore the downcased labels; they will be capitalized properly in step 7.
- If you already have the mailers i18n-ed, that's okay. If not, you may ignore them until step 8.
- The following Devise views need to be made translatable with the Rails translate (
t
) helper:- The
devise/views/shared/_links
partial: all the link names; - The
devise/views/shared/_errors
partial (since Devise 4.6) - In the Devise forms for each of the modules you use: all the strings that are not labels for Active Record attributes.
- The
Step Seven: The Active Record error messages
Next, we need to tweak the Active Record error messages that are related to the Devise functionality. Like "password can't be blank", and other validations.
The thing is, now that we have upgraded the Devise translations, the Active Record messages need tweaking as well because the inconsistency in style is really showing. So I tweaked them too, following the same rules as for the Devise translations, and making sure they are consistent.
You'll see that the format of the messages has changed: from "%{attribute} %{message}" to "%{message}". Now we are referencing the attribute within the translated string instead of forcing it at the start of the string, makes it so much easier to prettify the Dutch messages. I rather have the duplication than putting up with the sometimes weird messages.
The new message format is the reason why, in step 5, the email and password attributes needed to be downcased: in the error messages, they can appear anywhere within a sentence.
I also replaced the :taken
message. The Devise policy is to not reveal if an email address is 'not found' or 'invalid'. I extended that policy to the Rails' taken
message.
See this file if you want to follow the new messages style. Then replace the corresponding part of the Rails error: messages:
keys in the original nl.yml
file. One caveat: when you're adding validations for none Devise-related input, please double check if the error messages still make sense. (I need to investigate further; I only checked the ones that are related to Devise.)
Step Eight: Fix the labels we just broke
In step 5, we downcased the email and password translations for use in the new error messages. Now we need to fix their appearance in the labels. There are several options, but the one I like most is adding separate 'label' keys.
This adds some duplication for the user keys, but it keeps the labels in the forms clean.
1# in active_record.nl.yml add the label helper keys:
2nl:
3 activerecord:
4 attributes:
5 user:
6 current_password: Huidig wachtwoord
7 email: e-mailadres
8 password: wachtwoord
9 password_confirmation: Herhaal het wachtwoord
10 remember_me: Wil je ingelogd blijven?
11 models:
12 user: Gebruiker
13 helpers:
14 label:
15 user:
16 email: Emailadres
17 password: Wachtwoord
Note that there is already a :helpers
scope in the original nl.yml
. In general, it's a good idea to keep the keys for a scope together, for easy lookup (by humans).
The other changes we need to make are straightforward. No big surprises will pop up from here on. 🎉.
Step Nine: Update navigation links
Find all the links that point to Devise (think navigation) and i18n them with the Rails t
helper.
I like to keep those somewhere in the Devise scope of the .yml's. For example, I'd add t(".devise.sign_out")
to have :sign_out
in the same scope as :sign_up
and :sign_in
(see the example files).
Step Ten: Translate the mailers.
Translate the mailer views. There are a few options:
You can 'i18n' every string and link name, just like we did in step 6 with the other views. You may want to add a separate
mailer.*.yml
.Or you can add separate mailer views for each locale:
devise/mailer/reset_password.nl.html.erb
anddevise/mailer/reset_password.en.html.erb
. Each with their own text. Rails will pick the one that matches the locale that is set.
Alright! Now all the Devise strings are properly translated. Yay!
After Care: Declutter
In my views example, I extracted the shared keys (like :forgot_password
) and collected them in the general devise scope. This makes it easier to change them, but Rails can't find them automatically, so we need the verbose syntax (t("devise.forgot_password")
), because the dot syntax (t(".new_confirmation_mail")
) won't work.
If you go with the active_record.*.yml
, it makes sense to collect all the keys with active record scope into that file. Move the Active Record keys from the Rails *.yml
files into the corresponding active_record.*.yml
.
We also have two pairs of *.yml
files that duplicate the devise:
scope. Consider mixing the keys from the Devise views into the devise.*.yml
views. It is what the Devise-i18n gem does, and I like it. You'd do it like this:
1# mixing the view scopes into devise.nl.yml
2nl:
3 devise:
4 confirmations:
5 ...
6 registrations:
7 ...
8 edit:
9 ...
10 new:
11 ...
Some Tweaks and Tips
- I am not a fan of the header in the error message ('x errors prohibited this user from being saved'), so I removed that from the
errors
partial. - If you set the
default_locale
to:nl
, Rails will 'humanize' the keys if it can't find a translation. So,t(:some_untranslated_key)
will show asSome Untranslated Key
in the view. - With the I18n-tasks gem you can clean up unused keys and add missing keys. It's a great tool! However, my IDE has great i18n support and for me that turned out to be a quicker workflow than the gem.
Parting Is Such Sweet Sorrow
A quick final word before we part. When I started this journey I didn't expect it to be as much work as it turned out to be. I'm very happy that I now know where the complexity comes from. And future me and you now have a list of all the changes needed. Woohoo!
But even with this checklist in hand, think before you act: maybe it's worth living with the less optimal translations for a while…