Rails の i18n でどの言語を見せるか判断する

http://yurie.sexy/ のサイトの多言語化でいろいろ試行錯誤していましたがようやく落ち着いてきたので紹介してみます。そもそも i18n のやり方とか概念は他のページを参考にしてください。ここではどの locale で見せるかをどのように切り替えているかを紹介します。

## やりたいこと

基本方針は下記のようにします。

* params に locale が明示してあればその言語を使用する
* 明示してなければ環境変数 `HTTP_ACCEPT_LANGUAGE` を見に行き、最初に現れた locale を使用する（厳密にやるなら `q=` も見に行くべきかも）

ただし、いずれも、

* `I18n::available_locales` に存在しない locale の場合は無視する
* 判定できなかったときは `I18n.default_locale` をセット

とします。今回のケースで対応する言語は `:ja` と `:en` のみだったので、 `:ja` を見せるべきと判断できなかった人には全員 `:en` を出したいので、ぼくは日本人で日本人向けにつくったサイトだけれどもデフォルトは `:en` にしています。

## コード例

有効な言語とデフォルト言語をしっかり設定しておきます。

```rb:config/application.rb
    I18n.available_locales = %i(ja en)
    I18n.enforce_available_locales = true
    I18n.default_locale = :en
```

そして、 `ApplicationController` に下記のように追加しました。

```rb:app/controllers/application_controller.rb
  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 と解釈しています。

```rb:routes.rb
  scope '(/:locale)', constraints: { locale: /\w{2}/ } do
    get '/intro', controller: :home, action: :intro
    # :
    # :
  end
```

## View に各言語へのリンクをつける

ユーザビリティ、 SEO 対策のため、「このページの locale を変更したリンクをつけたい」ということがあると思います。下記のようにすればできました。

```rb
link_to('日本語', url_for(params.merge(locale: 'ja')))
```

ぼくは下記のようにしています。 HAML をそのまま載せてますが、 [Bootstrap の Navs と Dropdowns を組み合わせて](http://getbootstrap.com/components/#nav-dropdowns) 使っています。 `icon` は [FontAwesome::Sass](https://github.com/FortAwesome/font-awesome-sass) の機能です。

```haml:app/views/layouts/application.html.haml
%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 を書いてみました。

```rb:spec/requests/locale_spec.rb
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
```

もっといいやり方があればコメントで教えてください！

## 参考文献

* [Railsの多言語化対応 i18nのやり方を整理してみた！【国際化/英語化】 - 酒と泪とRubyとRailsと](http://morizyun.github.io/blog/i18n-english-rails-ruby-many-languages/)
* [Rails Internationalization (I18n) API — Ruby on Rails Guides](http://guides.rubyonrails.org/i18n.html)