Rails で国際化 (I18n) に対応する

このガイドでは、アプリケーションを単一の「英語以外の言語」に翻訳する方法や、多言語対応を実現する方法について解説します。

このガイドの内容:

  • Ruby on Railsにおける国際化(I18n)のしくみ
  • Railsアプリケーションでロケールの設定・切り替えを行う方法
  • 訳文の編成方法、およびコントローラ/ビュー/モデルで訳文を利用する方法
  • Active RecordのエラーメッセージやAction Mailerのメールタイトルを国際化機能で翻訳する方法
  • バックエンドのカスタマイズ方法や、翻訳関連の例外を処理する方法

目次

  1. 国際化とローカライズ
  2. Ruby on RailsにおけるI18nのしくみ
  3. アプリケーションを国際化する
  4. リクエスト間でロケールを管理する
  5. I18n API機能の概要
  6. Rails組み込みの訳文を使う
  7. 高度な設定とセットアップ
  8. 関連リンク

1 国際化とローカライズ

国際化(Internationalization: I18n)とは、アプリケーションが複数の言語や地域ごとのフォーマットに対応できるように準備するプロセスのことです。具体的には、通常、文字列や、日付・通貨のフォーマットといったロケール(locale)固有の要素をアプリケーションのコードから切り離し、抽象化することを指します。

ローカライズ(Localization: L10n)とは、そのようにして抽象化された要素に対して、訳文や地域ごとのフォーマットを実際に提供するプロセスのことです。

Railsアプリケーションを国際化するには、一般的に以下の手順が必要です。

  • アプリケーションが国際化(I18n)に対応するように設定する
  • 翻訳ファイルの置き場所をRailsに指定する
  • リクエストごとにロケールの設定・保持・切り替えを行う

アプリケーションをローカライズするには、一般的に以下の手順が必要です。

  • アプリケーション内のテキストに対する訳文を用意する
  • 日付・時刻・通貨などのロケール固有のフォーマットを追加またはカスタマイズする
  • 翻訳データを整理・管理する

2 Ruby on RailsにおけるI18nのしくみ

Railsには、国際化(以下I18n)の基盤となるフレームワークを提供するRubyのi18n gemが標準で組み込まれています。本ガイドにおける「I18n API」とは、I18n.t、I18n.l、およびRailsの翻訳ヘルパーなど、RailsアプリケーションがI18nフレームワークを利用するためのインターフェイスを指します。このシステムの一環として、Active Recordのバリデーションメッセージや日時フォーマットなど、Rails自体に含まれる静的な文字列は、既に国際化に対応可能になっています。

RailsにはI18nのコア機能がデフォルトで含まれています。rails-i18nなどの追加gemを利用すれば、ロケールデータの追加やその他の機能拡張が可能になりますが、基本的なI18n機能を利用するうえでは必須ではありません。

I18n APIは、主にアプリケーション内でユーザーに表示されるテキストを翻訳することを目的としています。モデルのコンテンツ「自体」(データベースに保存されたブログ記事など)を翻訳する場合は、I18nとは異なるアプローチが必要となります。

RubyのI18n gemは、以下の2つの部分で構成されています。

  • I18nフレームワークのpublic API:ライブラリの動作を定義するpublicメソッドを持つRubyモジュール。

  • それらのメソッドを実装するデフォルトのバックエンド(Simpleという名前)。

APIのユーザーとしてはI18nモジュールのpublicメソッドのみを使うべきですが、バックエンドの機能についても知っておくと役立ちます。

標準のSimpleバックエンドを、より高機能なバックエンド(リレーショナルデータベースやGetText辞書などに翻訳データを保存するものなど)に置き換えることも可能です。後述の「バックエンドを切り替える」のセクションを参照してください。

I18n APIの最も重要なメソッドは以下の通りです。

  • translate: 指定されたキーとロケールに対応する訳文を探索します。tというエイリアスが定義されているので、I18n.t("store.title")のように簡潔に呼び出せます。また、このヘルパーは訳文が見つからない場合を検出して、その場合のエラーメッセージを<span class="translation_missing">で囲んで出力します。

  • localize: DateオブジェクトやTimeオブジェクトを地域のフォーマットにローカライズします。lというエイリアスが定義されているので、I18n.l(Time.now)のように簡潔に呼び出せます。

また、I18nモジュールの設定は、以下のような属性読み書きアクセサで変更できます。

I18n.load_path << Rails.root.join("config", "locales", "es.yml")
I18n.locale = :es
I18n.default_locale = :en
I18n.available_locales = [:en, :es]
I18n.enforce_available_locales = true
I18n.exception_handler = MyExceptionHandler.new
I18n.backend = I18n::Backend::Simple.new

3 アプリケーションを国際化する

本セクションでは、基本的なRailsアプリケーションを国際化します。

3.1 ロケールの辞書を作成する

Railsアプリケーションには、デフォルトでconfig/locales/ディレクトリと、その中にen.ymlロケールファイルが含まれています。このファイルは、英語ロケール用の訳文を格納したYAML形式の辞書です。

このディレクトリにあるデフォルトのen.ymlロケールファイルには、訳文のサンプルが含まれています。

en:
  hello: "Hello world"

:enロケールでhelloというキーに対応する訳文を検索すると、"Hello world"という文字列が返されます。

I18n.t("hello")
# => "Hello world"

なお、I18nライブラリではデフォルトのロケールとして英語が使われているため、ロケールが明示的に設定されていない場合、翻訳の検索には:enが使われます。現在のロケールを変更する方法については、「リクエスト間でロケールを管理する」を参照してください。

多くの国際的なアプリケーションでは、:cs(チェコ語)、:th(タイ語)、:es(スペイン語)のように、ロケールの「言語」要素だけを使っています。しかし、同じ言語グループ内であっても、地域による違いが重要になる場合があります。たとえば米国を表す:"en-US"ロケールでは通貨記号として$(ドル記号)を使いますが、英国を表す:"en-GB"ロケールでは£(ポンド記号)を使います。このような地域ごとの設定なども問題なく区別できます。この場合は、:"en-GB"の辞書で"English - United Kingdom"の完全なロケール定義を用意するだけでできます。

3.2 ロケールファイルの編成

i18nライブラリに標準で付属しているSimpleバックエンドを使う場合、辞書データは平文テキストファイルとしてディスク上に保存されます。アプリケーションのあらゆる部分の翻訳をロケールごとに1つのファイルにまとめると、言語や訳文が増えたときに管理が難しくなることがあるため、ロケールファイルを階層構造で整理するのが一般的です。

以下に示す階層構造はあくまで一例なので、アプリケーションに適した構造を選択してください。 単一のロケールファイルが肥大化したり、内容が把握しにくくなってきた場合は、ファイルをさらに分割してください。

たとえば、config/localesディレクトリには以下のようにファイルを配置できます。

|-defaults
|---es.yml
|---en.yml
|-models
|---book
|-----es.yml
|-----en.yml
|-views
|---defaults
|-----es.yml
|-----en.yml
|---books
|-----es.yml
|-----en.yml
|---users
|-----es.yml
|-----en.yml
|---navigation
|-----es.yml
|-----en.yml

こうすることで、モデル名やモデル属性名、ビュー内部のテキスト、日付・時刻フォーマットなどのグローバルなデフォルトを、それぞれ分離して管理できます(他のI18nライブラリ用ストアでは、別の方法で分離しているものもあります)。

