Ruby on RailsでRuby-GetText-Packageを使う (Rails-2.3.2以降)

ruby-gettext-howto-ror

[戻る?]

注意: 本チュートリアルはRails-2.3.2以降+Ruby-GetText-Package-2.0.0以降に対応しています。

[古いバージョンのチュートリアルを見る]

このチュートリアルは、簡単なブログアプリケーションを例にRuby on Rails 2.3.2とRuby-GetText-Packageを使用する方法を説明するものです。

目次

ここではRuby on Rails 2.0.0以降でRuby-GetText-Packageを使う方法を説明します。 Ruby on Railsではrakeを使うことが多いので、この例ではpo/moファイルの管理についてもrakeを使うようにします。

なお、gettext_railsのsample/配下、あるいは、github.comにサンプルブログアプリケーションがありますので参考にしてください。

また、こちらにサンプルブログアプリケーションのスクリーンショットを置いておきます。合わせてご参照ください。

Ruby on RailsへRuby-GetTextの適用

前提

ここで扱うアプリケーションの情報は以下のようにします。

  • アプリケーション名: blog
  • テキストドメイン名: blog
  • バージョン: 1.0.0
  • 文字コード: UTF-8(Railsデフォルトの文字コードです。また、国際化アプリケーションを使う場合、文字コードはUTF-8にすることを推奨します。)

以下のようなテーブルを1つ持っていることとします。

# 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

大まかな作業の流れ

作業の流れを簡単に説明します。

  1. Railsアプリケーションblogを作成します。
  2. blogの各ファイルに対し、ローカライズ向けにファイルを編集します。
  3. poファイル(翻訳用カタログファイル)を抽出します。
  4. poファイルを翻訳します。
  5. moファイル(poファイルをGetTextが読めるような形に変換したファイル)を作成します。
  6. WWWブラウザで確認します。

ここでは、(1)が一段落したとして、(2)から説明していきます。

ファイルの編集

以下の順番でファイルを編集していきます。

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, gettext_railsはRailsのバージョンに依存しており、Railsのバージョンによって動作しないことが多いです。そこで使用するバージョンを固定することをお奨めします。 どのRailsバージョンで動作するか、は各ライブラリのREADME.rdocのSupport Matrixを参照してください。

ApplicationController

次に$RAILS_ROOT/app/controllers/application_controller.rbにどのテキストドメインを使用するか、記述します。ここまでは全てのアプリケーションで記述する必要があります。

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

これだけです。これだけ記述しておくだけでも、エラーメッセージが翻訳されたりいろいろと日本語化(厳密には多国語化)されることに気づくでしょう。

Note1 このテキストドメインはすpoファイル(リソースファイル)のファイル名になります。上記の例ではblog.poとなります。

Note2 過去の経緯から、よく以下のような記述を見かけますが、Ruby-GetText内部で同じ処理を行うため不要です。また、以下の記述方法はRJSと相性が悪いため、Ruby-GetTextを使わないとしても以下の表記はお奨めできません。

#不要なコード
before_filter :set_charset
private
def set_charset
  headers['Content-Type'] = "text/html;charset=utf-8"
end

なお、init_gettextはcharsetやContent-Typeを渡すこともできます。

ActionController::Base.init_gettext(textdomain, options = {})
'textdomain'をcontrollers/views/modelsにバインドします。
  • textdomain: テキストドメイン
  • options: オプションをHashで指定します。
    • :charset - 出力する文字コード。IANA登録名を使ってください。なお、この値はheaders['Content-Type']にも使用されます。デフォルトはUTF-8です。
    • :content_type - headers['Content-Type']の値(charset指定の前)。デフォルトは"text/html"です。
    • :locale_path - ロケールのパス({locale_path}/{lang}/LC_MESSAGES/{textdomain}.mo)。デフォルトは(RAILS_ROOT)/localeです。

これをApplicationControllerに記述すると全てのcontrollers/views/modelsに一つのテキストドメインを指定することになります。それぞれのコントローラ毎にテキストドメインを指定する場合は、それぞれのコントローラ毎に上記指定を記述すればよいでしょう。

