Over a million developers have joined DZone.

The Last Rails I18n Guide You’ll Ever Need: Part I

In this article, we will discuss how to add support for I18n in a Rails application, where to store translations, what localized views are, how to format dates and times, how to introduce pluralization rules and more.

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

Internationalization (dubbed as I18n as there are exactly eighteen characters between the first "I" and the last "n") means creating an application that can be adapted to various languages easily, without the need to do complex changes. This involves extracting various bits (like strings, date and currency formats) out of an application and then providing translations and formats for them. The latter is called localization and sometimes is dubbed as L10n. If your company is growing and seeking to go international, localization is an important step to do.

In this article, we will discuss how to add support for I18n in a Rails application, where to store translations, what localized views are, how to format dates and times, how to introduce pluralization rules and more. By the end of this article you will have a solid understanding of using I18n with Rails and be ready to employ the described techniques when building the real-world apps.

The source code for the demo app is available at GitHub.

Our First Translation

I18n was the Rails' core feature starting from version 2.2. It provides a powerful and easy to use framework that allows you to translate 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 5.0.0.1 but most of the described concepts apply to Rails 3 and 4 as well.

Go ahead and create a static pages controller with a single action:

pages_controller.rb

    class PagesController < ApplicationController
      def index
      end
    end

views/pages/index.html.erb

    <h1>Welcome!</h1>

Set up the root route:

config/routes.rb

    root to: '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 - this word has to be extracted somewhere and replaced with a more generic construct.

By default all translations should be placed inside the config/locales directory, divided into files. They are being loaded 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.

By default there is already an en.yml file inside the locales directory present, so let's change its contents to

config/locales/en.yml

    en:
      welcome: "Welcome!"

yml stays for YAML (Yet Another Markup Language) and it is 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 the nice guide to naming your keys.

The core method to lookup translations is called translate or simply t:

views/pages/index.html.erb

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

So 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!

Adding Support for an Additional Language

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)
  • Provide 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
  • Set it based on the user's location (not really recommended)

To keep things simple we will stick with the first solution. Introduce a new before_action inside the ApplicationController:

application_controller.rb

    before_action :set_locale

    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 fetch the default locale which, as you remember, is currently set to en.

Now try navigating to http://localhost:3000?locale=de and... you'll get an InvalidLocale error. Why is that? Add the following contents to the index page

views/pages/index.html.erb

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

    <%= I18n.available_locales %>

and reload the page (while stripping out the ?locale part). You'll note that only [:en] is being rendered meaning that we do not have any other 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

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 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!

The code for the before_action should be modified:

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.

Now when you switch to a 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 a tool like Firebug 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 translation for the welcome key when switching to Russian locale, so 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 cannot be found:

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

Let's create a new translations file for the Russian locale:

config/locales/ru.yml

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

Now everything should be working just great, however, be sure not to 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 can save you from a lot of typing. Having this knowledge, let's modify the view once again:

views/pages/index.html.erb

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

Localized Views

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

config/routes.rb

    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 set locale.

HTML Translations

I18n in Rails support HTML translations as well, however, 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

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

or add an _html suffix to the key:

    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:

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

and then say

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

That concludes Part I. Stay tuned for Part II, coming soon!

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
rails tutorial ,i18n ,ruby ,translation ,web dev

Published at DZone with permission of Ilya Bodrov. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}