#256 I18n Backends
- Download:
- source codeProject Files in Zip (99.2 KB)
- mp4Full Size H.264 Video (19.2 MB)
- m4vSmaller H.264 Video (12.4 MB)
- webmFull Size VP8 Video (28.2 MB)
- ogvFull Size Theora Video (26.1 MB)
国際化(internationalization)については、一番最近ではエピソード138[動画を見る, 読む]で取り上げました。デフォルト設定ではRailsは国際化の情報をYAMLファイルに保存しますが、今回のエピソードでは別のバックエンドの使い方を紹介します。
下の図は簡単なRailsサイトのページです。このページのヘッダーテキストを国際化してみましょう。
translateメソッド(短縮形はt
)を使えばRailsアプリケーションのテキストの一部を簡単に国際化できます。ただしその場合、アプリケーションがサポートする言語ごとにYAMLファイルを編集して、キーとその言語のテキストを追加する必要があります。大規模なアプリケーションの場合、この方法はすぐに退屈な作業になってしまいます。そもそも国際化テキストを書くのは本来は開発者の仕事ではありません。アプリケーションの管理者が国際化テキストの追加や編集ができるウェブ画面を提供できれば、ずっと便利になるでしょう。
ありがたいことにinternationalization gemは複数のバックエンドをサポートしています。つまりYAMLだけに縛られることなく、気に入ったデータベースバックエンドを選んで使うことができます。デフォルト設定ではRailsはSimple backendを使用して、YAMLファイルで翻訳文を管理します。今回のエピソードで使用するのはKey-Value backendというバックエンドで、翻訳文を関するためにどのようなkey-valueストアでも使用することができます。ActiveRecordに保存する機能を抽出した別のgemもあります。これも動くことは動くのですが、アプリケーションの各ページ上の翻訳文に頻繁にアクセスされるため、ActiveRecordというのはベストな方法とは言えないでしょう。翻訳文は、SQLデータベースではなくできればメモリ上に保持したいところです。この問題を解決するためにキャッシュ機能を利用できるかも知れませんが、翻訳文が修正された場合にはキャッシュを使用させないなどの制御に気を配らなくてはいけないのが煩雑です。これらの理由からkey-valueストアを選択することとし、今回のエピソードで紹介していきます。
バックエンドを変更する
まずヘッダの静的テキストを修正し、代わりに翻訳を使用するようにします。welcome
というキーに対応づけます。
<h1><%= t('welcome') %></h1>
そして、英語用のYAMLファイルに翻訳テキストを追加します。
en: welcome: "Welcome"
ここでページを再度読み込むと、YAMLファイルのテキストが表示されているのがわかります。
デフォルトのバックエンドを使った翻訳テキストがあるので、バックエンドを変えてみることにしましょう。key_value.rb
ファイルの最初のコメント部分に、使い方の説明と別のバックエンドを設定する方法が記述されています。
# I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
I18n::Backend::KeyValue
を新規に作成し、使用したいkey-valueストアを指定します。それが翻訳テキスト管理のバックエンドとして使用されます。このバックエンドのデータベースは、3つのメソッドに応答できる必要があります。キーの値を取得する、値を設定する、すべてのキーのリストを得る、の3つです。この情報はコメント欄に記述されています。Rubyのほとんどのkey-valueストアはこれらのメソッドをサポートしているので、そのまま使うことができます。
# * store#[](key) - Used to get a value # * store#[]=(key, value) - Used to set a value # * store#keys - Used to get all keys
これでアプリケーションの国際化用のバックエンドを入れ替える準備が整ったので、作業を始めることにしましょう。まず/config/initializers
ディレクトリ内にi18n_backend.rb
というファイルを新規作成します。
I18n.backend = I18n::Backend::KeyValue.new({})
コメント欄の例ではバックエンドとしてTokyo Cabinetを使用していますが、今回のアプリケーションをとりあえず動かすために空のハッシュを使用します。当然、実際のアプリケーションではこのようなことはしませんが、これは先ほどの3つのメソッドをサポートする一番単純なもので、デモ目的には十分です。ここでアプリケーションを再起動してトップページに行くと、タイトルが正しく表示されていません。
ソースコードを見ると、「翻訳がみつからない」とあります。
<h1><span class="translation_missing">en, welcome</span></h1>
アプリケーションが新しいバックエンドを使用しているので、翻訳がないと表示されています。YAMLファイルには残っているのですが、新しいバックエンドにはまだ翻訳がないからです。そこでウェブ画面を新たに作成して、ユーザが新しいバックエンドに翻訳文を追加できるようにします。これを、新しいTranslationsController
のindex
アクションで処理します。
$ rails g controller translations index
この新しいコントローラをresourceとして動作させたいので、routesファイルに自動生成されたルート(get "translations/index"
)を、resources
の呼び出しに書き換えます。
Intn::Application.routes.draw do resources :translations root :to => "home#index" end
index
アクションで、翻訳が存在する場合はそれを表示するために、新しいバックエンドからハッシュ形式の翻訳テキストを取得します。そのためにI18n.backend.store
を呼び出します。
class TranslationsController < ApplicationController def index @translations = I18n.backend.store end end
ビュー内で翻訳を順番に取得して表示していきます。
/app/views/translations/index.html.erb
<h1>Translations</h1> <ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul>
このコードがハッシュ内のすべての翻訳をループして、すべてのキーと値をリスト表示します。新しい翻訳を追加する手段が必要なので、リストの下にフォームを作ります。
<h1>Translations</h1> <ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul> <h2>Add Translation</h2> <%= form_tag translations_path do %> <p> <%= label_tag :locale %><br /> <%= text_field_tag :locale %> </p> <p> <%= label_tag :key %><br /> <%= text_field_tag :key %> </p> <p> <%= label_tag :value %><br /> <%= text_field_tag :value %> </p> <p><%= submit_tag "Submit" %></p> <% end %>
このフォームは、TranslationController
のcreate
アクションにPOSTします。フォームには次の3つのフィールドがあります。言語のロケール(例えば、英語の場合はen
)、キー(ビューファイル内の翻訳対象を特定します)、そして翻訳されたテキストです。
create
アクションで、I18n.backend.store_translations
を呼び出して、フォームに入力された値に基づいて新しい翻訳文を追加します。その時に3つの引数を指定します。1つ目はロケールで、フォームから取得します。2つ目はハッシュ形式のデータで、内容は自由です。フォーム上のフィールドから、キーと値を渡します。最後の引数escape
は、キーにおいてピリオドをエスケープするかどうかを決定します。ピリオドは、キーの区切りとして使うので、ここではfalse
に設定します。
def create I18n.backend.store_translations(params[:locale], ? {params[:key] => params[:value]}, :escape => false) redirect_to translations_url, :notice => "Added translations" end
新しく作ったフォームの動作を確認するために、トップページの翻訳がない部分を追加してみましょう。
トップページに戻ってみると、翻訳されたヘッダ文が新しく設定したバックエンドから取得され表示されています。
バックエンドにRedisを使用する
新しいバックエンドはうまく動くようになりましたが、値をRubyのハッシュに保存しているため、Webサーバを再起動したらすべて無くなってしまいます。翻訳文の保存のためには、より永続的な保存場所が必要です。そこでこのアプリケーションのために、永続的なkey-valueストアであるRedisを使用することにします。
もしMacを使用しているのであれば、Redisをインストールする一番簡単な方法はHomeBrewです。インストールは、次のコマンドを実行します。
$ brew install redis
インストールができたら、指示に従ってRedisサーバを起動します。通常は、簡単にredis-server
を実行するだけです。
このアプリケーションでRedisを使用するためにRedis gemをインストールします。そのためにGemfile
にgemへの参照情報を追加し、bundle
コマンドを実行します。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'redis'
これで、バックエンドの設定でハッシュを指定している部分を、Redisデータベースに置き換えられます。
I18n.backend = I18n::Backend::KeyValue.new(Redis.new)
最低限必要なのはこれだけですが、加えてデータベースを特定する:db
オプションを使用して、development (開発)、test (テスト)、production (本番)のそれぞれのモードで違うデータベースを指定することもできます。
バックエンドをkey-valueストアに変更したのに合わせて、翻訳画面のコードも変更します。現状ではハッシュ内をループしているものを、Redisデータベース内をループさせるよう、コードの以下の部分を変更します。
<ul> <% @translations.each do |key, value| %> <li><%= key %>: <%= value %></li> <% end %> </ul>
これを次のように変更します。
<ul> <% @translations.keys.each do |key| %> <li><%= key %>: <%= @translations[key] %></li> <% end %> </ul>
これで@translations
はRedisデータベースのインスタンスを参照するようになりました。しかしeach
には対応しないので、keys
内で繰り返し処理をおこなうように変更します。この部分では、キーとそれに対応する値を表示します。
これで翻訳文のための永続的な保存場所ができたので、アプリケーションのサーバを再起動してもフォームから追加した翻訳が失われることはありません。
代替用バックエンドを追加する
これで標準以外のバックエンドを設定することができましたが、もし引き続き翻訳の一部にYAMLファイルを使用したい場合はどうすればいいでしょうか? そこでここからは、Key-valueストアから値を取得できない部分について代替のYAMLファイルを使用する方法を説明します。
まず前に作成した初期設定ファイルでバックエンドを定義した部分を修正します。KeyValue
バックエンドを直接使用するのではなく、代わりにChain
バックエンドを使用します。Chain
バックエンドから複数のバックエンドが順番に呼び出され、与えられた翻訳キーに対応する値が見つかればそれが返されます。
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(Redis.new), I18n.backend)
最初にRedisバックエンドを、次にデフォルトバックエンドを指定しています。アプリケーションはRedisデータベースで翻訳文を探し、対応するキーを見つけられなかった場合、YAMLファイルを参照します。
この方法でバックエンドを設定すると、key-valueストアに直接アクセスするのが困難になります。この問題を回避するために、データベースを定数で指定して外に出します。
TRANSLATION_STORE = Redis.new I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(TRANSLATION_STORE), I18n.backend)
これによって、アプリケーション内でkey-valueストアにアクセスする必要がある場合、例えばTranslationsController
内で、この定数を使うことができます。I18n.backend.store
を呼び出すことはできません。
def index @translations = TRANSLATION_STORE end
Redisデータベースから保存された翻訳文を削除してトップページにアクセスしてみると、YAMLファイルからの翻訳文が表示されているのがわかります。
翻訳文を元に戻すとそちらが優先され、Redisデータベースからの値が表示されます。
今回のエピソードはこれで終わりです。これで、ウェブ画面から翻訳文を編集できるシステムができたので、YAMLファイルを直接編集しなくてもよくなりました。実稼働中のアプリケーションでこのようなことを行う場合、使い勝手をよくするにはいろいろと作り込める部分はありますが、基本的な部分は今回紹介したとおりです。
このトピックに関しての情報が欲しければ、Jose Valimの近刊“Crafting Rails Applications”をお勧めします。現在ベータ版が入手可能で、今回のエピソードを執筆するのに非常に参考になりました。