Ruby
I18n
RubyOnRails
Validation

railsのバリデーションメッセージの多言語化とgettext

概要

今作成中のrailsアプリで、多言語化の仕組みはgettextを採用しました(賛否両論あると思いますが私の好みです)。

ただ、バリデーションのエラーメッセージやテーブルのカラム名など、同じ文言を繰り返し使うようなものはデフォルトのrailsのI18nの仕組みの方が向いていると思い、併用するために調査しました。

デフォルトI18nで独自のメッセージを設定する方法と、調査中に出会ったgettextの不可解な挙動も合わせて、備忘録も兼ねて共有します。

ロケールの設定やI18n・gettextのセットアップについては触れません。

デフォルトI18nのバリデーションメッセージについて

例えば、Shopモデルのname属性のpresenceバリデーションだけ特別なメッセージに変更したいとします(あまりないとは思いますが)。

で、パッと思いついたのはこんな感じ・・・

# app/models/shop.rb
class Shop < ApplicationRecord
  validates :name, presence: {message: I18n.t('activemodel.errors.models.shop.blank')}

これは当然ですが期待通り動きません。validatesはクラスメソッドなので、最初にクラスがロードされる時に一度だけ評価されるので最初に読み込まれた時の言語で固定されてしまいます。

messageにはProcを渡すことが可能で、Procで渡せばリクエスト毎に評価されます。

# app/models/shop.rb
class Shop < ApplicationRecord
  validates :name, presence: {
    message: -> (rec, data) do
      I18n.t('activemodel.errors.models.shop.blank')
    end
  }

ちなみにlengthのように複数のメッセージがある場合はそれぞれ個別のキーで指定する必要があります。どんなキーがあるかはソースを見るのが手っ取り早いと思います。

  validates :name, length: {
    minimum: 1,
    too_short: -> (rec, data) do
      I18n.t('path.to.error.message')
    end
  }

では複数のモデルで使いまわしたいバリデーションのメッセージを変更する場合どうするか?

例えばbooleanの必須チェックはinclusionで行いますが、デフォルトのメッセージを変えてしまうのは本来の意味で使う時に不具合があるので、booleanのチェックで使うinclusionだけ変えたいと思います。

ところが、先ほどのメソッドを指定するパターンでは引数を渡すことができないため、カラム名を指定できないので親クラスに作っていくつかもモデルで使い回すことができません。

そういう場合はカスタムバリデーションクラスを書きます。

# app/validations/booloean_presence_validator.rb
class BooleanPresenceValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    # 第二引数に`:blank`と指定しているのは`I18n.t('errors.messages.blank')`と同義です。
    record.errors.add(attribute, :blank) unless [true, false].include?(value)
  end
end
# app/models/shop.rb
class Shop < ApplicationRecord
  validates :name, booloean_presence: true

こうしておけば複数のモデルで使い回せます。

gettextの不可解な挙動

不可解というのは、デフォルトのバリデーションメッセージを多言語化する時に最初に試した失敗する方法で、gettextではちゃんと翻訳ができてしまった(ように見えた)のです。

# app/models/shop.rb
class Shop < ApplicationRecord
  validates :name, presence: {message: _('を入力してください。')}

この状態で、まず、日本語のページを開いて、のちに英語のページを開くとちゃんと英語に翻訳されたんです。クラスメソッドなので2度評価されることがないのにそんなはずはないと思ってコードを追って見ました。

バリデーションのエラーメッセージが生成される場所はActiveModel::Errors::generate_messageメソッドで、その最後のところでI18n.translate(key, options)に渡され、keyからメッセージが生成されます。

translateの中では、config.backend.translateに最終的に処理が渡され、gettext使用時はconfig.backendにはGettextI18nRails::Backendが入ってるのでこれが呼ばれます。

gettextでは辞書のキーはソースに書いた文言そのもの、「を入力してください。」がキーになっているので、それでうまい具合に翻訳が成功していたんですね。

つまり、最初に英語のページが表示されるとキーがis required.になってしまい、翻訳がうまくいきません。最初に「(ように見えた)」と書いた通り、gettextでも、クラスメソッドでメッセージを設定する方法はうまくいかないということです。