キャッシュの多言語対応 の履歴(No.1)
更新- 履歴一覧
- 差分 を表示
- 現在との差分 を表示
- ソース を表示
- ソフトウェア/rails/言語ネゴシエーション/キャッシュの多言語対応 へ行く。
- 1
ページキャッシュの多言語対応†
まずは rails そのままで、ページキャッシュ機能をONにしてみる。
LANG:console $ jed config/environments/development.rb config.action_controller.perform_caching = true $ jed app/controllers/test_controller.rb class TestController < ApplicationController caches_page :index end $ (restart script/server) $ ls public/test* ls: cannot access public/test*: そのようなファイルやディレクトリはありません $ wgetc http://localhost:3000/test English: Test OK $ ls public/test* public/test.html $ cat public/test.html English: Test OK $ wgetc http://localhost:3000/test.en English: Test OK $ ls public/test* public/test.html public/test.en
test.html というファイルができてしまうと、 次回からは言語設定にかかわらず常にこれが表示されてしまうため、うまくいかない。
それに、test.en というキャッシュファイルもいけてない。
期待するのは、test.html.en や test.html.ja というファイルができること。
ページキャッシュは ActionController::Caching::Pages が作成するので、 このときファイル名に言語名やフォーマット名を正しく付ければよい。
ファイル名に付ける言語指定を、path として指定された物にするか 実際に使ったテンプレートの言語にするか。 提供できない言語は何度訪れても提供できないので、 path として指定された物を付けるのが正しそう。
ちなみに cache_page が暗黙に呼び出される時、 ActionController::Caching::Pages.cache_page には引数が与えられないので、 キャッシュファイル名は request.path から作る事になる。
LANG:ruby(linenumber) module ActionController::Caching::Pages 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.accepts_languages.first)) when String options else p= request.path.split('.') p.pop if ActionController::AbstractRequest.acceptable_language?(p.last) p[0]+= self.class.page_cache_extension if p.count==1 p << request.accepts_languages.first p.join('.') end self.class.cache_page(content || response.body, path) end end
テスト†
LANG:console $ (restart script/server) $ rm -r public/test* $ wgetc http://localhost:3000/test English: Test OK $ ls public/test* public/test.html.en $ wgetc http://localhost:3000/test.en English: Test OK $ ls public/test* public/test.html.en $ wgetc http://localhost:3000/test.ja 日本語:テスト ばっちり $ ls public/test* public/test.html.en public/test.html.ja $ wgetc http://localhost:3000/test 日本語:テスト ばっちり $ ls public/test* public/test.html.en public/test.html.ja $ wgetc http://localhost:3000/test/index 日本語:テスト ばっちり $ ls public/test* public/test.html.en public/test.html.ja public/test: index.html.ja $ wgetc http://localhost:3000/test/index/0 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja public/test/index.html.ja public/test: index index.html.ja public/test/index: 0.html.ja $ wgetc http://localhost:3000/test/index/0.html 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja public/test/index.html.ja public/test: index index.html.ja public/test/index: 0.html.ja $ wgetc http://localhost:3000/test/index/0.html.ja 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja public/test/index.html.ja public/test: index index.html.ja public/test/index: 0.html.ja
ページキャッシュの多言語化に対応させるための apache の設定†
まずは rails を apache から cgi 経由で production 環境で動くように設定する。
LANG:console $ cp db/development.sqlite3 db/production.sqlite3 $ ln -s (negotiation app path)/public/ /var/www/negotiation $ wgetc http://localhost/negotiation | html2text ... ****** Welcome aboard ****** ***** You’re riding Ruby on Rails! ***** **** About_your_application’s_environment **** ... $ cat > public/.htaccess SetEnv RAILS_RELATIVE_URL_ROOT /negotiation SetEnv RAILS_ENV production SetEnv RAILS_ACCEPTABLE_LANGUAGES ja|en RewriteEngine On RewriteRule ^$ index.html [QSA] RewriteRule ^([^.]+)$ $1.html [QSA] RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ dispatch.cgi [QSA,L] ^D $ rm -r public/test* $ ls db development.sqlite3 $ wgetc http://localhost/negotiation/test 日本語: テスト ばっちり $ ls db development.sqlite3 production.sqlite3 $ ls public/test* public/test.html.ja $ wgetc http://localhost/negotiation/test.en English: Test OK $ ls public/test* public/test.html.en public/test.html.ja
DRY の原則になるべく従いたいので、
SetEnv RAILS_ACCEPTABLE_LANGUAGES ja|en
をここに書いて、routes.rb での設定を無効化している。
とはいえ、後に見るように ja en を書かなければならないところは どうしても他に出てくる。RoR の外側なので仕方のないところ。
以下ページキャッシュのテスト。
apache2 は mod_rewrite, mod_negotiation, mod_cgi, mod_env, mod_setenvif が有効になっている。
また、デバッグ用に apache の設定で RewriteLogLevel を 2 とかに設定しておく。
後で必ず 0 に戻す事。
LANG:console $ rm -r public/test* $ wgetc http://localhost/negotiation/test.ja 日本語: テスト ばっちり $ ls public/test* public/test.html.ja $ wgetc http://localhost/negotiation/test 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log [perdir (DOCROOT)/negotiation/] pass through (DOCROOT)/negotiation/test.html.ja $ wgetc http://localhost/negotiation/test.ja 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log [perdir (DOCROOT)/negotiation/] pass through (DOCROOT)/negotiation/dispatch.cgi $ wget -O- --header="Accept-Language: en" http://localhost/negotiation/test エラー 406: Not Acceptable $ wgetc http://localhost/negotiation/test.en English: Test OK $ ls public/test* public/test.html.en public/test.html.ja $ wgetc http://localhost/negotiation/test 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log [perdir (DOCROOT)/negotiation/] pass through (DOCROOT)/negotiation/test.html.ja $ wget -qO- --header="Accept-Language: en" http://localhost/negotiation/test English: Test OK $ wget -qO- --header="Accept-Language: ja" http://localhost/negotiation/test 日本語: テスト ばっちり
何点か問題が指摘できる。
test.ja 形式の呼び出しにキャッシュが効かない†
まず、public/test.html.ja が作成された後も http://localhost/negotiation/test.ja の読み込みがキャッシュファイルではなく cgi 呼び出しに rewrite されている。
これは、
LANG:Shell(linenumber) RewriteCond %{HTTP_ACCEPT} html RewriteCond %{SCRIPT_FILENAME} !\.html\. RewriteCond %{SCRIPT_FILENAME} (.*)\.([a-z][a-z])$ RewriteCond %1.html.%2 -f RewriteRule (.*)\.([a-z][a-z])$ $1.html.$2 [L]
というルールを追加してやる事でキャッシュを読ませる事ができる。
このルールは、
- ACCEPT ヘッダに text/html が含まれていて
- ファイル名が .html. を含まず
- ファイル名が .ja や .en のように、2文字の拡張子で終わっていて
- .ja などの前に .html を追加すると、同名のファイルが見つかる
- のであれば、.html を追加して、test.en のような名前を test.html.en に書き換える
という意味になる。
406 エラーの回避†
次に、public/test.html.ja があって public/test.html.en が無い 状況で wget -O- --header="Accept-Language: en" http://localhost/negotiation/test すると 406 エラーになる。
これは、RewriteCond %{REQUEST_FILENAME} !-f が public/test.html.ja に対して成立しないにもかかわらず、実際には言語指定が折り合わないため、 変装するデータが見つからないという状況のようだ。
まずエラーになるのを避けるため、
LanguagePriority ja en ForceLanguagePriority Prefer Fallback
としてやることで、望みの言語が見つからない場合には ja, en の順に探して、 見つかった物を返すようにした。
キャッシュミスを検出†
上記だけだと英語を要求したのに日本語の答えが返って来る。
正しい完全な動作とは言えないので、
LANG:shell(linenumber) RewriteCond %{HTTP:Accept-Language} !$2 RewriteCond %{HTTP:Accept-Language} ([a-z][a-z]) RewriteRule (.*\.html\.)([a-z][a-z]) $1%1
を追加した。
この意味は、test.html.ja を {test.html.}{ja} の形に分けたとき、 後ろの {ja} が Accept-Language に含まれていなければ、 Accept-Language から望みの言語 ([a-z][a-z]) を取り出して、 test.html. の後ろに付ける。
ただこれだと、es や fr を求めてくる閲覧者に対しては 常にキャッシュが効かないという状況が生じる。
また、Accept-Language を En とか JA とか、 大文字を混ぜて指定されるとやはりキャッシュが効かない。
ここの記述が無くても、テストの最後の部分で確かめたように、 public/test.html.en と public/test.html.ja とが両方あれば Accept-Language が正常に働くので、そのページを表示した最初の一人だけが 被害を被り、test.en の形で言語を指定すればちゃんと読める。
また、以下のように、一度 cookie で言語指定してしまえば、 次からはキャッシュミスをしっかり検出可能。
という事で、記述が無くてもそれほどの害はない。
パフォーマンスが問題になるようであれば この3行は省略した方が良い。
言語指定を記憶する†
もう一点、wgetc http://localhost/negotiation/test.en の後の wgetc http://localhost/negotiation/test 呼び出しで 日本語が表示されてしまっている部分に不具合がある。
これは、直前に選択した言語を apache が覚えていないせい(あたりまえ)。
SetEnvIf Cookie "rails_language=([a-z][a-z])" prefer-language=$1
とすることで、cookie の値に応じて apache に言語を選ばせることができる。
cookie の値は信用できないので、[a-z][a-z] にマッチする時に限って 値を採用する。
言語指定のある時にキャッシュミスを検出†
LANG:shell(linenumber) RewriteCond %{ENV:prefer-language} ^[a-z][a-z]$ RewriteCond %{ENV:prefer-language} !=$2 RewriteRule (.*\.html\.)([a-z][a-z]) $1%{ENV:prefer-language} [L]
とすることで、cookie に値があるときは他の言語のキャッシュがあっても 望みのファイルが無ければ dispatch.cgi を呼ぶようにした。
詳しく説明すると:
test.html.ja が存在して test.html.en が存在しない時、
prefer-language の設定にかかわらず、
test への呼び出しは apache のネゴシエーションで test.html.ja になってしまう。
これを検出して test.html.en に書き直すため、
- ファイル名が (.*\.html\.)([a-z][a-z]) の形で、
- prefer-language に正しそうな値が入っているにも関わらず
- ファイル名に付いている言語指定が prefer-language に含まれていなければ
- 言語指定を prefer-language の物に置き換える
というルールを追加している。
当然 test.html.en は存在しないので、結果的に dispatch.cgi に rewrite されて test.html.en が作られる事になる。
cookie が存在するとき、Accept-Language の指定は無視したいので、 RewriteRule に [L] を付けておく。
.htaccess の最終形†
最終的に、public/.htaccess の内容は
# rails setting SetEnv RAILS_RELATIVE_URL_ROOT /negotiation SetEnv RAILS_ENV production SetEnv RAILS_ACCEPTABLE_LANGUAGES ja|en # save preferential language in cookie SetEnvIf Cookie "rails_language=([a-z][a-z])" prefer-language=$1 # avoid error due to cache miss LanguagePriority ja en ForceLanguagePriority Prefer Fallback # rewrite start RewriteEngine On # default format of ".html" RewriteCond %{HTTP_ACCEPT} html RewriteCond %{SCRIPT_FILENAME} !\.html\. RewriteCond %{SCRIPT_FILENAME} (.*)\.([a-z][a-z])$ RewriteCond %1.html.%2 -f RewriteRule (.*)\.([a-z][a-z])$ $1.html.$2 [L] # rails' default rules RewriteRule ^$ index.html [QSA] RewriteRule ^([^.]+)$ $1.html [QSA] RewriteCond %{REQUEST_FILENAME} !-f # change this to adapt to your server RewriteRule ^(.*)$ dispatch.cgi [QSA,L] # detect cache miss for prefer-language RewriteCond %{ENV:prefer-language} ^[a-z][a-z]$ RewriteCond %{ENV:prefer-language} !=$2 RewriteRule (.*\.html\.)([a-z][a-z]) $1%{ENV:prefer-language} [L] # detect cache miss for accept-language # omit this section if performance matters RewriteCond %{HTTP:Accept-Language} !$2 [NC] RewriteCond %{HTTP:Accept-Language} ([a-z][a-z]) RewriteRule (.*\.html\.)([a-z][a-z]) $1%1
となった。
まさに黒魔術。
Mongrel を使うのであれば†
dispatch.cgi ではなく Mongrel に渡すのであれば
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
の部分を
RewriteRule ^/(.*)$ balancer://cluster%{REQUEST_URI} [P,QSA,L]
にすればよい?
これは冒頭の pdf の記述のままなのだけれど、、、未確認。
何か動かなさそう。
テスト†
LANG:console $ rm public/test* $ wget -qO- --header="Accept: text/html" --header="Accept-Language: en" http://localhost/negotiation/test English: Test OK $ ls public/test* public/test.html.en $ wget -qO- --header="Accept: text/html" --header="Accept-Language: en" http://localhost/negotiation/test English: Test OK $ tail /var/log/apache2/rewrite.log initial URL equal rewritten URL: (DOCROOT)/negotiation/test.html.en $ wget -qO- --header="Accept: text/html" --header="Accept-Language: ja" http://localhost/negotiation/test 日本語: テスト ばっちり $ ls public/test* public/test.html.en public/test.html.ja $ wget -qO- --header="Accept: text/html" --header="Accept-Language: ja" http://localhost/negotiation/test 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log initial URL equal rewritten URL: (DOCROOT)/negotiation/test.html.ja $ wget -qO- --header="Accept: text/html" --header="Accept-Language: en" http://localhost/negotiation/test $ tail /var/log/apache2/rewrite.log initial URL equal rewritten URL: (DOCROOT)/negotiation/test.html.en $ rm public/test* $ wget -qO- --header="Accept: text/html" http://localhost/negotiation/test.ja 日本語: テスト ばっちり $ ls public/test* public/test.html.ja $ wget -qO- --header="Accept: text/html" http://localhost/negotiation/test.ja 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log initial URL equal rewritten URL: (DOCROOT)/negotiation/test.html.ja $ wget -qO- --header="Accept: text/html" http://localhost/negotiation/test.en English: Test OK $ ls public/test* public/test.html.en public/test.html.ja $ wgetc --header="Accept: text/html" http://localhost/negotiation/test.en English: Test OK $ cat tmp/wget.cookie # HTTP cookie file. # Edit at your own risk. dora.bk.tsukuba.ac.jp FALSE / FALSE 0 _negotiation_session BAh7BiIKZm... dora.bk.tsukuba.ac.jp FALSE / FALSE 0 rails_language en $ rm public/test.html.en $ wgetc --header="Accept: text/html" http://localhost/negotiation/test English: Test OK $ ls public/test* public/test.html.en public/test.html.ja
アクションキャッシュ†
まずは rails そのままで、アクションキャッシュ機能をONにしてみる。
LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController # caches_page :index caches_action :index def index end end $ rm public/test.* $ (restart script/server) $ wgetc http://localhost:3000/test.en 404: Not Found $ tail log/production.log Processing TestController#index to html (for 127.0.1.1 at 2009-06-08 21:56:33) [GET] Parameters: {"rails_language"=>"en"} Cookie set: rails_language=en; path=/ ActionController::RoutingError (No route matches {:action=>"index", :format=>"en"}): /vendor/rails/actionpack/lib/action_controller/routing/route_set.rb:370:in `generate' /vendor/rails/actionpack/lib/action_controller/url_rewriter.rb:208:in `rewrite_path' /vendor/rails/actionpack/lib/action_controller/url_rewriter.rb:187:in `rewrite_url' /vendor/rails/actionpack/lib/action_controller/url_rewriter.rb:165:in `rewrite' /vendor/rails/actionpack/lib/action_controller/base.rb:626:in `url_for' /vendor/rails/actionpack/lib/action_controller/caching/actions.rb:144:in `initialize'
キャッシュ内容を保存する際に使うキーが生成できずにエラーになっている。
このあたりの処理は、ActionController::Caching::Actions::ActionCachePath で行われている。
LANG:ruby(linenumber) module ActionController::Caching::Actions class ActionCachePath attr_reader :language def initialize(controller, options = {}, infer_extension=true) if infer_extension and options.is_a? Hash request_extension = extract_extension(controller.request) options = controller.params.merge( options.reverse_merge( :format => request_extension, :rails_language => controller.request.accepts_languages.first)) end path = controller.url_for(options).split('://').last if infer_extension @extension = request_extension add_extension!(path, @extension) end @path = URI.unescape(path) end private def add_extension!(path, extension) if extension p= path.split('.') p.insert(-2, extension) unless path =~ /\b#{Regexp.escape(extension)}\b/ p.join('.') end end def extract_extension(request) p= request.path.split('.') # drop file name p.shift # drop language p.pop if !p.empty? && ActionController::AbstractRequest.acceptable_language?(p.last) extension = p.join('.') # If there's no extension in the path, check request.format extension = request.cache_format if extension=="" extension end end end
8〜11行目の部分、元のコードでは controller.params とマージせずに うまく行っているのだけど、このコードでは controller.params とマージしないと action や id の指定が無視されるようだったので、対症療法として入れてある。
後で要チェック
request.accepts_languages.first の言語テンプレートファイルが存在しない時、 キャッシュキーには request.accepts_languages.first で示される言語指定が 付くにもかかわらず、中身は提供可能な言語で表示された内容になる。 注意が必要ではあるが、恐らくこれは正しい動作。
テスト†
LANG:console $ (restart script/server) $ wgetc http://localhost:3000/test 日本語:テスト ばっちり $ less log/development.log Cached fragment miss: views/localhost:3000/test.ja (0.1ms) $ wgetc http://localhost:3000/test 日本語:テスト ばっちり $ less log/development.log Cached fragment hit: views/localhost:3000/test.ja (0.1ms) $ wgetc http://localhost:3000/test.ja 日本語:テスト ばっちり $ less log/development.log Cached fragment hit: views/localhost:3000/test.ja (0.1ms) $ wgetc http://localhost:3000/test.en English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test.en (0.1ms) $ wgetc http://localhost:3000/test.en English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ wgetc http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ wgetc http://localhost:3000/test/index English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ wgetc http://localhost:3000/test/index/0 English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test/index/0.en (0.1ms) $ wgetc http://localhost:3000/test/index/0.html English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test/index/0.en (0.1ms)
http://localhost:3000/test/index.en へのアクセスに対して
views/localhost:3000/test.en が、
http://localhost:3000/test/index/0.html.en へのアクセスに対して
views/localhost:3000/test/index/0.en が、
それぞれ引かれていることに注意。
url_for で変換し直している効果が現れている。
routes の記述順によっては逆に冗長な記述への変換(/test を /test/index へ、など) が起こってしまう場合があるので、routes では省略形をより上の方に書くのが良いようだ。
フラグメントキャッシュ†
まずは例によって rails そのままで、フラグメントキャッシュ機能を使ってみる。
LANG:console $ echo "<% cache do %>Test <%= render :partial=>'ok' %><% end %>" > app/views/test/index.html.en.erb $ echo "<% cache do %>テスト <%= render :partial=>'ok' %><% end %>" > app/views/test/index.html.ja.erb $ jed app/controllers/test_controller.rb class TestController < ApplicationController # caches_page :index # caches_action :index def index end end $ (restart script/server) $ wgetc -qO- http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test (0.1ms)
やっぱりうまく行っていない。
ここは、views/localhost:3000/test.en となってほしいところ。
これは ActiveSupport::Cache のテリトリーらしい。
LANG:ruby(linenumber) module ActionController module Caching module Fragments def fragment_cache_key(key) ActiveSupport::Cache.expand_cache_key( key.is_a?(Hash) ? url_for(key.reverse_merge( :rails_language=>request.accepts_languages.first) ).split("://").last : key, :views) end end end end
テスト†
LANG:console $ (restart script/server) $ wgetc http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test.en (0.1ms) $ wgetc http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ wgetc http://localhost:3000/test.en English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ echo "<% cache :param1=>:test do %>Test <%= render :partial=>'ok' %><% end %>" > app/views/test/index.html.en.erb $ wgetc http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test.en?param1=test (0.1ms)
うまく行っているようだ。