Controller

ここからは必要に応じて適用してください。

GetTextの各関数を使うようにファイルを編集していきます。テキストドメインはApplicationControllerで指定したものが使われます。各ファイル毎にbindtextdomainを呼ぶ必要はありません。

以下は"BlogController"の例です。

app/controller/blog_controller.rb:

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

Note 日本人はとかく複数形・単数形を分けることに慣れていませんが、英語をはじめ多くの言語では複数形と単数形を厳密に分けます。なるべくn_()を使って複数形にも対応しておきましょう。

Views/ActionMailer

Views/ActionMailerはERBファイル(.erb)を使いますがそれ以外はControllerと同様です。

ここでは、edit.html.erbの例を挙げます。

app/views/blog/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>

ローカライズドテンプレートの使用

foo_ja.hml.erb, foo_ja_JP.html.erbと言う風にテンプレート自体を英語の元ファイルから分離させてしまうこともできます。例えば以下のようにファイルを配置します。

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

このケースでは日本語(jaロケール)の場合のみ、2番目のファイルが呼び出され、それ以外の時は1番目のファイルが呼び出されます。 render_partialの場合は、_foo.html.erb, _foo_ja.html.erbとすると、日本語の場合のみ_foo_ja.html.erbが呼び出されます。

Note メンテナンス性を考えるとこの機能はなるべく使わないことを推奨します。 こちらにその理由がありますのでご参照ください。なお、この日記のエントリに書かれているソースコードはすでにRuby-GetText-Package本体に取り込まれていますので無視してください。

Models

全てのテーブル名(クラス名)とフィールド名はmsgidとして抽出されます。その際のフォーマットは"ClassName|FieldName"です。ArticleというクラスのTitleというフィールドであれば、"Article|Title"になります。

妥当性チェックの文字列(validation messages)を変更したい場合は以下のようにすればよいでしょう。_()ではなくN_()を使っていることに注意してください。

app/models/article.rb:

class Article < ActiveRecord::Base
  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
  def validate
    unless title =~ /\A[A-Z]+\z/
      errors.add("title", _("%{fn} is not correct: %{title}") % {:title => title}) 
    end   
  end   
end

%{attribute}はフィールド名を表します。%{attribute}を省略した場合は、メッセージ表示時、先頭にフィールド名が追加されます。また、%countはvalidates_length_ofを使用した際の数値を表します。

また、以下のようにvalidateメソッドでもローカライズメッセージを利用できます。

app/models/article.rb:

class Article < ActiveRecord::Base
  protected
  def validate
    unless title =~ /\A[A-Z]+\z/
      errors.add("title", _("%{attribute} is not correct: %{title}") % {:title => title}) 
    end   
  end   
end

N_()ではなく_()を使っているところに注意してください。n_()も使えます。実はN_()でもローカライズされますが、上記の例のように%{title}として後から情報を埋め込む場合には_()を使う必要があります。 #ここの場合のN_()はクラスメソッドとして使うため、と考えると良いでしょう。

validatorメソッドで%{value}を使う

validates_(format|inclusion|exclusion)_ofの各メソッドは%{value}を値として使用できます。

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

untranslateオプション

untranslate, untranslate_allを使うと、対象テーブル・フィールドが翻訳対象としてpoファイルに出力されなくなります。翻訳者に訳す必要の無い項目まで提示するのはあまりよろしくないでしょうから、こまめにこの機能を使うと良いと思います。

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

class Article < ActiveRecord::Base
  untranslate_all
end

上のパターンでは、title, descriptionフィールドが翻訳対象としてpoファイルに出力されなくなります。下のパターンではArticleテーブルの情報が全てpoファイルに出力されなくなります。

なお、テーブルに関係なく翻訳対象外、としたい場合はGetText::ActiveRecordParser.init(config)の:untranslate_columnsオプションを使います。

データベースにない属性もローカライズの対象にする

"テーブル名|属性名"という形の文字列をN_()で囲んであげます。あとはデータベースのフィールド名と同様にvalidate等で使えます。

