Software localization

A Comprehensive Rails I18n Guide

Rails-i18n provides key locale data for Ruby and Rails. Learn how to use it for Rails internationalization and make your app adaptable for global use.
Software localization blog category featured image | Phrase

Rails internationalization is about creating a Rails application that can be adapted to various locales easily—without the need to make any changes later on in the development process. This involves extracting various bits like strings, dates, and currency formats out of your Rails application, and then providing translations for all of them. The latter is called "localization," if your business is growing and seeking to go international, localizing your Rails app will be a key part of your international strategy.

To get a better understanding of how to implement Rails i18n when building real-world apps, this Rails i18n guide will discuss the following best practices:

  • Details around Rails i18n
  • Where to store translations
  • What localized views are
  • How to format dates, times, and numbers
  • How to introduce pluralization rules and more

You can find the source code for the demo app on GitHub.

Our First Translation with Rails

The  Groundwork

So, I18n was the Rails' core feature starting from version 2.2. It provides a powerful and easy-to-use framework that allows translating an app into as many languages as you need. To see it in action while discussing various concepts, let's create a demo Rails application:

$ rails new I18nDemo

For this article, I am using Rails 6 but most of the described concepts apply to earlier versions as well. Go ahead and create a static pages controller called pages_controller.rb with a single action:

class PagesController < ApplicationController

  def index; end

end

views/pages/index.html.erb

<h1>Welcome!</h1>

Set up the root route inside config/routes.rb:

# ...

root 'pages#index'

So, we have the header on the main page that contains the hard-coded "Welcome!" word. If we are going to add support for multiple languages this is not really convenient — we need to extract this word somewhere and replace it with a more generic construct.

Storing Translations

By default, all translations live inside the config/locales directory, divided into files. They load up automatically as this directory is set as I18n.load_path by default. You may add more paths to this setting if you wish to structure your translations differently. For example, to load all the YAML and Ruby files from the locales directory and all nested directories, say:

# ...

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

inside your config/application.rb file. Our new app already contains an en.yml file inside the locales directory, so let's change its contents to: config/locales/en.yml

en:

  welcome: "Welcome!"

yml extension stands for YAML (Yet Another Markup Language) and it's a very simple format of storing and structuring data. The top-most en key means that inside this file we are storing English translations. Nested is the welcome key that has a value of "Welcome!". This string is an actual translation that can be referenced from the application. Here is a nice guide to naming your keys and a guide to I18n best practices in Rails. The core method to lookup translations is translate or simply t: views/pages/index.html.erb

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

Now instead of hard-coding an English word, we tell Rails where to fetch its translation. welcome corresponds to the key introduced inside the en.yml file. English if the default language for Rails applications so when reloading the page you'll see the same "Welcome!" word. Nice!

🔗 Resource » Check out our tutorial on localization with Slimkeyfy, which allows you to generate translation keys from identified translations, replaces these translations with the generated keys and adds everything to a YAML file—all automatically.

Adding Support for an Additional Language in Rails

Passing Locale Data

Surely you are eager to check how this all is going to work with the support for multiple languages. In order to do that we need a way to provide the language's name to use. There are multiple options available:

  • Provide language's name as a GET parameter (example.com?locale=en)
  • Specify it as a part of a domain name (en.example.com)
  • Provide it as a part of a URL (example.com/en/page). Technically, that's a GET parameter as well.
  • Set it based on the user agent sent by the browser
  • Adjust it basing on the user's location (not really recommended)

To keep things simple we will stick with the first solution. If you would like to learn about other techniques, take a look at our "Setting and Managing Locales in Rails" guide. Firstly, introduce a new before_action inside theApplicationController: application_controller.rb

# ...

before_action :set_locale

private

def set_locale

  I18n.locale = params[:locale] || I18n.default_locale

end

The idea is simple: we either fetch a GET parameter called locale and assign it to the I18n.locale option or read the default locale which, as you remember, is currently set to en.

Available Locales

Now try navigating to http://localhost:3000?locale=de and... you'll get an InvalidLocale error. Why is that? To understand what's going on, add the following contents to the index page: views/pages/index.html.erb

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

