概要
今作成中の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でも、クラスメソッドでメッセージを設定する方法はうまくいかないということです。