homeASCIIcasts

256:  国際化のバックエンド 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Naomi Fujimoto

国際化(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というキーに対応づけます。

/app/views/home/index.html.erb

<h1><%= t('welcome') %></h1>

そして、英語用のYAMLファイルに翻訳テキストを追加します。

/config/locales/en.yml

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というファイルを新規作成します。

/config/initalizers/i18n_backend.rb

I18n.backend = I18n::Backend::KeyValue.new({})

コメント欄の例ではバックエンドとしてTokyo Cabinetを使用していますが、今回のアプリケーションをとりあえず動かすために空のハッシュを使用します。当然、実際のアプリケーションではこのようなことはしませんが、これは先ほどの3つのメソッドをサポートする一番単純なもので、デモ目的には十分です。ここでアプリケーションを再起動してトップページに行くと、タイトルが正しく表示されていません。

新しいバックエンドで、翻訳が表示されない

ソースコードを見ると、「翻訳がみつからない」とあります。

<h1><span class="translation_missing">en, welcome</span></h1>

アプリケーションが新しいバックエンドを使用しているので、翻訳がないと表示されています。YAMLファイルには残っているのですが、新しいバックエンドにはまだ翻訳がないからです。そこでウェブ画面を新たに作成して、ユーザが新しいバックエンドに翻訳文を追加できるようにします。これを、新しいTranslationsControllerindexアクションで処理します。

$ rails g controller translations index

この新しいコントローラをresourceとして動作させたいので、routesファイルに自動生成されたルート(get "translations/index")を、resourcesの呼び出しに書き換えます。

/config/routes.rb

Intn::Application.routes.draw do
  resources :translations
  root :to => "home#index"
end

indexアクションで、翻訳が存在する場合はそれを表示するために、新しいバックエンドからハッシュ形式の翻訳テキストを取得します。そのためにI18n.backend.storeを呼び出します。

/app/controllers/translations_controller.rb

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>

このコードがハッシュ内のすべての翻訳をループして、すべてのキーと値をリスト表示します。新しい翻訳を追加する手段が必要なので、リストの下にフォームを作ります。

/app/views/translations/index.html.erb

<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 %>

このフォームは、TranslationControllercreateアクションにPOSTします。フォームには次の3つのフィールドがあります。言語のロケール(例えば、英語の場合はen)、キー(ビューファイル内の翻訳対象を特定します)、そして翻訳されたテキストです。

createアクションで、I18n.backend.store_translationsを呼び出して、フォームに入力された値に基づいて新しい翻訳文を追加します。その時に3つの引数を指定します。1つ目はロケールで、フォームから取得します。2つ目はハッシュ形式のデータで、内容は自由です。フォーム上のフィールドから、キーと値を渡します。最後の引数escapeは、キーにおいてピリオドをエスケープするかどうかを決定します。ピリオドは、キーの区切りとして使うので、ここではfalseに設定します。

/app/controllers/translations_controller.rb

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コマンドを実行します。

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'
gem 'redis'

これで、バックエンドの設定でハッシュを指定している部分を、Redisデータベースに置き換えられます。

/config/initializers/i18n_backend.rb

I18n.backend = I18n::Backend::KeyValue.new(Redis.new)

最低限必要なのはこれだけですが、加えてデータベースを特定する:dbオプションを使用して、development (開発)、test (テスト)、production (本番)のそれぞれのモードで違うデータベースを指定することもできます。

バックエンドをkey-valueストアに変更したのに合わせて、翻訳画面のコードも変更します。現状ではハッシュ内をループしているものを、Redisデータベース内をループさせるよう、コードの以下の部分を変更します。

/app/views/translations/index.html.erb

<ul>
  <% @translations.each do |key, value| %>
    <li><%= key %>: <%= value %></li>
  <% end %>
</ul>

これを次のように変更します。

/app/views/translations/index.html.erb

<ul>
  <% @translations.keys.each do |key| %>
    <li><%= key %>: <%= @translations[key] %></li>
  <% end %>
</ul>

これで@translationsはRedisデータベースのインスタンスを参照するようになりました。しかしeachには対応しないので、keys内で繰り返し処理をおこなうように変更します。この部分では、キーとそれに対応する値を表示します。

これで翻訳文のための永続的な保存場所ができたので、アプリケーションのサーバを再起動してもフォームから追加した翻訳が失われることはありません。

代替用バックエンドを追加する

これで標準以外のバックエンドを設定することができましたが、もし引き続き翻訳の一部にYAMLファイルを使用したい場合はどうすればいいでしょうか? そこでここからは、Key-valueストアから値を取得できない部分について代替のYAMLファイルを使用する方法を説明します。

まず前に作成した初期設定ファイルでバックエンドを定義した部分を修正します。KeyValueバックエンドを直接使用するのではなく、代わりにChainバックエンドを使用します。Chainバックエンドから複数のバックエンドが順番に呼び出され、与えられた翻訳キーに対応する値が見つかればそれが返されます。

/config/initializers/i18n_backend.rb

I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(Redis.new), I18n.backend)

最初にRedisバックエンドを、次にデフォルトバックエンドを指定しています。アプリケーションはRedisデータベースで翻訳文を探し、対応するキーを見つけられなかった場合、YAMLファイルを参照します。

この方法でバックエンドを設定すると、key-valueストアに直接アクセスするのが困難になります。この問題を回避するために、データベースを定数で指定して外に出します。

/config/initializers/i18n_backend.rb

TRANSLATION_STORE = Redis.new
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(TRANSLATION_STORE), I18n.backend)

これによって、アプリケーション内でkey-valueストアにアクセスする必要がある場合、例えばTranslationsController内で、この定数を使うことができます。I18n.backend.storeを呼び出すことはできません。

/app/controllers/translations_controller.rb

def index
  @translations = TRANSLATION_STORE
end

Redisデータベースから保存された翻訳文を削除してトップページにアクセスしてみると、YAMLファイルからの翻訳文が表示されているのがわかります。

代替バックエンドからの翻訳文が表示される

翻訳文を元に戻すとそちらが優先され、Redisデータベースからの値が表示されます。

Redisデータベースからの翻訳文が再度表示される

今回のエピソードはこれで終わりです。これで、ウェブ画面から翻訳文を編集できるシステムができたので、YAMLファイルを直接編集しなくてもよくなりました。実稼働中のアプリケーションでこのようなことを行う場合、使い勝手をよくするにはいろいろと作り込める部分はありますが、基本的な部分は今回紹介したとおりです。

このトピックに関しての情報が欲しければ、Jose Valimの近刊“Crafting Rails Applications”をお勧めします。現在ベータ版が入手可能で、今回のエピソードを執筆するのに非常に参考になりました。