10.3. Multilingual Rails Application

The approach for multilingual Rails applications is very similar to the monoligual, all-German Rails application described in Section 10.2, “A Rails Application in Only One Language: German”. But we need to define YAML language files for all required languages and tell the Rails application which language it should currently use. We do this via I18n.locale.

Using I18n.locale for Defining the Desired Language

Of course, a Rails application has to know in which language a web page should be represented. I18n.locale saves the current language and can be read by the application. I am going to show you this with a mini web shop example:
$ rails new webshop
  [...]
$ cd webshop
$
This web shop gets a homepage:
$ rails generate controller Page index
  [...]
$ rm public/index.html
$ 
We still need to enter it as root page in the config/routes.rb:
Webshop::Application.routes.draw do
  root :to => 'page#index'
  get "page/index"
end
We populate the app/views/page/index.html.erb with the following example:
<h1>Example Webshop</h1>
<p>Welcome to this webshop.</p>

<p><b>I18n.locale:</b> 
<%= I18n.locale %>
</p>
If we start the Rails server with rails server and go to http://0.0.0.0:3000/ in the browser, then we see the following web page:
Index Page page#index
As you can see, the default is set to "en" for English. Stop the Rails server with CTRL-C and change the setting for the default language to German in the file config/application.rb:
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]

config.i18n.default_locale = :de
If you then start the Rails server and again go to http://0.0.0.0:3000/ in the web browser, you will see the following web page:
Index Page page#index with config.i18n.default_locale = :de
The web page has not changed, but as output of <%= I18n.locale %> you now get 'de' for German (Deutsch), not 'en' for English as before.
Please stop the Rails server with CTRL-C and change the setting for the default language to en for English in the file config/application.rb:
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]

config.i18n.default_locale = :en
We now know how to set the default for I18n.locale in the entire application, but that only gets half the job done. A user wants to be able to choose his own language. There are various ways of achieving this. To make things clearer, we need a second page that displays a German text. Please create the file app/views/page/index.de.html.erb with the following content:
<h1>Beispiel Webshop</h1>
<p>Willkommen in diesem Webshop.</p>

<p><b>I18n.locale:</b> 
<%= I18n.locale %>
</p>

Setting I18n.locale via URL Path Prefix

The more stylish way of setting the language is to add it as prefix to the URL. This enables search engines to manage different language versions better. We want http://0.0.0.0:3000/de to display the German version of our homepage and http://0.0.0.0:3000/en the English version. The first step is adapting the config/routes.rb
Webshop::Application.routes.draw do

  scope "(:locale)", :locale => /en|de/ do
    root :to => 'page#index'
    get "page/index"
  end

end
Next, we need to set a before_filter in the app/controllers/application_controller.rb. This filter sets the parameter locale set by the route as I18n.locale:
class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :set_locale
 
  private
  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end
end
To test it, start Rails with rails server and go to the URL http://0.0.0.0:3000/de.
http://0.0.0.0:3000/de
Of course we can also go to http://0.0.0.0:3000/de/page/index:
http://0.0.0.0:3000/de/page/index
If we go to http://0.0.0.0:3000/en and http://0.0.0.0:3000/en/page/index we get the English version of each page.
But now we have a problem: by using the prefix, we initially get to a page with the correct language, but what if we want to link from that page to another page in our Rails project? Then we would need to manually insert the prefix into the link. Who wants that? That's why there is a solution in form of Rails.application.routes.default_url_options. We just need to expand our set_locale method accordingly in app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :set_locale
 
  private
  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
    Rails.application.routes.default_url_options[:locale]= I18n.locale 
  end
end
As a result, all links created with link_to and url_for (on which link_to is based) are automatically expanded by the parameter locale. You do not need to do anything else. All links generated via the scaffold generator are automatically changed accordingly.
Navigation Example
To give the user the option of switching easily between the different language versions, it makes sense to offer two links at the top of the web page. We don't want the current language to be displayed as active link. This can be achieved as follows for all views in the file app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html>
<head>
  <title>Webshop</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<p>
<%= link_to_unless I18n.locale == :en, "English", locale: :en %>
|
<%= link_to_unless I18n.locale == :de, "Deutsch", locale: :de %>
</p>

<%= yield %>

</body>
</html>
The navigation is then displayed at the top of the page.
Language Selection at Top of Page

Setting I18n.locale via Accept Language HTTP Header of Browser

