Ruby-GetText-Package HOWTO for Ruby on Rails

ruby-gettext-howto-rails

[Back]

Note: This tutorial is for Rails-2.3.2 or later and Ruby-GetText-Package-2.0.0 or later

[Older version HOWTO]

This tutorial explains how to use Ruby-GetText-Package on Ruby on Rails-2.3.2 with small blog application which include gettext_rails as the sample(See gettext_rails-x.x.x/sample/ in the gem or github.com.)

Let's start to develop L10n "blog" application

The application's information on this tutorial

  • Appliction name: blog
  • Textdomain name: blog
  • Version: 1.0.0
  • charset: UTF-8 - Recommended.

It has a table the name is "articles":

# db/schema.rb:
ActiveRecord::Schema.define(:version => 1) do
 create_table "articles", :force => true do |t|
   t.string   "title",       :default => "", :null => false
   t.text     "description", :default => "", :null => false
   t.date     "lastupdate"
   t.datetime "created_at"
   t.datetime "updated_at"
 end
end

Let's create the database and generate "blog" application using script/generate.

Install gettext_rails gems

(su)
$ gem install gettext_rails

Edit the files

config/environment.rb

Rails::Initializer.run do |config|
 :
 :
 config.gem "locale"
 config.gem "locale_rails", :version => '2.0.4'
 config.gem "gettext"
 config.gem "gettext_activerecord", :version => '2.0.4'
 config.gem "gettext_rails", :version => '2.0.4'
end

locale_rails, gettext_activerecord and gettext_rails depend on Rails version(because of monkey patching), so it's better to set the version obviously. See 'Support Matrix' section of README.rdoc what rails version is supported.

ApplicationController

app/controller/application_controller.rb:

class ApplicationController < ActionController::Base
  init_gettext "blog"
end

That's all! Now you can find the validation messages are localized.

init_gettext options

You can also set the charset and content_type here.

ActionController::Base.init_gettext(textdomain, options = {})
Bind 'textdomain' to all of the controllers/views/models.
  • textdomain: the textdomain
  • options:
    • :charset - the output charset. IANA character sets are recommanded. This value is used not only outputting the messages in the charset, but also the charset of "Content-Type" in the HTTP Header. Default is "UTF-8".
    • :content_type - the content type. Default is "text/html"
    • :locale_path - the path to locale directory. Default is RAILS_ROOT or plugin root directory.

If you want to separate the textdomain each controllers, you need to call init_gettext in the each controllers.

app/controller/articles_controller.rb:

class ArticlesController < ApplicationController
  init_gettext "blog"
  :
  :
end

Note Ruby-GetText-Package sets the HTTP header's content-type correctly. So you shouldn't set the charset by yourself like as follows:

#Needless!
before_filter :set_charset
def set_charset
  headers['Content-Type'] = "text/html;charset=utf-8"
end

Controllers

Since this section, you can use these functions as you need.

Edit controllers/views using GetText methods. This sample shows the code for a controller.

app/controller/articles_controller.rb:

class ArticlesController < ApplicationController
   :
   :
  def create
    @article = Article.new(params[:article])
    if @article.save
      flash[:notice] = _('Article was successfully created.')  #Here!
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end
   :
   :
end

Views/ActionMailer

Views/ActionMailer are also can use GetText methods such as _(), n_(), N_(), Nn_(). This sample shows the code for a view.

app/views/articles/edit.html.erb:

<h1><%= _('Editing article') %></h1>

<%= error_messages_for :article %>

<% form_for(@article) do |f| %>
  <p>
    <p><%= f.date_select :lastupdate, :use_month_numbers => true %></p>
    <p><%= f.text_field :title %></p>
    <p><%= f.text_area :description %></p>
    <p><%= f.submit _("Edit") %></p>
  </p>
<% end %>
<p>
 <%= link_to _('Show'), @article %> |
 <%= link_to _('Destroy'), @article, :confirm => _('Are you sure?'), :method => :delete %> |
 <%= link_to _('Back'), articles_path %>
