Decluttering Translations in Rails Apps

Written by Manu Raj on November 12, 2019; tagged under rails

Internationalization(i18n) or translations are an integral part of any application that caters to a global audience. Rails supports this out the box.

To add a set of translations to your rails app, it is fairly easy. By default, rails will provide an en.yml inside config/locales folder.

en:
  hello: "Hello world"

What this provides is that the key hello inside locale :en will have the value Hello world.

In this post we are going to focus on how to grow the translation files as the application gets bigger and avoid clutters and duplications as much as we can.

Consider an application with namespaces and a set of different contexts under it. It’s common to end up having a structure like below in translation files.

en:
  admin:
    attendance 
      policies:
        index:
          blank:
            title: Some title

We have some namespaces to cross until we reach the key we require which is title.

Consider the directory app/views/admin/attendance/policies for all the views for attendance namespace under admin. It contains several files index.html.slim, _table.html.slim, blank.html.slim

Now, to use that title for the file blank.html.slim we would be using it as t('admin.attendance.policies.index.blank.title'). This is not good, and this can only get worse if our keywords get bigger or if we have more namespaces.

Use rails lazy-lookup

Rails provides us with a lazy lookup functionality where in which we don’t have to provide the whole path to the translation if it is directly in sync with the path to the view.

Let me refactor the above and show you.

I updated the view file app/views/admin/attendance/policies/_blank.html.slim to have the translation as: t(.title)

Now I update the translation file as below (please note the hierarchy)

en:
  admin:
   attendance: 
      policies:
        blank:
          title: Some title

Here I removed the index and made the blank key come directly under the policies key. What this resembles is the same directory structure as we have in our app (check the path shared above).

If this structure matches, rails will automatically pick up t('.title').

Long chained paths are not required.

This would require us to re-arrange the translation file a bit but the extra effort would be worth it.

Grouping common terms

We have a lot of forms and components which make use of the same words as Submit, Cancel etc that are being used in many places. We should group them under minimal hierarchy i.e without a long chain. For example, form related things can be grouped under

en:
  form:
    submit: Submit
    cancel: Cancel

Then to be used as t('form.submit') or t(:submit, scope: :form) FYI: You can also chain scopes like this t(:key, scopes: [:scope_one, :scope_two]), let’s stick to minimal as possible

It may not be form but could be any other entity of the app that you could come across, end goal being to prevent this t('admin.some_module.some_resource.some_action.some_form') == "Submit" and t('admin.another_module.another_resource.another_action.another_form') == "Submit"

NOTE: I have seen the usage of human_attribute_name, it’s also good in abstracting out the complexity - wasn’t mentioned here because the intent of this write up was to primarily focus on views and partials that may not be dealing with model objects all the time, even the ones that are having a model object may not be displaying the exact name.

Splitting up the file

Since we have lots of modules we could go ahead and split up the translation file as well. Considering the above, any module-specific translations can be made to go under locales/admin/<module_name>/*.yml

The current example we are looking at have all the translations at this location: config/locales/en.yml.

Following the split-up approach, we will end up with the path like config/locales/admin/attendance/en.yml

Rails will lazy lookup the translation even in this manner.

Although splitting up would not help reduce redundancy, it would help in better arrangement.

Approach

Combining all three methods would be an ideal approach. Start with making lazy lookup work and then re-arrange the commonly used ones and finally splitting the files up. There is no hard and fast rule as in it should be done in the same order but eventually, we should be able to do all these.

Deciding a word to be moved globally or under a namespace is a preference, example if something like ‘Step 1’ is used in all modules then it could very well be moved out of the modules. Only the step definitions could change. If a word is only used in two modules or so it’s okay to still have it in the modules itself, but the ideal use of translations is to not repeat any words. i.e if we need to change something we have to change it only in one place.

TIP: While doing a refactor take a look at this gem i18n-tasks. It can help you find and manage missing and unused translations.

Reference

Check out all the nifty extras that you can do with the i18n api here Rails doc

If you have any questions or feedback, feel free to drop us a mail at team@codemancers.com.