3.3 ビューやコントローラで訳文を使う

以下はHomeControllerの例です。このコントローラのindexアクションはflashメッセージを設定し、app/views/home/index.html.erbテンプレートをレンダリングしています。

# config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
end
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = "Hello Flash"
  end
end
<!-- app/views/home/index.html.erb -->
<h1>Hello World</h1>
<p><%= flash[:notice] %></p>

このコードを国際化するには、文字列をRailsの#tヘルパー呼び出しに置き換えて、訳文のキーを指定します。見出しはビューで翻訳し、flashメッセージはコントローラ内で設定するときに翻訳している点にご注目ください。

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = t("home_flash")
  end
end
<!-- app/views/home/index.html.erb -->
<h1><%= t("home_title") %></h1>
<p><%= flash[:notice] %></p>

この状態でビューをレンダリングすると、"home_title"キーと"home_flash"キーの訳文が見つからないというエラーメッセージが表示されます。

<h1>
  <span class="translation_missing" title="translation missing: en.home_title">
    Home Title
  </span>
</h1>
<p>Translation missing: en.home_flash</p>

そこで、訳文を以下のようにロケールファイルに追加しましょう。

# config/locales/en.yml
en:
  home_title: Hello world!
  home_flash: Hello flash!
# config/locales/es.yml
es:
  home_title: ¡Hola mundo!
  home_flash: ¡Hola flash!

デフォルトのロケールは英語(en)なので、ページでは英語の文字列が表示されます。

Hello world!
Hello flash!

今度はデフォルトのロケールを以下のようにスペイン語(es)に設定すると、

# config/application.rb
config.i18n.default_locale = :es

レスポンスでめでたくスペイン語が表示されました。

¡Hola mundo!
¡Hola flash!

新しく追加したロケールファイルは、サーバーを再起動するまで反映されません。

3.4 訳文に変数を渡す

アプリケーションの国際化を成功させるには、ローカライズされたコードを抽象化するときに、そのロケールの文法規則について誤った先入観にとらわれないよう注意することが肝心です。あるロケールの基本的な文法規則(語順など)が、他のロケールでも同じとは限りません。

不適切な抽象化の例を以下に示します。この例では、訳文を構成する各部分の順序(ここではユーロ通貨記号€の表示位置)に関する誤った思い込みがあります。

<!-- app/views/products/show.html.erb -->
<%= "#{t('currency')}#{@product.price}" %>
# config/locales/nl.yml
nl:
  currency: "€ "
# config/locales/es.yml
es:
  currency: "€ "

@product.priceの値が100の場合、オランダ語(nl)の場合は"€ 100"と表示し、スペイン語(es)の場合は"100 €"と表示したいのですが、この抽象化ではオランダ語でもスペイン語でも"€ 100"と表示されてしまいます。

抽象化を正しく行うために、I18n gemには「変数の式展開」機能が含まれており、訳文定義で変数を使えるようにし、翻訳メソッドで変数に値を渡せるようにします。

適切な抽象化の例を以下に示します。

<!-- app/views/products/show.html.erb -->
<%= t('product_price', price: @product.price) %>
# config/locales/nl.yml
nl:
  product_price: "€ %{price}"
# config/locales/es.yml
es:
  product_price: "%{price} €"

この抽象化では、文法や約物(句読点や記号)がすべて定義自身の中で決定されているので、以下のように正しい訳文が出力されます。

I18n.t("product_price", price: 100, locale: :nl)
# => "€ 100"

I18n.t("product_price", price: 100, locale: :es)
# => "100 €"

Railsでは、数値や通貨の値をローカライズするnumber_to_currencyなどのヘルパーが提供されています。

defaultキーワードとscopeキーワードは予約されているため、変数名に使えません。これらのキーワードを使うとI18n::ReservedInterpolationKey例外が発生します。 ある訳文で式展開変数が期待されているにもかかわらず、#translateに変数の値が渡されない場合は、I18n::MissingInterpolationArgument例外が発生します。

3.5 日付・時刻フォーマットを追加する

時刻のフォーマットをローカライズするには、I18n.lにTimeオブジェクトを渡すか、Railsの#lヘルパーを使います(推奨)。:formatオプションを渡すことでフォーマットを指定できます。

<!-- app/views/home/index.html.erb -->
<h1><%= t("home_title") %></h1>
<p><%= flash[:notice] %></p>
<p><%= l(Time.now, format: :short) %></p>

たとえば、ロケールファイルで以下のように時刻の短縮表示用フォーマットを定義できます。

# config/locales/es.yml
es:
  time:
    formats:
      short: "%H:%M"

これでl(Time.current, format: :short, locale: :es)を実行すると、このフォーマットがスペイン語のロケールで使われるようになります。

フォーマット済みのタイムスタンプではなく、過去や未来を表すローカライズ済みフレーズを得るためのrelative_time_in_wordsヘルパーも提供しています。

<p><%= relative_time_in_words(3.minutes.from_now) %></p>
<p><%= relative_time_in_words(15.seconds.ago, include_seconds: true) %></p>

上を実行すると、以下のような時間表現の英語が表示されます。

in 3 minutes
less than 20 seconds ago

訳文は、デフォルトではdatetime.relativeスコープから検索され、distance_of_time_in_wordsで使われるものと同じ、ローカライズされた時間表現の文字列と組み合わされて表示されます。

I18nバックエンドが期待通りに動作するには、日付や時刻のフォーマットを追加する必要があるかもしれません。ただし、Railsのデフォルトフォーマットの訳文は、多くのロケールで既に用意されています。さまざまな既存のロケールファイルについては、rails-i18nリポジトリを参照してください。これらのファイルをconfig/locales/に配置すれば、アプリケーションで自動的に利用できるようになります。

3.6 ローカライズ済みビューテンプレートを表示する

個別の文字列を翻訳するだけでは追いつかないことがあります。ビューテンプレート全体がロケールごとに異なる場合、ローカライズ済みビューに切り替えてレンダリングできます。

たとえば、BooksControllerがあり、indexアクションを実行するとapp/views/books/index.html.erbテンプレートがレンダリングされるとします。 このビューテンプレートと同じディレクトリにindex.es.html.erbというローカライズ済みテンプレートを追加しておけば、ロケールが:esに設定されているときに、Railsはそのテンプレートをレンダリングします。一方、ロケールがデフォルトに設定されている場合は、汎用のindex.html.erbテンプレートがレンダリングされます。

<!-- app/views/books/index.html.erb -->
<h1>Books</h1>
<!-- app/views/books/index.es.html.erb -->
<h1>Libros</h1>

この機能は、YAMLやRubyの辞書に記述しきれないような大量の静的コンテンツを扱うときに有用です。ただし、ビューテンプレートに変更を加えた場合は、同じ変更をすべてのローカライズ版ビューテンプレートにも反映する必要がある点にご注意ください。

3.7 他のロケール向けの活用形ルールを設定する

Railsでは、英語以外の言語についても単数形・複数形のような活用形(inflection)を定義できます。複数の言語を対象とする活用形ルールはconfig/initializers/inflections.rbファイルで指定できます。このイニシャライザには英語の活用形の追加例が記載されており、他の言語についても同じ要領で活用形ルールを追加できます。

