26
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

TL;DR

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

再現方法

参考 : システムのgemにrailsをインストールせずrails newする

まずは、アプリを作る。

$ 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を持っているという関係。

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

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

という関係にする。

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: こんにちは

再現試験。

$ 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ファイルが配布されているので、それ使えって話。

どうしてあんなつらいメッセージが格納されたか。

気になったのでソースを追いかけてみる。

このメッセージはここが発端である。

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 はこちら

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 を経由して、

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にたどり着く。

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"が入っているので、以下のようになる。

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"
]

そして我々を惑わすメッセージはここで作られている。

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

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

keyの値
:"activerecord.errors.models.parent.attributes.base.restrict_dependent_destroy.many"
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はこちら(だと思う)

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はこちら(だよね?)

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されて、つらいメッセージが表示されたのですね(これ以上追いかけるのを諦めたので試合終了です)。

めでたしめでたし。

26
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?