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 Default 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
  [...]
$ 
We still need to enter it as root page in the config/routes.rb:
Webshop::Application.routes.draw do
  get "page/index"
  root '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>
<strong>I18n.locale:</strong> 
<%= 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:
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>
<strong>I18n.locale:</strong> 
<%= 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
    get "page/index"
    get '/', to: 'page#index'
  end

  root 'page#index'
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
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  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? Obviously there is a clever solution for this problem. We can set global default parameters for URL generation by defining a method called default_url_options in our controller.
So we just need to add this method in app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  before_filter :set_locale

  def default_url_options
    { locale: I18n.locale }
  end
   
  private
  def set_locale
    I18n.locale = params[:locale] || I18n.default_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", "data-turbolinks-track" => true %>
  <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
  <%= 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 Ruby on Rails of this value.
Please edit the app/controllers/application_controller.rb as follows:
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  before_filter :set_locale
   
  private
  def extract_locale_from_accept_language_header
    http_accept_language = request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
    if ['de', 'en'].include? http_accept_language
      http_accept_language
    else
      'en'
    end
  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 '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. Have a look at https://www.ruby-toolbox.com/categories/i18n#http_accept_language to find 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”).

Buy the new Rails 5.1 version of this book.

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
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  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", "data-turbolinks-track" => true %>
  <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
  <%= 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
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  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

Buy the new Rails 5.1 version of this book.

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
  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
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  
  before_filter :set_locale

  def default_url_options
    { locale: I18n.locale }
  end
                  
  private
  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end
end
This is the config/routes.rb
Webshop::Application.routes.draw do
  scope ':locale', locale: /en|de/ do
    resources :products
    get '/', to: 'products#index'
  end

  root 'products#index'
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", "data-turbolinks-track" => true %>
  <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
  <%= 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>
Start the Rails server with rails server.
$ rails server
=> Booting WEBrick
=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
[2013-07-17 22:16:19] INFO  WEBrick 1.3.1
[2013-07-17 22:16:19] INFO  ruby 2.0.0 (2013-06-27) [x86_64-darwin12.4.0]
[2013-07-17 22:16:19] INFO  WEBrick::HTTPServer#start: pid=42806 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
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.
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:
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 ../..
$ 

Buy the new Rails 5.1 version of this book.

If you know how Bundler works you can also insert the line gem 'rails-i18n' into the file Gemfile and then execute bundle install. 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>
  <thead>
    <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>
  </thead>

  <tbody>
    <% @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, method: :delete, data: { confirm: I18n.t('views.are_you_sure')} %></td>
      </tr>
    <% end %>
  </tbody>
</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>
  <strong><%= Product.human_attribute_name(:name) %>:</strong>
  <%= @product.name %>
</p>

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

<p>
  <strong><%= Product.human_attribute_name(:price) %>:</strong>
  <%= 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
  before_action :set_product, only: [:show, :edit, :update, :destroy]

  # GET /products
  # GET /products.json
  def index
    @products = Product.all
  end

  # GET /products/1
  # GET /products/1.json
  def show
  end

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

  # GET /products/1/edit
  def edit
  end

  # POST /products
  # POST /products.json
  def create
    @product = Product.new(product_params)

    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 action: 'show', status: :created, location: @product }
      else
        format.html { render action: 'new' }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /products/1
  # PATCH/PUT /products/1.json
  def update
    respond_to do |format|
      if @product.update(product_params)
        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.destroy
    respond_to do |format|
      format.html { redirect_to products_url }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_product
      @product = Product.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def product_params
      params.require(:product).permit(:name, :description, :price)
    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.

Thank you for your support and the visibility by linking to this website on Twitter and Facebook. That helps a lot!