Railsには、英語の活用形ルールが既に含まれています。たとえば"person"の複数形をpluralizeで求めると"people"が返されます。

"person".pluralize
# => "people"

ポルトガル語(pt_br)のカスタム複数形は、以下のように追加できます。

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:pt_br) do |inflect|
  inflect.irregular "aluguel", "aluguéis"
end

これで、活用形を使うヘルパーでこのロケールのルールが使われるようになります。

"aluguel".pluralize(:pt_br)
# => "aluguéis"

地域ロケールをI18n.fallbacksと組み合わせて使う場合、活用形ルールをフォールバック先のロケールから適用することも可能です。 たとえば、英国英語en-GBのフォールバック先として以下のように:enが明示的にマッピングされていれば、en-GBで:enの活用形ルールを再利用できます。

config.i18n.fallbacks = { "en-GB": :en }

4 リクエスト間でロケールを管理する

アプリケーションで複数のロケールに対応するには、HTTPリクエストが開始されるたびにロケールを設定し、そのリクエストの処理中は一貫してそのロケールが維持されるようにする必要があります。

I18n.locale=やI18n.with_localeを使わない限り、すべての訳文でデフォルトのロケールが使われます。コントローラではI18n.with_localeを使うのが最も安全です。I18n.with_localeは通常はaround_action内で使われ、そのリクエストに対してのみロケールが適用されるようにします。

I18n.localeの設定がすべてのコントローラで揃っていないと、同じスレッドやプロセスによって処理される後続のリクエストでI18n.localeが漏出する可能性があります。たとえば、あるPOSTリクエストでI18n.locale = :esを実行すると、ロケールを設定していないコントローラへの以後のすべてのリクエストに影響します(ただし、その特定のスレッドやプロセスに限られます)。こうした理由から、I18n.locale =の代わりに、漏出が発生しないI18n.with_localeを利用することもできます。

ロケールは、アプリケーションがロケール情報をどこに保存しているか、どこからロケール情報を得ているかに応じて、さまざまな方法で設定できます。

4.1 ロケールをリクエストのクエリパラメータで設定する

ロケールの設定方法としてよく使われる方法の1つは、ロケールをリクエストのクエリパラメータに含めることです。

たとえば、ApplicationControllerで以下のようなロケール用のconcernをincludeできます。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Locale
end
# app/controllers/concerns/locale.rb
module Locale
  extend ActiveSupport::Concern

  included do
    around_action :switch_locale
  end

  def switch_locale(&action)
    locale = params[:locale] || I18n.default_locale
    I18n.with_locale(locale, &action)
  end
end

この例では、http://example.com/books?locale=ptのように、ロケールをURLクエリパラメータで設定しています。

この方法を使う場合、http://localhost:3000?locale=ptはポルトガル語(pt)のロケールでレンダリングされ、http://localhost:3000?locale=deはドイツ語(de)のロケールを読み込みます。

4.2 ロケールをクエリパラメータとして保持する

ロケールをURLから取得する場合、params[:locale]を読み取れるというだけでは実は不十分です。あるリクエストで選択していたロケールを次のリクエストへ引き継ぐには、生成されるURLにもロケールを含めておく必要があります。さもないと、生成されたリンクをユーザーがクリックすると、それまで選択していたロケール設定が失われてしまいます。

link_toやルーティングヘルパーを呼び出すたびにlocale: I18n.localeを手動で追加するのは、手間がかかるうえに作業漏れも生じやすくなります。Railsでは、この処理をdefault_url_optionsメソッドで一元的に管理できます。これを定義しておけば、url_forやそれを基盤とするルーティングヘルパーが、自動的に現在のロケールをURLに含めるようになります。

たとえば、ApplicationControllerに以下のように記述できます。

# app/controllers/application_controller.rb
def default_url_options
  { locale: I18n.locale }
end

これで、url_forを利用するすべてのヘルパーメソッド(例: root_pathやroot_urlなどの名前付きルーティングヘルパー、またはbooks_pathや books_urlのようなリソースルーティングヘルパー)は、クエリ文字列にhttp://localhost:3001/?locale=jaのようにロケールを自動的に含めるようになりました。

ここまでできれば十分だと思われるかもしれません。しかし、URLの末尾に?locale=jaのようなクエリパラメータが追加されると、ユーザーにとって読みにくいと思われる可能性もあります。また、ロケールは概念的に、パスの他の部分よりも上位に配置することが多いものです。

多くのアプリケーションでは、次のセクションで説明しているように、ロケールをパス自体に含める方が合理的です。

4.3 ロケールをURLパスに組み込む

たとえば、英語版ページのURLはhttp://www.example.com/en/books、オランダ語版ページのURLはhttp://www.example.com/nl/booksのように、ロケールをパスの一部に組み込みたい場合があります。

これは、前述の default_url_optionsメソッドによるロケール設定方法と、ルーティングのscopeメソッドを組み合わせることで実現できます。

# config/routes.rb
scope "/:locale" do
  resources :books
end

これで、現在のロケールが英語(en)の場合、books_pathヘルパーが/en/booksを返すようになります。 http://localhost:3001/nl/booksにアクセスするとロケールがオランダ語(nl)に設定され、そのリクエスト中にbooks_pathヘルパーを呼び出すと/nl/booksが返されます。

default_url_optionsの戻り値はリクエスト単位でキャッシュされるため、ロケールセレクタ用のURLは、ループの各イテレーションで対応するI18n.localeを設定しながらヘルパーを呼び出す方法では生成できません。I18n.localeを変更するのではなく、ヘルパーに:localeオプションを明示的に渡すか、request.original_fullpathを編集してください。

ルーティングでロケールを強制したくない場合は、省略可能なパススコープ(丸かっこ()で表します)を使えます。

# config/routes.rb
scope "(:locale)", locale: /en|nl/ do
  resources :books
end

この方法なら、ロケールを指定せずにhttp://localhost:3001/booksなどのリソースにアクセスしてもRouting Errorが発生しなくなります。これは、ロケールが指定されていない場合にデフォルトのロケールを使いたいときに便利です。

ロケールをURLパスに組み込む場合、アプリケーションのroot URL(通常は「ホームページ」や「ダッシュボード」など)の設定には特別な注意が必要です。routes.rb 内のroot to: "dashboard#index"宣言はロケールに対応していないので、http://localhost:3001/nlのようなロケールを含むroot URLはそのままでは利用できません。

そのため、以下のようにURLをマッピングする必要があります。

# config/routes.rb
get "/:locale" => "dashboard#index"

ルーティング定義が誤って他のルーティングにマッチすることのないよう、ルーティングの順序に十分注意してください。

パスに基づくロケール設定の要件が複雑になってきた場合、リクエストがRailsのルーティングに到達する前に、Rackミドルウェア層でロケールを抽出・処理できます。これにより、ロケール処理をルーティング層から切り離せるようになります。 通常、この手法が必要になるのは、リクエストパスを書き換える必要が生じた場合や、アプリケーションをロケール固有のプレフィックス配下にマウントする必要が生じたときだけです。

4.4 ロケールをドメイン名で設定する

アプリケーションが配置されているドメインやサブドメインを元にロケールを設定することも可能です。

