言語ネゴシエーション のバックアップの現在との差分(No.2)

更新


  • 追加された行はこの色です。
  • 削除された行はこの色です。
SIZE(22){COLOR(RED){このページの内容は非常に古いです(Rails 1.x.x)。最新の Rails では洗練された国際化の機構が標準で入っているため、下記は読むだけ無駄な内容になっています。}}

~

>>>> [[Rails4 向けの内容はこちら>ソフトウェア/rails/国際化]]

~

----

[[公開メモ]]

#contents

* Rails で言語ネゴシエーション(Language Negotiation) [#h05f99dd]

http://blog.omdb-beta.org/

を見ながら、ブラウザの Language Negotiation の機能を使って
rails アプリケーションの多言語化する方法を調べてみた。

やりたい内容としては
- views/*/*.html.ja.erb と views/*/*.html.en.erb とを両方作っておき、
自動的に切り替えて読み込む
- キャッシュされたページも apache のネゴシエーション機能を使って
自動で切り替わるようにする
元々の情報が古いもので、結局かなりの部分を1から書いたので、
ここにメモしておく。

rails アプリの多言語化については他にも、
** やりたいこと [#w57d45b9]

-- ネゴシエーションっぽいもの
- [[ロケールによるテンプレート切り替え>http://www.yotabanana.com/lab/?date=20060224#p01]]
- ネゴシエーションではなく Ruby-GetText を使った方法やネゴシエーションとの比較
-- [[Railsで日本語を使う時に必須のパッケージ Ruby-GetText>http://blog.masuidrive.jp/articles/2006/07/03/gettext]]
-- [[Ruby on RailsでRuby-GetText-Packageを使う>http://www.yotabanana.com/hiki/ja/ruby-gettext-howto-ror.html]]
-- [[ruby-gettext>http://tech.feedforce.jp/ruby-gettext.html]]
-- [[Rails で行こう! - Ruby on Rails を学ぶ>http://d.hatena.ne.jp/elm200/20070719/1184856941]]
-- [[Rails のためのものぐさな Web アプリケーションの国際化手法>http://d.hatena.ne.jp/secondlife/20070207/1170835130]]
- rails エラーメッセージの日本語化
-- [[RubyOnRails を使ってみる 【第 5 回】 ActiveHeart>http://jp.rubyist.net/magazine/?0012-RubyOnRails]]
-- /usr/share/rails/actionpack/lib/action_view/locale/xx.yml とか
+ views/*/*.html.ja.erb とか views/*/*.html.en.erb とか、言語ごとに
テンプレートファイルを作っておき、閲覧者の環境設定、手動選択に合わせて
自動的に言語別テンプレートを切り替えて使う
+ キャッシュされたページも apache のネゴシエーション機能を使って
自動で言語を切り替えて送信できるようにする
+ エラーメッセージなど、テンプレートファイルの切換で対応しきれない部分は
I18n.t を使って多言語化できるようにする

などがあるみたい。
の3点。

以下手順。
rails アプリケーションには、通常通り

* negotiation という名前のアプリケーションの作成 [#vb4e8dd6]
http://rails.server/controller~
http://rails.server/controller/action~
http://rails.server/controller/action/id~
http://rails.server/controller/action/id.html~

 LANG:console
 $ rails negotiation
 $ cd negotiation
などの他、

* 開発用サーバーの起動と環境の確認 [#s35cbbb4]
http://rails.server/controller.ja~
http://rails.server/controller/action.ja~
http://rails.server/controller/action/id.ja~
http://rails.server/controller/action/id.html.ja~

 LANG:console
 $ script/server &
 $ wget -qO- http://localhost:3000/ | html2text
  ...
  
  ****** Welcome aboard ******
  ***** You’re riding Ruby on Rails! *****
  **** About_your_application’s_environment ****
  ...
  
 $ wget -qO- http://localhost:3000/rails/info/properties | html2text
  Ruby version            1.8.7 (i486-linux)
  RubyGems version        1.3.2
  Rails version           2.2.2
  Active Record version   2.2.2
  Action Pack version     2.2.2
  Active Resource version 2.2.2
  Action Mailer version   2.2.2
  Active Support version  2.2.2
  Edge Rails revision     unknown
  Application root        /home/samba/www/rails/negotiation
  Environment             development
  Database adapter        sqlite3
  Database schema version 0
のように言語を指定した形でもアクセス可能とする。

* test コントローラとビューの作成 [#k0a5e25a]
言語指定の無いアドレスでアクセスされたときには
ブラウザとのネゴシエーションによって使うテンプレートを決める。

 LANG:console
 $ script/generate controller Test
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/test
      exists  test/functional/
      create  app/controllers/test_controller.rb
      create  test/functional/test_controller_test.rb
      create  app/helpers/test_helper.rb
 $ jed app/controllers/test_controller.rb
  class TestController < ApplicationController
    def index
    end
  end
 $ echo "Test" > app/views/test/index.html.erb
 $ wget -qO- http://localhost:3000/test
  Test
** 言語の優先順位 [#w3484f9d]

* 多言語化したビューの作成 [#b2db2fc3]
使用する言語の優先順位を決めるための情報源としては、

今作ったビューを削除して、日本語版と英語版を作成。
+ ユーザーが手動で選んだ言語を使う prams[:rails_language]
+ ユーザーが以前に手動で選んだ言語を使う cookie[:rails_language]
+ ブラウザの言語設定にある優先順位を使う header['Accept-Language']
+ アプリケーションのデフォルトの優先順位を使う ENV['RAILS_ACCEPTABLE_LANGUAGES']

まだ rails に手を入れていないので、そのままではエラーになる。
** キャッシュに対する apache2 の言語ネゴシエーション [#w6b05fcf]

 LANG:console
 $ rm app/views/test/index.html.erb
 $ echo "Test" > app/views/test/index.html.en.erb
 $ echo "テスト" > app/views/test/index.html.ja.erb
 $ wget -O- http://localhost:3000/test
  500 Internal Server Error
rails のページキャッシュ機能は、表示内容を .html ファイルとして保存する事で、
次回からは rails を通さず、apache で直接処理させるというもの。

これを正しく表示できるようにするのが目標。
rails を通らないので、行うこととしては、~
適切な名前でキャッシュファイルを作成する事と、~
apache2 にキャッシュファイルを正しく認識させる事と、~
の2つになる。

* config/routes.rb の設定 [#w47da434]
** その他の多言語化方法 [#f2bd1cf6]

多言語化後は、各ページに対して
rails アプリの多言語化については、これまで Ruby-GetText, I18n, ActiveHeart など、
開発者ごとに異なる方法で行われてきた。

http://localhost:3000/test~
http://localhost:3000/test.ja~
http://localhost:3000/test/index~
http://localhost:3000/test/index.ja~
http://localhost:3000/test/index/0~
http://localhost:3000/test/index/0.ja~
http://localhost:3000/test/index/0.html~
http://localhost:3000/test/index/0.html.ja~
そんな中、rails 2.2 から、rails 本体の推奨する多言語化手法が I18n に決まったようなので、
今後は I18n を用いるアプリケーションが増えると考えられる。

などの形でアクセスできるようにする。
多言語化の方法には大きく分けて次の2つがある。
+ 言語ごとに別のテンプレートファイルを用いる方法
+ テンプレートファイルは共通にして、中で現れる文言を1つずつその都度翻訳する

- .ja などの言語指定がない場合には、まずはブラウザが指定する言語で表示する。
- .ja などの言語指定があれば、その言語で表示する
- 一旦言語指定を行ったら、以降は言語指定がない場合にも直前に行った言語指定と
同じ言語で表示する
- 表示したい言語のテンプレートファイルが無ければ、ある物を使って表示する
Ruby-GetText はこの両方の機能を持っているが、~
I18n は 2. のみを行うもの。

受け入れ可能な言語のリストを ActionController::AbstractRequest に持たせることにする。
1. は 2. に比べて「国際化対応というのは異なる文化慣習への対応でもあるから、
単なる文言の修正では済まないこともある。テンプレートごと差し替えてしまえば
そういうケースでも対応が楽。」という利点があると言われる。

 ActionController::AbstractRequest.acceptable_languages= :ja, :en
逆に 1. は 2. に比べて、アプリケーションの手直しでテンプレートに変更が
必要になったとき、アプリケーション開発者が翻訳済みのテンプレートに手を
入れられずに、テンプレートがメンテナンス不可能になる事態が生じる。

これ以外の言語が指定されても無視される。
というような欠点もあると言われている。[[参照>http://www.yotabanana.com/lab/?date=20060224#p01]]

route の設定は、
開発者が翻訳者を兼ねている、~
ページキャッシュを行う、~
あたりを考慮すると 2. も良い物だと思うので、実装したのがこの記事。

 map.connect ':controller/:action/:id.:rails_language', 
                     :requirements => { :rails_language => /ja|en/ }
* 試行錯誤の経過 [#e232eb64]

のようにする。/^(ja|en)$/ ではなく /ja|en/ とするのが正しいらしい。
+ [[開発&テスト用環境の構築>ソフトウェア/rails/言語ネゴシエーション/開発用&テスト用環境の構築]]
-- negotiation という名前のアプリケーションの作成
-- 開発用サーバーの起動と環境の確認
-- test コントローラとビューの作成
+ [[routesの設定と言語選択の優先順位>ソフトウェア/rails/言語ネゴシエーション/routesの設定と言語選択の優先順位]]
-- 多言語化したビューの作成
-- config/routes.rb の設定
-- 言語が指定されていない時を含めた言語選択の優先順位]]
+ [[言語別テンプレートの選択>ソフトウェア/rails/言語ネゴシエーション/言語別テンプレートの選択]]
-- 言語の優先順位を元に利用するテンプレートを選択する]]
--- memoize について
-- テンプレートファイルの拡張子を正しく認識させる
+ [[キャッシュの多言語対応>ソフトウェア/rails/言語ネゴシエーション/キャッシュの多言語対応]]
-- ページキャッシュの多言語対応
-- ページキャッシュの多言語化に対応させるための apache の設定
-- アクションキャッシュの多言語対応
-- フラグメントキャッシュの多言語対応
+ [[プラグイン化と公開準備>ソフトウェア/rails/言語ネゴシエーション/プラグイン化と公開準備]]
-- プラグイン化
-- テストケース
-- fcgi 化
+ [[github を使った公開>ソフトウェア/rails/言語ネゴシエーション/github を使った公開]]
-- git リポジトリ(ローカル)の作成
-- プロジェクトを github に登録
-- git を使った編集作業例

そのためのコードを以下のように書いた。
* プラグインのダウンロード [#l2dd967e]

config/routes.rb
 LANG:ruby(linenumber)
 class ActionController::AbstractRequest
 
   def self.acceptable_languages=(v)
     @acceptable_languages = v.is_a?(Array) ? v : [v]
     class << @acceptable_languages
       def to_regexp
         Regexp.new join("|")
       end
     end
   end
 
   def self.acceptable_languages
     @acceptable_languages
   end
 
   def self.acceptable_language?(l)
     @acceptable_languages.include?(l.to_sym)
   end
 
 end
 
 ActionController::AbstractRequest.acceptable_languages= :ja, :en
 
 ActionController::Routing::Routes.draw do |map|
 
   lang_regexp= ActionController::AbstractRequest.acceptable_languages.to_regexp
 
   map.connect ':controller/:action/:id.:rails_language', 
                       :requirements => { :rails_language => lang_regexp }
   map.connect ':controller/:action.:rails_language', 
                       :requirements => { :rails_language => lang_regexp }
   map.connect ':controller.:rails_language', 
                       :requirements => { :rails_language => lang_regexp }
   map.connect ':controller/:action/:id'
 
   map.connect ':controller/:action/:id.:format.:rails_language',
                       :requirements => { :rails_language => lang_regexp }
   map.connect ':controller/:action/:id.:format'
 
 end
github からどうぞ。

** テスト [#s18f0fb5]
http://github.com/osamutake/RailsLanguageNegotiationPlugin/tree/master

 LANG:console
 $ jed app/controllers/test_controller.rb
  class TestController < ApplicationController
    def index
      render :text => params[:rails_language].inspect
    end
  end
 $ wget -qO- http://localhost:3000/test
  nil
 $ wget -qO- http://localhost:3000/test.ja
  "ja"
 $ wget -qO- http://localhost:3000/test.html.ja
  404: Not Found
 $ wget -qO- http://localhost:3000/test/index
  nil
 $ wget -qO- http://localhost:3000/test/index.ja
  "ja"
 $ wget -qO- http://localhost:3000/test/index.html.ja
  404: Not Found
 $ wget -qO- http://localhost:3000/test/index/0.html
  nil
 $ wget -qO- http://localhost:3000/test/index/0.html.ja
  "ja"
* プラグインの使い方 [#sb57c7b2]

* 言語が指定されていない時を含めた言語選択の優先順位 [#k5f2df0a]

優先順位は以下のようにする。

+ 指定された言語 (指定されていれば)
+ 前回指定された言語 (もしあれば session に保存しておく)
+ ブラウザから送られる HTTP_ACCEPT_LANGUAGE (もしあれば)
+ ActionController::AbstractRequest.acceptable_languages に記述された言語(記述された順)
* TODO [#ye7b917c]

この順で提供可能な言語(作成されている言語テンプレート)を検索して、
見つかった物を使って表示する。
** ページキャッシュ上での言語切換(Cookie 書き換え) [#o28ad6fc]

優先順位を決定するコードは以下の通り:
http://rails.server/controller.ja~
などの、言語指定の付いたアドレスで参照してページキャッシュが返される場合、
Cookie の書き換えが起きないので、表示言語の切換ができない。

config/environment.rb の末尾に
 LANG:ruby
 class ActionController::AbstractRequest
 
   # will be set by ActionView::Base._pick_template
   attr :language, true
 
   def language_order!
     @language_order= ! @env['HTTP_ACCEPT_LANGUAGE'] ? [] :
       @env['HTTP_ACCEPT_LANGUAGE'].split(",").collect {|l|
         l.gsub(/;.*$/, '').gsub(/-.*$/, '').downcase.to_sym
       }.delete_if { |l|
         !ActionController::AbstractRequest.acceptable_language?(l)
       }
     @language_order+= ActionController::AbstractRequest.acceptable_languages
     @language_order.unshift(session[:rails_language].to_sym) if session[:rails_language]
     @language_order.uniq!
   end
 
   def language_order
     @language_order ||= language_order!
   end
 
 end
 
 class ActionController::Base
   before_filter :set_language
   def set_language
     session[:rails_language]= params[:rails_language] if params[:rails_language] && 
           ActionController::AbstractRequest.acceptable_language?(params[:rails_language])
     request.language_order!
   end
 end
ページに JavaScript を埋め込んで、location.href に言語指定が含まれている時に
Cookie を書き換える事で対応できるはず。

** テスト [#h4a02e5d]
 <%= link_to "link text", { :rails_language=>:ja }, { :on_click="document.cookie='rails_language=ja'" } %>

 LANG:console
 $ jed app/controllers/test_controller.rb
  class TestController < ApplicationController
    def index
      render :text => request.language_order.inspect + "\n"
    end
  end
 $ wget -qO- http://localhost:3000/test
  [:ja, :en]
 $ wget -qO- --header="ACCEPT_LANGUAGE:en" http://localhost:3000/test
  [:en, :ja]
 $ wget -qO- --header="ACCEPT_LANGUAGE:ja" http://localhost:3000/test
  [:ja, :en]
 $ wget -qO- http://localhost:3000/test
  [:ja, :en]
 $ wget -qO- http://localhost:3000/test.en
  [:ja, :en]
 $ alias wgetc="wget -qO- --load-cookies=tmp/wget.cookie --save-cookies=tmp/wget.cookie --keep-session-cookies"
 $ wgetc http://localhost:3000/test
  [:ja, :en]
 $ cat tmp/wget.cookie
  # HTTP cookie file.
  # Generated by Wget on 2009-06-06 13:18:08.
  # Edit at your own risk.
  
  localhost:3000  FALSE   /       FALSE   0       _test_session   BAh7BiIKZmxhc2h...
 $ wgetc http://localhost:3000/test.en
  [:en, :ja]
 $ cat tmp/wget.cookie
  # HTTP cookie file.
  # Generated by Wget on 2009-06-06 13:20:42.
  # Edit at your own risk.
  
  localhost:3000  FALSE   /       FALSE   0       _test_session   BAh7BzoTcmFpbHN...
 $ wgetc http://localhost:3000/test
  [:en, :ja]
 $ wgetc http://localhost:3000/test.ja
  [:ja, :en]
 $ wgetc http://localhost:3000/test
  [:ja, :en]
のような感じ。

正しく切り換えができている事が分かる。
もちろん、ブラウザ側で javascript や cookie を off にされていれば
動作しないが、それは仕方のないところ。

ACCEPT_LANGUAGE や rails_language に不正な言語指定が送られても
はじくようにしてある。
<noscript> タグで javascript を on にするよう書いておくべき。

* 言語の優先順位を元に利用するテンプレートを選択する [#tdc62b56]
** ActionMailer のネゴシエーション [#c283a97a]

テンプレートの選択は ActionView::Base._pick_template で行われる。
こちらもしないと片手落ちか。

デフォルトではコントローラ名とアクション名を controller/action という形に
繋げたテンプレートファイル名に対してこの _pick_template_sub が呼ばれ、
以下の順で実際に適用するテンプレートファイルを見つけ出す。
参照 > http://d.hatena.ne.jp/kusakari/20090226/1235616295

テンプレートファイル名を明示的に指定して render が呼ばれた場合や、
レイアウトファイル、部分テンプレート(partial) に対する render でも、、
実際のテンプレートファイルを探すにはこの関数が呼び出される。
と思ったら、何もしなくても上記で対応しているみたい。

高速化のため、この関数が呼び出される時点で、
self.view_paths[file_name_without_extention] にテンプレートファイルの
一覧が作成されていて、.erb などの拡張子を除いたファイル名を与えると、
(もしテンプレートファイルが存在すれば)それを ActionView::Template 
オブジェクトとして取り出す事ができるようになっている。
テストケースを追加すべき。

template_format は routes で出てきた params[:format] のことで、~
wget -qO- http://localhost:3000/test/index/0.html~
なら "html" が、~
wget -qO- http://localhost:3000/test/index/0.xml~
なら "xml" が、~
wget -qO- http://localhost:3000/test/index/0.png~
なら "png" が、入っている。~
指定されていない場合には "html" になる。
* コメント [#kfcbe2ab]

以下は,テンプレートファイルを探す手順:
+ 与えられたファイル名から .erb などの拡張子を取り除く
+ #{template_file_name_without_extention}.#{template_format} があればそれを使う
+ template_file_name_without_extention があればそれを使う
+ partial からの呼び出しでは、ルートテンプレートの format_and_extention を使って~
#{template_file_name}.#{first_render.format_and_extension} があればそれを使う
+ フォーマットが js つまり JavaScript だったら、~
#{template_file_name}.html があればそれを使う
+ 以上で見つからなければ、app/views フォルダ以外にあるかもしれないので、
とりあえずそのまま与えられた文字列を ActionView::Template.new に渡してみる
気づいた点など、ぜひ突っ込みを入れて下さい。

言語ごとのテンプレートファイルを使うには、これらの他に .ja などを加えた
テンプレートも検索する事になる。もちろん request.language_order の順で。
#article_kcaptcha

_pick_template_sub は template_file_name に lang=".ja" を加えた
テンプレートを検索するヘルパー関数として作成した。~
lang="" とすれば、もとの _pick_template と同様の動作になる。

#{template_file_name}.#{first_render.format_and_extension} の検索について:~
first_render.format_and_extension は "html.erb" のようなものになる可能性があるので、
.ja をこの間に挟んで "html.ja.erb" を作らなければならない。~
first_render.format_and_extension が "html.ja.erb" の形をしている場合には、
以下のコードでは "html.ja.ja.erb" を探す事になってしまうけれど、実害はないはず。

memoize/unmemoize については次項を参照

 LANG:ruby(linenumber)
 class ActionView::Base
 
 private
 
   def _pick_template_sub(template_file_name, lang="")
     if template = self.view_paths["#{template_file_name}.#{template_format}#{lang}"]
       return template
     elsif template = self.view_paths["#{template_file_name}#{lang}"]
       return template
     elsif (first_render = @_render_stack.first) && first_render.respond_to?(:format_and_extension)
       m= first_render.format_and_extension.match(/(.*)(\.\w+)?$/)
       template = self.view_paths["#{template_file_name}.#{m[1]}#{lang}#{m[2]}"]
       return template
     end
     if template_format == :js && template = self.view_paths["#{template_file_name}.html#{lang}"]
       @template_format = :html
       return template
     end
     nil
   end
   memoize :_pick_template_sub
 
   unmemoize :_pick_template
   def _pick_template(template_path)
     return template_path if template_path.respond_to?(:render)
 
     path = template_path.sub(/^\//, '')
     if m = path.match(/(.*)\.(\w+)$/) && ActionView::Template.valid_extension?(m[2])
       template_file_name, template_file_extension = m[1], m[2]
     else
       template_file_name = path
     end
 
     # search for localized version
     if controller && controller.respond_to?(:request)
       controller.request.language_order.each do |lang|
         if template = _pick_template_sub(template_file_name, ".#{lang}")
           controller.request.language= lang
           return template
         end
       end
     end
 
     # search for not localized version
     if template = _pick_template_sub(template_file_name)
       template
     else
       # not found in view_paths
       template = ActionView::Template.new(template_path, view_paths)
 
       if self.class.warn_cache_misses && logger
         logger.debug "[PERFORMANCE] Rendering a template that was " +
           "not found in view path. Templates outside the view path are " +
           "not cached and result in expensive disk operations. Move this " +
           "file into #{view_paths.join(':')} or add the folder to your " +
           "view path list"
       end
 
       template
     end
   end
 
 end

** memoize について [#o48332bd]

_pick_template は効率化のため memoize されている。=> [[参照>http://wota.jp/ac/?date=20081025#p11]]

ところが多言語化した後は controller.request.language_order の値によって
_pick_template 関数の返す値が変化するため、_pick_template を memoize するわけに行かない。

効率を落とさないためには、代わりに _pick_template_sub を memoize すればいい。

ということで unmemoize/memoize を入れたのが上のコード。

ActiveSupport::Memoizable.unmemoize が無いので実装した。

 LANG:ruby(linenumber)
 module ActiveSupport
   module Memoizable
     def unmemoize(*symbols)
       symbols.each do |symbol|
         original_method = :"_unmemoized_#{symbol}"
         memoized_ivar = MEMOIZED_IVAR.call(symbol)
         class_eval <<-EOS, __FILE__, __LINE__
           raise "Not memoized #{symbol}" if !method_defined?(:#{original_method})
           undef #{symbol}
           alias #{symbol} #{original_method}
           #{memoized_ivar}= nil
         EOS
       end
     end
   end
 end

実際には上のコードでは _pick_template を上書きしてしまっているので unmemoize の必要はない。

上書きしたコードをもう一度 memoize し直すような用途には unmemoize をしておかないと
いけないのと、memoize されてないことを明示するため、unmemoize を入れた。

** テスト [#nb0c36b7]

 LANG:console
 $ jed app/controllers/test_controller.rb
  class TestController < ApplicationController
    def index
    end
  end
 $ echo "Test" > app/views/test/index.html.en.erb
 $ echo "テスト" > app/views/test/index.html.ja.erb
 $ rm tmp/wget.cookie
 $ wgetc -qO- http://localhost:3000/test
  テスト
 $ wgetc --header="ACCEPT_LANGUAGE:en" http://localhost:3000/test
  Test
 $ wgetc -qO- http://localhost:3000/test
  テスト
 $ wgetc -qO- http://localhost:3000/test.en
  Test
 $ wgetc http://localhost:3000/test
  Test
 $ wgetc --header="ACCEPT_LANGUAGE:ja" http://localhost:3000/test
  Test

** レイアウトに関するテスト [#j36ad09b]

 LANG:console
 $ echo "日本語: <%= @content_for_layout %>" > app/views/layouts/application.html.ja.erb
 $ echo "English: <%= @content_for_layout %>" > app/views/layouts/application.html.en.erb
 $ wgetc -qO- http://localhost:3000/test.ja
  日本語: テスト
 
 $ wgetc -qO- http://localhost:3000/test.en
  English: Test
  
** 部分テンプレートに関するテスト [#vc77e9b2]

 LANG:console
 $ echo "Test   <%= render :partial=>'ok' %>" > app/views/test/index.html.en.erb
 $ echo "テスト <%= render :partial=>'ok' %>" > app/views/test/index.html.ja.erb
 $ echo "OK"       > app/views/test/_ok.html.en.erb
 $ echo "ばっちり" > app/views/test/_ok.html.ja.erb
 $ wgetc -qO- http://localhost:3000/test.ja
  日本語:テスト ばっちり
  
  
 $ wgetc -qO- http://localhost:3000/test.en
  English: Test   OK
  
  

* テンプレートファイルの拡張子を正しく認識させる [#tdc62b56]

以上、とってもうまく行っているように見えるけど、実は問題がある。

 LANG:console
 $ wgetc --save-header -qO- http://localhost:3000/test.en
  HTTP/1.1 200 OK 
  Etag: "122cdccf079db5eebad7e19ccc3b311b"
  Connection: Keep-Alive
  Content-Type: html.en; charset=utf-8
  Date: Sat, 06 Jun 2009 06:31:12 GMT
  Server: WEBrick/1.3.1 (Ruby/1.8.7/2008-08-11)
  X-Runtime: 11ms
  Content-Length: 21
  Cache-Control: private, max-age=0, must-revalidate
  Set-Cookie: _test_session=BAh7BzoTcm...
  
  English: Test   OK
  
  
 $

どこが問題かというと、
 Content-Type: html.en; charset=utf-8

の部分。

使っているテンプレートファイル名が 
 test/index.html.en.erb 

なので、これが
 {test/}{index}.{html.en}.{erb}

のように分割されて、html.en の部分を mime-type として返してしまっている。

この部分を受け持っているのは ActionView::Template クラスで、変更すべきは
 {test/}{index}.{html}.{en}.{erb}

のように正しく切る、というところのみなのだが、実際に直すのは多少大がかりになる。

 LANG:ruby(linenumber)
 module ActionView
   class Template
     attr_accessor :language
 
     def initialize(template_path, load_paths = [])
       template_path = template_path.dup
       @base_path, @name, @format, @language, @extension = split(template_path)
       @base_path.to_s.gsub!(/\/$/, '') # Push to split method
       @load_path, @filename = find_full_path(template_path, load_paths)
 
       # Extend with partial super powers
       extend RenderablePartial if @name =~ /^_/
     end
 
     unmemoize :format_and_extension
     def format_and_extension
       (extensions = [format, language, extension].compact.join(".")).blank? ? nil : extensions
     end
     memoize :format_and_extension
 
     unmemoize :path
     def path
       [base_path, [name, format, language, extension].compact.join('.')].compact.join('/')
     end
     memoize :path
 
     unmemoize :path_without_extension
     def path_without_extension
       [base_path, [name, format, language].compact.join('.')].compact.join('/')
     end
     memoize :path_without_extension
 
     unmemoize :path_without_format_and_extension
     def path_without_format_and_extension
       [base_path, name].compact.join('/')
     end
     memoize :path_without_format_and_extension
 
     private
 
       # Returns file split into an array
       #   [base_path, name, format, language, extension]
       def split(file)
         if m = file.match(/^(.*\/)?([^\.]+)(?:\.(\w+)(?:\.(\w+)(?:\.(\w+)(?:\.(\w+))?)?)?)?$/)
           if m[6] # multi part format
             [m[1], m[2], "#{m[3]}.#{m[4]}", m[5], m[6]]
           elsif m[5]
             if ActionController::AbstractRequest.acceptable_language?(m[4])
               [m[1], m[2], m[3], m[4], m[5]]
             else # multi part format
               [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]]
             end
           elsif m[4] # no format
             if valid_extension?(m[4])
               if ActionController::AbstractRequest.acceptable_language?(m[3])
                 [m[1], m[2], nil, m[3], m[4]]
               else # Single format
                 [m[1], m[2], m[3], nil, m[4]]
               end
             else
                 [m[1], m[2], m[3], m[4], nil]
             end
           else
             if valid_extension?(m[3])
               [m[1], m[2], nil, nil, m[3]]
             elsif ActionController::AbstractRequest.acceptable_language?(m[3])
               [m[1], m[2], nil, m[3], nil]
             else
               [m[1], m[2], m[3], nil, nil]
             end
           end
         end
       end
   end
 end

split はかなり重たい関数なので memoize したくなるが、
実際には ActionView::Template のインスタンス自体が
ActionView::Base.view_paths にキャッシュされるので、
その必要は無い。

** テスト [#he34416a]

 LANG:console
 $ wgetc --save-header -qO- http://localhost:3000/test.en
  HTTP/1.1 200 OK 
  Etag: "122cdccf079db5eebad7e19ccc3b311b"
  Connection: Keep-Alive
  Content-Type: text/html; charset=utf-8
  Date: Sat, 06 Jun 2009 06:43:20 GMT
  Server: WEBrick/1.3.1 (Ruby/1.8.7/2008-08-11)
  X-Runtime: 13ms
  Content-Length: 21
  Cache-Control: private, max-age=0, must-revalidate
  
  English: Test   OK
  
  
今度はばっちり。

* ページキャッシュの多言語対応 [#z6cc2a4b]

キャッシュファイルは ActionController::Caching が作成する。
ファイルの名前を付ける部分で url_for が呼ばれているので、
この引数に :rails_language を渡せばよい。

 LANG:ruby(linenumber)
 module ActionController
   module Caching
       def cache_page(content = nil, options = nil)
         return unless perform_caching && caching_allowed
 
         path = case options
           when Hash
             url_for(options.merge(
                 :only_path => true, 
                 :skip_relative_url_root => true, 
                 :format => params[:format],
                 :rails_language => request.language))
           when String
             options
           else
             request.path
         end
 
          self.class.cache_page(content || response.body, path)
       end
   end
 end

** テスト [#mee8ebea]


Counter: 9532 (from 2010/06/03), today: 1, yesterday: 0