Rails Internationalization (I18n) — Seven Best Practices That You Should Know About

I really love crafting web applications with the Ruby on Rails framework. Creating small maintainable applications is not a problem at all — your code is beautifully structured and everything fits in. However, as your app grows, it may become significantly harder to organize parts of the code so that it does not turn into something monstrous and unmanageable. This, of course, applies to internationalization as well.

In this article you will learn about seven I18n best practices, including advice that will help organize your translations better and take full advantage of Rails power. I am assuming that you already know the basics behind internationalization and localization in Rails, but if you wish to refresh your knowledge, take a look at this official guide or our article describing the topic thoroughly. Have a nice reading!

Split Translations

Large applications may have hundreds of translations keys. If they are stored in a single file it becomes really hard to manage them, therefore it’s a good idea to split your messages into various files stored in separate folders. For example, you may have a folder named models storing translations for the models’ attributes, forms to translate form-related stuff etc:

  • locales
  • models
  • en.yml
  • ru.yml
  • forms
  • en.yml
  • ru.yml

However, by default this is not going to work as Rails does not load translations from the nested directories. To fix this, you’ll need to add the following line of code inside your config/application.rb file:

config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]

load_path is a setting to announce your custom translation files and it should do the trick.

Take Advantage of Nesting

It is not advised to store all your translation messages on the same level of nesting (using flat naming). For example, this is not a recommended practice:

  • en:
  • submit: ‘Submit’
  • log_out: ‘Log Out’
  • blog: ‘Browse Blog’
  • errors: ‘Errors were found:’

As you see, all the messages here are messed up — they relate to different pieces of the application but still they are mixed together. It is much better to introduce the parent keys to nest translations of the same type. For example, you might have something like that:

  • en:
  • forms:
  • submit: ‘Submit’
  • errors: ‘Errors were found’
  • main_menu:
  • log_out: ‘Log Out’
  • blog: ‘Browse Blog’

This way the messages are grouped and it is easier to manage them. By the way, when translating Rails models you must follow this principle, as attributes’ names should be nested properly:

en:
activerecord:
attributes:
category:
name: 'Name'

Give Keys Sensible Names

This sounds like an obvious advice, but still don’t forget about it. Your translation keys should be named in such a manner that it is easy to understand what is their purpose. It becomes even more important for large applications. I really recommend browsing translations for the Devise gem as a nice example of now to organize and name your keys.

Employ “Lazy” Lookups

One cool thing about I18n in Rails is that when naming and nesting your keys properly, you can write less code inside the views and controllers. How is that possible? Well, let’s say you have a view called about.html.erb stored inside the app/views/pages folder. Inside you wish to display the page’s title:

<%= t('pages.about.title') %>

However, this code can be as simple as:

<%= t(:title) %>

In order for this to work, you need to nest the title key under the pages.about scope:

pages:
about:
title: 'About us'

This works, because the scope is written properly: it contains the folder’s and the view’s name (pages and about respectively).

The same approach works for controllers, so having the following translation in place:

orders:
create:
success: 'Your order is created!'

You may create a flash message easily:

class OrdersController < ApplicationController
def create
# ...
flash[:success] = t(:success)
end
end

It’s really convenient, so don’t be lazy to employ the “lazy” lookups!

Enforce Available Locales

By default each Rails application uses English locale and does not explicitly say which languages are supported. This is okay for applications that are not meant to be localized, but if you are planning to support multiple locales, then list them inside your config.rb file:

config.i18n.available_locales = [:en, :de, :ru]

This is convenient, because later you can take advantage of this array (fetch it using I18n.available_locales() method). For instance, this array may be employed to generate the proper routing scopes based on the languages' codes:

scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do

So you will get routes like /en/blog or /ru/shop. Even if the available locales change, the routes file will not require any changes.

Moreover, this setting can come in handy when implementing a switching locale feature. Specifically, it can be used to check whether a user is requesting a supported locale and fallback to the default one if not. It does not really matter how exactly this feature is built, but suppose you have a method to extract locale data:

def extract_locale
parsed_locale = request.subdomains.first
end

You may then easily check whether the requested locale is supported or not:

def extract_locale
parsed_locale = request.subdomains.first
I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

available_locales() returns an array with symbols, so here we convert them to strings and then check whether the requested language code was found inside. You can read more on this technique at our Setting and Managing Locales in Rails I18n article.

Utilize Localized Views

Some developers tend to place all translations inside the YAML files regardless of their length and complexity. In some cases, however, that’s not really convenient. Suppose you have a page that looks totally different depending on the chosen language. Of course, you might do something like this:

<h1><%= t(:welcome) %></h1>

<p><%= t(:special_offer) %></p>

<p><%= t(:about) %></p>

Then you would have to define these keys, but the corresponding messages are too large:

welcome: 'Welcome!'
special_offer: 'Here is our cool special offer! And then more text here...'
about: 'Long text goes here.... and more text... even more...'

Alternatively, you may take advantage of HTML translations but that’s not going to help a lot — you still have long messages that are hard to maintain.

What you can do instead, is stick with the localized views that allows you to store totally different content for each locale. They are employed by simply prefixing your views with the locale’s code, for example about.ru.html.erb and about.en.html.erb. Rails will render the proper view automatically depending on the value returned by the I18n.locale() method.

Take Advantage of Variables

Another important thing some novice developers tend to forget about is the fact that you can pass variables to your translations in Rails. Suppose, for example, you wish to display how many new messages has the user received. Of course, you may simply employ string interpolation like this:

<%= "#{t(:message_count)}#{@messages.count}" %>

But even though this piece of code is small, it looks overly complex and too ugly. Instead, let’s allow the message_count to accept a variable:

message_count: "%{count} new messages"

Then simply pass a hash to the t method as the second argument:

<%= t(:message_count, count: @message.count) %>

Clean and simple. You can further extend this example by introducting pluralization rules that also rely on variables.

Helper Methods

Rails offers us a bunch of powerful helper methods that can be used to display datetime and month select boxes, convert numbers to currency and display distance of time in words. Some of them work as magic and can really save you from writing many lines of code every time.

However, be warned that some of these methods are somewhat expensive in tems of processing time. distance_of_time_in_words is one of such methods - it says how much time has passed since the provided datetime. If you are planning to use this method extensively in some view (the typical example is the comments block to say how old the comment is), then maybe it is better to reconsider and put it away. You might think about employing fragment caching here but then you'll need to constantly expire it. Time runs fast and the phrase "the comment was published X days ago" should be constantly updated.

Another solution to this problem is performing the calculation on the client-side. For example, there is a great library called Moment.js that supports this feature (and many others).

Stick with PhraseApp!

Working with translations is hard: you might easily miss some translations for a specific language which will lead to user’s confusion. And so PhraseApp can make your life easier!

Grab your 14-days trial. PhraseApp supports many different languages and frameworks, including JavaScript of course. It allows to easily import and export translations data. What’s cool, you can quickly understand which translation keys are missing because it’s easy to lose track when working with many languages in big applications. On top of that, you can collaborate with translators as it’s much better to have professionally done localization for your website.

Conclusion

So, in this article we have discussed some common best practices advised when working with I18n in Rails. I hope it will help you crafting maintainable and beautiful applications! Still, if you think that I have missed something, do not hesitate to share your opinion in the comments. Also, if you wish to learn more about internationalizing Rails applications, you may be interested in our article The Last Rails I18n Guide You’ll Ever Need.

That’s all for today, folks. I thank you for staying with me and happy coding!


Originally published at PhraseApp Blog.