Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

i18n YAML ファイル中の日本語/英語のズレを検知する単体テスト

こんにちは クラウド経費開発チームクラウド債務支払開発チーム の 宮村(みやむー) @miyamura.koyo です。

この記事は Money Forward Engineering Advent Calendar 2022 の6日目です。

今回は i18n YAML ファイル中の日本語/英語のズレを検知する単体テストについて書こうと思います。

i18n のズレ

i18n を使って文言管理をすると、YAML ファイルに書かれた翻訳ファイルで日本語と英語にズレが発生することがあります。

例えばこんな感じの YAML ファイルを考えると日本語には ruby というキーに対応する翻訳があるのですが、英語にはない、というような状況が発生します。

ja:
  money: お金
  ruby: Ruby
en:
  money: money

上記のズレが発生したまま英語設定の環境で ruby を参照すると I18n::MissingTranslation エラーが出てしまいます。

書き忘れないように気をつければいいという話はあるのですが、とはいえ人間が書くものなのでたまにウッカリ書き忘れることもありますよね。 また、エラー監視しているとたまに I18n::MissingTranslation が出てるのを観測しており、その度に手で直していて「こういう時こそテストで教えてほしい!」と思うようになりました。

i18n 翻訳ズレを検知する方法

ということで何かいい方法ないかと調べてみることにしました。 ざっと調べてみたところ i18n-taskslocaler という gem を見つけました。

しかし今回の課題を解決するためにそこまで高機能なものが欲しいわけではなく、また gem として入れてメンテナンスし続けるほど困っているわけでもないので導入するか悩んでいました。 また、ビューやコントローラーなどいろんなファイルを調べてくれる都合上、実行時間も少し気になりました。

やりたい要件をまとめてみると

  • テスト実行時間が長くなりすぎないこと
  • 最低限リポジトリにある YAML ファイルの翻訳ズレを検知してくれること
    •  ※ Gem が提供している i18n までチェックしなくてもいい

という感じだったので、これくらいならサッと自作してしまった方が早いかも?と思ったので作ってみました。

どう作るか

まず思いつくのがズレを検知するスクリプトを作成して定期的にチェックする方法です。 しかしこれだと定期的にチェックする運用が発生するし、できればズレた段階で気づけるようにしたいです。

色々考えた結果 「ズレがあるかテストする rspec を作成し、それを CI で回っている単体テストに組み込んで、自動でチェックできるようにする」 という方法を採用しました。

すでに PR に対して単体テストを CI で回す仕組みはあるので、そちらに乗っかる作戦です! これならズレを発生した当事者が気づけるので運用しやすいですね。

作成した rspec の概要

ということで作ってみました。

# spec/i18n/i18n_check_spec.rb
require "rails_helper"

RSpec.describe "I18n key check" do
  context 'Check no diff each translation in i18n .yml files' do
    def find_all_locales(pathnames)
      pathnames
      .reduce({}) { |acc, pathname| acc.deep_merge(load_yaml(pathname)) }
    end

    def load_yaml(pathname)
      pathname.read.then { |file| YAML.safe_load(file, aliases: true) }
    end

    def flatten_nested_hash(hash, previous_key = nil, flattened_hash = {})
      return if hash.nil?

      hash.each do |key, value|
        current_key = [previous_key, key.to_s].compact.join('.')

        if value.is_a?(Hash)
          flatten_nested_hash(value, current_key, flattened_hash)
          next
        end

        flattened_hash[current_key] = value
      end

      flattened_hash
    end

    context 'config/locales' do
      let!(:exclude_files) do
        #対象外の yaml ファイルはここに列挙して除外する
        [
          "config/locales/excludable/ja.yml"
        ].map do |exclude_file|
          Rails.root.join(exclude_file).to_s
        end
      end

      let!(:todo_i18n_whitelist) do
        i18n_todo = Rails.root.join("spec/i18n/i18n_check_todo.yml").then { |pathname| load_yaml(pathname) } || {}
        i18n_ignore = Rails.root.join("spec/i18n/i18n_check_ignore.yml").then { |pathname| load_yaml(pathname) } || {}

        i18n_ignore.merge(i18n_todo) { |_key, oldval, newval| (oldval || []) + (newval || []) }
      end

      let!(:target_pathname) { Rails.root.join('config/locales').glob('**/*.yml').reject { |pathname| exclude_files.include?(pathname.to_s) } }
      let!(:all_locale_hash) { find_all_locales(target_pathname) }
      let!(:ja_locale_keys) { flatten_nested_hash(all_locale_hash["ja"]).keys }
      let!(:en_locale_keys) { flatten_nested_hash(all_locale_hash["en"]).keys }

      context 'Japanese translation exists, but English does not' do
        it { expect(ja_locale_keys - en_locale_keys).to match_array(todo_i18n_whitelist["ja_en"]) }
      end

      context 'English translation exists, but Japanese does not' do
        it { expect(en_locale_keys - ja_locale_keys).to match_array(todo_i18n_whitelist["en_ja"]) }
      end
    end
  end
