実行環境:
ruby 1.9.3
Rails 3.1.3
I18n
文字列を国際化する I18n の使い方のまとめです。
今回は、その2としてロケールの切り替えについて考えてみたいと思います。
表示ロケールを決定する流れ
複数のロケールを定義している場合、表示するロケールがアクセス毎に判断することになります。それをどうやって(どの情報を使って)判断するか、というお話です。
- ロケール値が明示的に指定されていたら、そのロケールで表示
- ロケール値を URL のクエリ(パラメータ)で指定
- ロケール値を URL のパス名で指定(実態はクエリと同じ)
- ロケール値をセッション情報で指定
- アクセス先のURLのドメイン名で判別(複数のドメイン名で運用している場合のみ)
- TLD(Top Level Domain)で判別
- サブドメインで判別
- 接続元の情報に応じて、適切なロケールで表示
- HTTP_ACCEPT_LANGUAGEから判断
- IPアドレスから判断
- (とりあえず)デフォルトのロケール(config.I18n.default_locale)で表示
1.〜4.の方法を適当に組み合わせてやります。(4.は方法と呼べるものでもないが。)
ミニマムにやるなら 1. と 4. 、ちゃんとやるんなら1. と 3. と 4.の組み合わせが一般的だと思います。
2. は「yoursite.com と yoursite.jp」(TLDで判別)、あるいは「ja.yoursite.com と fr.yousite.com]
(サブドメインで判別) など複数の(サブ)ドメイン名で運用している場合のみ可能な技です。
ロケールの切り替えのざっくりイメージは
- ミニマムにやる場合(1. と 4.)
- サイトに最初にアクセスするとデフォルトのロケールで表示
- 途中で日本語での表示に変更したいユーザーは日の丸アイコンをクリック
という感じ
- ちゃんとやる場合(1. と 3. と 4.)
- 接続元の情報から定義済みのロケールがあれば、そのロケールを使用して表示
- なければデフォルトのロケールで表示
- ユーザーが別の表示に変更したい場合は、表示したい言語の国旗アイコンをクリック
という感じ
ロケール値の受け渡し
複数のロケールをユーザーが切り替えられるようにするには、同じユーザーの連続したアクセス(同一セッション)ではロケール値を引き継げないといけません。じゃないと、一度ロケールを切り替えてもアクセスする度にロケールの設定が元に戻ってしまうことになります。
ロケール値の受け渡しの方法のあれこれ(もないけど…)
session情報(または cookie)でロケール値を受け渡す
現在のロケール値を session 情報に格納し、以降のアクセスについては session 情報から取り出して使用します。
ロケール値が切り替わった時に session 情報に格納
session[:locale] = I18n.locale
アクセスの度に session 情報から locale 値をセットする
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base
before_filter :restore_locale
private
def restore_locale
I18n.locale = session[:locale] if session[:locale]
end
end
同一セッション内で共有する情報なので、session 情報としてとっておく、というのはストレートな考え方です。やり方も簡単。
でもあまり推奨されないらしいです(^^;
RESTful な考え方だと同じURLでアクセスしたら基本的に表示される内容は同じであるべき、ってことらしい。
URLのクエリのパラメータとして毎回ロケール値を渡す
基本となるシンプルな考え方です。
アクセスする時のイメージはこんな感じ
URL の例
http://www.yoursite.com/topics?locale=ja
アクセスの度に params から locale 値をセットする
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base
before_filter :restore_locale
private
def restore_locale
I18n.locale = params[:locale] if params[:locale]
end
end
URLのパラメータで渡すということは、別のページに移動したりする際に毎回 locale の値をパラメータに含めてあげる必要があります。
#{RAILS.ROOT}/app/views/people/show.html.erb
<%= link_to people_path(:locale => I18n.locale) %>
と、こんな感じでいけるでしょう。
ですが、これをすべてのリンクに追加してまわるのはちょっとイケてないので、常にパラメータに locale を追加してもらえるように ApplicationController で設定をします。
#{#RAILS.RPPT}/app/controllers/application_controller.rb
def url_options
{ :locale => I18n.locale }.merge(super)
end
これでOKです。
でも URL に毎回「?locale=ja」とかくっついてあんまし格好よくなくね?と思った方は、もう一手間を加えて URL のパスの一部としてロケール値を渡すようにしましょう(次項へ続く)
URLのパスの一部でロケール値を渡す(実質的にはクエリで渡すのと同じ)
前項のパラメータでロケール値を渡すの続きです。Rails3の強力なルーティングの力を借りてクエリの形をしているパラメータをパスの一部の形に変えることができます。config/routes.rb をちょいと変更するだけです。
ルーティング変更前↓
#{RAILS.ROOT}/config/routes.rb
resources :people
ルーティング変更後↓
#{RAILS.ROOT}/config/routes.rb
scope "/:locale" do
resources :people
end
これで URL が以下のように変わります。
URL変更前↓
http://yoursite.com/people?locale=ja
URL変更後↓
http://yousite.com/ja/people
ルーティングの設定は、アクセスされたURLの解析だけでなく、Railsアプリケーション内でリンクを作る場合にも使用されるので <%= link_to 'People', people_path %> で作成されるパスも自動的にかわります(めちゃ便利!)
これで完璧ですぅ(^^)
ドメイン名(サブドメイン名)からのロケールの判定
ホスト名の一部からロケールとして使用する文字列を抜き出します。
TLD名からロケールを判定
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base
before_filter :set_locale
private
def set_locale
I18n.locale = extrace_locale_from_tld
end
def extract_locale_from_tld
request.domain.split('.').last
end
end
サブドメイン名からロケールを判定
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base
before_filter :set_locale
private
def set_locale
I18n.locale = extrace_locale_from_subdomain
end
def extract_locale_from_subdomain
request.subdomains.first
end
end
こんな感じでやればOKです。とりあえず引っ張ってきた文字列のロケールはRailsアプリケーション内に用意されているという前提です。そこんとこをチェックする場合は次項参照。
接続元に応じたロケールの判断
接続元の情報ですので HTTP_REQUEST の内容で判断します。
HTTP_ACCEPT_LANGUAGE で判断
HTTP_REQUEST の中の HTTP_ACCEPT_LANGUAGE の最初の言語名(=最初の2文字)をとってきます。
HTTP_ACCEPT_LANGUAGE にはカンマで区切って複数の言語が優先度とともに書いてあるので、優先度の高い順にロケールの準備の有り/無しを判定しながら決めていくのが筋のような気がしますが、ここでは省略。
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base
before_filter :set_locale
private
def set_locale
extracted_locale = extract_locale_from_accept_language
I18n.locale = (I18n::available_locales.include? extracted_locale.to_sym) ?
extracted_locale : I18n.default_locale
end
def extract_locale_from_accept_language
request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
end
end
I18n::available_locales 配列の中に定義済みのロケールのリストがあるので、そこと比較して定義されていればそのロケールを、なければデフォルトロケールを使用します。
IP アドレスで判断
HTTP_REQUEST の中のリモートIPアドレスをとってきます。
IPアドレスから国を判断する必要がありますが、MaxMind 社の GeoIP ライブラリが使えます。
ruby のライブラリや Apache モジュール(mod_geoip)、Gem も複数存在するようなので好みの方法を使用してください。
以下は
cjheath/geoip の Gem を使ったサンプルです。
#{RAILS.ROOT}/Gemfile
gem 'geoip'
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base
require 'geoip'
before_filter :set_locale
private
def set_locale
extracted_locale = extract_locale_from_ip
I18n.locale = (I18n::available_locales.include? extracted_locale.to_sym) ?
extracted_locale : I18n.default_locale
end
def extract_locale_from_ip
geoip ||= GeoIP.new(Rails.root.join(“lib/GeoIP.dat”))
country_location = @geoip.country(request.remote_ip)
country_location.country_code2.downcase
end
end
まとめ(=全部盛り)
ロケールの判定を上記の諸々をまとめてやる場合は以下のように書けばよいかと。上で説明したきたロケールの取得方法をそれぞれ準備してあげて、優先順位を決めて1つめの方法で取得できなければ、次の方法、次の方法…といった感じで extracted_locale 変数にセットします。取得したロケールが定義済みロケールのリスト(I18n::available_locales)にあるかどうかをチェックし、なければデフォルトを使う、という流れです。
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base
require 'geoip'
before_filter :set_locale
private
def set_locale
extracted_locale = params[:locale] ||
extract_locale_from_subdomain ||
extract_locale_from_tld ||
extract_locale_from_accept_language ||
extract_locale_from_ip
I18n.locale = (I18n::available_locales.include? extracted_locale.to_sym) ?
extracted_locale : I18n.default_locale
end
def extract_locale_from_subdomain
request.subdomains.first
end
def extract_locale_from_tld
request.domain.split('.').last
end
def extract_locale_from_accept_language
request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
end
def extract_locale_from_ip
geoip ||= GeoIP.new(Rails.root.join(“lib/GeoIP.dat”))
country_location = @geoip.country(request.remote_ip)
country_location.country_code2.downcase
end
end
必要のないものは抜いてくださいまし。