<%= I18n.available_locales %>

Next, reload the page while stripping out the ?locale=de part. You'll note that only [:en] is being rendered meaning that we do not have any other available locales available at all. To fix that, add a new gem into the Gemfile: Gemfile

# ...

gem 'rails-i18n'

rails-i18n provides locale data for Ruby and Rails. It stores basic translations like months' and years' names, validation messages, pluralization rules, and many other ready-to-use stuff. Here is the list of supported languages. Run:

$ bundle install

Then boot the server and reload the page once again. Now you'll see a huge array of supported languages. That's great, but most likely you won't need them all, therefore let's redefine the available_locales setting: config/application.rb

# ...

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

Now we support the English and Russian languages. Also, while we are here, let's set a new default locale for our app for demonstration purposes: config/application.rb

# ...

config.i18n.default_locale = :ru

Don't forget to reload the server after modifying this file!

Switching Locale

The code for the before_action should be modified to check whether the requested locale is supported: application_controller.rb

def set_locale

  locale = params[:locale].to_s.strip.to_sym

  I18n.locale = I18n.available_locales.include?(locale) ?

      locale :

      I18n.default_locale

end

As long as we've used symbols when defining available locales, we should convert the GET parameter to a symbol as well. Next, we check whether this locale is supported and either set it or use the default one. We should also persist the chosen locale when the users visit other pages of the site. To achieve that, add a new default_url_options method to the application_controller.rb :

# ...

def default_url_options

  { locale: I18n.locale }

end

Now all links generated with routing helpers (like posts_path or posts_url) will contain a locale GET parameter equal to the currently chosen locale. The users should also be able to switch between locales, so let's add two links to our application.html.erb layout:

<body>

  <ul>

    <li><%= link_to 'Русский', root_path(locale: :ru) %></li>

    <li><%= link_to 'English', root_path(locale: :en) %></li>

  </ul>

  <!-- ... -->

</body>

Providing More Translations

Now when you switch to the Russian language (or any other language you added support for, except for English), you'll note that the header contains the "Welcome" word, but without the "!" sign. Use Developer Tools in your browser and inspect the header's markup:

<h1>

  <span class="translation_missing" title="translation missing: ru.welcome">Welcome</span>

</h1>

What happens is Rails cannot find the translation for the welcome key when switching to Russian locale. It simply converts this key to a title and displays it on the page. You may provide a :default option to the t method in order to say what to display if the translation is not available:

t('welcome', default: 'Not found...')

To fix that, let's create a new translations file for the Russian locale: config/locales/ru.yml

ru:

  welcome: 'Добро пожаловать!'

Now everything should be working just great, but make sure you don't fall for some common mistakes developers usually do while localizing an app. Also note that the t method accepts a :locale option to say which locale to use: t 'welcome', locale: :en.

Using Scopes

Having all translations residing on the same level of nesting is not very convenient when you have many pages in your app:

en:

  welcome: 'Welcome!'

  bye: 'Bye!'

  some_error: 'Something happened...'

  sign_in: 'Sign in'

  and_so_on: 'Many more messages here'

As you see, those translations are messed up and not structured in any way. Instead, we can group them using scopes: config/locales/en.yml

en:

  pages:

    index:

      welcome: "Welcome!"

config/locales/en.yml

ru:

  pages:

    index:

      welcome: 'Добро пожаловать!'

So now the welcome key is scoped under the pages.index namespace. To reference it you may use one of these constructs:

t('pages.index.welcome')

t('index.welcome', scope: :pages)

t(:welcome, scope: 'pages.index')

t(:welcome, scope: [:pages, :index])

What's even better, when the scope is named after the controller (pages) and the method (index), we can safely omit it! Therefore this line will work as well:

<%= t '.welcome' %>

When placed inside the pages/index.html.erb view or inside the index action of the PagesController. This technique is called "lazy lookup" and it can save you from a lot of typing. Having this knowledge, let's modify the views/pages/index.html.erb view once again:

<h1><%= t('.welcome') %></h1>

Localized Views

