時々とおまわり

プログラミング雑記

Railsのフォームのsubmitをhelpers.submit.createで翻訳できる理由を調べた

FBCRailsi18nのプラクティスを進めています。 _form.html.erb 内のフォーム内のsubmitボタンの翻訳情報をhelpers.submit.createhelpers.submit.update などで参照できるのか分からなかったのでソースコードを読んで調べてみました。 ちなみにVSCodeソースコードを開く設定は前回の記事に書いたので参考にしてみてくださ。

調べる対象のRailsのコードです。

# app/views/books/_form.html.erb

<%= form_with(model: book, local: true) do |form| %>
...
    <%= form.label :title %>
    <%= form.text_field :title %>
...

    <%= form.submit %> # ここの翻訳情報をどう参照しているのか知りたい
<% end %>
# config/locales/ja.yml

ja:
  helpers:
    submit:
      create: 登録する # newメソッド時に参照される
      update: 更新する # editメソッド時に参照される

ソースコードを読むこと自体が初めてだったので解釈や読む場所が違うなど間違っている可能性があるかもしれません。 間違い等はコメント等で指摘していただけると助かります。

結論

i18n gemとrails gem で以下3点が設定されており、自動で翻訳情報の参照先を切り替えているということが分かりました。

仕組みを調べるにあたって特に分からなかった以下の4つを個別にまとめました。

  • Q1: helpers.submit.create で参照するロケールファイルはどこにあるのか?
  • Q2. helpers.submit.create で参照するロケールファイルはどこで設定されているのか?
  • Q3. i18nt メソッドのロケールの切り替えはどこで設定されているのか?
  • Q4. 翻訳情報の参照先のcreateupdate の切替はどこで設定されているのか?

Q1. helpers.submit.create で参照するロケールファイルはどこにあるのか?

以下のファイルに参照するロケールファイルが定義されていました。

  • gem: rails
  • ファイル: actionview/lib/action_view/locale/en.yml
"en":
...

  helpers:
  ...
    submit:
      create: 'Create %{model}'
      update: 'Update %{model}'
      submit: 'Save %{model}'

rails/en.yml at main · rails/rails

この翻訳情報を参照してhelpers.submit.create で翻訳できるようになっているようです。

Q2. helpers.submit.create で参照するロケールファイルはどこ設定でされているのか?

以下のファイルで設定されていました。

  • gem: rails
  • ファイル: actionview/lib/actionview.rb
# lib/action_view.rb

ActiveSupport.on_load(:i18n) do
  I18n.load_path << File.expand_path("action_view/locale/en.yml", __dir__)
end

rails/action_view.rb at main · rails/rails

参照するロケールファイルのactionview/lib/action_view/locale/en.ymlload_path に設定されています。load_path についてはRailsガイドに詳細が記載されています。load_path にデフォルトのロケールファイルであるactionview/lib/action_view/locale/en.yml を設定することで結果的にen.helpers.submit.create で翻訳できるようになっているのだと思います。

訳文読み込みパス (I18n.load_path) はファイルへのパスの配列であり、自動的に読み込まれます。このパスを設定することで、訳文のディレクトリ構造やファイル命名スキームをカスタマイズできます。

Rails 国際化(i18n)API - Railsガイド

Q3. i18nt メソッドのロケールの切り替えはどこで設定されているのか?

以下のファイルでメソッドが定義されていました。

  • gem: i18n
  • ファイル: lib/i18n.rb
  • メソッド: translate

translate メソッドのソースコード

# lib/i18n.rb

module I18n
...

    def translate(key = nil, throw: false, raise: false, locale: nil, **options) # TODO deprecate :raise
      locale ||= config.locale # ロケール設定
      raise Disabled.new('t') if locale == false
      enforce_available_locales!(locale)

      backend = config.backend

      result = catch(:exception) do
        if key.is_a?(Array)
          key.map { |k| backend.translate(locale, k, options) }
        else
          backend.translate(locale, key, options)
        end
      end

      if result.is_a?(MissingTranslation)
        handle_exception((throw && :throw || raise && :raise), result, locale, key, options)
      else
        result
      end
    end
    alias :t :translate # tメソッドを定義(エイリアス)

...
end

trancelate メソッドのロケール設定で呼ばれるlocaledefault_locale メソッドについては以下のファイルで定義されていました。

  • gem: i18n
  • ファイル: lib/i18n/config.rb
  • メソッド: localedefault_locale

trancelate メソッドのロケール設定で呼ばれるlocale メソッド、その中で呼ばれるdefault_localeソースコード

# lib/i18n/config.rb

module I18n
  class Config
  ...

    def locale
      defined?(@locale) && @locale != nil ? @locale : default_locale
    end

    def locale=(locale)
      I18n.enforce_available_locales!(locale) # 渡されたロケールが使用不可で例外を発生させる
      @locale = locale && locale.to_sym
    end
    ...

    def default_locale
      @@default_locale ||= :en
    end

    ...
end

i18n/i18n.rb at master · ruby-i18n/i18n

i18ntメソッドはtranslate メソッドのエイリアスとして定義されており、メソッド内のconfig.localeロケール設定されています。 translate メソッドのlocale オプションがnil の場合はデフォルトロケールen が設定され、そうでない場合は指定したロケールがシンボルで渡されてロケールが設定されるようです。

Q4. 翻訳情報の参照先のcreateupdate の切替はどこで設定されているのか?

form_with メソッド内のsubmit はレコードの有無によって翻訳情報の参照先を自動的に選択される仕様になっています。

  • レコード無し: helpers.submit.create を参照
  • レコード有り: helpers.submit.update を参照
# app/views/books/_form.html.erb

<%= form_with(model: book, local: true) do |form| %>
...
    <%= form.label :title %>
    <%= form.text_field :title %>
...

    <%= form.submit %> 
<% end %>
# config/locales/ja.yml

ja:
  helpers:
    submit:
      create: 登録する # newメソッド時に参照される
      update: 更新する # editメソッド時に参照される

以下のファイルで翻訳情報の参照先を自動的に切り替えるsubmit_default_value メソッドが定義されていました。

  • gem: rails
  • ファイル: lib/action_view/helpers/form_helper.rb
  • メソッド: submit_default_value

submit_default_value メソッドのソースコード

# 
module ActionView
  module Helpers
  ...
        def submit_default_value
          object = convert_to_model(@object)
          key    = object ? (object.persisted? ? :update : :create) : :submit # レコードが存在する場合はupdate、存在しない場合はcreate

          model = if object.respond_to?(:model_name)
            object.model_name.human
          else
            @object_name.to_s.humanize
          end

          defaults = []
          # Object is a model and it is not overwritten by as and scope option.
          if object.respond_to?(:model_name) && object_name.to_s == model.downcase
            defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}"
          else
            defaults << :"helpers.submit.#{object_name}.#{key}"
          end
          defaults << :"helpers.submit.#{key}"
          defaults << "#{key.to_s.humanize} #{model}"

          I18n.t(defaults.shift, model: model, default: defaults)
        end
        ...

  end
end

rails/form_helper.rb at main · rails/rails

persisted? メソッドでレコードの有無を判断し:update:create を切り替えてkey に代入し、:"helpers.submit.#{key}" で文字列展開して参照するキーを作成しているようです。

persisted? | Railsドキュメント

送信されているhttpメソッドで切り替えていると予想していましたが、レコードの有無で判断していると知ることができました。

まとめ

調べた結果を簡単にまとめると「railsi18n の2つのgemがいい感じに翻訳情報の参照先を切り替えてsubmitボタンの多言語化ができるようになっている」ということになりました。 初めてgemのソースコードを読んでみて難しさと学びの多さを感じることができました。 Railsはとても複雑で全てを理解することは不可能ですがポイントを絞って読んでみることで理解が深まりそうです。 今後も気になる部分は積極的にソースコードを読んでみたいと思います。

参照

Railsコードを読んでみた - Qiita

persisted? | Railsドキュメント