ロケールをドメイン名に基づいて設定するには、ロケールごとに個別のドメインを用意する必要があります。 たとえば、ロケールを含まないwww.example.comドメインではデフォルトの英語ロケールを読み込み、www.example.esドメインではスペイン語(es)のロケールを読み込むといった具合です。

ロケールをドメイン名で設定することで、以下のメリットを得られます。

  • ロケールがURLの一部として明確に示される。
  • ユーザーはドメイン名を見るだけで、そのWebページの表示言語をすぐ理解できる。
  • 検索エンジンは、このようにドメイン別に異なる言語のコンテンツが置かれ、ドメイン同士が相互リンクしていることを好む。

このように設定するには、ApplicationControllerで以下のように実装します。

# app/controllers/concerns/locale.rb
around_action :switch_locale

def switch_locale(&action)
  locale = extract_locale_from_domain || I18n.default_locale
  I18n.with_locale(locale, &action)
end

# トップレベルドメインからロケールを取得する、なければ+nil+を返す
# ローカル環境で同じようにアクセスするには、/etc/hostsファイルに以下を記述する必要がある
#   127.0.0.1 application.com
#   127.0.0.1 application.it
#   127.0.0.1 application.pl
def extract_locale_from_domain
  parsed_locale = request.host.split(".").last
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

同じ要領で、ロケールをサブドメインを使って設定することも可能です。 この方法は多くの場合、ロケールごとに独自のトップレベルドメインを使うよりも、設定や運用が行いやすくなります。

# リクエストのサブドメインからロケールを取り出す(http://it.application.local:3000のような形式)
# この動作をローカルPCで行なうためには
# /etc/hostsファイルに以下のように記述する必要がある
#   127.0.0.1 it.application.local
#
# さらに、config/environments/development.rbに以下の設定を追加する必要もある
#   config.hosts << 'it.application.local:3000'
def extract_locale_from_subdomain
  parsed_locale = request.subdomains.first
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

アプリケーションにロケール切り替えメニューを取り付ける場合は、以下のような記述が使えるでしょう。

link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}")

ここではAPP_CONFIG[:deutsch_website_url]の部分にhttp://www.application.deのような値を設定すると仮定します。

ドメイン名からロケールを取得する方法には前述のメリットがありますが、ドメインごとに異なるローカライズ版(言語バージョン)を提供できない場合や、提供したくない場合もあるでしょう。そのような場合は、前述のように、ロケールをクエリパラメータやリクエストURLのパスに含める方法をお使いください。

ロケールをコントローラベースで抽出する方法は、シンプルなアプリケーションであれば問題なく使えます。production環境でロケールをホスト情報やパス情報から取得するアプリケーションの場合、Rackミドルウェアを使ってルーティングの前の段階でロケールを抽出し、Rack環境に保存できます。RailsアプリケーションのRackミドルウェアについて詳しくは、Rackガイドを参照してください。

4.5 ロケールをユーザーが自由に設定する

アプリケーションで認証されたユーザーに、好みのロケールをアプリケーションのインターフェイスで設定させることも可能です。この場合、ユーザーが選択したロケール設定をデータベースに保存しておいて、その情報を元にユーザーからの認証済みリクエストごとにロケールを設定します。

ユーザーが保存した設定を適用する前に、その値がI18n.available_localesに含まれていることを必ず確認しておきましょう。

# app/controllers/concerns/locale.rb
around_action :switch_locale

def switch_locale(&action)
  locale = Current.user&.locale
  locale = I18n.default_locale unless I18n.available_locales.map(&:to_s).include?(locale)

  I18n.with_locale(locale, &action)
end

アプリケーションが、URLやその他のリクエストに基づいてロケールを設定可能になっている場合は、どの値を優先するかを決定しておく必要があります。多くのアプリケーションでは、URLで明示的に指定されたロケールを、ユーザーが保存した設定よりも優先すべきです。

4.6 ロケールを暗黙で選択する

ロケールがリクエストで明示的に指定されていない場合、アプリケーションがロケールを推測しようとすることがあります。この処理は基本的にフォールバックとして扱うのがベストです。通常は、「URL」「ドメイン」「ユーザー設定」から得られる明示的なロケールを優先すべきです。

4.6.1 ロケールをLanguageヘッダーから推測する

Accept-Language HTTPヘッダーは、リクエストへのレスポンスで使いたい言語を示します。ブラウザは通常、このヘッダーの値をユーザーの言語設定に基づいて設定します。そのため、多くの場合ロケール推測時の最初の選択肢として最適です。

Accept-Languageヘッダーを使った簡単な実装は、たとえば以下のような感じになります。

# app/controllers/concerns/locale.rb
def switch_locale(&action)
  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
  locale = extract_locale_from_accept_language_header || I18n.default_locale
  logger.debug "* Locale set to '#{locale}'"
  I18n.with_locale(locale, &action)
end

private
def extract_locale_from_accept_language_header
  request.env["HTTP_ACCEPT_LANGUAGE"]&.scan(/^[a-z]{2}/)&.first
end

このコード例は意図的に簡略化してあります。ヘッダーには「複数の言語」「en-GBなどの地域サブタグ」「優先度を表すq値」などが含まれる可能性があるため、ほとんどの場合、実際のコードではこれよりも堅牢な解析処理が必要です。

accept_languageやlocale Rackミドルウェアなどのライブラリは、この処理をより確実に実装するのに役立ちます。

4.7 ロケールをIP地理情報から推測する

リクエストを送信するクライアントのIPアドレスは、クライアントの地理上の位置を推測するのに使えますが、ロケールを得るのに使えることもあります。GeoLite2 Countryなどのサービスや、geocoderなどのgemは、このアプローチの実装に利用できます。

ただしこのシグナルは、実際にはAccept-Languageヘッダーを利用する場合に比べて信頼性が低下します。ユーザーのネットワーク上の位置情報が必ずしもユーザーが望む言語と一致するとは限らず、IPアドレスに基づく特定は、VPN、携帯電話事業者、プロキシ、あるいは企業内ネットワークの影響を受ける可能性があります。

4.8 セッションやcookieに含まれるロケールを保存することについて

セッションやcookieは、特にURLやその他のリクエストの情報に明示的なロケールが含まれていない場合に、ユーザーが以前選択したロケールを保存するのに役立ちます。ただし、多くのアプリケーションでは、これらの情報をロケール決定の正式な情報源として扱うよりも、フォールバックの手段にとどめておく方が適切です。

ロケールは多くの場合、URLで透過的に扱えるようにする方式が最適です。これにより、アプリへのリンクを渡されたすべてのユーザーが、常に同じコンテンツを同じ言語で表示できるようになるからです。また、RESTfulな設計で一般に期待される振る舞いにも整合します。

5 I18n API機能の概要

以下のセクションでは、I18n.translateとtranslateビューヘルパーメソッドの両方の活用例を交えながら、I18n API について詳しく解説します。

5.1 訳文を取得する

5.1.1 基本的な参照、スコープ、ネストしたキー

訳文(translation)の取得は、キーを指定することで行います。文字列キーは"books.index.title"のようなキー表記との相性がよいので、デフォルトで使うのに適しています。

I18n.t "books.index.title"

translateメソッド(およびエイリアスのtメソッド)には:scope オプションも渡せます。スコープには、訳文キーの「名前空間」、すなわちスコープを指定する1個以上の追加キーを含められます。

