Cookie を使ったロケールの切り替え

2009/01/04

前回では、サンプルアプリケーション asagao を国際化(i18n)するための第一歩を踏み出しました。

今回は、ロケールを切り替える機能を加えましょう。

まず、app/controllers/application.rbAVAILABLE_LOCALES という新たな定数を定義します。

class ApplicationController < ActionController::Base
  AVAILABLE_LOCALES = %w(en ja)

  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_asagao_session_id'
(省略)

値は、'en''ja' という 2 つの文字列からなる配列です。%w(en ja) という書き方は括弧の内側の文字列を空白文字で分割して配列にする特別な表記法です。

続いて、シングルトン・リソース locale のためのコントローラ locales を作ります。

> ruby script/generate controller locales show

このコントローラでユーザーのロケールを切り替えることにします。

config/routes.rb を編集して、locale リソースを登録します。

(省略)
  
  # マイアカウントコントローラ
  map.resource :account
  
  # マイロケールコントローラ
  map.resource :locale

  # 基本のURLパターン
  map.connect ':controller/:action/:id.:format'
  map.connect ':controller/:action/:id'
end

ついでに、Rails 2.0 で現れた新しい書き方でトップページへのルーティングを指定することにしましょう。

  map.connect '', :controller => 'main'

を、次のように書き換えます。

  map.root :controller => 'main'

こうすることで、root_path というメソッドが使えるようになります。


テスト駆動で実装していきましょう。

仕様の要点は次の通り:

  • ユーザーの現在のロケールは、クッキー変数 my_locale に文字列として格納する。
  • GET /locale で現在のロケールとその他のロケールのリストを表示する。
  • 各ロケールは HTML リンクであり、ユーザーがそのひとつをクリックすると /locale に対して locale パラメータを PUT する。
  • PUT /locale はクッキー変数 my_locale を書き換える。
  • PUT /locale には、ロケール選択ページの直前に表示されていたページの URL を referer パラメータで渡し、処理が終わったらその URL にリダイレクトする。

test/functionals/locales_controller_test.rb を開いてください。

require 'test_helper'

class LocalesControllerTest < ActionController::TestCase
  # Replace this with your real tests.
  test "the truth" do
    assert true
  end
end

Rails 2.2 式の新しいテストメソッドの書き方です。これは、次のように書くのと同じです。

require 'test_helper'

class LocalesControllerTest < ActionController::TestCase
  # Replace this with your real tests.
  def test_the_truth
    assert true
  end
end

少し読みやすくなったかもしれません。


では、仕様をテストに翻訳します。

require 'test_helper'

class LocalesControllerTest < ActionController::TestCase
  # 現在の自分のロケールを表示する。
  test "should show my locale" do
    get :show
    assert_response :success
    assert_equal 'ja', assigns(:my_locale)
  end

  # 現在の自分のロケールを変更する。
  # referer パラメータが指定されていなければ、トップページにリダイレクトする。
  test "should change my locale" do
    put :update, { :locale => 'en' }
    assert_equal ['en'], cookies['my_locale']
    assert_redirected_to root_path
  end

  # 現在の自分のロケールを変更する。
  # referer パラメータが指定されていれば、その URL にリダイレクトする。
  test "should change my locale with referer" do
    url = 'http://example.com/main/news'
    put :update, { :locale => 'en', :referer => url }
    assert_equal ['en'], cookies['my_locale']
    assert_redirected_to url
  end
end

ポイントは、機能テストにおけるクッキーの表現方法です。cookies['my_locale'] は OK ですが、cookies[:my_locale] は NG です。

また、cookies['my_locale'] は配列を返します。そこで比較対象は 'en' ではなく、['en'] となっています。

テストを実行して、ちゃんと失敗することを確認します。

Loaded suite test/functional/locales_controller_test
Started
EF
Finished in 0.069264 seconds.

  1) Error:
test_should_change_my_locale(LocalesControllerTest):
ActionController::UnknownAction: No action responded to update. Actions: 
exception, exception=, rescue_action_without_handler translation missing: 
ja, support, array, sentence_connector show
(省略)

  2) Failure:
