akishin999の日記

調べた事などを書いて行きます。

I18n のバックエンドにデータベースを使用する

Rails 3.2.7 で I18n のバックエンドに RDBMS を使う、というのを試してみました。
i18n-active_record という gem を使うようです。

まずはお試し用のプロジェクトを作成します。
RDBMS には MySQL を選択しました。

$ rails new i18n_backend_example -d mysql --skip-bundle
$ cd i18n_backend_example/

Gemfile を編集します。

$ vi Gemfile

i18n-active_record を追加。

gem 'i18n-active_record', git:'git://github.com/svenfuchs/i18n-active_record.git', require: 'i18n/active_record'

変更したら bundler を実行し、インストールを行います。

$ bundle install --path=vendor/bundle

インストールが完了したら、config/initializers/locale.rb というファイルを作成し、以下のように I18n.backend を初期化します。

require 'i18n/backend/active_record'
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)

データベースに存在しない key が指定された場合、従来通り YAML ファイルも見てくれるようにしたいので、ここでは I18n::Backend::Chain を使用しています。

次にマイグレーションファイルを作成します。
model ジェネレータを使用して Translation モデルを作成しても良いのですが、ここではマイグレーションファイルのみを作成しました。
バックエンドとして動作させるにはテーブルだけあればモデルは無くても問題ないようです。

$ rails g migration CreateTranslations

db/migrate 以下に生成されたマイグレーションファイルの内容を以下のように編集します。

class CreateTranslations < ActiveRecord::Migration
  def up
    create_table "translations", :force => true do |t|
      t.string   "locale"
      t.string   "key"
      t.text     "value"
      t.text     "interpolations"
      t.boolean  "is_proc",    :default => false
      t.datetime "created_at", :null => false
      t.datetime "updated_at", :null => false    
      t.timestamps
    end
  end

  def down
    drop_table :translations
  end
end

マイグレーション実行前に、config/database.yml のデータベース接続設定を環境に合わせて変更しておいて下さい。
準備が出来たらマイグレーションを実行します。

$ rake db:create && rake db:migrate

これでデータベース、テーブルが作成されたので翻訳データを投入してみます。
ここでは Translation モデルを作成していないので、dbconsole から SQL でデータを投入しました。

$ rails dbconsole
mysql> insert into translations(locale, `key`, `value`, created_at, updated_at) values("ja", "hoge", "ほげ", now(), now());

これで準備が出来ました。
rails console を起動して動作確認を行います。

$ rails c
Loading development environment (Rails 3.2.7)
irb(main):001:0> I18n.locale = :ja
=> :ja
irb(main):002:0> I18n.t(:hoge)
  I18n::Backend::ActiveRecord::Translation Load (0.3ms)  SELECT `translations`.* FROM `translations` WHERE `translations`.`locale` = 'ja' AND (`key` IN ('hoge') OR `key` LIKE 'hoge.%')
=> "ほげ"

Translations テーブルに投入した翻訳データが取得できていることが分かります。

次に、データベースに存在しない key が指定された場合に、YAML ファイルを見てくれているかを確かめるため、config/locales/ja.yml を作成します。

$ vi config/locales/ja.yml

データベースに存在しない key を設定しておきます。

ja:
  fuga: "ふが"

再度 rails console を起動し、先ほど YAML ファイルに追加したキーを指定してみます。

$ rails c
Loading development environment (Rails 3.2.7)
irb(main):001:0> I18n.locale = :ja
=> :ja
irb(main):002:0> I18n.t(:fuga)
  I18n::Backend::ActiveRecord::Translation Load (0.8ms)  SELECT `translations`.* FROM `translations` WHERE `translations`.`locale` = 'ja' AND (`key` IN ('fuga') OR `key` LIKE 'fuga.%')
=> "ふが"

YAML ファイルに設定した翻訳データが無事取得できました。

I18n::Backend::Chain を生成する際に先に I18n::Backend::ActiveRecord.new を指定しているので、データベースで検索が行われた後、見つからなかった場合に YAML ファイルの内容が検索されます。
両方に同じ key が存在している場合、 I18n::Backend::Chain に指定した順で検索が行われ、先に見つかった方が返されるようです。

実際に YAML 以外に翻訳データを持ちたい場合、RailsCasts にあるように KeyValueStore を使う方がパフォーマンス的に良い選択だと思いますが、使い慣れた RDBMS をそのまま使える点や、RDBMS 以外のミドルウェアを用意せずに使用できるのはお手軽で嬉しいですね。

参考:

6.1 Using Different Backends
http://guides.rubyonrails.org/i18n.html#using-different-backends

svenfuchs/i18n-active_record ・ GitHub
https://github.com/svenfuchs/i18n-active_record

Crowd Interactive Tech Blog :: Load i18n translations into your activerecord database
http://blog.crowdint.com/2012/07/26/load-i18-translations-into-your-activerecord-database.html

#256 I18n Backends - RailsCasts
http://railscasts.com/episodes/256-i18n-backends?language=ja&view=asciicast