あなたはいくつ知っている?Rails I18nの便利機能大全!

  • 43
    いいね
  • 0
    コメント

I18n、使ってますか?
多言語化しなくても、アプリケーション内で文言を統一するなどの用途で使用している方も多いのではないかと思います。

一見、YAMLを書いて、viewやcontrollerで参照するぐらいの単純な機能に見えますが、実はかなりたくさんの便利機能があるので、まとめてみました。

日付や時刻のフォーマットを定義する

普段よく使うのはI18n#tだけど、日付や時刻をローカライズするI18n#lもある。

config/locales/ja.yml
ja:
  date:
    formats:
      default: "%Y/%m/%d"
      long: "%Y年%m月%d日(%a)"
      short: "%m/%d"
  time:
    formats:
      default: "%Y/%m/%d %H:%M:%S"
      long: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
      short: "%y/%m/%d %H:%M"
> I18n.l(Date.today)
=> "2017/06/25" # 指定がない場合はdefaultを使う
> I18n.l(Date.today, format: :short)
=> "06/25"
> I18n.l(Time.now)
=> "2017/06/25 15:57:25"
> I18n.l(Time.now, format: :long)
=> "2017年06月25日(日) 15時57分46秒 +0900"

Rubyスクリプトで訳文を定義する

訳文は通常YAMLで定義するけど、実はRubyスクリプトで定義することもできる。

config/locales/ja.rb
{
  ja: {
    hello: "こんにちは"
  }
}
> I18n.t(:hello)
=> "こんにちは"

Rubyなので、スクリプトで定義を生成できる。
できるけど、いい活用方法が思いつかない。。

config/locales/ja.rb
{
  ja: (1..101).map{|i| ["dog#{i}", "#{i}匹わんちゃん"]}.to_h
}
> I18n.t(:dog1)
=> "1匹わんちゃん"
> I18n.t(:dog101)
=> "101匹わんちゃん"

訳文としてlambdaを与えることもできる。
できるけど、訳文にコードを入れるのはI18nの守備範囲を逸脱している気がする。。

config/locales/ja.rb
{
  ja: {
    date: {
      formats: {
        forever5: ->(date, prms) {
          # 例なので投げやりな計算
          today = Date.today
          month_diff = (today.year*12 + today.month) - (date.year*12 + date.month) - 60
          "#{prms[:name]}さんは、5歳#{month_diff}ヶ月です"
        }
      }
    }
  }
}
> I18n.l(Date.new(1973, 4, 2), name: "ガチャピン", format: :forever5)
=> "ガチャピンさんは、5歳470ヶ月です"

I18nの訳文を動的に追加する

I18n::Backend::Base#store_translationsを使う。

> I18n.backend.store_translations :ja, hello: 'こんにちは'
> I18n.t(:hello)
=> "こんにちは"

ロケールごとにViewのテンプレートを切り替える

ヘルプページなど、内容の大部分が文章であるときなどに便利。
例えば、HelpControllerindexアクションを呼ぶ場合、通常はapp/views/helps/index.html.erbが使われるが、index.ja.html.erbを置いておくと、ロケールが:jaのときにこちらを使ってくれる。

ネストされた訳文の階層をシンボルで指定する

config/locales/ja.yml
ja:
  foo:
    bar:
      test: "テスト"

この場合、以下のように、階層を.でつなげた文字列で指定するのが一般的。

> I18n.t("foo.bar.test")
=> "テスト"

:scopeを使うことで、キーと階層を分けて指定することもできる。

> I18n.t("bar.test", scope: :foo)
=> "テスト"
> I18n.t(:test, scope: "foo.bar")
=> "テスト"
> I18n.t(:test, scope: [:foo, :bar])
=> "テスト"

階層を動的に変える場合は、文字列の式展開でやるよりキレイになるかも。

翻訳時に変数を渡す

config/locales/ja.yml
ja:
  dogs: "#{num}匹わんちゃん"
> I18n.t(:dogs, num: 101)
=> "101匹わんちゃん"

訳文が見つからない場合に出す文字列を指定する

:defaultオプションを指定すると、訳文が見つからない場合にそちらを見てくれる。
どんなユースケースがあるんだろう。。

> I18n.t('undefined.key')
=> "translation missing: ja.undefined.key"
# 文字列を渡すと、それをそのまま出す
> I18n.t('undefined.key', default: "undefined!")
=> "undefined!"
# シンボルを渡すと、それをキーとして訳文を探す
> I18n.t('undefined.key', default: :"foo.bar.test")
=> "テスト"
# 配列を渡すと、先頭から順に見ていく
> I18n.t('undefined.key', default: [:"foo.bar.test", "undefined!"])
=> "テスト"

ロケールを明示的に与える

:localeオプションでロケールを明示的に与えることができる。
あまりユースケースが思いつかない。テストのときとか…?

config/locales/ja.yml
ja:
  hello: こんにちは
config/locales/en.yml
en:
  hello: Hello
> I18n.t(:hello)
=> ”こんにちは”
> I18n.t(:hello, locale: :en)
=> "Hello"

訳文にHTMLを使用する

キー名がhtml、もしくは_htmlで終わっていれば、html_safeとみなされ、viewで使う場合にエスケープされない。ただし、式展開%{}の中身はエスケープされる。

config/locales/ja.yml
ja:
  hello_str: "<b>こんにちは!</b>"
  hello_html: "<b>こんにちは!</b>"
  hello:
    html: "<b>こんにちは!</b>"
  hello_with_name:
    html: "<b>こんにちは!%{name}さん</b>"