When a user goes to your web page for the first time, you ideally want to immediately display the web page in the correct language for that user. To do this, you can read out the accept language field in the HTTP header. In every web browser, the user can set his preferred language (see http://www.w3.org/International/questions/qa-lang-priorities). The browser automatically informs the web server and consequently Rails of this value.
Please edit the app/controllers/application_controller.rb as follows:
class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :set_locale
 
  private
  def extract_locale_from_accept_language_header
    request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
  end

  def set_locale
    I18n.locale = extract_locale_from_accept_language_header || I18n.default_locale
  end
end
And please do not forget to clean the settings in the section called “Setting I18n.locale via URL Path Prefix” out of the config/routes.rb:
Webshop::Application.routes.draw do
  get "page/index"
  root :to => 'page#index'
end
Now you always get the output in the language defined in the web browser. Please note that request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first does not catch all cases. For example, you should make sure that you support the specified language in your Rails application in the first place. There are some ready-made gems that can easily do this job for you. Just use the search engine of your choice to go online and look for them.

Saving I18n.locale in a Session

Often you want to save the value of I18n.locale in a session (see Section 8.2, “Sessions”).

Tip

The approach described here for sessions will of course work just the same with cookies. See Section 8.1, “Cookies”.
To set the value, let's create a controller in our web shop as example: the controller SetLanguage with the two actions english and german:
$ rails generate controller SetLanguage english german
  [...]
$ 
In the file app/controllers/set_language_controller.rb we populate the two actions as follows:
class SetLanguageController < ApplicationController
  def english
    I18n.locale = :en
    set_session_and_redirect
  end

  def german
    I18n.locale = :de
    set_session_and_redirect
  end

  private
  def set_session_and_redirect
    session[:locale] = I18n.locale
    redirect_to :back
    rescue ActionController::RedirectBackError
      redirect_to :root
  end
end
Finally, we also want to adapt the set_locale methods in the file app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :set_locale
 
  private
  def set_locale
    I18n.locale = session[:locale] || I18n.default_locale
    session[:locale] = I18n.locale
  end
end
After starting Rails with rails server, you can now set the language to German by going to the URL http://0.0.0.0:3000/set_language/german and to English by going to http://0.0.0.0:3000/set_language/english.
Navigation Example
To give the user the option of switching easily between the different language versions, it makes sense to offer two links at the top of the web page. We don't want the current language to be displayed as active link. This can be achieved as follows for all views in the file app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html>
<head>
  <title>Webshop</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<p>
<%= link_to_unless I18n.locale == :en, "English", set_language_english_path %>
|
<%= link_to_unless I18n.locale == :de, "Deutsch", set_language_german_path %>
</p>

<%= yield %>

</body>
</html>
The navigation is then displayed at the top of the page.
Language Selection at Top of Page

Setting I18n.locale via Domain Extension

If you have several domains with the extensions typical for the corresponding languages, you can of course also use these extensions to set the language. For example, if a user visits the page http://www.example.com he would see the English version, if he goes to http://www.example.de then the German version would be displayed.
To achieve this, we would need to go into the app/controllers/application_controller.rb and insert a before_filter that analyses the accessed domain and sets the I18n.locale :
class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :set_locale
 
  private
  def set_locale
    case request.host.split('.').last
    when 'de'
      I18n.locale = :de
    when 'com'
      I18n.locale = :en
    else
      I18n.locale = I18n.default_locale
    end 
  end
end

Tip

To test this functionality, you can add the following items on your Linux or Mac OS X development system in the file /etc/hosts:
0.0.0.0 www.example.com
0.0.0.0 www.example.de
Then you can go to the URL http://www.example.com:3000 and http://www.example.de:3000 and you will see the corresponding language versions.

Which Approach is the Best?

I believe that a combination of the approaches described above will lead to the best result. When I first visit a web page I am happy if I find that the accept language HTTP header of my browser is read and implemented correctly. But it is also nice to be able to change the language later on in the user configuration (in particular for badly translated pages, English language is often better). And ultimately it has to be said that a page that is easy to represent is worth a lot for a search engine, and this also goes for the languages. Rails gives you the option of easily using all variations and even enables you to combine them together.

Multilingual Scaffolds

As an example, we use a mini webshop in which we translate a product scaffold. The aim is to make the application available in German and English. The Rails application:
$ rails new webshop
  [...]
$ cd webshop
$ rails generate scaffold Product name description 'price:decimal{7,2}'
  [...]
$ rake db:migrate
  [...]
$ 
We define the product model in the app/models/product.rb
class Product < ActiveRecord::Base
  attr_accessible :description, :name, :price

  validates :name,
            :presence => true,
            :uniqueness => true,
            :length => { :within => 2..255 }

  validates :price,
            :presence => true,
            :numericality => { :greater_than => 0 }
end
When selecting the language for the user, we use the URL prefix variation described in the section called “Setting I18n.locale via URL Path Prefix”. We use the following app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :set_locale
 
  private
  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
    Rails.application.routes.default_url_options[:locale]= I18n.locale
  end
end
This is the config/routes.rb
Webshop::Application.routes.draw do
  scope "(:locale)", :locale => /en|de/ do
    root :to => 'products#index'
    resources :products
  end
end
Then we insert the links for the navigation in the app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html>
<head>
  <title>Webshop</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<p>
<%= link_to_unless I18n.locale == :en, "English", locale: :en %>
|
<%= link_to_unless I18n.locale == :de, "Deutsch", locale: :de %>
</p>

<%= yield %>

</body>
</html>
And before we start Rails with rails server, we delete the file public/index.html so the root page displayed shows the list of all products.
$ rm public/index.html 
$ rails server
=> Booting WEBrick
=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-06-01 10:43:06] INFO  WEBrick 1.3.1
[2012-06-01 10:43:06] INFO  ruby 1.9.3 (2012-04-20) [x86_64-darwin11.3.0]
[2012-06-01 10:43:06] INFO  WEBrick::HTTPServer#start: pid=51412 port=3000
If we go to http://0.0.0.0:3000 we see the normal English page.
Basic version
If we click the option German, nothing changes on the page, apart from the language navigation right at the top.
Basic version de
For both languages, the file app/views/products/index.html.erb is rendered. It contains the following code:
<h1>Listing products</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Description</th>
    <th>Price</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @products.each do |product| %>
  <tr>
    <td><%= product.name %></td>
    <td><%= product.description %></td>
    <td><%= product.price %></td>
    <td><%= link_to 'Show', product %></td>
    <td><%= link_to 'Edit', edit_product_path(product) %></td>
    <td><%= link_to 'Destroy', product, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New Product', new_product_path %>
