我々が開発している Rails アプリのひとつに、国際化が必要なものがあります。
Rails には国際化(i18n)機能があるため、それを利用すると簡単にロケールに応じてメッセージに切り替えができます。
国際化機能の詳細はここでは触れないため、Rails ガイドの「Rails 国際化(I18n) API」を参照してください。
少し前、その Rails アプリでとあるメッセージが正しく表示されていないことに気づきました。
画面には “translation missing” と表示されてしまっていました。
このときはロケールファイルのメッセージのキーを typo していて、view で参照しているキーと一致していなかったので
メッセージが表示できていませんでした。
<%# view 側では title を参照しているが… %>
<% t('.title') %>
# config/locales/en.yml
en:
users:
index:
titles: "Users" # ロケールファイルでは titles と複数形になっていた (typo)
Rails のデフォルト設定では、メッセージのキーが見つからない場合には “translation missing” と表示されますが、
ワーニングメッセージなどは出力されないため、見落としが起こることがあります。
今回発生したのもモーダルウィンドウのメッセージだったため、バグに気付くのが少し遅れました。
ミスが発生してしまうのは致し方ないのですが、それに気づきづらいのはあまり良い状況ではありません。
ということで、この翻訳メッセージ不足の対策について調べてみました。
Rails ガイドでは、 config.i18n.raise_on_missing_translations
という設定が紹介されています。
この設定を有効にすると、メッセージが見つからない場合に例外が発生するようになります。
この挙動を活かし、Rails の test モードのときに有効にしておくと、CI でメッセージの不足に気づけます。
# config/environments/test.rb
Rails.application.configure do
config.i18n.raise_on_missing_translations = true
end
なお、この設定は「実行時に」メッセージ不足を検出して例外を発生させるため、テストケースが不足している場合は問題は検出できません。
残念なことに、今回のケースでは view のテストが足りていなかったため、この設定を有効にしてもエラーは発生しませんでした。
また、Rails のバージョンにも注意が必要です。この設定は Rails 6.1 から導入されたものですが、Rails 7.1 未満の場合は I18n.t
でのメッセージ不足ではエラーが発生しません。
view やコントローラの #t
メソッドだけを使っている場合は問題ありませんが、 I18n.t
を併用している場合は問題が検出できない可能性があります。
Rails ガイドには、 config.i18n.raise_on_missing_translations
の他に I18n.exception_handler
という設定が紹介されています。
メッセージが見つからない場合に、独自の例外ハンドラを呼び出すことができます。
# config/initializers/i18n.rb
module I18n
class RaiseExceptForSpecificKeyExceptionHandler
def call(exception, locale, key, options)
if key == "special.key"
"translation missing!" # raiseせずにこれを返すこと
elsif exception.is_a?(MissingTranslation)
raise exception.to_exception
else
raise exception
end
end
end
end
I18n.exception_handler = I18n::RaiseExceptForSpecificKeyExceptionHandler.new
Rails ガイドの説明では、メッセージが見つからない場合の挙動全般をこの設定でハンドリングできるように説明されていますが、実はこの説明は正しくありません。
現状では I18n.t
で翻訳する場合には例外ハンドラが呼び出されますが、view やコントローラの #t
メソッドで翻訳する場合は例外ハンドラは呼び出されず、
これまで通り “translation missing” が返されます。
コードを調べてみた限りだと、view やコントローラ側では I18n.t
を直接呼ぶのではなく、薄いラッパー層が挟まれており、
メッセージが見つからない場合は “translation missing” と返すになっているようです。
この設定に関しては i18n gem と Rails が統合されきっていない印象があります。
I18n.exception_handler が view に対応していないのなら、ハックしてしまえばよいのではないか、ということでこんなモンキーパッチを書いてみました。
# NOTE: translation missing が発生した場合にログを記録する
# I18n.exception_handler 経由でログを出力するよう設定しても、view 内では効果がないため、
# ActionView::Helpers::TranslationHelper#missing_translation をオーバーライドしてログ出力させる
module ActionView
module Helpers
module TranslationHelper
alias orig_missing_translation missing_translation
def missing_translation(key, options)
Rails.logger.tagged("i18n") { Rails.logger.warn("translation missing: #{I18n.locale}, #{key}, #{options}") }
orig_missing_translation(key, options)
end
end
end
end
このようなモンキーパッチを当てることで、メッセージが見つからない場合のハンドリングが可能です。
この例ではログを出力するようにしていますが、メッセージを差し替えることや例外を発生させることももちろん可能です。
なお、このモンキーパッチは ActionView 向けです。これに加えて ActionController 向けのモンキーパッチも用意する必要があります。
ここまでは Rails 本体の機能を使った対策を紹介しましたが、外部ツールを使ったアプローチをひとつ紹介します。
ロケールファイルをメンテナンスするツールとして i18n-tasks という gem があります。
この i18n-tasks はメッセージの過不足の検出や自動翻訳、ロケールファイルのリファクタリング(削除やソート、リネームなど)を行うことができます。
今回の用途には、メッセージ不足の検出を行う i18n-tasks missing
コマンドが利用できます。
$ bundle exec i18n-tasks missing --types=used
#StandWithUkraine
Missing translations (2) | i18n-tasks v1.0.13
+--------+-----+----------------------------------------------------------------------+
| Locale | Key | Value in other locales or source |
+--------+-----+----------------------------------------------------------------------+
| all | bar | app/controllers/signup_controller.rb:19 |
| all | foo | app/views/users/show.html.erb:75 |
+--------+-----+----------------------------------------------------------------------+
(ファイル名は編集しました)
今回の目的に一番マッチしているように思えたのですが、動的にキーを組み立てているコードには対応していないようです。
たとえば、以下のようなコードには対応していません。
<% t("user.#{current_account.status}.name") %>
README の Dynamic Keys によると i18n-tasks-use
というコメントを書くことで、外部からヒントを与えることができるようです。
また、 search.strict
を false に設定することで、あいまいな検出もできるようです。
当初、なにかいい対策はないかと Rails ガイドを読んだ際は
config.i18n.raise_on_missing_translations
を有効にして、CI でメッセージ不足を検出するI18n.exception_handler
を使ってログを出力し、テストで見つけられなかったメッセージ不足を検出するというふたつの方法を併用しようと考えていました。
しかし、後者については view やコントローラをサポートしていないことがわかったため、採用を見送りました。
Rails ガイドで紹介しているのだし、なにか方法があるのではとコードを読んだ結果、モンキーパッチを当てる方法を編み出しましたが、
これはあくまでハックであり、将来の Rails のバージョンアップで動かなくなる可能性があるため、採用を見送りました。
最終的には i18n-tasks gem を見つけたので、 raise_on_missing_translations
と i18n-tasks を併用することにしました。
config.i18n.raise_on_missing_translations
I18n.exception_handler
raise_on_missing_translations
と i18n-tasks gem を併用することにしたみなさんも Rails の設定を調整して、翻訳メッセージ不足をやっつけましょう。