Example application

NOTE
If you want the latest API reference for globalize plugin, run rake doc:plugins to generate rdoc documentation. You can find it later in app/doc/plugins/globalize directory.

NOTE
You need to apply some changes below to make it work with Oracle.

2. Creating rails application

Firstly create a very simple rails app (just one table) to work with.

rails globalize

This should create a skeleton of your application.

Now you need to create a database to work with, i.e. “globalize_development” and configure config/database.yml file correctly.

Next create the only model:

script/generate model Product

This will create also a migrate file db/migrate/001_create_products.rb, which you can use to create products table.

class CreateProducts < ActiveRecord::Migration
  def self.up
    create_table :products do |t|
      t.column :name, :string
      t.column :company, :string
      t.column :description, :text
      t.column :price, :integer, :default => 0
      t.column :created_on, :date
      t.column :updated_on, :date
    end
  end
  def self.down
    drop_table :products
  end
end

Run
rake migrate

and if there were no errors you should have new products table in your database.

Now you can add some functionality to your app:


script/generate scaffold admin/product
script/generate controller product

The first line will generate the whole admin panel for your products table and the second one will add a controller, which will be used by “user” part of the app.

Now it’s time for some cosmetic changes:

  • remove created_on and updated_on input elements from app/views/admin/products/_form.rhtml partial – you don’t need them as they these fields are updated automatically
  • copy index, list and show actions from app/controllers/admin/products_controller.rb to app/controllers/product_controller.rb
  • copy list.rhtml and show.rhtml files from app/views/admin/products/ to app/views/product/
  • remove edit, destroy and new functionality from list.rhtml and show.rhtml files you’ve just copied
  • delete or rename public/index.html
  • add layouts for admin and user parts

Next add routing rules for user and admin parts. Add these 2 lines to your config/routes.rb file:


# You can have the root of your site routed by hooking up '' 
# -- just remember to delete public/index.html.
map.connect '', :controller => 'product', :action => 'index'
map.connect 'admin', :controller => 'admin/products', :action => 'index'

Now you’ve got your own basic rails app with full CRUD functionality for admin and read functionality for user.

3. Installing globalize plugin

To install the plugin just type:
script/plugin install svn://svn.globalize-rails.org/globalize/branches/for-1.1

Now go to vendor/plugins and rename for-1.1 directory to globalize.

Now run:

rake globalize:setup

and you’ve just finished installing globalize!

NOTE
You can run rake -T to see all available rake tasks – including these added by globalize.

WARNING
In current revision (197) there’s a problem with built_in column in globalize_translation table, which causes all translations added by the user to be marked as built in.
I’m not sure if it’s just problem with my configuration, but to check if you also have this problem, type this in the console:


include Globalize
Locale.set "en-US" 
Locale.set_translation("foo", "bar")
ViewTranslation.find(:first, :conditions => "tr_key = 'foo'").built_in?

If you get false, then your translation is not being set as built-in and everything is ok.
If not, close the console and open the file vendor/plugins/globalize/tasks/data.rake and change line #63:
t.column :built_in, :boolean, :default => true
to
t.column :built_in, :boolean#, :default => true
Now run

rake globalize:teardown
rake globalize:setup

Open the console and set all fields in globalize_translation table as built_in

include Globalize
ViewTranslation.update_all "built_in = true" 

Now it should work correctly: all translations that already exist in the table should be marked as built-in and all new translations added by you won’t be anymore.
Note that due to the unorthodox way Globalize handles the database (without migrations), this change won’t be reflected in schema.rb.

4. Making your application globalized

4.1 Configuration

First you need to set the base language. Add these lines to config/environment.rb file:


# Include your application configuration below
include Globalize
Locale.set_base_language 'en-US'
LOCALES = {'pl' => 'pl-PL',
           'en' => 'en-US',
           'es' => 'es-ES',
           'fr' => 'fr-FR'}.freeze

Of course you can select any language as your base language. In LOCALES hash we will store all available locales for this app. Remember to restart the server after you made changes in environment.rb file.

NOTE
It’s important to never change the base language once you’ve started populating the database.

You can pass the information about the language, which should be displayed, in the session variable or in the url. Here just the second method is presented, as it allows you i.e. to bookmark the page in selected language etc.

Now you need to modify your config/routes.rb file a bit. Add this line before the lines you added before:


map.connect ':locale/:controller/:action/:id'

You also need to set the current language somewhere. Edit your app/controllers/application.rb file:


class ApplicationController < ActionController::Base
  before_filter :set_locale
  def set_locale
    if !params[:locale].nil? && LOCALES.keys.include?(params[:locale])
      Locale.set LOCALES[params[:locale]]
    else
      redirect_to params.merge( 'locale' => Locale.base_language.code )
    end
  end
end

Try to display your main page again – you should see “en” in the url before the controller name.

Now just add possibility to switch between languages and move to the next chapter.

To switch between languages you can use i.e. following link:


<%= link_to "pl", {:controller => controller.controller_name, :action => controller.action_name, :locale => 'pl', :id => params[:id]} %>