Now we still need to find a way of translating the individual elements of this page appropriately and as generically as possible.

Text Blocks in YAML Format

Now we need to define the individual text blocks for I18n.t. The corresponding directories still have to be created first:
$ mkdir -p config/locales/models/product
$ mkdir -p config/locales/views/product
$
To make sure that the YAML files created there are indeed read in automatically, you need to insert the following lines in the file config/application.rb:
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', 'models', '*', '*.yml').to_s]
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', 'views', '*', '*.yml').to_s]
config.i18n.default_locale = :en
German
Please create the file config/locales/models/product/de.yml with the following content.
# ruby encoding: utf-8

de:
  activerecord:
    models:
      product: 'Produkt'
    attributes:
      product:
        name: 'Name'
        description: 'Beschreibung'
        price: 'Preis'
In the file config/locales/views/product/de.yml we insert a few values for the scaffold views:
# ruby encoding: utf-8

de:
  views:
    show: Anzeigen
    edit: Editieren
    destroy: Löschen
    are_you_sure: Sind Sie sicher?
    back: Zurück
    edit: Editieren
    product:
      index:
        title: Liste aller Produkte
        new_product: Neues Produkt
      edit:
        title: Produkt editieren
      new:
        title: Neues Produkt
      flash_messages:
        product_was_successfully_created: 'Das Produkt wurde erfolgreich angelegt.'
        product_was_successfully_updated: 'Das Produkt wurde erfolgreich aktualisiert.'
Finally, we copy a ready-made default translation by Sven Fuchs from his github repository https://github.com/svenfuchs/rails-i18n:
$ cd config/locales
$ curl -O https://raw.github.com/svenfuchs/rails-i18n/master/rails/locale/de.yml
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  4940  100  4940    0     0   9574      0 --:--:-- --:--:-- --:--:-- 11932
$ cd ../..
$ 

Note

If you know how Bundler works you can also insert the line gem 'rails-i18n' into the file Gemfile and then execute bundle. This gives you all language files from the repository.
The file config/locales/de.yml contains all required formats and generic phrases for German that we need for a normal Rails application (for example days of the week, currency symbols, etc). Use your favorite editor to have a look in there to get an impression.
English
As most things are already present in the system for English, we just need to insert a few values for the scaffold views in the file config/locales/views/product/en.yml:
en:
  views:
    show: Show
    edit: Edit
    destroy: Delete
    are_you_sure: Are you sure?
    back: Back
    edit: Edit
    product:
      index:
        title: List of all products
        new_product: New product
      edit:
        title: Edit Product
      new:
        title: New product
      flash_messages:
        product_was_successfully_created: 'Product was successfully created.'
        product_was_successfully_updated: 'Product was successfully updated.'

Equipping Views with I18n.t