I18n.t "record_invalid", scope: "activerecord.errors.messages"
# => "Validation failed: Name can't be blank"

上のコードは、Active Recordのエラーメッセージ辞書から"record_invalid"メッセージを検索します。

以下のように、キーとスコープをドット区切りのキーとしてまとめて指定することも可能です。

I18n.translate "activerecord.errors.messages.record_invalid"
# => "Validation failed: Name can't be blank"

したがって、以下の4つの呼び出しはすべて等価です。

I18n.t "activerecord.errors.messages.record_invalid"
# => "Validation failed: Name can't be blank"
I18n.t "errors.messages.record_invalid", scope: "activerecord"
# => "Validation failed: Name can't be blank"
I18n.t "record_invalid", scope: "activerecord.errors.messages"
# => "Validation failed: Name can't be blank"
I18n.t "record_invalid", scope: ["activerecord", "errors", "messages"]
# => "Validation failed: Name can't be blank"
5.1.2 デフォルト値

:defaultオプションでデフォルト値を指定すると、訳文が見つからない場合にこの値が返されます。

I18n.t "missing", default: "訳文なし"
# => '訳文なし'

:defaultの値にシンボルキーを指定しておくと、訳文が見つからないときにそのキーの訳文が使われるようになります。

デフォルト値に複数の値を指定することも可能です。その場合、最初に値が得られたものが返されます。 以下の例では、最初に"missing"キーで訳文の取得を試み、次に:also_missingキーで訳文の取得を試みます。どちらの訳文も取得できなかったので、デフォルトの"訳文なし"文字列が返されます。

I18n.t "missing", default: [:also_missing, "訳文なし"]
# => '訳文なし'
5.1.3 一括参照と名前空間参照

以下のようにキーを配列で渡すと、複数の訳文を一度に参照できます。

I18n.t ["odd", "even"], scope: "errors.messages"
# => ["奇数が必要です", "偶数が必要です"]

1個のキーを指定して、グループ化された訳文のハッシュを一括で取得することも可能です(ハッシュはネストする可能性があります)。

たとえば以下のコードは、「すべての」Active Recordエラーメッセージをハッシュとして受け取れます。

I18n.t "errors.messages"
# => {:inclusion=>"がリストに含まれていません", :exclusion=> ... }

訳文をハッシュで一括取得するときに訳文内でネストしている%{}式展開(interpolation)を有効にしたい場合は、パラメータにdeep_interpolation: trueオプションを渡す必要があります。 以下の辞書があるとします。

en:
  welcome:
    title: "Welcome!"
    content: "Welcome to the %{app_name}"

deep_interpolation: trueオプションを指定しないと、ネストした式展開%{}は処理されず、そのまま出力されます。

I18n.t "welcome", app_name: "book store"
# => {:title=>"Welcome!", :content=>"Welcome to the %{app_name}"}

I18n.t "welcome", deep_interpolation: true, app_name: "book store"
# => {:title=>"Welcome!", :content=>"Welcome to the book store"}
5.1.4 訳文キーの名前空間を省略する

Railsには、ロケールを「ビュー」内部で参照するときに便利な省略記法が実装されています。 以下のような辞書があるとします。

es:
  books:
    index:
      title: "Título"

app/views/books/index.html.erbテンプレートの内部では、以下のようにドット.で始まる".title"キーを書くだけでbooks.index.titleの訳文を得られます。

<%= t ".title" %>

パーシャル単位での自動スコープは、translateビューヘルパーでのみ利用可能です。

訳文取得の省略記法は、コントローラでも利用できます。

en:
  books:
    create:
      success: Book created!

これは、flashメッセージを簡潔に設定するのに便利です。

class BooksController < ApplicationController
  def create
    # ...
    redirect_to books_url, notice: t(".success")
  end
end

5.2 複数形にする

英語を含む多くの言語では、名詞の単数形は1種類、複数形も1種類だけです(例: "1 message"と"2 messages")。

その他の言語(アラビア語、日本語、ロシア語など多数)では文法がさまざまに異なっており、複数形の数が英語より多いものもあれば少ないものもあります。これらに対応するため、I18n APIでも柔軟な複数形化機能が用意されています。

:countという式展開変数には特殊な役割が与えられており、通常の訳文への式展開に使われるほかに、複数形化バックエンドで定義された複数形化ルールに沿って、個数に適した複数形を選択するのにも使われます。デフォルトでは、英語の複数形化ルールのみが適用されます。

I18n.backend.store_translations :en, inbox: {
  zero: "no messages", # optional
  one: "one message",
  other: "%{count} messages"
}
I18n.translate "inbox", count: 2
# => '2 messages'

I18n.translate "inbox", count: 1
# => 'one message'

I18n.translate "inbox", count: 0
# => 'no messages'

以下は、英語ロケール(:en)の複数形化アルゴリズムです。

lookup_key = :zero if count == 0 && entry.has_key?(:zero)
lookup_key ||= count == 1 ? :one : :other
entry[lookup_key]

つまり、ここでは:oneと表記されている訳語が単数形と見なされ、それ以外はすべて複数形と見なされます(ゼロも複数形と見なされます)。 ただし:zeroエントリが存在する場合は、countがゼロの場合に:otherではなく:zeroエントリが用いられます。

キーの探索で、複数形に適したハッシュが返されない場合は、I18n::InvalidPluralizationData例外が発生します。

5.2.1 ロケール固有のルール

I18n gemにはPluralizationバックエンドが用意されており、これを用いて特定のロケールに特化したルールを有効にできます。 これをSimpleバックエンドにincludeし、複数形化アルゴリズムをローカライズしたものをi18n.plural.ruleという形で訳文ストアに保存します。

I18n::Backend::Simple.include(I18n::Backend::Pluralization)
I18n.backend.store_translations :pt, i18n: {
  plural: { rule: lambda { |n| [0, 1].include?(n) ? :one : :other } }
}
I18n.backend.store_translations :pt, apples: {
  one: "one or none",
  other: "more than one"
}

I18n.t "apples", count: 0, locale: :pt
# => 'one or none'

または、rails-i18nという別のgemを用いて、ロケール固有の複数形化ルールのさらに充実したセットを提供する方法もあります。

5.3 ロケールの設定と受け渡し

ロケールは、擬似グローバルなI18n.locale(これはTime.zoneなどと同様にThread.currentを使います)に設定することも、#translateや#localizeのオプションとして渡すことも可能です。

i18n gemは、I18n.localeをThread.currentで保持します。同一スレッドで複数のリクエストを処理するアプリケーションでは、I18n.localeではなくI18n.with_localeなどを使って、各リクエストの前後で明示的にロケールを設定する必要があります。

ロケールを明示的に渡さない場合は、I18n.localeが使われます。

I18n.locale = :de
I18n.t "foo"    # de.foo を探索する
I18n.l Time.now # de.time.formats.default を探索する

ロケールを明示的に渡す場合は以下のようにします。

I18n.t "foo", locale: :br    # br.foo を探索する
I18n.l Time.now, locale: :es # es.time.formats.default を探索する

I18n.localeのデフォルトはI18n.default_localeであり、デフォルトのロケールは:en(英語)です。 デフォルトのロケールは以下のように設定できます。

I18n.default_locale = :de