This will display current page in selected language, but it will only work with the default route.

NOTE
You could iterate through LOCALE hash to create a link for every supported language or create select input field.

NOTE by Marc-André Lafortune
I prefer to use the preferences set in the user’s client to determine the default locale. The user can then decide to override it by clicking on a button you provide, in which case I want to store that in a cookie so that preference is remembered forever. Here’s my filter:


# Will set the cookie 'locale' if (and only if) an explicit parameter 'locale'
# is passed (and is acceptable)
# If no cookie exists, we look through the list of desired languages for the
# first one we can accept.
#  
  def set_locale
    accept_locales = LOCALES.keys # change this line as needed, must be an array of strings
    cookies[:locale] = params[:locale] if accept_locales.include?(params[:locale])
    Locale.set(cookies[:locale] || (request.env["HTTP_ACCEPT_LANGUAGE"] || "").scan(/[^,;]+/).find{|l| accept_locales.include?(l)})
  end

4.2 Translating static content.

First you need to generate translate controller.


script/generate controller admin/translate

Now edit your new controller:

class Admin::TranslateController < ApplicationController
  def index    
    @view_translations = ViewTranslation.find(:all, :conditions => [ 'built_in IS NULL AND language_id = ?', Locale.language.id ], :order => 'text')
  end
  def translation_text
    @translation = ViewTranslation.find(params[:id])
    render :text => @translation.text || ""  
  end
  def set_translation_text
    @translation = ViewTranslation.find(params[:id])
    previous = @translation.text
    @translation.text = params[:value]
    @translation.text = previous unless @translation.save
    render :partial => "translation_text", :object => @translation.text  
  end
end

Let’s focus on the index action. We fetch all non-built-in translations for current locale and order them by text column. This will put all untranslated fields at the beginning of @view_translation array.
You can change the conditions i.e.:

  def index
    @view_translations = ViewTranslation.find(:all, 
      :conditions => [ 'text IS NULL AND language_id = ?', Locale.language.id ], :order => 'tr_key')
  end

This way you’ll get untranslated fields only.
Remember to set admin layout for this controller.

Now create app/views/admin/translate/_translation_text.rhtml partial, which is used in translation_text action.


<%= translation_text || '[no translation]' %>

To translate the static content you can use in_place_editor helper, which is built-in in Rails 1.1. Just remember to add

<%= javascript_include_tag :defaults %>

in the layout file for the admin part (e.g. create an application-wide app/views/layouts/application.rhtml layout – you can copy most of the generated products.rhtml layout there, just change the title and add the javascript tag).

Next create app/views/admin/translate/_translation_form.rhtml partial for the form, which will be used to provide translations.


<!--[form:translate]-->
<p>
<label for="tr_<%= tr.id %>"><%=tr.tr_key%></label>
<br />
<span id="tr_<%= tr.id %>">
<%= render :partial => 'translation_text', :object => tr.text %>
</span>
<%= in_place_editor "tr_#{tr.id}", 
    :url => { :action => :set_translation_text, :id => tr.id },
    :load_text_url => url_for({ :action => :translation_text, :id => tr.id })%>
</p>
<!--[eoform:translate]-->

NOTE
If you want more information about in_place_editor helper look here.

In the next step we will use 2 useful helpers. Add the following code to app/helpers/application_helper.rb file:


  def base_language_only
    yield if Locale.base?
  end
  def not_base_language
    yield unless Locale.base?
  end

NOTE
These helpers come from Jeremy Voorhis’ Canada on Rails slides, which you can find here.

Still in app/views/admin/translate, create index.rhtml and edit it:


<% base_language_only do -%>
<div id="language"><h1>Please choose language for translation</h1></div>
<% end -%>
<% not_base_language do -%>
<div id="language"><h1><%= "Language: " + Locale.language.native_name %></h1></div>
<div>
<% @view_translations.each do |tr| -%>
  <%= render :partial => 'translation_form', :locals => {:tr => tr}%>
<% end -%>
</div>
<% end  -%>

We don’t want to translate english strings to english again, so the translation forms will only be displayed if current language is different than the base one.

Add a new product if you haven’t done this already – go to localhost:3000/admin/, press ‘New product’ and fill out the form.

Open app/views/product/list.rhtml and show.rhtml and add .t method to all strings you want to translate i.e. change 'Show' to 'Show'.t etc. This way you’ll get translated user part of the application.

To add these strings to the database they must be displayed first, so go to localhost:3000/pl/product. Next go to localhost:3000/pl/admin/translate. You should see the list of strings to which you’ve added .t method. Translate them and go to localhost:3000/pl/product again.

NOTE
If you don’t get any strings on localhost:3000/pl/admin/translate page to translate, make sure that:

  • you’re not displaying that page with the base locale
  • you added .t method to the strings
  • you displayed pages, to which you added .t method

NOTE
Here’s useful thread showing how to add untranslated strings automatically: http://rubyforge.org/pipermail/railsi18n-discussion/2006-August/000124.html

4.3 Translating model data.