test_should_show_my_locale(LocalesControllerTest)
    [test/functional/locales_controller_test.rb:8:in `test_should_show_my_locale'
     /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `__send__'
     /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `run']:
<"ja"> expected but was
<nil>.

続いて、locales コントローラを実装します。

class LocalesController < ApplicationController
  def show
    if AVAILABLE_LOCALES.include?(cookies[:my_locale])
      @my_locale = cookies[:my_locale]
    else
      @my_locale = I18n.default_locale.to_s
    end
  end

  def update
    if AVAILABLE_LOCALES.include?(params[:locale])
      cookies[:my_locale] = params[:locale]
    end
    if params[:referer]
      redirect_to params[:referer]
    else
      redirect_to root_path
    end
  end
end

ポイントは、配列(Array)のインスタンスメソッド include? の使い方です。

  if cookies[:my_locale] == 'en' || cookies[:my_locale] == 'ja'

と書く代わりに、次のように書いています。将来、ロケールの個数が増えるかもしれませんので、こう書くべきです。

  if AVAILABLE_LOCALES.include?(cookies[:my_locale])

テストが成功することを確認します。

> ruby -Itest test/functional/locales_controller_test.rb
Loaded suite test/functional/locales_controller_test
Started
...
Finished in 0.086196 seconds.

3 tests, 6 assertions, 0 failures, 0 errors

次に、HTML テンプレートを作成します。app/views/locales/show.html.erb を開きます。

Rails 2.0 から HTML テンプレートの拡張子は .html.erb となりました。

.erb の部分は、テンプレートエンジンの名前を示しており、ERB 以外のテンプレートエンジンが利用できることを示唆しています。

次のように修正します。

<h1><%= t('title.switch_language') %></h1>
<p><%= t('locales.select_language') %></p>
<ul>
<% ApplicationController::AVAILABLE_LOCALES.each do |locale| -%>
  <li>
    <%=
      link_to_unless(
        locale == I18n.locale.to_s,
        t('locales.language_name.' + locale),
        { :locale => locale, :referer => request.env['HTTP_REFERER'] },
        { :method => :put }) do
        "<strong>#{t('locales.language_name.' + locale)}</strong>"
      end
    %>
  </li>
<% end -%>
</ul>

app/views/shared/_menu_bar.rhtml を次のように修正します。

<% menu_items = [
  { :link => { :controller => '/main', :action => 'index' },
    :name => t('title.top') },
  { :link => { :controller => '/main', :action => 'activities' },
    :name => t('title.our_activities') },
  { :link => { :controller => '/main', :action => 'news'},
    :name => t('title.news') },
  { :link => { :controller => '/blog_entries', :action => 'index' },
    :name => t('title.blog') },
  { :link => locale_path, :name => t('title.switch_language') }
  ]
  if @current_user
    menu_items << { :link => { :controller => '/members',
                               :action => 'index',
                               :sort => nil, :group_id => nil },
                    :name => t('title.members') }
  end
  if @current_user and @current_user.administrator?
    menu_items << { :link => { :controller => '/admin/main',
                               :action => 'index' },
                    :name => t('title.administration') }
  end -%>
<div id='menu_bar'>
<% menu_items.each_with_index do |item, index| -%>
  <% if index > 0 %>&;&;&;|&;&;&;<% end -%>
  <%= menu_link_to item -%>
<% end -%>
</div>

cookies[:my_locale] の値に従って言語を切り替えるように、app/controllers/application.rb を修正します。

class ApplicationController < ActionController::Base
  AVAILABLE_LOCALES = %w(en ja)

  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_asagao_session_id'

  before_filter :select_locale
  before_filter :resume_session
  
  private
  # ロケールの選択
  def select_locale
    if AVAILABLE_LOCALES.include?(cookies[:my_locale])
      I18n.locale = cookies[:my_locale]
    end
  end
(省略)

最後に、翻訳ファイルを作成します。

config/locales/titles_en.yml

en:
  title:
    news: News
    top: TOP
    our_activities: Our Activities
    news: News
    blog: Blog
    switch_language: Switch Language
    members: Members
    administration: Administration

config/locales/titles_ja.yml

ja:
  title:
    top: TOP
    our_activities: 私たちの活動
    news: ニュース
    blog: ブログ
    switch_language: 言語の切り替え
    members: 会員名簿
    administration: 管理ページ

config/locales/locales_en.yml

en:
  locales:
    select_language: Please select your preferred language below.
    language_name:
      en: English
      ja: Japanese

config/locales/locales_en.yml

ja:
  locales:
    select_language: このサイトで使用する言語を次の中から選択してください。
    language_name:
      en: 英語
      ja: 日本語

本格的に翻訳作業を始めたときに整理がつきやすいよう、コントローラごとに翻訳ファイルを作ることにしました。


これで、簡単に日本語と英語の間でロケールを切り替えることができるようになりました。

翻訳されたasagaoのメニューバー

本日はここまで。