end

実行するリポジトリの config/locales 以下の .yml 拡張子を持つファイル全てを取得して、日本語("ja")と英語("en")をキーにした hash を作って Array#- で比較することでズレを検知する仕組みです。 (なお実行対象外のファイルは exclude_files に書いて除外しています) Ruby は配列の差分を簡単に計算できるからとてもいいですね...! https://docs.ruby-lang.org/ja/latest/method/Array/i/=2d.html

また思ったよりズレがあったので i18n_check_todo.yml ファイルに記載することで一旦無視できるようにしました。 さらに、ズレていても問題ない場合が一部存在していたので i18n_check_ignore.yml に追記して恒久的に無視できるようにしました。 なおこの辺りは Rubocop の命名と合わせて直感的にわかりやすいようにしました。

# spec/i18n/i18n_check_todo.yml
# 直すべきだが現状は直せていない i18n のキーのズレを記述する。
# spec/i18n/i18n_check_spec.rb のエラー出力を書くことでテスト対象から除外される。

# 日本語にあって英語にない翻訳
ja_en:
  # 例:
  # - "money.yen"

# 英語にあって日本語にない翻訳
en_ja:
  # 例:
  # - "money.yen"
# spec/i18n/i18n_check_ignore.yml
# i18n のキーのズレを許容するリストを記述する。
# spec/i18n/i18n_check_spec.rb のエラー出力を書くことでテスト対象から除外される。

# 日本語にあって英語にない翻訳
ja_en:
  # 例:
  # - "money.yen"

# 英語にあって日本語にない翻訳
en_ja:
  # 例:
  # - "money.yen"

実行例

実行して失敗した場合は以下のような形式で出ます。 なお失敗したキーは、必要に応じて i18n_check_ignore.ymli18n_check_todo.yml にそのまま書いて対応できるよう設計しています。

$ bundle exec rspec spec/i18n/i18n_check_spec.rb

     Failure/Error: it { expect(en_locale_keys - ja_locale_keys).to match_array(todo_i18n_whitelist["en_ja"]) }

       expected collection contained:  ["xxx.yyy.zzz", "111.222.333"]
       actual collection contained:    ["xxx.yyy.zzz", "111.222.333", "money.yen"]
       the extra elements were:        ["money.yen"]

実行時間

1000ファイル以上の YAML ファイルがあるリポジトリで動かしてみても概ね1秒程度で実行が完了します!

$ bin/rspec spec/i18n/i18n_check_spec.rb
..

Finished in 1.14 seconds (files took 0.27882 seconds to load)
2 examples, 0 failures

運用してみた結果

実際これ書いてみて実行してみたところ、思ったよりたくさんズレていたので、まずそれらは i18n_check_todo.yml に記載して一旦リリースしました。 もちろんその後、 i18n_check_todo.yml に記載したズレを時間を見つけて一気に直しました!世界が平和に・・・! (ちなみに結構大変でしたw)

これをリリースして数ヶ月以上経ちましたが、その間一度も I18n::MissingTranslation は出ませんでした 👍 さらに開発中にうっかり翻訳漏れを作ってしまっても CI が落ちるので気付けるため、新規にズレが発生することもなくなりました! (チームメンバーがこれに引っかかっているのを見てニヤニヤしてみたり笑)

結果的に小さい spec ファイルで実現できました。 もし今後もっとしっかりチェックしたくなった場合でも気軽に拡張したり、捨てたりできるので、今後の運用もしやすいものにできたのがお気に入りポイントですね!

まとめ

i18n YAML ファイル中の日本語/英語のズレを検知する単体テストの実装と運用結果について紹介しました。 今回は2つの言語間のズレを検知するのみでしたが、工夫することで複数ファイルも対応できる構成になっているので、多言語対応が必要な場合でも参考になるかと思います。

なお実装中に、先輩の野田さんにアドバイスいただきました。この場を借りて感謝いたします!

以上 Money Forward Engineering Advent Calendar 2022 の6日目でした。 また次の日も見てね!


マネーフォワード福岡拠点では、エンジニアを募集しています!

アドベントカレンダーが活発な企業に興味のある方はぜひ 👍

株式会社マネーフォワード 福岡 の求人一覧

福岡開発拠点のサイトもあるのでぜひみてください!

マネーフォワード 福岡開発拠点


マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。

【会社情報】 ■Wantedly株式会社マネーフォワード福岡開発拠点関西開発拠点(大阪/京都)

【SNS】 ■マネーフォワード公式noteTwitter - 【公式】マネーフォワードTwitter - Money Forward Developersconnpass - マネーフォワードYouTube - Money Forward Developers