First you need to decide which fields you want to translate. In our Product model we’ll translate name and description fields.

Add this line to app/models/product.rb:


translates :name, :description

We will allow adding new products only for the base language. Modify app/views/admin/products/list.rhtml file:


<% base_language_only do -%>
<%= link_to 'New product', :action => 'new' %>
<% end -%>

In the same directory create new file _translation_form.rhtml. Copy contents of _form.rhtml partial to this new file, but remove company and price input fields as they won’t be translated. As you may have already figured out this form will be used to provide translations for the Product model.

Now create another file _translation_data.rhtml and put the following code inside:


<div>
  <b>Name:</b> <%=h @product.name %>
  <b>Description:</b> <%=h @product.description %>
</div>

This file will be used as the reference and will show data in the base language.

Now it’s time to modify edit.rhtml file.


<h1>Editing product</h1>
<% base_language_only do -%>
  <%= start_form_tag :action => 'update', :id => @product %>
    <%= render :partial => 'form' %>
    <%= submit_tag 'Edit' %>
  <%= end_form_tag %>
<% end -%>
<% not_base_language do -%>
<div style="float:left;">
  <h2><%= Locale.language.native_name%></h2>
  <%= start_form_tag( {:action => 'update', :id => @product} ) %>
    <%= render :partial => 'translation_form' %>
    <%= submit_tag 'save translation' %>
  <%= end_form_tag %>
</div>
<div style="float:right;">
  <% @product.switch_language(Locale.base_language.code) do -%>
    <h2><%= Locale.base_language.native_name%></h2>
    <%= render :partial => 'translation_data' %>
  <% end -%>
</div>
<% end -%>
<%= link_to 'Show', :action => 'show', :id => @product %> | 
<%= link_to 'Back', :action => 'list' %>

Navigate to localhost:3000/pl/admin/products, find the product you added, and click on Edit.
If the current language is the base one, the full version of the edit form is displayed. If not, the translation form is displayed on the left and the original data on the right, as the reference for the translator.

NOTE
.switch_language method used in this code is provided by globalize_extension plugin written by Olivier Amblet from Liquid Concept, which can be found here. It provides many other useful extensions for globalize plugin. Just install it into your vendor/plugins directory

NOTE
If you’re going to translate many models it would probably be good idea to put the code above into a partial and just pass an object to translate:


render :partial "translation_page", :locals => {:object => @product}

To inform user that the text hasn’t been yet translated you can use the following helper (also from Jeremy Voorhis’ slides mentioned above):


  def translation_availability_for object_name, facet, message = nil
    not_base_language do
      message ||= content_tag 'p', '(Translation not available)'.t
      object = instance_variable_get "@#{object_name}" 
      message if object && !object.send( facet ).blank? &&
      object.send( "#{facet}_is_base?" )
    end
  end

Here’s the example of its usage:

<%= translation_availability_for :product, :description %>
<%=h @product.description %>

4.4 Translating dates

To translate dates you can use localize method or its shorter form loc. It takes format string as the paramater, exactly the same as strftime method.
I.e.


Product.find(:first).created_on.loc("%A %d-%B-%Y")

should give “Wednesday 14-June-2006” for English language and “Środa 14-Czerwiec-2006” for Polish.

countries table has field date_format, which you can use to specify custom format for different languages. Example for Polish language:


country = Country.find(:first, :conditions => "code = 'PL'")
country.update_attribute(:date_format, "%d/%m/%y")

Globalize uses cache to minimize database usage, so sometimes you need to clear it to make changes visible:

Locale.clear_cache
Locale.set "pl-PL" 
Locale.country.date_format

Now you can just use

product.created_on.loc(Locale.country.date_format)

and you’ll get different date format for every language.

In a template use for example:


@product.send(column.name).localize("%d %B %Y") if column.name.to_s == 'created_on' 

4.5 Translating error messages

Globalize provides modified version of error_messages_for helper. Unfortunatelly it doesn’t have translated error messages in the database yet. But you can easily add them by yourself. Here’s how to do it.

—-
I couldn’t find any way to do it on this page, so I came up with my own solution, using the rails console script. Fairly simple:


Locale.set 'pl-PL' # you might want to iterate throught LOCALES.each_value here
ActiveRecord::Errors.default_error_messages.each_value do |error_msg|
  error_msg.translate
end

Benol

and if your base language isn’t English?
—-

4.6 Translating currency

WARNING
Current implementation of Currency class has some bugs. They should be fixed in the next revision.

Globalize provides specialized class to represent money in ActiveRecord models – Globalize::Currency.
To use it you need to modify app/models/product.rb file. Add the following line:

composed_of :price, :class_name => "Globalize::Currency", :mapping => [ %w(price cents) ]

Now product.price will return Globalize:Currency object. To display the actual value you’ve got 2 possibilities:
  • .to_s method
  • .format method, which can take optional parameters: if :code => true is specified, format using international 3-letter currency code; if :country is specified as well, use that country’s currency code for formatting

Text by: szymek (83.15.148.82)

revision 1 · 25.06.08 00:39 · by: Sven