About this entry




View Translation: Globalizing a Ruby on Rails Application by Adding Multilingual Support to the Views

Using the globalize plug-in for Ruby on Rails out-of-the-box makes it easy to add support for various languages. It can handle the management of translated terms and translation of views (displaying the messages on each page in the selected language). This article gives a brief introduction about to how to implement view translation with globalize in an actual application.

Liked it? !

Index

  1. Everybody on the Same Page
  2. Configuring Globalize
  3. Initial Configuration of Languages
  4. How View Translations Work
  5. Automating the Translation of Messages
  6. Allowing the User to Switch between Languages
  7. Trying it Out
  8. Related Articles
  9. Coming Soon

 

Everybody on the Same Page

In order to show how to provide multilingual support to a Ruby on Rails application, I will illustrate the steps by implementing globalize in our basic RESTful social_graph application. Please give it a quick glimpse before continuing with this article, if you haven't done so already.

Configuring Globalize

The first step is to install the globalize plug-in. I general it is a good idea to install plugins as svn externals, so that you can easily update them later:

cd social_graph
ruby script/plugin install -x svn://svn.globalize-rails.org/globalize/branches/for-1.2

I suggest you rename the recently created folder social_graph/vendor/plugins/for-1.2 to social_graph/vendor/plugins/globalize. Doing this with svn externals is a bit tricky. First you need to redirect the svn:externals for vendor/plugins/:

svn pe svn:externals vendor/plugins/

Then make sure the line with the globalize url matches globalize:

globalize  http://svn.globalize-rails.org/svn/globalize/branches/for-1.2

If the command fails with the error message "svn: None of the environment variables SVN_EDITOR, VISUAL or EDITOR is set, and no 'editor-cmd' run-time configuration option was found," its because you haven't set up your external editor. Set it and then rerun the previous command. For instance, to set up textmate as your external editor do

export SVN_EDITOR='mate -w'

When you update the svn repository, it will create the globalize folder:

svn update

Delete the original for-1.2 created by the plugin install command:

rm -fR vendor/plugins/for-1.2

And if you have your code in an svn repository, commit the changes:

svn ci vendor/plugins

To build the globalize tables on your database, run the following setup command:

rake globalize:setup

This creates the tables within the development database. To create the tables to the production database run the same command adding RAILS_ENV=production at the end.

There is a bug with the version of globalize of the time this document was written. There is a field that is defined incorrectly in one of the tables. It is the built_in field in the globalize_translations. It should be true for translations already included and false for added traslations. Unfortunately it's default value is true, so all added translations incorrectly are marked as "built-in". To correct this we will create a migration:

ruby script/generate migration correct_globalize_built_in_field

Now update the newly created migration social_graph/db/migrate/003_correct_globalize_built_in_field.rb to

class CorrectGlobalizeBuiltInField < ActiveRecord::Migration
def self.up
change_column :globalize_translations, :built_in, :boolean, :default => false
end

def self.down
change_column :globalize_translations, :built_in, :boolean, :default => true
end
end

Finally, run the migration:

rake db:migrate

Initial Configuration of Languages

Let's start by hard-coding the supported languages and the chosen language. In this example I will start with two languages: English and Spanish. English will be the base language, meaning that all messages will first be written in English and then translated into Spanish. Spanish will be the default language for the application. For now this will be hard-coded into the initialization code, but we will change that later. The best place to add this code is within the initializers., where the application's configuration goes. Create a new file config/initializers/globalize.rb with the following content:

LANGUAGES = {'en' => {:locale => 'eng-US', :title => 'English'}, 'es' => {:locale => 'spa-GT', :title => 'Spanish'}}
ACCEPTED_LANGUAGES = LANGUAGES.collect{|l| l[0]}.freeze
locale = LANGUAGES['en'][:locale]
Globalize::Locale.set_base_language locale
Globalize::Locale.set LANGUAGES['es'][:locale]

Now make sure you specify the encoding for the database connection to unicode. This will ensure that characters such as accents, umlauts, and tildes get stored correctly in the database. To do this, add the line:

encoding: utf8

to each environment in social_graph/config/database.yml. For instance, the development database configuration will end up looking something like this:

development:
adapter: mysql
database: social_graph_development
username: rubyuser
password: rubydude
host: localhost
encoding: utf8

How View Translations Work

View translations means translating the messages within the views into the appropriate language.

In order to translate a message, all you have to do is call the t (short for translate) method for the string containing the message (globalize extended the String class to include this method). Really! It's that simple. So in the view, want to display the message 'create' in the selected language? Simply output 'create'.t. So the now globalized social_graph/app/views/acquaintances/new.rhtml view will look like this:

<h1><%= 'New acquaintance'.t %></h1>
<%= error_messages_for :acquaintance %>
<% form_for(:acquaintance, :url => acquaintances_path) do |f| %>
<p>
<b><%= 'Name'.t %></b><br />
<%= f.text_field :name %>
</p>
<p>
<b><%= 'Relation'.t %></b><br />
<%= f.select('relation_id', @relations.collect {|r| [ h(r.title), r.id ] }, { :include_blank => false }) %> (<%= link_to 'New relation'.t, new_relation_path, { :target => '_blank' } %>)
</p>
<p>
<b><%= 'Description'.t %></b><br />
<%= f.text_area :description %>
</p>
<p>
<%= submit_tag 'Create'.t %>
</p>
<% end %>
<%= link_to 'Back'.t, acquaintances_path %>

All I did was to turn all messages into strings and display the string returned by the t method. But wait a minute! If I test this page by opening the following URL in the browser:

http://localhost:3000/acquaintances/new

I still see everything in English! That's because we haven't translated the messages yet. Creating an automated way to translate the messages is the next step.

Automating the Translation of Messages

Next I will show you how to create a controller with its views to provide a user-friendly way of translating the messages. For this, we will create a translations controller (based on http://www.globalize-rails.org/globalize/show/Adding+a+Translation+View, but with some changes). Start by generating the controller with a single action index:

ruby script/generate controller translations index 

The simplest way to design the translation view is to have a single page (index.rhtml) that displays all messages that have not yet been translated into the selected language and provide in-line text editing to the appropriate translation. The index action for the translations controller (social_graph/app/controllers/translations_controller.rb) needs to send the list of untranslated messages to the view. It will look like this:

def index
@translations = Globalize::ViewTranslation.find(:all, :conditions => {:text => nil, :language_id => Globalize::Locale.language.id}, :order => 'tr_key')
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @translations.to_xml }
end
end

The index view will display a table with the message and as an in-line text editor the translation of the messages:

<%= javascript_include_tag :defaults %> 
<% language = Globalize::Locale.language %>
<h1><%= "#{'Language'.t}: #{language.english_name.t}" %></h1>
<table border="1">
<tr>
<th>id</th>
<th>key</th>
<th>qty</th>
<th>translation</th>
</tr>
<% for @translation in @translations do %>
<tr>
<td><%= @translation.id %></td>
<td><%= @translation.tr_key %></td>
<td><%= @translation.pluralization_index %></td>
<td>
<span id="tr_<%= @translation.id %>">
<%= render :partial => 'show' %>
</span>
</td>
</tr>
<%= in_place_editor "tr_#{@translation.id}", :url => {:action => 'set_translation', :id => @translation}, :load_text_url => {:action => 'get_translation', :id => @translation} %>
<% end %>
</table>

Since it relies on the in_place_editor, which used to ship within Rails but from version 2.0 it was extracted as a plugin, you need to install it running this command:

ruby script/plugin install in_place_editing

The above code uses three elements we are yet to implement. First, it relies on a partial called show to display the message. Create a file called social_graph/app/views/translations/_show.rhtml with the following content:

<% if @translation.text.blank? %>
<%= "[#{'no translation'.t}]" %>
<% else %>
<%= @translation.text %>
<% end %>

For the next two elements, notice that the in_place_editor call relies on two additional actions within the translations_controller: set_translation and get_translation. With these implemented, social_graph/app/controllers/translations_controller.rb will end up looking like this:

class TranslationsController < ApplicationController
def index
@translations = Globalize::ViewTranslation.find(:all, :conditions => {:text => nil, :built_in => false, :language_id => Globalize::Locale.language.id}, :order => 'tr_key')
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @translations.to_xml }
end
end

def get_translation
@translation = Globalize::ViewTranslation.find(params[:id])
render :partial => 'show'
end

def set_translation
@translation = Globalize::ViewTranslation.find(params[:id])
previous = @translation.text
@translation.text = params[:value]
@translation.text = previous unless @translation.save
render :partial => 'show'
end
end

If you rather have the translations main page display all messages instead of only the untranslated messages, in the first line of the index action simply get rid of the :text => nil condition. It look like this:

@translations = Globalize::ViewTranslation.find(:all, :conditions => {:built_in => false, :language_id => Globalize::Locale.language.id}, :order => 'tr_key')

Why aren't we using resource-based routing for the translations_controller? Instead of using get_translation we could have called a show action through GET translations/1 and instead of using set_translation we could have called an update action called by PUT translation/1, right? Wrong. There is no way to send to the :url and :load_text_url options in the in_place_editor the methods 'get' and 'put'; those AJAX calls are invariably made through the method 'post' which doesn't map to anything when there is an id (remember that in resource-based routing, POST /translations would map to the create action; POST /translation/1 is not mapped to anything).

For messages to actually come up in the table, they must have been called at least once, so make sure to try this URL in the browser first:

http://localhost:3000/acquaintances/new

and then this one:

http://localhost:3000/translations

Just in case you don't know Spanish, here is the table of messages used so far and their corresponding Spanish translations:

key translation
Back Regresar
Create Crear
Description Descripción
Language Lenguaje
Name Nombre
New acquaintance Nuevo conocido
New relation Nueva relación
no translation sin traducción
Relation Relación
Spanish Español

Having added these messages, try reloading

http://localhost:3000/acquaintances/new

and voila! You have a page displaying fully in Spanish.

Allowing the User to Switch between Languages

Hard-coding the selected language was just a temporary solution to quickly taste the globalize plugin in action. Now we need to provide the user a way where he can select the language of his choice. There are two basic strategies to "remember" the user's language of choice:

  1. We could modify the routing so that the language code becomes part of the URL.
  2. We could store the language as part of the session.

The first solution is the cleanest because it provides a stateless way for the server to "know" the language. Nevertheless, since making the necessary changes to the routing compatible with resource-based routing is quite a challenge, I will opt for the second strategy in this article.

First we need to re-enable sessions, if you had followed the previous tutorial that disabled sessions. Within social_graph/app/controllers/application.rb replace this line:

session :disabled => true

with this one:

session :session_key => '_social_graph_session_id'

Let's start by getting rid of the hard-wired selection of Spanish by deleting the last line of the apps configuration in social_graph/config/environment.rb. The last paragraph will then only be:

# Include your application configuration below
LANGUAGES = {'en' => {:locale => 'eng-US', :title => 'English'}, 'es' => {:locale => 'spa-GT', :title => 'Spanish'}}
ACCEPTED_LANGUAGES = LANGUAGES.collect{|l| l[0]}.freeze
locale = LANGUAGES['en'][:locale]
Globalize::Locale.set_base_language locale

The new default will be the default language of the user's browser / operating system if it is included within the accepted languages; if it isn't we'll just take the base language as the default language for that particular user. When the default language is determined, it will be stored in the session. This is done within social_graph/app/controllers/application.rb, which will look like this:

class ApplicationController < ActionController::Base
# Pick a unique cookie name to distinguish our session data from others'
session :session_key => '_social_graph_session_id'

before_filter :set_locale

def set_locale
language = session[:language]
if language.nil? || !ACCEPTED_LANGUAGES.include?(language)
language = (request.env["HTTP_ACCEPT_LANGUAGE"] || "").scan(/[^,;]+/).collect {|l| l[0..1]}.find{|l| ACCEPTED_LOCALES.include?(l)}
language = Globalize::Locale.base_language.code if language.nil? || !ACCEPTED_LOCALES.include?(language)
session[:language] = language
end
Globalize::Locale.set LANGUAGES[language][:locale]
end
end

The above before_filter statement will call set_locale before an action is rendered.

The user should be able to change the default language. Let's create a controller called main which will include the behavior of changing the language:

ruby script/generate controller main

This controller, social_graph/app/controllers/main_controller.rb, will hold for now only one action: change_language:

class MainController < ApplicationController
def change_language
session[:language] = params[:id] unless params[:id].blank?
redirect_to :back
end
end

It will receive the language code as the ID and will register it in the session. Notice the redirect_to :back line which will refresh the current page to the new language.

The call to change_language will be made from the application's layout, social_graph/app/views/layouts/application.rhtml, where we'll display links to all available languages which will be available from all views:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title><%= "#{controller.controller_name.capitalize}: #{controller.action_name}" %></title>
<%= stylesheet_link_tag 'scaffold' %>
</head>
<body>
<% code = Globalize::Locale.language.code
languages = []
lang_title = String.new
for language in ACCEPTED_LANGUAGES
Globalize::Locale.switch_locale(LANGUAGES[language][:locale]) {lang_title = LANGUAGES[language][:title].t}
if language==code
languages << "<i>#{lang_title}</i>"
else
languages << link_to(lang_title, :controller => 'main', :action => 'change_language', :id => language)
end
end %>
<p align="right"><%= languages.join(' | ') %></p>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>

Trying it Out

Now try it out loading the following URL:

http://localhost:3000/acquaintances/new

and switching between English and Spanish. You have just made your application enter whole new markets. Congratulations!

Related Articles

  • Adding multilingual support to the models, i.e. displaying the content coming from the database in the selected language.

Coming Soon

  • Support for languages using complex scripts, such as Chinese, Tibetan, and Dzongkha. Globalize is great, but it falls short when the languages you want to support require specific fonts and font sizes or use Unicode ranges that may not be interpreted correctly by the browser. In this article I will show how to extend globalize to easily support these special cases.

Liked it? !

Posted on November 15th | 0 comments | Filed Under: Ruby on Rails