app/view/home/index.html.erb
<div><%= t('hello_str') %></div>
<div><%= raw t('hello_str') %></div>
<div><%= t('hello_html') %></div>
<div><%= t('hello.html') %></div>
<div><%= t('hello_with_name.html', name: "ガチャピン") %></div>
<div><%= t('hello_with_name.html', name: "<span>ガチャピン</span>") %></div>

スクリーンショット 2017-06-25 17.14.34.png

訳文を一括取得する

キーを配列で渡すことによって、訳文を一括取得可能。
CSVのヘッダを取ってくるときとかに便利かも?

config/locales/ja.yml
ja:
  csv:
    header:
      user:
        customer_id: 顧客番号
        name: 名前
        address: 住所
> I18n.t([:customer_id, :name, :address], scope: [:csv, :header, :user])
=> ["顧客番号", "名前", "住所"]

特定のViewファイル専用の訳文を簡単に参照する

Viewファイルのパスに沿った階層で訳文を定義することで、Viewファイルの中で簡単に参照できる。

config/locales/ja.yml
ja:
  users:
    index:
      title: 顧客一覧
    form:
      same_address: 顧客住所と同じ
app/views/users/index.html.erb
<%= t(".title") %>
<%# => "顧客一覧” %>
app/views/users/_form.html.erb
<%= t(".same_address") %>
<%# => "顧客住所と同じ” %>

モデル名やモデルのアトリビュート名を取得する

指定された階層に置くことで、Model.model_name.humanでモデル名を、Model.human_attribute_name(attribute)でアトリビュート名を取得できる。

config/locales/ja.yml
ja:
  activerecord:
    # モデル名
    models:
      user: 顧客
    # モデルごとのアトリビュート名
    attributes:
      user:
        name: 名前
        address: 住所
  # 全モデル共通のアトリビュート名
  attributes:
    created_at: 作成日時
    updated_at: 更新日時
> User.model_name.human
=> "顧客"
> User.human_attribute_name(:name)
=> ”名前”
> User.human_attribute_name(:created_at)
=> "作成日時"

与えた数によって単数形/複数形を切り替える

countというパラメーターを渡すことで、数に応じて単数形/複数形を切り替えることができる。

config/locales/ja.yml
ja:
  family:
    one: "独身"
    other: "%{count}人家族"
> I18n.t(:family, count: 1)
=> "独身"
> I18n.t(:family, count: 101)
=> "101人家族"

Model.model_name.humanModel.human_attribute_name(attribute)もほぼ同様だけど、countを与えなかった場合はoneが使われる。

config/locales/ja.yml
ja:
  activerecord:
    models:
      anpan:
        one: アンパンマン
        other: アンパンメン
> Anpan.model_name.human
=> "アンパンマン"
> Anpan.model_name.human(count: 1)
=> "アンパンマン"
> Anpan.model_name.human(count: 100)
=> "アンパンメン" 
> Anpan.model_name.human(count: Anpan.count)
=> "アンパンメン" 

one otherだけでなく、定義次第でいろいろ対応可能。
参考:https://github.com/svenfuchs/i18n/blob/master/test/test_data/locales/plurals.rb

ActiveRecordのバリデーションメッセージを定義する

バリデーションに引っかかった場合、以下の優先順位でメッセージが検索される。

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

例えば、以下のようなバリデーションが設定されている場合、

app/models/user.rb
class User < ActiveRecord::Base
  validates :name, presence: true
end

presenceに対応するメッセージのキー名はblankなので、nameがバリデーションに引っかかると以下の順にエラーメッセージが検索される。

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

また、一部のバリデーションは、オプションとしてそのバリデーションに関連する数値が渡ってくるので、それを組み込むこともできる。

バリデーションに対するメッセージキーとオプションはこちらを参照。

Action Mailerメールの件名を訳文を定義する

mailメソッドに件名が渡されなかった場合、デフォルトで[mailer_scope].[action_name].subjectというキーで件名を検索する。

config/locales/ja.yml
ja:
  user_mailer:
    welcome:
      subject: ご登録ありがとうございます。

その他のI18n関連メソッド

ActionView

distance_of_time_in_words

投稿から大体◯日とか、表示するアレ。
http://api.rubyonrails.org/v5.1/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words
こんな感じで定義する。

datetime_select / select_month

月の表示名は、"date.month_names"で定義する。
また、"date.order"で年月日の表示順も切替可能。

ActionView::Helpers::NumberHelper

http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_to_phone

  • number_to_currency
    • 数値を通貨フォーマットに変換する
  • number_with_precision
    • 数値を特定の桁で丸める
  • number_to_percentage
    • 数値をパーセントに変換する
  • number_with_delimiter
    • 数値に桁区切り文字を追加する
  • number_to_human_size
    • バイト単位の数値を人間に読みやすい桁に変換する

これらのメソッドの一部のオプションはI18nの定義ファイルに設定することで、ロケールごとに変えることが可能。

config/locales/ja.yml
jp:
  number:
    currency:
      format:
        unit: "円"
        format: "%n%u"
        negative_format: "-%n%u"
        precision: 0
<%= number_to_currency(123456789) %>
<%# => 123,456,789円 %>

ActiveSupport

Array#to_sentence

複数の要素をつなげて文章っぽくしてくれる。

config/locales/ja.yml
ja:
  support:
    array:
      words_connector: "、"
      two_words_connector: "と"
      last_word_connector: "、そして"
> %w(地震 雷 火事 親父).to_sentence
=> "地震、雷、火事、そして親父"
> %w(ぐり ぐら).to_sentence
=> "ぐりとぐら"

参考

http://guides.rubyonrails.org/i18n