RubyでYAMLファイルを読むとネストしたHashにできます。

1a:
2  b:
3    c1: hogehoge
4    c2: fugafuga

であればこうなります。

 1require 'yaml'
 2hash = YAML.load_file('さっきのyml')
 3
 4# hashは↓と同じものになる
 5{
 6  a: {
 7    b: {
 8      c1: 'hogehoge',
 9      c2: 'fugafuga',
10    }
11  }
12}

ところでRailsの多言語対応のための仕組みI18nではYAMLを使いますよね。で、I18n.l('activerecord.errors.messages.record_invalid')などとして、キーを渡すと翻訳が返ってきます。

このキーの部分をドットでsplitしてYAMLを読み込んだHashを辿れば目的の翻訳にたどり着けるわけですが、その逆の全てのキーを結合してフルパスみたいにしたHashが欲しくなったのでどうやってやるのかなーと調べました。

(そもそもなんて呼んだらいいのかわからないので、便宜的にフルパスとでも呼んでおきます。)

つまり、↓のようなYAMLファイルがあったら(rails-i18nのja.ymlを拝借しています)、

1ja:
2  activerecord:
3    errors:
4      messages:
5        record_invalid: "バリデーションに失敗しました: %{errors}"
6        restrict_dependent_destroy:
7          has_one: "%{record}が存在しているので削除できません"
8          has_many: "%{record}が存在しているので削除できません"

↓こうなってほしいのです。

1{
2  "ja.activerecord.errors.messages.record_invalid" => "バリデーションに失敗しました: %{errors}",
3  "ja.activerecord.errors.messages.restrict_dependent_destroy.has_one" => "%{record}が存在しているので削除できません",
4  "ja.activerecord.errors.messages.restrict_dependent_destroy.has_many" => "%{record}が存在しているので削除できません",
5}

調べてみてさっとできそうな方法がわからなかったので、再起的に書いてみることにしました。

 1module HashKeyJoiner
 2  module_function
 3
 4  def join(hash, divider = '.')
 5    h = {}
 6    each_path(hash.dup, '', divider) do |path, value|
 7      h[path] = value
 8    end
 9    h
10  end
11
12  def each_path(object, path, divider = '.', &block)
13    if object.is_a?(Hash)
14      object.each do |key, value|
15        div = path.empty? ? '' : divider
16        next_path = [path, key].join(div)
17        each_path value, next_path, divider, &block
18      end
19    else
20      yield path, object
21    end
22  end
23end

Hashを拡張するのもよいのですが、専用のモジュールにして呼ぶだけにしておきました。

Hashを辿りながら、次の子要素がHashであれば再帰処理、終端の文字列であれば新しいHashに値を追加をしています。途中経過のpathを結合していきながらたどっているので、キーのフルパスで新しいHashを作れます。

↓のように呼び出すと、

1yaml = YAML.load_file('./ja.yml')
2puts HashKeyJoiner.join(yaml)

↓のようにHash化されます。

1{
2  "ja.activerecord.errors.messages.record_invalid"=>"バリデーションに失敗しました: %{errors}",
3  "ja.activerecord.errors.messages.restrict_dependent_destroy.has_one"=>"%{record}が存在しているので削除できません",
4  "ja.activerecord.errors.messages.restrict_dependent_destroy.has_many"=>"%{record}が存在しているので削除できません",
5  ...
6}

使いみちはあまりなさそうですが、ひとまず僕の目的は達成。