こんにちは クラウド経費開発チーム 、クラウド債務支払開発チーム の 宮村(みやむー) @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-tasks
や localer
という 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.yml
や i18n_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】 ■マネーフォワード公式note ■Twitter - 【公式】マネーフォワード ■Twitter - Money Forward Developers ■connpass - マネーフォワード ■YouTube - Money Forward Developers