</p>

error_messages_for, error_message_on are also localized (you don't need to do anything here).

Localized templates for Views/ActionMailer

ActionController::Base.render :text is overridden to find localized templates such as foo_ja.hml.erb, foo_ja_JP.html.erb.

(e.g.)

/app/views/articles/list.html.erb
/app/views/articles/list_ja.html.erb

In this case, list_ja.html.erb is used when language is "ja" only. Otherwise list.rhml is used. You can use this way for render :partial. Use _foo.html.erb, _foo_ja.html.

Models

All of the table names(class name) and field names are extracted as msgids to the po-file(the format is "ClassName|FieldName", (e.g.) "Article|Title"). So you don't need to do anything. Default validation/error messages are also localized automatically.

If you want to add your own validation messages, you can also use GetText way:

# app/models/article.rb:
class Article < ActiveRecord::Base
  # Simple 
  validates_presence_of :title
  validates_length_of :description, :minimum => 10

  # With messages (Use N_() here)
  validates_presence_of :title, :message => N_("%{attribute} can't be empty!")
  validates_length_of :description, :minimum => 10, :message => N_("%{attribute} is too short (min is %{count} characters)")

  protected
  # Your own validations (Use _() instead of N_()).
  def validate
    unless title =~ /\A[A-Z]+\z/
      errors.add("title", _("%{attribute} is not correct: %{title}") % {:title => title}) 
    end   
  end   
  # Your own validations with the lazy evaluation (Use :default => N_()).
  # Use this if you need to show error messages in 2 or more languages.
  # (gettext_activerecord-2.0.2 or later)
  def validate
    unless title =~ /\A[A-Z]+\z/
      errors.add("title", :default => N_("%{attribute} is not correct: %{value}"), :value => title) 
    end   
  end   
end

%{attribute} means "field name". You can use it freely. If you omit %{attribute}, the field name is precede the message. %d is the number of the message which is used with validates_length_of.

Custom messages in validates_* or set_error_message(title|explanation) should be used GetText.N_(), not GetText._().

%{value} for validators

validates_(format|inclusion|exclusion)_of accepts %{value} as the value.

validates_inclusion_of :name, :in => %w(a, b), :message => N_("%{attribute} can't be %{value}")

Translate the attributes which doesn't exist in your Database

Use N_() like as N_("ClassName|Attribute").

(e.g.1) the User table which doesn't have both of the password and password_confirmation fields.

class User < ActiveRecord::Base
  attr_accessor :password, :password_confirmation

  N_("User|Password")
  N_("User|Password confirmation")

  validates_presence_of :password

  def validate
    errors.add("password", N_("%{attribute} is wrong!")) unless password == password_confirmation
  end
end

(e.g.2) has_one relationships.

class Person < ActiveRecord::Base
  has_one :profile
  validates_associated :profile
  N_('Person|Profile')
end

Untranslation option

You may want to prevent to extract some table names(class names) and field names to the po-file. Because those table names/field names won't appear on HTML.

In this case, you can use untranslate, untranslate_all methods.

class Article < ActiveRecord::Base
  untranslate :title, :description
end

class Article < ActiveRecord::Base
  untranslate_all
end

untranslate(*fields) is for each fields in the Model. untranslate_all is for whole the Model.

STI models

The table/field names of STI models(e.g. Manage < User < ActiveRecord::Base) won't appear the po-file. But to put the word "ActiveRecord::Base" in your model file, the file is handled as ActiveRecord::Base class.

# app/models/manager.rb
class Manager < User   # ActiveRecord::Base   # <- Important!
end

Translate the title/explanation on the error message dialog

You can override your own title/explanation on the top of the error messages.

class ApplicationController < ActionController::Base
  init_gettext ....
  :
  :
  ActionView::Helpers::ActiveRecordHelper::L10n.set_error_message_title(
    N_("An error was found in %{record}"), N_("%{num} errors were found in %{record}"))
  ActionView::Helpers::ActiveRecordHelper::L10n.set_error_message_explanation(
    N_("The error is:"), N_("The errors are:"))
end

You also be able to set message_title/message_explanation as the parameter of error_messages_for. If you want to set these messages in each erb files.

# app/views/users/edit.html.erb
<%= error_messages_for 'user', {
   :message_title => Nn_("Singular Custom Error message %{record}: %{num}", "Plural Custom Error message %{record}: %{num}"),
   :message_explanation => Nn_("Singular Custom Error explanation %{num}", "Plural Custom Error explanation %{num}")
} %>

ActionView::Helpers::FormBuilder#label localization

It's simple but useful ;).

# app/views/article/edit.html.erb
<% form_for(@article) do |f| %>
  <p><%= f.label :title %></p>
<% end %> 
=> <p><label for="article_title">(Translated)Title</label></p>

In this case, "Article|Title"(which is extracted as the msgid of "Article" model) is used as the msgid and the translated string is shown as the label.

Of course, you can use your own localized message with label method.

# app/views/article/edit.html.erb
<% form_for(@article) do |f| %>
  <p><%= f.label :title, _("Foo Bar Title") %></p>
<% end %> 
=> <p><label for="article_title">(Translated)Foo Bar Title</label></p>

Localized Routing

Using ":lang" in config/routes.rb, you can use localized URLs.

# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  # Localized Routing.
  map.connect '/:lang/:controller/:action/:id'
  map.connect '/:lang/:controller/:action/:id.:format'
end

This :lang is used as param[:lang] and it is treated as the highest priority locale.

The URL of this routing is below:

http://www.foobar.com/ja_JP/foos/show
http://www.foobar.com/fr/foos/show

Then, url_for, link_to and foos_path are:

link_to "Click", :controller => :foos, :action => :show, :lang => lang
url_for :controller => :foos, :action => :show, :lang => lang
url = foos_path(:lang => lang)

Rails Engines

See Ruby-GetText-Package HOWTO for Ruby Engines

Rails Plugins

You can localize your Rails Plugins easily even if they do not depend on Rails Engines.

require 'gettext_rails'

module FooPlugin
  include GetText

  bindtextdomain("foo_plugin", :path => File.join(RAILS_ROOT, "vendor/plugins/foo/locale"))
  :
  :
end

In this example, the FooPlugin module binds "foo_plugin" textdomain. And the mo files are in vendor/plugins/foo/locale/#{lang}/LC_MESSGES/foo_plugin.mo.

Rakefile

Create lib/tasks/gettext.rake like as:

desc "Update pot/po files."
task :updatepo do
  require 'gettext_rails/tools'  #HERE!
  GetText.update_pofiles("blog", Dir.glob("{app,lib,bin}/**/*.{rb,erb,rjs}"), "blog 1.0.0")
end

desc "Create mo-files"
task :makemo do
  require 'gettext_rails/tools'  #HERE!
  GetText.create_mofiles
end

See HOWTO maintain po/mo files for more details.

Put "require 'gettext_rails/tools'" line into the task blocks. If you put the line on the top of Rakefile, some other tasks will be failure.

Note to use gettext_rails/tools with rake gems:unpack

Add Rails::GemDependency.add_frozen_gem_path on the top of Rakefile.

the parser options for Models

Usually you don't need to care about this section, but you can also change the behaviour of the rgettext parser for Models.

Add(Change) the "updatepo" task below to Rakefile:

desc "Update pot/po files."
task :updatepo do
  require 'gettext/tools'
  GetText::ActiveRecordParser.init(:use_classname => false, :db_mode => "development")  # default db_mode is development.
  GetText.update_pofiles("blog", Dir.glob("{app,lib,bin}/**/*.{rb,erb,rjs}"), "blog 1.0.0")
end
GetText::ActiveRecordParser.init(config)
Set some preferences to parse Model files.
  • config: a Hash of the config. It can takes some values below:
    • :db_yml: the path of database.yml. Default is "config/database.yml".
    • :db_mode: the mode of the database. Default is "development"
    • :activerecord_classes: an Array of the superclass of the models. The classes should be String value. Default is ["ActiveRecord::Base"]
    • :untranslate_classes: an Array of the module/class names. These modules/classes are ignored as the msgids. Default is ["ActiveRecord::Base", "ActiveRecord::SessionStore::Session"]].
    • :untranslate_columns: an Array of the field names. These fields are ignored as the msgids. Default is ["id"].
    • :use_classname: If true, the msgids of ActiveRecord become "ClassName|FieldName" (e.g. "Article|Title"). Otherwise the ClassName is not used (e.g. "Title"). Default is true.