Now, if your views contain too much static text, you may introduce the localized views instead. Suppose, we need to create an "About Us" page. Add a new route:

# ...

get '/about', to: 'pages#about'

# ...

And then create two views with locale's title being a part of the file name: views/pages/about.en.html.erb

<h1>About Us</h1>

<p>Some text goes here...</p>

views/pages/about.ru.html.erb

<h1>О нас</h1>

<p>Немного текста...</p>

Rails will automatically pick the proper view based on the currently set locale. Note that this feature also works with ActionMailer!

HTML Translations

Rails i18n supports HTML translations as well, but there is a small gotcha. Let's display some translated text on the main page and make it semibold: views/pages/index.html.erb

<%= t('.bold_text') %>

config/locales/en.yml

en:

  pages:

    index:

      bold_text: '<b>Semibold text</b>'

config/locales/ru.yml

ru:

  pages:

    index:

      bold_text: '<b>Полужирный текст</b>'

This, however, will make the text appear as is, meaning that the HTML markup will be displayed as a plain text. To make the text semibold, you may say: https://gist.github.com/bodrovis/d0abc45dedea28642ea75e9efca9d105 or add an _html suffix to the key:

en:

  bold_text_html: '<b>Semibold text</b>'

Don't forget to modify the view's code:

<%= t('.bold_text_html') %>

Another option would be to nest the html key like this:

en:

  bold_text:

      html: '<b>Semibold text</b>'

and then say:

<%= t('.bold_text.html') %>

Translations for ActiveRecord

Scaffolding New Resources

Now suppose we wish to manage blog posts using our application. Create a scaffold and apply the corresponding migration:

$ rails g scaffold Post title:string body:text

$ rails db:migrate

When using Rails 3 or 4, the latter command should be:

$ rake db:migrate

Next add the link to create a new post: views/pages/index.html.erb

<!-- ... -->

<%= link_to t('.new_post'), new_post_path %>

Then add translations: config/locales/en.yml

en:

  pages:

    index:

      welcome: "Welcome!"

      new_post: 'New post'

config/locales/ru.yml

ru:

  pages:

    index:

      welcome: 'Добро пожаловать!'

      new_post: 'Добавить запись'

Boot the server and click this new link. You'll see a form to create a new post, but the problem is that it's not being translated. The labels are in English and the button says "Создать Post". The interesting thing here is that the word "Создать" (meaning "Create") was taken from the rails-i18n gem that, as you remember, stores translations for some common words. Still, Rails has no idea how to translate the model's attributes and its title.

Adding Translations for ActiveRecord

To fix this problem, we have to introduce a special scope called activerecord: config/locales/ru.yml

ru:

  activerecord:

    models:

      post: 'Запись'

    attributes:

      post:

        title: 'Заголовок'

        body: 'Текст'

config/locales/en.yml

en:

  activerecord:

    models:

      post: 'Post'

    attributes:

      post:

        title: 'Title'

        body: 'Body'

So the models' names are scoped under the activerecord.models namespace, whereas attributes' names reside under activerecord.attributes.SINGULAR_MODEL_NAME. The label helper method is clever enough to translate the attribute's title automatically, therefore, this line of code inside the _form.html.erb partial does not require any changes:

<%= f.label :title %>

Next, provide some basic validation rules for the model: models/post.rb

# ...

validates :title, presence: true

validates :body, presence: true, length: {minimum: 2}

After that try to submit an empty form and note that even the error messages have proper translations thanks to the rails-i18n gem! The only part of the page left untranslated is the "New Post" title and the "Back" link — I'll leave them for you to take care of.

Translating E-mail Subjects

You may easily translate subjects for your e-mails sent with ActionMailer. For example, create PostMailer inside the mailers/post_mailer.rb file:

class PostMailer < ApplicationMailer

  def notify(post)

    @post = post

    mail to: 'example@example.com'

  end

end

Note that the subject parameter is not present but Rails will try to search for the corresponding translation under the post_mailer.notify.subject key: en.yml

en:

  post_mailer:

    notify:

      subject: "New post was added!"

ru.yml

ru:

  post_mailer:

    notify:

      subject: "Была добавлена новая запись!"

