Globalizing Your Rails Application

Problem

Contributed by: Christian Romney

You need to support multiple languages, currencies, or date and time formats in your Rails application. Essentially, you want to support internationalization (or i18n).

Solution

The Globalize plug-in provides most of the tools you'll need to prepare your application for the world stage. For this recipe, create an empty Rails application called global:

$ rails global

Next, use Subversion to export the code for the plug-in into a folder called globalize under vendor/plugins:

$ svn export \
> http://svn.globalize-rails.org/svn/globalize/globalize/branches/for-1.1\
> vendor/plugins/globalize

If your application uses a database, you'll need to set it up to store international text. MySQL, for example, supports UTF-8 encoding out of the box. Configure your database.yml file as usual, making sure to specify the encoding parameter:

config/database.yml:

development:
 adapter: mysql
 database: global_development
 username: root
 password:
 host: localhost
 encoding: utf8

Globalize uses a few database tables to keep track of translations. Prepare your application's globalization tables by running the following command:

$ rake globalize:setup

Next, add the following lines to your environment:

config/environment.rb:

require 'jcode'
$KCODE = 'u'
include Globalize 
Locale.set_base_language('en-US')

Your application is now capable of globalization. All you need to do is create a model and translate any string data it may contain. To really test Globalize's capabilities, create a Product model complete with a name, unit_price, quantity_on_hand, and updated_at fields. First, generate the model:

$ ruby script/generate model Product

Now define the schema for the product table in the migration file. You also want to include a redundant model definition here in case future migrations rename or remove the Product class.

db/migrate/001_create_products.rb:

class Product < ActiveRecord::Base 
 translates :name 
end class CreateProducts < ActiveRecord::Migration 
 def self.up 
 create_table :products do |t| 
 t.column :name, :string 
 t.column :unit_price, :integer 
 t.column :quantity_on_hand, :integer 
 t.column :updated_at, :datetime 
 end 
 Locale.set('en-US') 
 Product.new do |product| 
 product.name = 'Little Black Book' 
 product.unit_price = 999 
 product.quantity_on_hand = 9999 
 product.save 
 end 
 Locale.set('es-ES') 
 product = Product.find(:first) 
 product.name = 'Pequeño Libro Negro' 
 product.save 
 end 
 def self.down 
 drop_table :products 
 end 
end

Note that you must change the locale before providing a translation for the name. Go ahead, and migrate the database now:

$ rake db:migrate

You might have noticed the unit price is an integer field. Using integers eliminates the precision errors that arise when floats are used for currency (a very, very bad idea). Instead, we store the price in cents. After the migration has completed, modify the real model class to map the price to a locale-aware class included with Globalize. (Note that this doesn't perform currency conversion, which is beyond the scope of this recipe.)

app/models/product.rb:

class Product < ActiveRecord::Base 
 translates :name 
 composed_of :unit_price, :class_name => "Globalize::Currency", 
 :mapping => [ %w(unit_price cents) ] 
end

Now generate a controller to show off your application's new linguistic abilities. Create a Products controller, with a show action:

$ ruby script/generate controller Products show

Modify the controller as follows:

app/controllers/products_controller.rb:

class ProductsController < ApplicationController 
 def show 
 @product = Product.find(params[:id]) 
 end 
end

You can set the locale in a before_filter inside ApplicationController:

app/controllers/application.rb:

class ApplicationController < ActionController::Base
 before_filter :set_locale 
 def set_locale
 headers["Content-Type"] = 'text/html; charset=utf-8' 
 default_locale = Locale.language_code
 request_locale = request.env['HTTP_ACCEPT_LANGUAGE']
 request_locale = request_locale[/[^,;]+/] if request_locale
 @locale = params[:locale] || 
 session[:locale] ||
 request_locale || 
 default_locale
 session[:locale] = @locale
 begin
 Locale.set @locale
 rescue ArgumentError
 @locale = default_locale
 Locale.set @locale
 end
 end end

Note that the Content-Type header is set to use UTF-8 encoding. Lastly, you'll want to modify the view:

app/views/products/show.rhtml:

<h1><%= @product.name.t %></h1> 
<table> 
<tr> 
 <td><strong><%= 'Price'.t %>:</strong></td> 
 <td><%= @product.unit_price %></td> 
</tr> 
<tr> 
 <td><strong><%= 'Quantity'.t %>:</strong></td> 
 <td><%= @product.quantity_on_hand.localize %></td> 
</tr> 
<tr> 
 <td><strong><%= 'Modified'.t %>:</strong></td> 
 <td><%= @product.updated_at.localize("%d %B %Y") %></td> 
</tr> 
</table>

Before you run the application, you must provide translations for the literal strings 'Price', 'Quantity', and 'Modified' found in the template. To do so, fire up the Rails console.

$ ruby script/console

Now enter the following:

>> Locale.set_translation('Price', Language.pick('es-ES'),'Precio') 
>> Locale.set_translation('Quantity', Language.pick('es-ES'),'Cantidad') 
>> Locale.set_translation('Modified', Language.pick('es-ES'),'Modificado')

Your application is ready to be viewed. Start your development server:

$ ruby script/server -d

Assuming your server is running on port 3000, point your browser to to see the English version. To see the Spanish version, point your browser here: .

Discussion

shows how you can specify the locale via a query string parameter. You can also use the standard HTTP Accept-Language header. Explicit parameters take precedence over defaults, and the application can always fall back to 'en-US' if things get scary.

Figure 5-9. A globalized Rails application, displaying content in both English and Spanish

You can also include the locale as a route parameter by modifying routes.rb and replacing the default route.

config/routes.rb:

# Install the default route as the lowest priority. 
map.connect ':locale/:controller/:action/:id'

You then access the Spanish language version product page here at . Globalization takes some effort in any language or framework, and while proper Unicode support is not yet included in Ruby, the Globalize plug-in takes the sting out of the most common localization tasks.

See Also