"ClassName|FieldName" uses GetText.sgettext. So you don't need to translate the left-side of "|". See Documents for Translators for more details.

Run "updatepo" and "makemo" tasks

To create po/blog.pot, run "updatepo" task.

$ rake updatepo

To create mo-files, run "makemo" task.

$ rake makemo

To update po/{lang}/blog.po, run "updatepo" task again.

$ rake updatepo

See HOWTO maintain po/mo files for more details.

Run application

Did you run "rake makemo"? Then, your application has been localized.

Let's try your application!

Run blog application:

$ script/server

Then, access http://localhost:3000/blog/ via WWW browser. The application shows localized message what WWW browser requested.

If you want to try other languages, use "lang" parameter as follows:

http://localhost:3000/blog/?lang=ja_JP

See How to get the locale information from WWW borwser for more detail.

Tips, FAQ

Run your application in a language only

If an application supports L10n but you only need to use it in your language then set GetText.locale= first.

class ApplicationController < ActionController::Base
  GetText.locale = "ja"
  init_gettext "myapp"
end

Note that you need to call GetText.locale= before init_gettext.

Restrict the locales

Use locale_rails's I18n.supported_locales to restrict the locales.

I18n.supported_locales = Dir[File.join(RAILS_ROOT, "locale/*")].collect{|v| File.basename(v)}

