How to make your Rails Application Multilingual

Rails gem approach, MySQL approach and a Multilingual Rails structure

Most of the world doesn’t speak English. That’s where internationalization and localization come in. Rails has a great i18n API.
Reference: http://guides.rubyonrails.org/i18n.html

It provides an easy-to-use and extensible framework for translating your application to a single custom language other than English or for providing multi-language support in our application.

I18n API:
The most important methods of the I18n API are:
translate # Lookup text translations
localize # Localize Date and Time objects to local formats

These have the aliases #t and #l so you can use them like this:

I18n.t 'app.title'
I18n.l Time.now

Rails-i18n gem:
Installation:
Add to the Gemfile:

gem 'rails-i18n', github: 'svenfuchs/rails-i18n', branch: 'master' # For 5.x

Configuration:
By default rails-i18n loads all locale files, pluralization and transliteration rules available in the gem. This behaviour can be changed, if we specify in config/environments/* the locales which have to be loaded via I18n.available_locales option:

config.i18n.available_locales = ['es-CO', :de]

or

config.i18n.available_locales = :nl

We can use another gem also:

Globalize gem:
Rails I18n de-facto standard library for ActiveRecord model/data translation. Globalize builds on the I18n API in Ruby on Rails to add model translations to ActiveRecord models.

Installation:
When using bundler put this in our Gemfile:

gem 'globalize', '~> 5.0.0'
To use globalize with Rails 5 add this in our Gemfile
gem 'activemodel-serializers-xml'

Documentation: https://github.com/globalize/globalize

DB Design for Multilingual App (English and Arabic):

Approach for supporting 2 or 3 languages:

1. Column Approach:

Create column approach model with language columns;
`title_en` varchar(255) NOT NULL,
`title_ar` varchar(255) NOT NULL,

Now, the way you would query it is also simple enough. We may do it by automatically selecting the right columns according to the chosen language.

Advantages:

  • Simplicity – easy to implement
  • Easy querying – no JOINs required
  • No duplicates – doesn’t have duplicate content

Disadvantages:

  • Hard to maintain – works in easy way for 2-3 languages, but it becomes a really hard when you have a lot of columns or a lot of languages
  • Hard to add a new language

2. Translation table Approach:
Having a related translation table for each translatable table.

CREATE TABLE PRODUCT (id int, PRICE NUMBER(18, 2))
CREATE TABLE PRODUCT_tr (id int, product_id INT FK, languagecode varchar, name text, description text)

This way if we have multiple translatable column it would only require a single join to get it and it may be easier to import items together with their related translations.

  • Doesn’t require altering the database schema for new languages (and thus limiting code changes)
  • Doesn’t require a lot of space for unimplemented languages or translations of a particular item
    Provides the most flexibility
  • We don’t end up with sparse tables
  • We don’t have to worry about null keys and checking that we’re displaying an existing translation instead of some null entry.

Multilingual App with Rails-5 (English and Arabic):
The approach for multilingual Rails applications is very similar to the monolingual. Here 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.

Setting I18n.locale via URL Path Prefix
We want http://0.0.0.0:3000/ar to display the Arabic version and http://0.0.0.0:3000/en the English version.

config/routes.rb

Myapp::Application.routes.draw do
 scope "(:locale)", :locale => /en|ar/ do
  root :to => 'page#index'
  get "page/index"
 end
end

Set a before_filter in the 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
 end
end

To test it, Go to the URL;

http://0.0.0.0:3000/ar

Or

http://0.0.0.0:3000/ar/page/index

Setting a default language:

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, We do not need to do anything else. All links generated via the scaffold generator are automatically changed accordingly.

Setting I18n.locale via Accept Language HTTP Header of Browser:

app/controllers/application_controller.rbclass 
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

Here we can change clean our routes file:

config/routes.rb: 
Myapp::Application.routes.draw do 
 get "page/index" 
 root :to => 'page#index'
end

Storing I18n.locale in Session

app/controllers/set_mylanguage_controller.rb:
class SetMylanguageController < ApplicationController
 #English
 def en
  I18n.locale = :en
  set_session_and_redirect
 end

 #Arabic
 def ar
  I18n.locale = :ar
  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

Change in application controller:

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

For setting Arabic:

http://0.0.0.0:3000/set_mylanguage/ar

For setting English:

http://0.0.0.0:3000/set_mylanguage/en

Text Blocks in YAML Format:

Create the below directories first:

$ mkdir -p config/locales/models/item
$ mkdir -p config/locales/views/item

Insert the following lines into 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

We will have to create a file here; config/locales/models/item
config/locales/models/item/ar.yml for Arabic
and make sure that en.yml is present for English.

English YAML:
As most things are already present in the system for English
Insert the below into en.yml file.

# ruby encoding: utf-8
en:
 views:
  show: Show
  edit: Edit
  destroy: Delete
  are_you_sure: Are you sure?
  back: Back
  item:
   index:
     title: List of all items
   new:
     title: New Item
   flash_messages:
     item_was_successfully_created: ‘Item was successfully created.'

Arabic YAML:
We will have to insert values/texts in Arabic into ar.yml like in en.yml
Even we can 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/ar.yml

Translating in View:
We can use human_attribute_name() for the attributes and the links need to be translated with I18n.t. We can use number_to_currency() to show the amount/price in formatted form:

Examples:

 

<% = t ‘views.item.index.listing_items’ %>


<% = link_to I18n.t('views.show'),@item %>
<%= Item.human_attribute_name(:name) %><%=Item.human_attribute_name(:description) %> 

Translating Flash Messages in the Controller

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

For other ready-made language translations:
https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale

LUBAIB CEEJEY
Sr. Ruby on Rails Developer