5.4 安全なHTML訳文を使う

キー名をhtmlにするか、キー名の末尾に_htmlを追加すると、訳文の文字列は「HTML safe」とマーキングされます。

訳注: 具体的には、文字列でhtml_safe?を呼び出すとtrueを返すようになります。詳しくは「安全な文字列」を参照してください。

これらのキーをビューで使うと、訳文に含まれるHTMLはエスケープされません。

# config/locales/en.yml
en:
  welcome: <b>welcome!</b>
  hello_html: <b>hello!</b>
  title:
    html: <b>title!</b>
<!-- app/views/home/index.html.erb -->
<%= t('welcome') %>
<%= raw t('welcome') %>
<%= t('hello_html') %>
<%= t('title.html') %>

レンダリング結果は以下です。

&lt;b&gt;welcome!&lt;/b&gt;
<b>welcome!</b>
<b>hello!</b>
<b>title!</b>

訳文に含まれる式展開(interpolation)は必要に応じてエスケープされます。 以下のyamlがあるとします。

en:
  welcome_html: "<b>Welcome %{username}!</b>"

ユーザーが入力したusernameを安全に渡すには、以下のようにします。

<%# 安全: 必要に応じてエスケープされる %>
<%= t('welcome_html', username: @current_user.username) %>

逆に、安全(safe)とマーキングされた文字列を式展開に渡すと、エスケープされずに「そのまま」式展開されますのでご注意ください。

「HTML safe」とマーキングされた訳文テキストへの自動変換は、translate(またはt)ヘルパーメソッドでのみ可能です。このメソッドは、ビューとコントローラで利用できます。

6 Rails組み込みの訳文を使う

RailsのI18nは、I18n.tを直接呼び出す以外にも、フレームワークのさまざまな機能で利用されています。 本セクションでは、Active Record、Action Mailer、フォームヘルパー、その他のRailsヘルパーで使われている翻訳の規約について解説します。

6.1 Active Recordモデル

Model.model_name.humanメソッドやModel.human_attribute_name(attribute)メソッドを使うことで、モデル名と属性名の訳文を透過的に参照できるようになります。

たとえば以下のような訳文があるとします。

en:
  activerecord:
    models:
      user: Customer
    attributes:
      user:
        login: "Handle"
      # Userの"login"属性は"Handle"という語に翻訳される

この場合、User.model_name.humanは"Customer"を返し、User.human_attribute_name("login")は"Handle"を返します。

User.model_name.human
# => "Customer"

User.human_attribute_name("login")
# => "Handle"

以下のように、モデル名を複数形にしたものを訳文に加えることもできます。

en:
  activerecord:
    models:
      user:
        one: Customer
        other: Customers

これにより、User.model_name.human(count: 2)は複数形の"Customers"を返します。 count: 1または引数なしの場合、単数形の"Customer"を返します。

User.model_name.human(count: 2)
# => "Customers"

User.model_name.human
# => "Customer"

指定のモデル内のネストした属性にアクセスする必要が生じた場合は、訳文ファイルのモデルレベルでモデル/属性のようにネストして記述する必要があります。

en:
  activerecord:
    attributes:
      user/role:
        admin: "Admin"
        contributor: "Contributor"

これで、User.human_attribute_name("role.admin")は"Admin"を返します。

User.human_attribute_name("role.admin")
# => "Admin"

ActiveModelをincludeするクラスを使っており、かつActiveRecord::Baseを継承しない場合は、上述のキーパスのactiverecordをactivemodelに置き換えてください。

6.1.1 エラーメッセージのスコープ

Active Recordのバリデーションエラーメッセージも、I18nで簡単に訳文に置き換えられます。 Active Recordは、メッセージの訳文を置ける名前空間をいくつか提供しています。名前空間が複数あるのは、モデル・属性・バリデーションごとに異なるメッセージと訳文を提供できるようにするためです。また、単一テーブル継承(STI)も透過的に扱われます。

このしくみは、アプリケーションで必要に応じて最適なメッセージを柔軟に選択できる強力な手段となります。

以下のUserモデルでは、name属性でバリデーションが行われています。

class User < ApplicationRecord
  validates :name, presence: true
end

この場合、エラーメッセージのキーは:blankになります。この例では、以下のキーを記載順に探索し、最初に見つかったものを結果として返します。

activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

より抽象的に説明すると、以下のリストの順でマッチする最初のキーを返します。

activerecord.errors.models.[model_name].attributes.[attribute_name].[key]
activerecord.errors.models.[model_name].[key]
activerecord.errors.messages.[key]
errors.attributes.[attribute_name].[key]
errors.messages.[key]

モデルで継承を使っている場合は、メッセージ探索は継承チェインに対しても行われます。

たとえば以下のように、Userモデルを継承したAdminモデルがあるとします。

class Admin < User
  validates :name, presence: true
end

このとき、Active Recordは以下を上から順にメッセージを探索します。

activerecord.errors.models.admin.attributes.name.blank
activerecord.errors.models.admin.blank
activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

以上のように、モデル継承チェインのさまざまな場所や、属性・モデル・デフォルトの各スコープで使われている多数のエラーメッセージに対して、専用の訳文を提供できます。

6.1.2 エラーメッセージ内での式展開

翻訳されたモデル名・属性名・値は、それぞれmodel、attribute、valueという名前でいつでも訳文内の式展開%{}に使えます。

たとえば、"can't be blank"というデフォルトのエラーメッセージの代わりに"Please fill in your %{attribute}"のように属性名を展開できます。

以下の表で式展開の列にcountが記載されている行の項目では、countの値に応じて複数形化を行えます。

バリデーション 利用可能なオプション メッセージ 式展開
confirmation - :confirmation attribute
acceptance - :accepted -
presence - :blank -
absence - :present -
length :within, :in :too_short count
length :within, :in :too_long count
length :is :wrong_length count
length :minimum :too_short count
length :maximum :too_long count
uniqueness - :taken -
format - :invalid -
inclusion - :inclusion -
exclusion - :exclusion -
associated - :invalid -
non-optional association - :required -
numericality - :not_a_number -
numericality :greater_than :greater_than count
numericality :greater_than_or_equal_to :greater_than_or_equal_to count
numericality :equal_to :equal_to count
numericality :less_than :less_than count
numericality :less_than_or_equal_to :less_than_or_equal_to count
numericality :other_than :other_than count
numericality :only_integer :not_an_integer -
numericality :in :in count
numericality :odd :odd -
numericality :even :even -
comparison :greater_than :greater_than count
comparison :greater_than_or_equal_to :greater_than_or_equal_to count
comparison :equal_to :equal_to count
comparison :less_than :less_than count
comparison :less_than_or_equal_to :less_than_or_equal_to count
comparison :other_than :other_than count

6.2 Action Mailerメールの件名を訳文に置き換える

mailメソッドに件名が渡されなかった場合、Action Mailerは既存の訳文の利用を試みます。 探索するときのキーは、<メーラーのスコープ>.<アクション名>.subjectというパターンで構築されます。

# user_mailer.rb
class UserMailer < ActionMailer::Base
  def welcome(user)
    #...
  end
end
en:
  user_mailer:
    welcome:
      subject: "Welcome to Rails Guides!"

訳文に式展開用のパラメータを渡したい場合は、メーラー内でdefault_i18n_subjectメソッドを使います。