(例)Userテーブルでパスワード、確認用パスワードのフィールドはデータベースに無い場合(ちょっと例が悪いか・・・)

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", _("%{fn} is wrong!")) unless password == password_confirmation
  end
end

(例2) has_oneを使った場合

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

STIなモデルを翻訳対象にする

STIなモデルでも(updatepoでテーブル名・カラム名を)翻訳対象にしたい場合は以下のように"ActiveRecord::Base"の文字列をファイル中に記述してください。

class AdminUser < User   # ActiveRecord::Base
end

updatepoタスクでは、ActiveRecord向けに文字列抽出をする際に、そのファイル中に"ActiveRecord::Base"があればそれはActiveRecord::Baseのサブクラスである、とみなします。 したがって直接継承関係が無くても上記のように記述すればよいわけです。

エラーメッセージのタイトル部分をカスタマイズ

エラーメッセージのタイトル部分をカスタマイズする場合、app/controller/application.rb辺りで以下のように記述します。

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

上記はアプリケーション全体の設定ですが、error_messages_forにエラーメッセージのタイトル部分を指定することもできます。各Viewごとに表示を適切なメッセージに変更したい場合はこちらを使うと良いでしょう。

# 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}")
} %>

ActionWebService

Ruby-GetText-PackageをActionWebServiceで使うことができます。このとき、クライアントはロケール情報をサーバ側に渡すために"lang"パラメータを送信する必要があります。

# app/apis/product_api.rb
class ProductApi < ActionWebService::API::Base
  api_method :find_product_by_id, :expects => [:lang => :string], :returns => [{:string => String}]
end

# app/controller/backend_controller.rb
class BackendController < ApplicationController
  wsdl_service_name 'Backend'
  web_service_api ProductApi
  def find_product_by_id(id)
   _("foo") + ...
  end
end

ルーティング

":lang"をconfig/routes.rbで使用することでルーティング時にロケールを指定することができます。

ActionController::Routing::Routes.draw do |map|
 # http://yourhost/main_ja/が呼ばれた場合、クライアントの言語(Accept-Language)に依存せずに、
 # 日本語を指定してblogコントローラを呼び出します。
 map.connect 'main_ja', :lang=>'ja', :controller=>'blog', :action=>'list'

 # 同様にhttp://yourhost/blog/ja/list/が呼ばれた場合に"ja"を:langパラメータとして指定します。
 map.connect ':controller/:lang/:action/:id'
end

このように、Ruby-GetText-Packageは常に@params["lang"]の値を優先的に使用します。

Rakefile

次にメンテナンスのためのコードをRakefileに追記します。

desc "Update pot/po files."
task :updatepo do
  require 'gettext_rails/tools'
  GetText.update_pofiles("blog", #テキストドメイン名(init_gettextで使用した名前) 
                         Dir.glob("{app,config,components,lib}/**/*.{rb,erb,rjs}"),  #ターゲットとなるファイル
                         "blog 1.0.0"  #アプリケーションのバージョン
                         )
end

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

GetText.update_pofilesの第2引数はターゲットとなるファイルは全て含むようにしてください。 また、task毎にrequire 'gettext_rails/tools'している点も注意してください。こうしないと、doc:app taskが失敗します。

この辺のpo/moファイルのメンテナンス方法についてはpo/moファイルのメンテナンス方法(英語)も参考にしてください。

rake gems:unpack使用時の注意

rake gems:unpackし、gem uninstall gettextをしたい場合、 Rakefileの先頭にRails::GemDependency.add_frozen_gem_pathを指定してください。 #gettext_rails/toolsが見つからない、というエラーが出る場合があります。

翻訳文字列の抽出(poファイル化)

ここまできたら後は簡単です。

まず、$RAILS_ROOT配下の各種.rb/.erbファイルからpo/blog.potファイルを生成します。

$ rake updatepo

この際、ActiveRecordのフィールド名などを参照するためにDBアクセスを行います。 したがって、事前にDBを起動しておき、config/database.xmlが適切に設定されている必要があります。

あとは、このblog.potを翻訳してくれる人に配ります。

さて、翻訳ファイルが戻ってきたとします。次にそのファイルをpo配下の各ロケール名ディレクトリ配下に置きます。ファイル名はblog.poとします。

$RAILS_ROOT/po
           |- blog.pot
           |- fr/blog.po  ←フランス語
           `- ja/blog.po  ←日本語

po → mo変換

$RAILS_ROOTで以下のように実行します。

$ rake makemo

moファイルは、以下のフォルダに格納されます。

$RAILS_ROOT/locale/
           |- fr/LC_MESSAGES/blog.mo  ←フランス語
           `- ja/LC_MESSAGES/blog.mo  ←日本語

これで、後はscript/serverを起動して確認すれば、アプリケーションの文字列が適切な言語に翻訳されるでしょう。

テキストドメインのスコープ

テキストドメインのスコープとして、以下の4つをあげます。

  • アプリケーションで1つのテキストドメイン
  • コントローラ1つに付き1つのテキストドメイン
  • ヘルパーにアプリケーション本体とは別のテキストドメインを持たせる
  • ライブラリ自身に(アプリケーションとは別の)テキストドメインを持たせる

アプリケーションで1つのテキストドメイン

前述の例では、$RAILS_ROOT/app/controllers/application_controller.rbを編集しました。この方法ではアプリケーションに一つのテキストドメイン、ということになります。ほとんどのRailsアプリケーションではこれで十分なはずです。

コントローラ1つに付き1つのテキストドメイン

コントローラ毎にテキストドメインを分けること可能です。blog_controller.rb, bbs_controller.rb毎にpoファイルを使い分けたい、ということもあるかもしれません。その場合は、以下のようにすると良いでしょう。

$RAILS_ROOT/app/controllers/application.rb

class ApplicationController < ActionController::Base
end

$RAILS_ROOT/app/controllers/blog_controller.rb

class BlogController < ApplicationController
  init_gettext "blog"
end

$RAILS_ROOT/app/controllers/bbs_controller.rb

class BbsController < ApplicationController
  init_gettext "bbs"
end

プラグイン

Ruby on Rails向けに作った汎用的なプラグインを他でも使い回すことを考えます。

その場合は、開発者がアプリ開発者とは異なるでしょうから、当然、テキストドメインもそれ独自に持ちたいはずです。moファイルのインストール場所も$RAILS_ROOT/localeではなくて、$RAILS_ROOT/vendor/plugins/(プラグイン名)/locale/#{lang}/LC_MESSGES/(プラグインのテキストドメイン).moに置くようにすると配布の時に便利です。

require 'gettext_rails'

module FooPlugin
  include GetText

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

これで、FooプラグインのテキストドメインはRAILS_ROOT/vendor/plugins/foo/locale/#{lang}/LC_MESSGES/foo.moにバインドされます。

ライブラリ自身に(アプリケーションとは別の)テキストドメインを持たせる

そのライブラリがActionViewやControllerと完全に独立しているのであれば、通常の方法でライブラリをGetText化できます。

require 'gettext'
module YourLib
  include GetText

  bindtextdomain("yourlib", :path => "/your/mo/path")

  class Foo
    def foo
      _("foo")
    end
  end
end

この場合、moファイルのパスをどこにするのか注意してください。

通常のライブラリと同様であれば、特に指定せずにデフォルトのままで問題ないはずです。 (そして、moファイルはシステムのデフォルトの格納先に配置します。)

その他の情報

ロケールを強制指定

以下のようにinit_gettextする前にGetText.locale=(locale)を使うと強制的にロケールを指定できます。アプリケーションが「日本語専用」などの場合に利用してください。

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

対象とするロケールを限定する

アプリケーションとして使用するロケールを限定する場合、locale_railsが提供するI18n.supported_localesを使います。 RAILS_ROOT/locale配下にある言語であればそのアプリケーションが対応している、といえると思いますので、その場合は以下のようにすると良いでしょう。

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

デフォルトの言語を替えたい場合(en以外)は、I18n.default_localeを合わせて設定してください。

I18n.default_locale = "ja"

なお、I18n.supported_localesにdefault_localeが入っていなくても、最終的にdefault_localeにフォールバックします。逆に言うと、en以外にフォールバックさせたい場合は、必ずI18n.default_localeを指定してください。