To set the default locale except "en", use I18n.default_locale.

I18n.default_locale = "ja"

Even if the default_locale isn't in I18n.supported_locales, default_locale is used as the last fallback locale.

before/after_init_gettext

Since1.8 These methods are same with before/after_filter. The functions are called before/after initializing gettext on each request(get the user locale, select textdomain of the user locale).

(e.g.1) Set the language from

If you want to run your application in a language, see previous section, but you want to keep the ability to change to any other languages, too. Try this sample:

class ApplicationController < ActionController::Base
  before_init_gettext :default_locale
  def default_locale
    if (cookies["lang"].nil? or cookies["lang"].empty?)
      set_locale "zh_CN"
    else
      set_locale cookies["lang"]
    end
  end 
  init_gettext "myapp"
end

This sample uses cookie. So you need to set your locale to the cookie in other places like as user-preference page. And also you may want to use session or database not cookie.

(e.g.2) Initialize other localized library

class ApplicationController < ActionController::Base
 after_init_gettext :ya_l10n
 def ya_l10n
   YetAnotherL10n.set_locale(locale)
 end
 init_gettext "myapp"

Set the locale in a View(or multi-lingual content in same view)

If you want to set the locale in a View, use set_locale method.

<% set_locale "fr" %>
<%=_("Hello world") %>
<% set_locale "en" %>
<%=_("Hello world") %>

Note that you can't GetText.locale= here. Because your application includes GetText::Rails module and it's functions instead of GetText.

Make Ruby-GetText as an option of your app

Sometimes you may want to use your application without Ruby-GetText if the environment doesn't have Ruby-GetText.

I wrote a sample "pseudo_gettext.rb". Include this in your application, and require this instead of "gettext_rails".

Caching

You can use Action/Fragment caches. The functions are extended by Ruby-GetText to manage data in each locales.

Page caching is not supported. I don't have any idea how do this now because both the http server and the rails app need to help each other to return the cache/non cache data. Tell me if you have good idea about Page caching.