# user_mailer.rb
class UserMailer < ActionMailer::Base
  def welcome(user)
    mail(to: user.email, subject: default_i18n_subject(user: user.name))
  end
end
en:
  user_mailer:
    welcome:
      subject: "%{user}, welcome to Rails Guides!"

6.3 フォームヘルパー

フォームヘルパーでラベルやエラーメッセージを生成すると、モデルや属性の訳文が使われます。 たとえば、明示的にラベルを指定しなかった場合、form_withヘルパーはラベルのテキストとしてhuman_attribute_nameを自動的に呼び出します。

<%= form_with model: @user do |form| %>
  <%= form.label :login %>
  <%= form.text_field :login %>
<% end %>

訳文は以下だとします。

en:
  activerecord:
    attributes:
      user:
        login: "Handle"

ラベルは以下のようにレンダリングされます。

<label for="user_login">Handle</label>

6.4 Action Viewのヘルパーメソッド

Action Viewヘルパーの多くは、日付・時刻・数値・通貨のフォーマットでロケールデータを利用します。

  • distance_of_time_in_words(期間表現): 訳文への置き換えと複数形化を行い、秒・分・時などの数値を式展開します。

    distance_of_time_in_words(Time.current, Time.current + 3.minutes)
    # => "3 minutes"
    

    訳語はdatetime.distance_in_wordsスコープにあるものが使われます。

  • relative_time_in_words(相対期間表現): distance_of_time_in_wordsを元に構築されており、その時間が過去か未来かによってローカライズ版の接頭語や接尾語を追加します。

    relative_time_in_words(3.minutes.from_now)
    # => "in 3 minutes"
    relative_time_in_words(3.minutes.ago)
    # => "3 minutes ago"
    

    訳語はdatetime.relativeスコープにあるものが使われます。

  • datetime_selectとselect_month(セレクトボックス): 月名を訳語に置き換え、生成されたselectタグで展開します。

    <%= select_month(Date.new(2024, 3, 1)) %>
    

    レンダリングされる<option>ラベルには、date.month_namesスコープにある月名の訳語が使われます。 datetime_selectヘルパーの「年・月・日」の表示順は、(オプションを明示的に指定しない限り)date.orderスコープにある順序オプションを参照します。 日付セレクトボックス用のすべてのヘルパーでは、datetime.promptsスコープにある訳語がプロンプトのテキストに使われます(該当する場合)。

  • number_to_currency、number_with_precision、number_to_percentage、number_with_delimiter、number_to_human_sizeヘルパーの数値フォーマットには、numberスコープにある数値フォーマットが使われます。

    number_to_currency(10)
    # => "$10.00"
    

6.5 Active Modelのメソッド

Active Modelの「モデル名」「属性名」「バリデーションエラーメッセージ」は、人間にとって読みやすくするための訳語への置き換えに対応しています。

  • model_name.humanとhuman_attribute_name: activerecord.modelsスコープやactiverecord.attributesスコープに訳語が存在する場合は、モデル名や属性名を訳語に置き換えます。

    User.model_name.human
    # => "Customer"
    

    エラーメッセージのスコープで既に述べたように、継承されたクラス名(STIで使われる場合など)も訳語の置き換えに対応しています。

  • ActiveModel::Errors#generate_message: 内部でmodel_name.humanやhuman_attribute_nameを呼び出しています。 (このメソッドはActive Modelのバリデーションで使われますが、手動で呼び出すことも可能です)

    user.errors.generate_message(:name, :blank)
    # => "can't be blank"
    

    エラーメッセージのスコープで既に述べたように、エラーメッセージや継承されたクラス名についても訳語の置き換えに対応しています。

  • ActiveModel::Error#full_messageとActiveModel::Errors#full_messages: エラーメッセージの冒頭に属性名を追加します。このときのフォーマットはerrors.formatから参照されます(デフォルト: "%{attribute} %{message}")。

    user.errors.full_messages
    # => ["Name can't be blank"]
    

    デフォルトのフォーマットをカスタマイズするには、アプリのロケールファイルでオーバーライドします。 個別のモデルや属性ごとにフォーマットをカスタマイズする方法については、config.active_model.i18n_customize_full_messageを参照してください。

6.6 Active Supportのメソッド

Active Supportにも、ロケール固有のフォーマットルールを利用するヘルパーがあります。

  • Array#to_sentence: support.arrayスコープにあるフォーマット設定を利用します。

    ["apples", "oranges", "pears"].to_sentence
    # => "apples, oranges, and pears"
    

7 高度な設定とセットアップ

7.1 訳文の読み込みパスとデフォルトのロケールを設定する

Railsは、config/locales/ディレクトリに配置された訳文ファイルを、自動的に訳文用の読み込みパスに追加します。デフォルトでは.rbファイルと.ymlファイルが利用可能で、多くのアプリケーションではこれで十分です。

Rails自身の訳文ファイルも同じ要領で配置されています。 たとえば、activemodel/lib/active_model/locale/en.ymlにあるActive Modelのバリデーションメッセージや、activesupport/lib/active_support/locale/en.ymlにある日付・時刻のフォーマットなどを参照してください。 デフォルトのSimpleバックエンドを利用している場合、訳文データはYAMLファイルまたは通常のRubyハッシュとして保存できます。

訳文の読み込みパス(I18n.load_path)は、Railsが自動的に読み込む訳文ファイルのリストです。ディレクトリ構造や命名規則を変更したい場合は、このパスをカスタマイズできます。

これらの訳文データは、訳文が最初に参照されたタイミングでバックエンドによって遅延読み込みされます。バックエンドの実装は、必要に応じて後から切り替えることも可能です。

訳文ファイルを追加したり、デフォルトのロケールを変更したりするには、config/application.rb設定ファイルで以下のようにconfig.i18nを設定します。

config.i18n.load_path += Dir[Rails.root.join("my", "locales", "*.{rb,yml}")]
config.i18n.default_locale = :de

読み込みパスは、訳文が参照される前に設定されている必要があります。

I18nの設定を直接扱う必要がある場合は、たとえば以下のようにイニシャライザで設定することも可能です。

# config/initializers/locale.rb

# I18nライブラリに訳文の探索場所を指示する
I18n.load_path += Dir[Rails.root.join("lib", "locale", "*.{rb,yml}")]

# アプリケーションでの利用を許可されているロケールのリストを渡す
I18n.available_locales = [:en, :pt]

# デフォルトロケールを:en以外に変更する
I18n.default_locale = :pt

アプリケーション設定ファイルでは、config.i18n.*で設定することが推奨されます。 I18n.load_pathに直接パスを追加する場合、外部のgemによる訳文は同じ方法では上書き「できません」。

訳文データをSimpleバックエンドで保存する場合、YAMLファイル(.yml)または通常のRubyファイル(.rb)を利用できます。 YAMLフォーマットは多くのRailsアプリケーションで採用されていますが、ホワイトスペースや特殊文字の扱いがデリケートなので、構文ミスがあるとロケールファイルが正しく読み込まれないことがあります。

構文エラーを早期に検出したい場合や、ロケールデータでlambdaなどの値を使う必要がある場合には、Rubyのロケールファイルが便利です。

