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
プロセス管理ではよくある「親が子を残して死ぬことは許されない」
rubyでプロセス管理の話になり「あー、これ殺せてないね」「親子供は皆殺ししなきゃ!」という物騒な話に花を咲かせてた。たのしい。
— えにぐま (@enigma63) 2016年5月17日
という関係にする。
class Child < ActiveRecord::Base
has_many :child, dependent: :restrict_with_error
end
デフォルトロケールを日本語にする。
#...snip...
config.i18n.default_locale = :ja
#...snip...
再現に必要な最小限のlocaleファイルを作成。
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
このメッセージの通りなら、こんな感じのファイルを作成しなければならない。
ja:
activerecord:
errors:
models:
parent:
attributes:
base:
restrict_dependent_destroy:
many: "%{record}が存在しているので削除できません"
一応これでも動くけど、他にdependent: :restrict_with_error
なotherモデルが追加されるとこうなる。
ja:
activerecord:
errors:
models:
parent:
attributes:
base:
restrict_dependent_destroy:
many: "%{record}が存在しているので削除できません"
other:
attributes:
base:
restrict_dependent_destroy:
many: "%{record}が存在しているので削除できません"
すごくDRYじゃ無くて、半端ないツラみ。
対策
正しくはこう。
ja:
errors:
messages:
restrict_dependent_destroy:
many: "%{record}が存在しているので削除できません"
というか、こういうlocaleファイルが配布されているので、それ使えって話。
どうしてあんなつらいメッセージが格納されたか。
気になったのでソースを追いかけてみる。
このメッセージはここが発端である。
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 はこちら。
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 を経由して、
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にたどり着く。
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"
が入っているので、以下のようになる。
[
:"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"
]
そして我々を惑わすメッセージはここで作られている。
key = defaults.shift
Array#shiftは配列の先頭を返却して、配列から削除するメソッドである。そのため、この操作の後、keyと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"
]
I18n.translateはこちら(だと思う)
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はこちら(だよね?)
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されて、つらいメッセージが表示されたのですね(これ以上追いかけるのを諦めたので試合終了です)。
めでたしめでたし。