http://yurie.sexy/ のサイトの多言語化でいろいろ試行錯誤していましたがようやく落ち着いてきたので紹介してみます。そもそも i18n のやり方とか概念は他のページを参考にしてください。ここではどの locale で見せるかをどのように切り替えているかを紹介します。
やりたいこと
基本方針は下記のようにします。
- params に locale が明示してあればその言語を使用する
- 明示してなければ環境変数
HTTP_ACCEPT_LANGUAGE
を見に行き、最初に現れた locale を使用する(厳密にやるならq=
も見に行くべきかも)
ただし、いずれも、
-
I18n::available_locales
に存在しない locale の場合は無視する - 判定できなかったときは
I18n.default_locale
をセット
とします。今回のケースで対応する言語は :ja
と :en
のみだったので、 :ja
を見せるべきと判断できなかった人には全員 :en
を出したいので、ぼくは日本人で日本人向けにつくったサイトだけれどもデフォルトは :en
にしています。
コード例
有効な言語とデフォルト言語をしっかり設定しておきます。
I18n.available_locales = %i(ja en)
I18n.enforce_available_locales = true
I18n.default_locale = :en
そして、 ApplicationController
に下記のように追加しました。
before_action :set_locale
private
# I18n.locale をセットする
def set_locale
I18n.locale = locale_in_params || locale_in_accept_language || I18n.default_locale
end
# params の locale の値(優先すべき)
# @return [Symbol]
# params から取った locale
# 有効な値でなければ :en
# 取得できなかった場合 nil
def locale_in_params
if params[:locale].present?
params[:locale].to_sym.presence_in(I18n::available_locales) || I18n.default_locale
else
nil
end
end
# 環境変数 HTTP_ACCEPT_LANGUAGE を順に検証し、最初に一致した有効な locale を返す
# @return [Symbol] 環境変数 HTTP_ACCEPT_LANGUAGE から取った locale 。取得できなかった場合 nil
def locale_in_accept_language
request.env['HTTP_ACCEPT_LANGUAGE']
.to_s # nil 対策
.split(',')
.map{ |_| _[0..1].to_sym }
.select { |_| I18n::available_locales.include?(_) }
.first
end
# 全リンクに locale 情報をセットする
# @return [Hash] locale をキーとするハッシュ
def default_url_options(options = {})
{ locale: I18n.locale }
end
パスで locale を扱う
params[:locale]
に値が入ればその方法は問いませんが、今回は下記のようにしてみました。パスの最初に 2 文字のものがあればそれを locale と解釈しています。
scope '(/:locale)', constraints: { locale: /\w{2}/ } do
get '/intro', controller: :home, action: :intro
# :
# :
end
View に各言語へのリンクをつける
ユーザビリティ、 SEO 対策のため、「このページの locale を変更したリンクをつけたい」ということがあると思います。下記のようにすればできました。
link_to('日本語', url_for(params.merge(locale: 'ja')))
ぼくは下記のようにしています。 HAML をそのまま載せてますが、 Bootstrap の Navs と Dropdowns を組み合わせて 使っています。 icon
は FontAwesome::Sass の機能です。
%ul.dropdown-menu{role: 'menu'}
:ruby
locales = {
ja: '日本語',
en: 'English',
fr: 'Français'
}
- locales.each do |locale, label|
- label = (I18n.locale == locale ? icon('check') : '') + label
%li= link_to(label, url_for(params.merge(locale: locale)))
テストの例
Rspec の Request Spec を書いてみました。
require 'rails_helper'
describe 'I18n' do
context 'パスに言語指定がある場合' do
it 'その言語とする' do
get '/ja'
expect(I18n.locale).to eq :ja
end
it '無効な言語の場合はデフォルトの英語とする' do
get '/zh'
expect(I18n.locale).to eq :en
end
end
context '環境変数 HTTP_ACCEPT_LANGUAGE がある場合' do
it 'HTTP_ACCEPT_LANGUAGE の言語を使用する' do
get '/', nil, { HTTP_ACCEPT_LANGUAGE: 'ja' }
expect(I18n.locale).to eq :ja
end
it '国コードがついていても正しく判断できる' do
get '/', nil, { HTTP_ACCEPT_LANGUAGE: 'ja-JP' }
expect(I18n.locale).to eq :ja
end
it '複数の言語がある場合、最初に一致した有効な言語を使用する' do
get '/', nil, { HTTP_ACCEPT_LANGUAGE: 'zh,ja' }
expect(I18n.locale).to eq :ja
end
it '有効な言語がない場合、デフォルトの英語とする' do
get '/', nil, { HTTP_ACCEPT_LANGUAGE: 'zh,cn' }
expect(I18n.locale).to eq :en
end
it 'パスに言語指定もある場合、パスの指定を優先する' do
get '/ja', nil, { HTTP_ACCEPT_LANGUAGE: 'en' }
expect(I18n.locale).to eq :ja
end
end
context 'いずれもない場合' do
it 'デフォルトで英語になる' do
get '/'
expect(I18n.locale).to eq :en
end
end
end
もっといいやり方があればコメントで教えてください!