Ruby on RailsでRuby-GetText-Packageを使う
ruby-gettext-howto-ror
Ruby-GetText-PackageはRuby on Railsのローカライゼーションを強力にサポートします。
Railsのための機能
- Controller/Viewを翻訳する機能
- Modelを翻訳する機能(テーブル名、カラム名)
- クライアントロケールの自動取得
- (Controller/ViewだけではなくModelについても)翻訳対象文字列を自動で抽出しpoファイルを生成する機能
- 複数のテキストドメインを作る機能
- ロケールごとのAction/Fragmentキャッシュ
- エラーメッセージの翻訳(ビルトイン)
- いくつかのヘルパー関数の翻訳(ビルトイン)
ここではRuby on Rails 2.0.0以降でRuby-GetText-Packageを使う方法を説明します。 Ruby on Railsではrakeを使うことが多いので、この例ではpo/moファイルの管理についてもrakeを使うようにします。
なお、Ruby-GetText-Packageに付属のsamples/rails/配下にサンプルブログアプリケーションがありますのでそちらも参考にしてください。
また、こちらにサンプルブログアプリケーションのスクリーンショットを置いておきます。合わせてご参照ください。
Ruby on RailsへRuby-GetTextの適用
前提
ここで扱うアプリケーションの情報は以下のようにします。
- アプリケーション名: blog
- テキストドメイン名: blog
- バージョン: 1.0.0
- 文字コード: UTF-8(国際化アプリケーションを使う場合、文字コードは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
大まかな作業の流れ
作業の流れを簡単に説明します。
- Railsアプリケーションblogを作成します。
- blogの各ファイルに対し、ローカライズ向けにファイルを編集します。
- poファイル(翻訳用カタログファイル)を抽出します。
- poファイルを翻訳します。
- moファイル(poファイルをGetTextが読めるような形に変換したファイル)を作成します。
- WWWブラウザで確認します。
ここでは、(1)が一段落したとして、(2)から説明していきます。
ファイルの編集
以下の順番でファイルを編集していきます。
config/environment.rb
Ruby on Rails等のサーバアプリケーションでは、アプリケーションが使用する文字コードが、サーバシステムが使用している文字コードとは異なる場合が多いです。したがって、Ruby-GetText-Packageを使う・使わない、のどちらの場合でも、使用する文字コードを明示的に指定する必要があります。文字コードを指定しないと、文字列処理、正規表現を使った処理で「ハマる」かもしれません。
Ruby on Railsの場合は、config/environment.rbの先頭に以下のような記述を追加すると良いでしょう。
$KCODE='s' # 文字コードを指定します(s=Shift_JIS, e=EUC-JP等)。
#デフォルトで'u'(UTF-8)ですので通常は指定する必要はありません。
require 'jcode' # Stringクラスのメソッドなどを$KCODEに指定した
# 文字コードで適切に動作するように置き換えます。
次に、Ruby-GetText-Packageを使用するために、同じくconfig/environment.rbの最後の方にrequire 'gettext/rails'という行を追加します。
require 'gettext/rails' # Ruby-GetText-Packageを使う宣言です。(Rails-2.1以前)
(Rails-2.1以降の場合) 2.1以降の場合は、require 'gettext/rails'を呼ぶのではなく、config.gemを使います。
Rails::Initializer.run do |config| : : config.gem "gettext", :lib => "gettext/rails" end
ApplicationController
次にrails_root/app/controllers/application.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_("%{fn} can't be empty!")
validates_length_of :description, :minimum => 10, :message => N_("%{fn} is too short (min is %d characters)")
protected
def validate
unless title =~ /\A[A-Z]+\z/
errors.add("title", _("%{fn} is not correct: %{title}") % {:title => title})
end
end
end
%{fn}はフィールド名を表します。%{fn}を省略した場合は、メッセージ表示時、先頭にフィールド名が追加されます。また、%dはvalidates_length_ofを使用した際の数値を表します。
また、以下のようにvalidateメソッドでもローカライズメッセージを利用できます。
app/models/article.rb:
class Article < ActiveRecord::Base
protected
def validate
unless title =~ /\A[A-Z]+\z/
errors.add("title", _("%{fn} is not correct: %{title}") % {:title => title})
end
end
end
N_()ではなく_()を使っているところに注意してください。n_()も使えます。実はN_()でもローカライズされますが、上記の例のように%{title}として後から情報を埋め込む場合には_()を使う必要があります。 #ここの場合のN_()はクラスメソッドとして使うため、と考えると良いでしょう。
validatorメソッドで%{val}を使う
Since 1.92.0/Rails-2.1.0 validates_(format|inclusion|exclusion)_ofの各メソッドは先ほどの%dと同様に%{val}を値として使用できます。
validates_inclusion_of :name, :in => %w(a, b), :message => N_("%{fn} can't be %{val}")
untranslateオプション
Since 1.7.0 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のサブクラスである、とみなします。 したがって直接継承関係が無くても上記のように記述すればよいわけです。
エラーメッセージのタイトル部分をカスタマイズ
Since 1.7.0 Rails本体の機能には無いのですが、Ruby-GetTextの拡張機能としてエラーメッセージのタイトル部分をカスタマイズすることができます。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
Since 1.90.0 こちらもRuby-GetText-Packageの拡張機能ですが、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
Since 1.4.0 . 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://yourahost/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/utils'
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/utils'
GetText.create_mofiles(true, "po", "locale")
end
GetText.update_pofilesの第2引数はターゲットとなるファイルは全て含むようにしてください。 また、task毎にrequire 'gettext/utils'している点も注意してください。こうしないと、doc:app taskが失敗します。
この辺のpo/moファイルのメンテナンス方法についてはpo/moファイルのメンテナンス方法(英語)も参考にしてください。
翻訳文字列の抽出(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.rbを編集しました。この方法ではアプリケーションに一つのテキストドメイン、ということになります。ほとんどのRailsアプリケーションではこれで十分なはずです。
コントローラ1つに付き1つのテキストドメイン
コントローラ毎にテキストドメインを分けること可能です。blog_controller.rb, bbs_controller.rb毎にpoファイルを使い分けたい、ということもあるかもしれません。その場合は、以下のようにすると良いでしょう。
rails_root/app/controllers/application.rb
require 'gettext/rails' 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
プラグイン
Since 1.6.0 Ruby on Rails向けに作った汎用的なプラグインを他でも使い回すことを考えます。 その場合は、開発者がアプリ開発者とは異なるでしょうから、当然、テキストドメインもそれ独自に持ちたいはずです。moファイルのインストール場所もRAILS_ROOT/localeではなくて、RAILS_ROOT/vendor/plugins/(プラグイン名)/locale/#{lang}/LC_MESSGES/(プラグインのテキストドメイン).moに置くようにすると配布の時に便利です。
require 'gettext/rails'
module FooPlugin
include GetText::Rails
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
GetText.bindtextdomain("yourlib", "/your/mo/path")
class Foo
def foo
_("foo")
end
end
end
この場合、moファイルのパスをどこにするのか注意してください。
通常のライブラリと同様であれば、特に指定せずにデフォルトのままで問題ないはずです。 (そして、moファイルはシステムのデフォルトの格納先に配置します。)
その他の情報
ロケールを強制指定
以下のようにinit_gettextする前にGetText.locale=(locale)を使うと強制的にロケールを指定できます。アプリケーションが「日本語専用」などの場合に利用してください。
require 'gettext/rails' class ApplicationController < ActionController::Base
GetText.locale = "ja" init_gettext "myapp"
end
ブラウザからのアクセスと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 "lang" 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
Since1.8 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双方でうまく連携をとる必要があるため、今のところ上手な解決策が思いつきません。何かいいアイデアがある方は教えてくださいませ。
ChangeLog
- 2008-08-02: Ruby-GetText-Package 1.92.0に合わせてアップデート。
- 2008-02-01: Ruby-GetText-Package 1.90.0に合わせてアップデート。
- 2007-07-08: Ruby-GetText-Package 1.10.0に合わせてアップデート。
- 2006-09-14: Ruby-GetText-Package 1.8.0に合わせてアップデート。
- 2006-08-23: require 'gettext'をapplication.rbからenvironment.rbに移動した。pointed out by 黒田さん
- 2006-08-08: 「set_charsetは不要」の記述を追加。
- 2006-07-17: Ruby-GetText-Package 1.7.0に合わせてアップデート。
- 2006-05-07: Ruby-GetText-Package 1.5.0に合わせてアップデート。
- 2006-01-16: Ruby-GetText-Package 1.1.1に合わせてアップデート。
- 2005-09-04: 新規作成
キーワード:
参照:[Ruby-GetText-Package]