訳文データをYAMLファイルに保存する場合、誤動作防止のため、一部のキーについては引用符で囲んでエスケープする必要があります。 エスケープが必要なキーは以下の通りです。

  • true, on, yes
  • false, off, no

エスケープの例:

# config/locales/en.yml
en:
  success:
    'true':  'True!'
    'on':    'On!'
    'false': 'False!'
  failure:
    true:    'True!'
    off:     'Off!'
    false:   'False!'
# 適切にエスケープした場合
I18n.t "success.true"  # => 'True!'
I18n.t "success.on"    # => 'On!'
I18n.t "success.false" # => 'False!'

# エスケープしていない場合
I18n.t "failure.false" # => Translation Missing
I18n.t "failure.off"   # => Translation Missing
I18n.t "failure.true"  # => Translation Missing

7.2 独自の訳文を保存する

Active Supportにデフォルトで付属するSimpleバックエンドを使っている場合、ファイルフォーマットに純粋なRubyファイルやYAMLが使えます。それ以外のバックエンドでは、異なるフォーマットが許可されている場合や、異なるフォーマットが要求される場合があります。

たとえば、Rubyのハッシュで訳文を提供する場合は、以下のような感じになります。

{
  pt: {
    foo: {
      bar: "baz"
    }
  }
}

上と同等のYAMLファイルは以下のような感じになります。

pt:
  foo:
    bar: baz

どちらの場合も、トップレベルにはロケール名を配置します。 "foo"は名前空間キー、"bar"は"baz"という訳文のキーです。

以下は、Active Supportのen.ymlで実際に使われている訳文YAMLファイルの例です。

en:
  date:
    formats:
      default: "%Y-%m-%d"
      short: "%b %d"
      long: "%B %d, %Y"

上の設定により、以下の4つのコードはいずれも:short日付フォーマット"%b %d"を返します。

I18n.t "date.formats.short"                # => "%b %d"
I18n.t "formats.short", scope: "date"      # => "%b %d"
I18n.t "short", scope: "date.formats"      # => "%b %d"
I18n.t "short", scope: ["date", "formats"] # => "%b %d"

一般に、訳文はYAMLで保存することをオススメします。 ただし、特殊な日付フォーマットを使う場合などは、ロケールデータの一部にlambdaを保存するためにRubyファイル形式を選ぶことも可能です。

7.3 バックエンドを切り替える

Active Supportで利用できる組み込みのSimpleバックエンドは、さまざまな理由により、Ruby on Railsでは「正常に動作する可能性の高い、最も単純な動作にのみ対応します」。つまり、Simpleバックエンドで動作が保証されているのは、英語および英語に極めて近い言語ぐらいしかないということです。同様に、Simpleバックエンドは訳文の読み出しのみが可能であり、訳文を動的に任意のフォーマットで保存する機能はありません。

もちろんこの制限は絶対ではなく、Ruby I18n gemを利用してSimpleバックエンド実装をより適切なものに差し替えることは可能です。これを行うには、I18n.backend=セッターにバックエンドのインスタンスを渡します。

たとえばSimpleバックエンドを、複数のバックエンドをチェインできるChainバックエンドに置き換える場合を考えてみましょう。これは、基本的にはSimpleバックエンドに保存された標準的な訳文を使いたいが、アプリケーションのカスタム訳文はデータベースなどの別のバックエンドに保存しておきたい、というような場合に便利です。

Chainバックエンドを使うと、Active Recordのバックエンドを使いつつ、フォールバック先は(デフォルトの)Simpleバックエンドになります。

I18n.backend = I18n::Backend::Chain.new(
  I18n::Backend::ActiveRecord.new,
  I18n.backend
)

この設定で訳文が探索されると、最初にActive Recordバックエンドをチェックし、次にSimpleバックエンドによって読み込まれたロケールファイルにフォールバックします。

I18n.t("welcome")
# "welcome"の訳文を最初はActive Recordバックエンドで探索し、
# 次にYAMLまたはRubyのロケールファイルを探索する

これは、デフォルトの訳文をバージョン管理されたロケールファイルに保持しておき、翻訳者や管理者が実行時に一部の訳文を更新できるようにしたい場合に便利です。

7.4 I18nの例外を処理する

I18n APIでは以下の例外が定義されています。これらの例外は、想定外の条件が発生した場合にバックエンドによって発生します。

例外 理由
I18n::MissingTranslationData リクエストされたキーに対応する訳文が見つからない
I18n::InvalidLocale I18n.localeに設定されたロケールが無効(nilなど)
I18n::InvalidPluralizationData countオプションが渡されたが訳文データが複数形化に対応していない
I18n::MissingInterpolationArgument 訳文側で必要な式展開用の引数が渡されなかった
I18n::ReservedInterpolationKey 訳文に含まれる式展開の変数名に予約済みの変数名(scopeかdefault)が使われている
I18n::UnknownFileType I18n.load_pathに追加されたファイル種別をバックエンドが認識できない
7.4.1 I18n::MissingTranslationDataの処理をカスタマイズする

config.i18n.raise_on_missing_translationsがfalse(すべての環境のデフォルト)の場合は、訳文が見つからないことを示すエラーメッセージが返されます。訳文の見つからないキーやスコープがメッセージに含まれているので、これを手がかりにコードを修正できます。

config.i18n.raise_on_missing_translationsがtrueの場合、訳文が見つからないときに通常の"Translation missing: ..."メッセージを返さず、エラーをraiseします。この値が:strictに設定されていると、モデルの訳文が見つからない場合にもエラーをraiseします。訳文が不足していることを早期に発見できるよう、test環境ではこの設定を有効にしておくのがオススメです。

以下は訳文が存在しない場合のデフォルトの振る舞いです。

I18n.t("missing")
# => "Translation missing: en.missing"

I18n.t!("missing")
# => I18n::MissingTranslationData: Translation missing: en.missing

config.i18n.raise_on_missing_translationsをtrueにすると、同じキーを探索したときに以下のようにエラーをraiseします。

I18n.t("missing")
# => I18n::MissingTranslationData: Translation missing: en.missing

I18n.t("inbox", locale: nil)
# => I18n::InvalidLocale

I18n.t("product_price", locale: :nl)
# => I18n::MissingInterpolationArgument

この振る舞いをさらにカスタマイズしたい場合は、config.i18n.raise_on_missing_translations = falseを設定したうえで、I18n.exception_handlerを実装します。カスタム例外ハンドラには、procか、callメソッドを持つクラスを利用できます。

# config/initializers/i18n.rb
module I18n
  class RaiseExceptForSpecificKeyExceptionHandler
    def call(exception, locale, key, options)
      if key == "special.key"
        "translation missing!" # raiseせずにこのメッセージを返す
      elsif exception.is_a?(MissingTranslation)
        raise exception.to_exception
      else
        raise exception
      end
    end
  end
end

I18n.exception_handler = I18n::RaiseExceptForSpecificKeyExceptionHandler.new

こうすると、I18n.t("special.key")を除くすべての例外を、デフォルトのハンドラと同様にraiseします。

また、translateビューヘルパーで訳文が見つからないときの処理はI18n.exception_handlerを経由するので、ここにもカスタムハンドラが適用されます。

8 関連リンク

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイド協賛プラン - バナー画像

Railsガイドは下記の協賛企業から継続的な支援を受けています。もしご興味あれば、協賛プランから気軽にお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加