The subject may contain interpolation, for example:

en:

  post_mailer:

    notify:

      subject: "New post %{title} was added!"

In this case, utilize the default_i18n_subject method and provide value for the variable:

class PostMailer < ApplicationMailer

  def notify(post)

    @post = post

    mail to: 'example@example.com', subject: default_i18n_subject(title: @post.title)

  end

end

Date and Time

Now let's discuss how to localize date and time in Rails.

Some More Ground Work

Before moving on, create a post either using a form or by employing db/seeds.rb file. Also add a new link to the root page: pages/index.html.erb

<%= link_to t('.posts'), posts_path %>

Then translate it: config/locales/en.yml

en:

  pages:

    index:

      posts: 'Posts'

config/locales/ru.yml

ru:

  pages:

    index:

      posts: 'Список записей'

Tweak the posts index view by introducing a new column called "Created at": views/posts/index.html.erb

<table>

  <thead>

    <tr>

      <th>Title</th>

      <th>Body</th>

      <th>Created at</th>

      <th colspan="3"></th>

    </tr>

  </thead>

  <tbody>

    <% @posts.each do |post| %>

      <tr>

        <td><%= post.title %></td>

        <td><%= post.body %></td>

        <td><%= post.created_at %></td>

        <td><%= link_to 'Show', post %></td>

        <td><%= link_to 'Edit', edit_post_path(post) %></td>

        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>

      </tr>

    <% end %>

  </tbody>

</table>

Localizing Datetime

We are not going to translate all the columns and titles on this page — let's focus only on the post's creation date. Currently, it looks like "2016-08-24 14:37:26 UTC" which is not very user-friendly. To localize a date or time utilize the localize method (or its alias l):

l(post.created_at)

The result will be "Ср, 24 авг. 2016, 14:37:26 +0000" which is the default (long) time format. The rails-i18n gem provides some additional formats — you can see their masks here (for the dates) and here (for the times). If you have used Ruby's strftime method before, you'll notice that those format directives (%Y, %m, and others) are absolutely the same. In order to employ one of the predefined formatting rules, say l post.created_at, format: :long. If you are not satisfied with the formats available by default, you may introduce a new one for every language:

ru:

  time:

    formats:

      own: '%H:%M:%S, %d %B'

Now this format can be used inside the view by saying l post.created_at, format: :own. If, for some reason, you don't want to define a new format, the mask may be passed directly to the :format option: l post.created_at, format: '%H:%M:%S, %d %B'. Just like the t method, l also accepts the :locale option: l post.created_at, locale: :en. The last thing to note here is that the rails-i18n gem also has translations for the distance_of_time_in_words, distance_of_time_in_words_to_now, and time_ago_in_words methods, so you may employ them as well: time_ago_in_words post.created_at.

Localizing Numbers

Rails has an array of built-in helper methods allowing to convert numbers into various localized values. Let's take a look at some examples.

Converting Numbers to Currency

In order to convert a given number to currency, use a self-explanatory number_to_currency method. Basically, it accepts an integer or a float number and turns it into a currency string. It also accepts a handful of optional parameters:

  • :locale — locale to be used for formatting (by default, the currently set locale is used)
  • :unit — denomination for the currency, default is $. This setting obeys the :locale setting therefore for the Russian locale, roubles will be used instead
  • :delimeter — thousands delimiter
  • :separator — separator between units

The rails-i18n gem provides formatting for common currencies (based on the set locale), therefore you may simply say:

<%= number_to_currency 1234567890.50 %>

It will print "$1,234,567,890.50" for English locale and "1 234 567 890,50 руб." for Russian.

Converting Numbers to Human Format

What's interesting, Rails even has a special number_to_human method which convert the given number to human-speaken format. For instance:

number_to_human(1234567)

This string will produce "1.23 Million" for English and "1,2 миллион" for Russian locale. number_to_human accepts a handful of arguments to control the resulting value.

Converting Numbers to Phones

Numbers may also be converted to phone numbers with the help of number_to_phone method:

number_to_phone(1235551234)

