ActiveRecordのvalidatesで表示されるエラーメッセージのフォーマットを変更する

# 何に困っているか

ActiveRecordのvalidatesでエラーになった時に表示されるメッセージを`%{attribute}%{message}`ではないフォーマットにしたい。
例えば、User.nameのvalidatesエラーで`この名前は１文字以上入力してください。`みたいなエラーメッセージを表示したい。
さらに、そのフォーマットを適用するスコープを限定したい。

# validatesのエラーメッセージを変更するには

## 0. サンプルアプリの概要

今回の環境は`ruby:2.5.2`、`rails:6.0.0`です。

Userモデルを持つアプリを用意します。

```shell
$ rails new validation_message_sample
$ rails g model user name:string
```
`name`にvalidationを設定します。

```ruby
class User < ApplicationRecord
  validates :name, presence: true
end
```

この状態でvalidationエラーを発生させるとエラーメッセージはこうなります。

```shell
$ rails c
Running via Spring preloader in process 27748
Loading development environment (Rails 6.0.0)
irb(main):001:0> user = User.new
   (2.0ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> false
irb(main):003:0> user.errors.full_messages
=> ["Name can't be blank"]
```

これを`この名前は１文字以上入力してください。`に変更します。

## 1. attribute名を日本語にする

まずは`Name`となっている箇所を`名前`に変更します。

これには`rails-i18n`というgemを利用します。
Gemfileに以下を追記して、`bundle install`を実行します。

```:Gemfile
gem 'rails-i18n`
```

次に`config/application.rb`にi18nの設定を追加します。

```config/application.rb

module ValidationMessageSample
  class Application < Rails::Application
    config.load_defaults 6.0

    # 下の２行を追加する
    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.yml').to_s]
  end
end
```

`config/local/models/ja.yml`ファイルを作成し、変更したいattribute名を定義します。

```config/local/models/ja.yml
ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        name: 名前
```

この状態で実行すると、以下のようになります。

```shell
$ rails c
Running via Spring preloader in process 28562
Loading development environment (Rails 6.0.0)
irb(main):001:0> user = User.new
   (3.0ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> false
irb(main):003:0> user.errors.full_messages
=> ["名前を入力してください"]
```

attribute名が`名前`になりました。
ついでにデフォルトのメッセージも日本語になりました。

## 2. :messageを利用する

`:message`オプションを利用することで、メッセージを変更することができます。
詳細は[Rails Guide](https://railsguides.jp/active_record_validations.html#message)を参照してください。

```ruby
class User < ApplicationRecord
  validates :name, presence: { message: 'は１文字以上入力してください。' }
end
```

実行するとこうなりました。

```shell
$ rails c
Running via Spring preloader in process 29059
Loading development environment (Rails 6.0.0)
irb(main):001:0> user = User.new
   (1.5ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> false
irb(main):003:0> user.errors.full_messages
=> ["名前は１文字以上入力してください。"]
```

では、attribute名の前に`この`という文字列を表示しようとしてみます。

```app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: { message: "この%{attribute}は１文字以上入力してください。" }
end
```

実行してみます。

```shell
$ rails c
Running via Spring preloader in process 29190
Loading development environment (Rails 6.0.0)
irb(main):001:0> user = User.new
   (1.6ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> false
irb(main):003:0> user.errors.full_messages
=> ["名前この名前は１文字以上入力してください。"]
```

うーん、頭にもattribute名が付いてしまいます。
これは、エラーメッセージのデフォルトフォーマットが`%{attribute}%{message}`となっているためです。

## 3. カスタムメソッドを利用する

[カスタムメソッド](https://railsguides.jp/active_record_validations.html#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)を利用して、エラーメッセージを変更する。

`errors.add(:カスタムメソッド名, エラーメッセージ)`でエラーメッセージを追加できますが、結局カスタムメソッド名が文頭に表示されてしまうので、`:base`を利用することにします。
コードは以下の通り。

```app/models/user.rb
class User < ApplicationRecord
  validate :name_presence

  private

  def name_presence
    return if name.present?

    errors.add(:base, "この名前は１文字以上入力してください.")
  end
end
```

実行するとこうなります。

```shell
$ rails c
Running via Spring preloader in process 33134
Loading development environment (Rails 6.0.0)
irb(main):001:0> user = User.new
   (2.5ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> false
irb(main):003:0> user.errors.full_messages
=> ["この名前は１文字以上入力してください."]
```

確かに意図通りにエラーメッセージを変更することはできました。

しかし、attribute名をわざわざベタ打ちしてしまっていて[DRY](https://ja.wikipedia.org/wiki/Don%27t_repeat_yourself)じゃないし、本来の`:base`の用途ではないので、あまり良い解ではないと考えます。

## 4. エラーメッセージのフォーマットを変更する

[この記事](https://blog.bigbinary.com/2019/04/22/rails-6-allows-to-override-the-activemodel-errors-full_message-format-at-the-model-level-and-at-the-attribute-level.html)によると、Rails6.0.0からエラーメッセージのフォーマットをスコープを限定して変更できるようになったようです。

スコープを限定するために以下の設定を追記します。

```
require_relative 'boot'

require 'rails/all'

Bundler.require(*Rails.groups)

module ValidationMessageSample
  class Application < Rails::Application
    config.load_defaults 6.0

    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.yml').to_s]
    config.active_model.i18n_customize_full_message = true  # この行を追加
  end
end
```

config.active_model.i18n_customize_full_messageはデフォルトがfalseなので、この設定を実施しておかないと機能しません。（これに気づかずにハマりました・・・）

エラーメッセージのフォーマットを以下の通り設定します。

```config/local/models/ja.yml
ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        name: 名前
    errors:
      models:
        user:
          attributes:
            name:
              format: '%{message}'
              blank: この%{attribute}は１文字以上入力してください。
```

ちなみにUserモデルは下の通りです。

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

実行するとこうなります。

```shell
$ rails c
Running via Spring preloader in process 34119
Loading development environment (Rails 6.0.0)
irb(main):001:0> user = User.new
   (2.4ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> false
irb(main):003:0> user.errors.full_messages
=> ["この名前は１文字以上入力してください。"]
```

遂に目的を達成する実装にたどり着くことが出来ました :star2: 

一応、サンプルコードを[GitHub](https://github.com/okeicalm/validation_error_message_sample)に置いてます。

# 参考文献

https://qiita.com/Ushinji/items/242bfba84df7a5a67d5b
https://qiita.com/suketa/items/5af12acd88ebc8a2a1a5