translation missing: ja.activerecord.errors.models.hoge.attributes.base.restrict_dependent_destroy.many の対処方法

# TL;DR

ここ読んで適切なgemをインストールするなり、localeファイルを設置せよ。
https://github.com/svenfuchs/rails-i18n

# 再現方法
参考 : [システムのgemにrailsをインストールせずrails newする](http://qiita.com/youcune/items/222777415f00d19cccb4)

まずは、アプリを作る。

```bash
$ mkdir i18ntest
$ cd i18ntest
$ echo 'source "https://rubygems.org"' > Gemfile
$ echo 'gem "rails", "~> 4.2"' >> Gemfile
$ bundle install --path vendor/bundle
$ bundle exec rails new .
```

続いてモデルを作る。parentというモデルがたくさんのchildを持っているという関係。

```bash
$ bin/rails g model parent 
$ bin/rails g model child parent:references
$ bin/rake db:migrate
```

プロセス管理ではよくある「親が子を残して死ぬことは許されない」

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">rubyでプロセス管理の話になり「あー、これ殺せてないね」「親子供は皆殺ししなきゃ！」という物騒な話に花を咲かせてた。たのしい。</p>&mdash; えにぐま (@enigma63) <a href="https://twitter.com/enigma63/status/732526218247954432">2016年5月17日</a></blockquote>

という関係にする。

```app/models/parent.rb
class Child < ActiveRecord::Base
  has_many :child, dependent: :restrict_with_error
end
```

デフォルトロケールを日本語にする。

```config/application.rb
    #...snip...
    config.i18n.default_locale = :ja
    #...snip...
```

再現に必要な最小限のlocaleファイルを作成。

```config/locales/ja.yml
ja:
  hello: こんにちは
```

再現試験。

```irb
$ bin/rails c
irb(main):001:0> p = Parent.create
irb(main):002:0> c = Child.create(parent: p)
irb(main):003:0> p.destroy
=> false
irb(main):004:0> p.errors.messages
=> {:base=>["translation missing: ja.activerecord.errors.models.parent.attributes.base.restrict_dependent_destroy.many"]}
irb(main):005:0>
```

これ。このメッセージでドツボ。

```
translation missing: ja.activerecord.errors.models.parent.attributes.base.restrict_dependent_destroy.many
```

このメッセージの通りなら、こんな感じのファイルを作成しなければならない。

```config/locales/ja.yml
ja:
  activerecord:
    errors:
      models:
        parent:
          attributes:
            base:
              restrict_dependent_destroy:
                many: "%{record}が存在しているので削除できません"
```

一応これでも動くけど、他に`dependent: :restrict_with_error`なotherモデルが追加されるとこうなる。

```config/locales/ja.yml
ja:
  activerecord:
    errors:
      models:
        parent:
          attributes:
            base:
              restrict_dependent_destroy:
                many: "%{record}が存在しているので削除できません"
        other:
          attributes:
            base:
              restrict_dependent_destroy:
                many: "%{record}が存在しているので削除できません"
```

すごくDRYじゃ無くて、半端ないツラみ。


# 対策

正しくはこう。

```config/locales/ja.yml
ja:
  errors:
    messages:
      restrict_dependent_destroy:
        many: "%{record}が存在しているので削除できません"
```

というか、こういうlocaleファイルが[配布](https://github.com/svenfuchs/rails-i18n)されているので、それ使えって話。

# どうしてあんなつらいメッセージが格納されたか。
気になったのでソースを追いかけてみる。

このメッセージは[ここ](https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/associations/has_many_association.rb#L16)が発端である。

```ruby:activerecord/lib/active_record/associations/has_many_association.rb#L16
        when :restrict_with_error
          unless empty?
            record = klass.human_attribute_name(reflection.name).downcase
            owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
            false
          end
```

errors#add は[こちら](https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/errors.rb#L299)。

```ruby:activemodel/lib/active_model/errors.rb#L299
    def add(attribute, message = :invalid, options = {})
      message = normalize_message(attribute, message, options)
      if exception = options[:strict]
        exception = ActiveModel::StrictValidationFailed if exception == true
        raise exception, full_message(attribute, message)
      end

      self[attribute] << message
    end
```

[normalize_message](https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/associations/has_many_association.rb#L16) を経由して、

```ruby:activerecord/lib/active_record/associations/has_many_association.rb#L16
    def normalize_message(attribute, message, options)
      case message
      when Symbol
        generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
      when Proc
        message.call
      else
        message
      end
    end
```

[generate_message](https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/errors.rb#L412)にたどり着く。

```ruby:activemodel/lib/active_model/errors.rb#L412
    def generate_message(attribute, type = :invalid, options = {})
      type = options.delete(:message) if options[:message].is_a?(Symbol)

      if @base.class.respond_to?(:i18n_scope)
        defaults = @base.class.lookup_ancestors.map do |klass|
          [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
            :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
        end
      else
        defaults = []
      end

      defaults << options.delete(:message)
      defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
      defaults << :"errors.attributes.#{attribute}.#{type}"
      defaults << :"errors.messages.#{type}"

      defaults.compact!
      defaults.flatten!

      key = defaults.shift
      value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)

      options = {
        default: defaults,
        model: @base.model_name.human,
        attribute: @base.class.human_attribute_name(attribute),
        value: value
      }.merge!(options)

      I18n.translate(key, options)
    end
```

`defaults`にメッセージのパスの候補を色々と積み上げて、`I18n.translate`に渡している。`defaults`の値はどうなっているかというと、上記の例では、@baseにはParentのインスタンスが入っていて、attributeには`:base`、typeは`"restrict_dependent_destroy.many"`が入っているので、以下のようになる。

```ruby:defaultsの値
[
  :"activerecord.errors.models.parent.attributes.base.restrict_dependent_destroy.many",
  :"activerecord.errors.models.parent.attributes.restrict_dependent_destroy.many",
  :"activerecord.errors.messages.restrict_dependent_destroy.many",
  :"errors.attributes.base.restrict_dependent_destroy.many",
  :"errors.messages.restrict_dependent_destroy.many"
]
```

そして我々を惑わすメッセージは[ここ](https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/errors.rb#L432)で作られている。

```activemodel/lib/active_model/errors.rb#L432
      key = defaults.shift
```

Array#shiftは配列の先頭を返却して、配列から削除するメソッドである。そのため、この操作の後、keyとdefaultsはこのようになっている。

```ruby:keyの値
:"activerecord.errors.models.parent.attributes.base.restrict_dependent_destroy.many"
```

```ruby:defaultsの値
[
  :"activerecord.errors.models.parent.attributes.restrict_dependent_destroy.many",
  :"activerecord.errors.messages.restrict_dependent_destroy.many",
  :"errors.attributes.base.restrict_dependent_destroy.many",
  :"errors.messages.restrict_dependent_destroy.many"
]
```

I18n.translateは[こちら](https://github.com/svenfuchs/i18n/blob/master/lib/i18n.rb#L144)(だと思う)

```ruby:i18n.rb#L144
    def translate(*args)
      options  = args.last.is_a?(Hash) ? args.pop.dup : {}
      key      = args.shift
      backend  = config.backend
      locale   = options.delete(:locale) || config.locale
      handling = options.delete(:throw) && :throw || options.delete(:raise) && :raise # TODO deprecate :raise

      enforce_available_locales!(locale)
      raise I18n::ArgumentError if key.is_a?(String) && key.empty?

      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
      result.is_a?(MissingTranslation) ? handle_exception(handling, result, locale, key, options) : result
    end
    alias :t :translate
```

keyはsymbolなので、 `backend.translate(:ja, :"activerecord.errors.models.parent.attributes.base.restrict_dependent_destroy.many", options)` が呼び出されます。


backend.translateは[こちら](https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/base.rb#L24)(だよね?)

```ruby:lib/i18n/backend/base.rb#L24
      def translate(locale, key, options = {})
        raise InvalidLocale.new(locale) unless locale
        entry = key && lookup(locale, key, options[:scope], options)

        if entry.nil? && options.key?(:default)
          entry = default(locale, key, options[:default], options)
        else
          entry = resolve(locale, key, entry, options)
        end

        if entry.nil?
          if (options.key?(:default) && !options[:default].nil?) || !options.key?(:default)
            throw(:exception, I18n::MissingTranslation.new(locale, key, options))
          end
        end
#...snip
```

というわけで、ここで`I18n::MissingTranslation`がthrowされて、つらいメッセージが表示されたのですね(これ以上追いかけるのを諦めたので試合終了です)。

めでたしめでたし。