This will produce "123-555-1234" string but the resulting value may be further adjusted with arguments like :area_code, :extension, and others.

Converting Numbers to Sizes

Your numbers may be easily converted to computer sizes with the help of number_to_human_size method:

<%= number_to_human_size(1234567890) %>

This will produce "1.15 GB" for English and "1,1 ГБ" for Russian locale.

Pluralization Rules and Variables

Different languages have, of course, different pluralization rules. For example, English words are pluralized by adding an "s" flection (except for some special cases). In Russian pluralization rules are much complex. It's up to developers to add properly pluralized translations but Rails does heavy lifting for us. Suppose we want to display how many posts the blog contains. Create a new scope and add pluralization rules for the English locale: locales/en.yml

en:

  posts:

    index:

      count:

        one: "%{count} post"

        other: "%{count} posts"

The %{count} is the variable part — we will pass a value for it when using the t method. As for the Russian locale, things are a bit more complex: locales/ru.yml

ru:

  posts:

   index:

     count:

       zero: "%{count} записей"

       one: "%{count} запись"

       few: "%{count} записи"

       many: "%{count} записей"

       other: "%{count} записи"

Rails determines automatically which key to use based on the provided number. Now tweak the view: views/posts/index.html.erb

<!-- ... -->

<%= t('.count', count: @posts.length) %>

Note that there is also a config/initialializers/inflections.rb file that can be used to store inflection rules for various cases. Lastly, you may provide any other variables to your translations utilizing the same approach described above:

en:

  my_mood: "I am %{mood}"

Then just say:

https://gist.github.com/bodrovis/335e50f63456c04782f2d8485b49ba2c

Phrase Makes Your Life Easier!

Keeping track of translation keys as your app grows can be quite tedious, especially if you support many languages. After adding some translation you have to make sure that it does present for all languages. Phrase is here to help you!

A (Very) Quick Start

  • Create a new account (a 14 days trial is available) if you don't have one, fill in your details, and create a new project (I've named it "I18n demo")
  • Navigate to the Dashboard - here you may observe summary information about the project
  • Open Locales tab and click Upload File button
  • Choose one of two locale files (en.yml or ru.yml). The first uploaded locale will be marked as the default one, but that can be changed later
  • Select Ruby/Rails YAML from the Format dropdown
  • Select UTF-8 for the Encoding
  • You may also add some Tags for convenience
  • Upload another translations file

Now inside the Locales tab you'll see two languages, both having a green line: it means that these locales are 100% translated. Here you may also download them, edit their settings and delete.

Adding Another Language

Next suppose we want to add support for German language and track which keys need to be translated.

  • Click Add locale button
  • Enter a name for the locale ("de", for example)
  • Select "German - de" from the Locale dropdown
  • Click Save

Now the new locale appears on the page. Note there is a small message saying "9 untranslated" meaning that you will have keys without the corresponding translation. Click on that message and you'll see all the keys we've added while building the demo app. Now simply click on these keys, add a translation for them, and click Save (this button may be changed to Click & Next). Note that there is even a History tab available saying who, when and how changed translation for this key. When you are done return to the Locales tab and click the "Download" button next to the German locale: you'll get a YAML file. Copy it inside the locales directory and the translation is ready! Localization is not only about translation and you may be not that familiar with the language you plan to support. But that's not a problem - you can ask professionals to help you! Select Order Translations from the dropdown next to the locale, choose provider, provide details about your request, and click "Calculate price". Submit the request and your professional translations will be ready in no time.

Conclusion

In this Rails i18n guide, we discussed internationalization and localization in Rails. We set up basic translations, introduced localized views, translated ActiveRecord attributes and models, localized date and time, and also provided some pluralization rules. Hopefully, now you are feeling more confident about using Rails i18n. For additional information, you may refer to this official Rails guide and read up some info on the rails-i18n GitHub page. Storing translations inside the YAML files is not the only approach in Rails, so you may be interested in a solution called Globalize - with the help of it you may store translations in the data.

Now that you're all set with the basics of i18n in Rails, are you ready for a deep dive? Check out the following tutorials if you'd like to learn more: