Software localization
A Comprehensive Rails I18n Guide

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: