Skip to content

Formオブジェクトのロケールファイルの定義方法(i18n)

Published: at 10:00

Table of contents

Open Table of contents

Formオブジェクトを利用した場合の翻訳キーはどれ?

通常DBに紐づくモデル、つまりActiveRecord::Baseを継承したクラスに対応するフォームを作成する場合、次のようなロケールファイルを作成することで、バリデーションメッセージや表示される項目を多言語化することができます。

ja:  activerecord: # activerecordをキーにする    attributes:      user:        name: 名前        password: パスワード

一方DBへの保存だけではなく、複雑なユースケースを実装する場合にはFormオブジェクトを作って対処します。例えばフォームから入力されたデータを用いて、メール送信を行うような場合です。

Formオブジェクトは複数のモデルを扱ったり、DB以外の操作が伴うため、基本的にはActiveModel::Modelをincludeした形で実装します。

また、form_withからの送信先判定を行うために、to_modelをオーバーライドし、メインとなるModelクラスに向けることもあります。

…こうなってくると、i18n用のロケールファイルをどうやって書いたらいいのかよくわからなくなってきたので、Railsがどのようにロケールファイルを解釈しているのか調べました。

要約

前提

対象バージョン

サンプルケース

モデル:User

class User < ApplicationRecordend

フォームオブジェクト:UserForm

class UserForm    include ActiveModel::Model  include ActiveModel::Attributes  attr_reader :user  def initialize(user = User.new, **attributes)    @user = user    attributes = default_attributes if attributes.empty?    super(attributes)  end  validates :name, presence: true  # バリデーションがいっぱい  attribute :name, :string  # カラム定義も様々  def save    # 実際の処理  end  def to_model    user  endend

RailsがModelのi18nを探索する手順

バリデーションメッセージ

バリデーション用のエラーメッセージの組み立ては、ActiveModel::Error.generata_messageで行われています。

rails/activemodel/lib/active_model/error.rb at main · rails/rails
Ruby on Rails. Contribute to rails/rails development by creating an account on GitHub.
rails/activemodel/lib/active_model/error.rb at main · rails/rails favicon github.com
rails/activemodel/lib/active_model/error.rb at main · rails/rails
def self.generate_message(attribute, type, base, options) # :nodoc:      type = options.delete(:message) if options[:message].is_a?(Symbol)      value = (attribute != :base ? base.read_attribute_for_validation(attribute) : nil)      options = {        model: base.model_name.human,        attribute: base.class.human_attribute_name(attribute, { base: base }),        value: value,        object: base      }.merge!(options)      if base.class.respond_to?(:i18n_scope)        i18n_scope = base.class.i18n_scope.to_s        attribute = attribute.to_s.remove(/\[\d+\]/)        defaults = base.class.lookup_ancestors.flat_map do |klass|          [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",            :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]        end        defaults << :"#{i18n_scope}.errors.messages.#{type}"        catch(:exception) do          translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))          return translation unless translation.nil?        end unless options[:message]      else        defaults = []      end      defaults << :"errors.attributes.#{attribute}.#{type}"      defaults << :"errors.messages.#{type}"      key = defaults.shift      defaults = options.delete(:message) if options[:message]      options[:default] = defaults      I18n.translate(key, **options)    end
ActiveModel::Errors
Active Model Errors Provides error related functionalities you can include in your object for handling error messages and interacting with Action View helpers.
ActiveModel::Errors favicon api.rubyonrails.org

抜粋するとこんな感じです。

def self.generate_message(attribute, type, base, options) # :nodoc:    # 1: 対象のクラスについて、i18n_scopeを実行し、キーを取得する。  if base.class.respond_to?(:i18n_scope)    i18n_scope = base.class.i18n_scope.to_s    attribute = attribute.to_s.remove(/\[\d+\]/)    defaults = base.class.lookup_ancestors.flat_map do |klass|      [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",        :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]    end    # 省略  end  # 2. 取得できた情報でデータを翻訳文を作成し、つっこむ  key = defaults.shift  defaults = options.delete(:message) if options[:message]  options[:default] = defaults  # 3. 2で作成されたデータで翻訳処理を行う(ロケールに応じたファイルを選択する)  I18n.translate(key, **options)end

ここで肝になるのはi18n_scopeというメソッドです。このメソッドにより、activerecordを見にいくのか、activemodelを見にいくのかが決まります。

ではその実装はどうなっているのかというと、ActiveModelの場合、次のようになっています。

# Returns the +i18n_scope+ for the class. Override if you want custom lookup.def i18n_scope  :activemodelend
rails/activemodel/lib/active_model/translation.rb at 75a9e1be75769ae633a938d81d51e06852a69ea3 · rails/rails
Ruby on Rails. Contribute to rails/rails development by creating an account on GitHub.
rails/activemodel/lib/active_model/translation.rb at 75a9e1be75769ae633a938d81d51e06852a69ea3 · rails/rails favicon github.com
rails/activemodel/lib/active_model/translation.rb at 75a9e1be75769ae633a938d81d51e06852a69ea3 · rails/rails

一方で、ActiveRecordでも同じメソッドがオーバーライドされており、次のように実装されています。

# Set the i18n scope to override ActiveModel.def i18n_scope # :nodoc:  :activerecordend
rails/activerecord/lib/active_record/translation.rb at 75a9e1be75769ae633a938d81d51e06852a69ea3 · rails/rails
Ruby on Rails. Contribute to rails/rails development by creating an account on GitHub.
rails/activerecord/lib/active_record/translation.rb at 75a9e1be75769ae633a938d81d51e06852a69ea3 · rails/rails favicon github.com
rails/activerecord/lib/active_record/translation.rb at 75a9e1be75769ae633a938d81d51e06852a69ea3 · rails/rails

これにより、翻訳対象のクラスが

が翻訳キーとして採用されることになります。

ここで、バリデーションメッセージについては、Formオブジェクト、つまりActiveModelで実装されたものです。そのため、エラーメッセージの翻訳キーはactivemodel始まりになります。

ja:    activemodel:        user_form:            name: 名前            password: パスワード

カラム(画面表示項目)

カラム(画面表示項目)の翻訳についても先のi18n_scopeで説明がつきます。

form_withでフォームを作成する際に、to_modelによって、ActiveRecordであるUser側にクラス判定が向くため、ActiveRecordi18n_scopeが採用されます。

そのため、以下のようなロケールファイルを記述します。

ja:    activerecord:        user:            name: 名前            password: パスワード

なおUserモデルに含まれない項目をFormオブジェクトに定義した場合も、翻訳ファイルはactiverecord下に記述することに注意が必要です。


以上Formオブジェクトでの翻訳キーの参照先についての調査でした。