Rails I18n, the power of internationalisation: the burden of processes!

Photo by Paul Jarvis on Unsplash

My name is Frantisek and I am a committed engineer at Per Angusta. Coming from an international background, I’ve always been passionate about the challenge of application internationalisation (I18n). You would say, begin with English (or your local language) and you will see later how it works. The main question though is: Is it worth going further with I18n?

If you plan on being a B2C, world-wide app, then your answer is obvious. But what if you are a small bunch of tech guys working on a productivity app? Have you ever though about future?

I would like to share with you some way of thinking about i18n in Ruby on Rails, but I think it may be applicable even more universally.

CheatSheet ✓

What about beginning with a small cheatsheet? How many of these practices do you know in Rails? How many of them do you use frequently? Let’s see.

  • Lazy Lookup: I18n.t('.name')
  • Model names: User.model_name.human
  • Model attributes: User.human_attribute_name(:login)
  • Model Pluralisation: User.model_name.human(count: 3)
  • Standard Pluralisation: I18n.t('.name', count: 2)
  • References in YML files: title: :activerecord.models.user.one
  • Validations Errors fallbacks: activerecord.errors.models.[model_name].attributes.[attribute_name].[error_message_key]
  • HTML safe YML: welcome_html: "<b>Welcome %{username}!</b>"
Photo by William Daigneault on Unsplash

Rails magic 🧙🏻‍♀️

If you are on the Rails, you are pretty used to tons of helpers, conventions etc… And that is god damn useful! The good news is that the embedded i18n in Rails was designed the “Rails ways”. It is worth mentioning the official documentation, because you would find pretty much in it (not all 😏). In this part, I would like to mention several interesting practices in i18n, that will certainly help anybody wanting to scale up (internationally)!!

1. Locales file structure: what a mess ?

When your project begins to be wide and internationalised you can be easily overwhelmed with models, views, controllers, namespaces etc… In such case, the “structure” of your files is important. It can be easy to make your own structure in you locale files as a translation is looked-up by its “key”. If your key is correct in the file, your translation will work. When I was beginning projects, I was never thinking about what kind of structure I would use. Sadly, that ended up with a en.yml file that extends over thousands of lines. A highly unmaintainable solution, do you agree? (unless you love CMD+F)

For me, the solution is obvious: ”Convention over Configuration” and I18n on Rails is helpful here.

We separate our YML files to the folders given by the rails structure. The YML files themselves are also structured that way. This is a bit more effort in the beginning, but it is very nice to maintain, because you always know, where you have to put your locales and always know where to look for them!*

*source: a nice article from JENS FREILING

2. Be lazy, use Lazy Lookup!

As a newbie, I was wondering: What the hell is this? It is Rails conventional (and convenient) way to look up the locale inside views. I18n will automatically lookup for translation, based on file names and structure. It works in partials too! With this functionality, I was avoiding long, very long keys looking like this:

I18n.t(‘index.details.informations.stores.address.street’)

and replacing it with the following:

I18n.t(‘.street’)

2. Let Model helpers help you

I18n for Rails comes with a convenient method to look up directly to Model “name”, “attributes” and “nested-attributes” translations. This changed a lot of the code I wrote. You can even use it with automatic pluralisation by appending (count: 42)to it. You can even use it dynamically by changing the ModelName at the front.

# Model NAMES:
User.model_name.human
User.model_name.human(count: 42) # pluralisation
# Model ATTRIBUTES:
User.human_attribute_name("login")
# Model NESTED ATTRIBUTES:
User.human_attribute_name("role.admin")
# tip: useful with ENUMS

4. Use References

This is a feature Rails Documentation does not mention but appears very useful for expanding and internationalised projects.

Here is a sample of a YML file that mixes specific translations and references to shared/default keys. I found it very helpful to reference global translations. You may wonder why just not using I18n.t('shared.back')? I think it is easier to create all the keys used PER view/partial. If you make usage of a global one, then to reference it. It will be useful the day you have to change it. Particularly if you use multi-tenancy and a specific tenant wants to change one of its translations but you not at shared scope.

en:
views:
surveys:
index:
back: :shared.back
placeholder: ‘Search surveys…’

⚠️ Avoid referencing keys from one partial to an other for example, in the case you move the partial’s folder one day! Use references only with keys that are not keen to change! (for example: activerecord, models, shared, global, …)

5. Model and Attributes Errors 🔞

With ActiveRecord, Rails comes with prepared error messages that are stored at the lowest level: errors.messages.blank. You will see according to the default ActiveRecord fallback tree bellow that you can implement error messages on a model attribute level. I18n will automatically fallback to the first translation available of the :blank error for example.

activerecord.errors.models.[model_name].attributes.[attribute_name].blank
activerecord.errors.models.[model_name].blank
activerecord.errors.messages.blank
errors.attributes.[attribute_name].blank
errors.messages.blank => standard Rails errors

What about interpolating an error message? You get a handy set of variables available: model, attribute and value. For example, you can consider the following code. The following error would translate correctly, no need to specify any message.

errors.add(:due_date) 
=> "You have to fill in due_date for Event!"
# with this Model validation
validates :due_date, presence: true
# with this locale tree
activerecord:
errors:
models:
task:
attributes:
due_date:
blank: "You have to fill in %{attribute} for %{model}!"
Photo by SpaceX on Unsplash

Go big? Go fast! 🚀

Let’s stop with all of this documentation and see something else: translating ENUMS. I found an excellent help on StackOverflow (as always). The answer of Repolês was very instructive and innovative! Go ahead and check-it out!

A colleague pointed-out that this solution seems a little-bit overkill for the purpose. Indeed, to get things work quickly, you can use human_attribute_name with nested attributes as showed bellow.

en:
activerecord:
attributes:
user:
status: “Status”
user/status:
active: “Active”
pending: “Pending”
archived: “Archived”
User.human_attribute_name(“status.pending”)
=> “Pending”

I hope this has opened your minds on internationalisation!

🎉 Happy i18n 🎉

I am preparing an exciting article about how to create a custom i18N backend for your app, in order to use ActiveRecord i18n Translation model that is loaded on the app boot! This should enable you to create custom translations for your clients (multi-tenancy) and even let your clients change it on their own!

--

--