ブラウザからのアクセスとCookieへの情報格納

ロケール情報は、QUERY_STRINGのlangの値 > Cookieのlangの値 > WWWブラウザが返すHTTP_ACCEPT_LANGUAGE環境変数 > "en"(英語) の順番で決まります。QUERY_STRINGのlangの値とは、例えば、http://foo/?lang=ja という感じでURLの最後に追加する方法です。 QUERY_STRINGのlangの値やCookieのlangの値はアプリケーション側で適切に設定する必要があります。

したがって、最初に確認するときは、http://foo/?lang=ja とアクセスしてみれば日本語になっていることがわかるでしょう。

また、ここでは上記のようにQUERY_STRINGからlangを受け取りCookieに設定して返す機能を紹介します。

Session情報を使う場合はそちらに設定しても良いかもしれません。

class BlogController < ApplicationController
      :
      :
  def cookie_locale
    cookies["lang"] = @params["lang"]
    flash[:notice] = _('Cookie &quot;lang&quot; is set: %s') % @params["lang"]
    redirect_to :action => 'list'
  end
end

あとは、View側からcoolie_localeをアクションとして呼び出すようにします。

<%= link_to _("Set Locale to japanese"), :action => "cookie_locale", :lang => "ja" %>

出力する文字コードを指定する

出力する文字コードを指定することもできます。デフォルトではUTF-8が使われます。

require 'gettext/rails'

class ApplicationController < ActionController::Base
  init_gettext "blog", :charset => "Shift_JIS"
end

最初の"Shift_JIS"の部分は設定ファイル等で設定できるようにしておくと良いでしょう。

ところで、charsetにnilを設定するとどうなるでしょうか。 その場合、ブラウザ側が指定した文字コード(見つからない場合はUTF-8)が使われます。日本語だとEUC-JPやShift_JISになることが多いでしょう。したがって、アプリケーションは適切な文字コードでクライアントにコンテンツを返すことができます。モバイルフォン等では有効でしょう。

ただし、アプリケーション側も適切な文字コードに変換する必要があるので注意してください。

例えば、データベースはShift_JISで作った方が良いかもしれません。あるいは、Iconvを使って出力文字列を変換する必要があるかもしれません。

クライアントがShift_JISしかサポートしない環境(モバイルフォン向けのWWWアプリケーション等)を想定する場合以外は、このような国際化アプリケーションではUTF-8を使うようにすると良いでしょう。

before/after_init_gettext

before/after_filterと同様に使います。ユーザのリクエスト毎に発生するGetTextの初期化処理(ロケールの取得と使用するTextdomainの選択)を行う前後に処理を追加することができます。

例えば、QUERY_STRINGの"foo"パラメータにロケールが指定されている場合はそれを使い、指定されていない場合は日本語を使う、というような場合は以下のようにします。

class ApplicationController < ActionController::Base
  before_init_gettext :foo
  def foo
    GetText.locale = cgi.params["foo"] ? "ja" : cgi.params["foo"]
  end
  init_gettext "myapp"

DBアクセスやセッションにあるユーザ情報から、使用するロケールを取得する、というような場合にこの機能を使うことができると思います。

after_init_gettextはロケールが決まった後に呼び出されます。他のローカライズライブラリを併用するときにそのライブラリにロケール情報を渡す時に使うことを想定しています。

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

1つのViewで多言語同時表示

set_localeを使うと複数の言語を使うことができます。この場合、出力する文字コードを統一しておく必要があります(通常はUTF-8になるでしょう)。

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

キャッシュについて

RailsにはPage/Action/Fragmentの3種類のキャッシュがありますが、Action/Fragmentについてはロケール毎に管理するように拡張されており、特に(GetTextを使わない)アプリケーションと同様にRailsのキャッシュ機能を使用することができます。

Pageキャッシュについてはロケールの特定をhttpサーバ、Rails双方でうまく連携をとる必要があるため、今のところ上手な解決策が思いつきません。何かいいアイデアがある方は教えてくださいませ。