Please edit the listed view files as specified.
_form.html.erb
In the file app/views/products/_form.html.erb we need to change the display of the validation errors in the top section to I18n.t. The names of form errors are automatically read in from activerecord.attributes.product:
<%= form_for(@product) do |f| %>
  <% if @product.errors.any? %>
    <div id="error_explanation">
      <h2><%= t 'activerecord.errors.template.header', :model => Product.model_name.human, :count => @product.errors.count %></h2>

      <ul>
      <% @product.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :description %><br />
    <%= f.text_field :description %>
  </div>
  <div class="field">
    <%= f.label :price %><br />
    <%= f.text_field :price %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
edit.html.erb
In the file app/views/products/edit.html.erb we need to integrate the heading and the links at the bottom of the page with I18n.t:
<h1><%= t 'views.product.edit.title' %></h1>

<%= render 'form' %>

<%= link_to I18n.t('views.show'), @product %> |
<%= link_to I18n.t('views.back'), products_path %>
index.html.erb
In the file app/views/products/index.html.erb we need to change practically every line. In the table header I use human_attribute_name(), but you could also do it directly with I18n.t. The price of the product is specified with the helper number_to_currency. In a real application, we would have to specify a defined currency at this point as well.
<h1><%= t 'views.product.index.listing_products' %></h1>

<table>
  <tr>
    <th><%= Product.human_attribute_name(:name) %></th>
    <th><%= Product.human_attribute_name(:description) %></th>
    <th><%= Product.human_attribute_name(:price) %></th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @products.each do |product| %>
  <tr>
    <td><%= product.name %></td>
    <td><%= product.description %></td>
    <td><%= number_to_currency(product.price) %></td>
    <td><%= link_to I18n.t('views.show'), product %></td>
    <td><%= link_to I18n.t('views.edit'), edit_product_path(product) %></td>
    <td><%= link_to I18n.t('views.destroy'), product, confirm: I18n.t('views.are_you_sure'), method: :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to I18n.t('views.product.index.new_product'), new_product_path %>
new.html.erb
In the app/views/products/new.html.erb we need to adapt the heading and the link:
<h1><%= t 'views.product.new.title' %></h1>

<%= render 'form' %>

<%= link_to I18n.t('views.back'), products_path %>
show.html.erb
In the app/views/products/show.html.erb we again use human_attribute_name() for the attributes. Plus the links need to be translated with I18n.t. As with the index view, we again use number_to_currency() to show the price in formatted form:
<p id="notice"><%= notice %></p>

<p>
  <b><%= Product.human_attribute_name(:name) %>:</b>
  <%= @product.name %>
</p>

<p>
  <b><%= Product.human_attribute_name(:description) %>:</b>
  <%= @product.description %>
</p>

<p>
  <b><%= Product.human_attribute_name(:price) %>:</b>
  <%= number_to_currency(@product.price) %>
</p>

<%= link_to I18n.t('views.edit'), edit_product_path(@product) %> |
<%= link_to I18n.t('views.back'), products_path %>

Translating Flash Messages in the Controller

Finally, we need to translate the two flash messages in the app/controllers/products_controller.rb for creating (create) and updating (update) records, again via I18n.t:
class ProductsController < ApplicationController
  # GET /products
  # GET /products.json
  def index
    @products = Product.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @products }
    end
  end

  # GET /products/1
  # GET /products/1.json
  def show
    @product = Product.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @product }
    end
  end

  # GET /products/new
  # GET /products/new.json
  def new
    @product = Product.new

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @product }
    end
  end

  # GET /products/1/edit
  def edit
    @product = Product.find(params[:id])
  end

  # POST /products
  # POST /products.json
  def create
    @product = Product.new(params[:product])

    respond_to do |format|
      if @product.save
        format.html { redirect_to @product, notice: I18n.t('views.product.flash_messages.product_was_successfully_created') }
        format.json { render json: @product, status: :created, location: @product }
      else
        format.html { render action: "new" }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  # PUT /products/1
  # PUT /products/1.json
  def update
    @product = Product.find(params[:id])

    respond_to do |format|
      if @product.update_attributes(params[:product])
        format.html { redirect_to @product, notice: I18n.t('views.product.flash_messages.product_was_successfully_updated') }
        format.json { head :no_content }
      else
        format.html { render action: "edit" }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /products/1
  # DELETE /products/1.json
  def destroy
    @product = Product.find(params[:id])
    @product.destroy

    respond_to do |format|
      format.html { redirect_to products_url }
      format.json { head :no_content }
    end
  end
end

The Result

Now you can use the scaffold products both in German and in English. You can switch the language via the link at the top of the page.

Updates about this book will be published on my Twitter feed.