@kyanny's blog

My thoughts, my life. Views/opinions are my own.

i18next.js をテスト環境で正しく設定する方法

CI環境下では pass するがローカルマシンで実行すると fail するテストがあった。テスト環境用の i18n.init のオプションが不十分なのが原因だった。

ローカルでのみ落ちるテストは pluralization rule の単数形・複数形にまつわるもので、以下のような翻訳リソースがあるとき、 "2 activities" を期待している部分が "2 activity" となってしまいアサーションが失敗する、というものだった。

locales/en-US/translation.json

{
  "activity": "__count__ activity",
  "activity_plural": "__count__ activities"
}

実行結果が "1 activity" になってしまうのならともかく、 "2 activity" はおかしい。このテストは Mocha と PhantomJS で実行しており、調べた結果、端末のシェルの環境変数 LANG が ja_JP.UTF-8 だと fail し、 en_US.UTF-8 だと pass していた。

その時点での、テストランナー用の HTML に書かれていた i18next の初期化オプションは以下。なお window.__locale にはすでに locales/en-US/translation.json の内容が代入されているものとする。

i18n.init({
  customLoad: function(lng, ns, options, loadComplete) {
    loadComplete(null, window.__locale); // or loadComplete('some error'); if failed
  },
  load: 'current',
  fallbackLng: false
});

この場合 lng オプションを設定していないので、 i18next の言語設定はブラウザ(この場合 PhantomJS)の言語設定により決定される。 PhantomJS は環境変数 LC_ALL や LANG の値を利用するので、端末のシェルの LANG が ja_JP.UTF-8 になっている状態でテストを実行すると i18n.lng() === 'ja' となる。

日本語の pluralization rule は英語と異なり、数値によって単数と複数が変化しない。 "__count__個の活動" という翻訳リソース一種類で「1個の活動」「2個の活動」の両方をまかなえる。

しかし、テスト環境用には英語向けの翻訳リソースデータしか用意していなかったため、 i18next は日本語の pluralization rule に基づいて "activity_plural" ではなく "activity" のほうを選び、 "2 activity" という期待はずれの翻訳結果が得られてしまったのだ。

i18n.init の初期化オプションを以下のように変更することで、他のコードを変更することなく、ローカル環境でも CI 環境でもテストが pass するようになった。

i18n.init({
  lng: 'en-US',
  resStore: {
    'en-US': {
      translation: window.__locale
    }
  },
  fallbackLng: 'en-US'
});

lng を明示的に en-US にすることで PhantomJS のブラウザ言語(つまり端末のシェルの環境変数 LANG)に左右されず、常に i18n.lng() === 'en-US' でテストが実行されるようになる。なお、テスト環境用に en-US の翻訳リソースをセットしていることからもわかるように、このアプリケーションのデフォルト言語は英語である。

テストの内容によっては i18n.setLng を利用して言語設定を変更することもある。英語と日本語では姓名の並び順が逆なので、氏名の入力フォームのテキストフィールドの表示順も逆になっていることをテストしたい、など。そういう場合に ja の翻訳リソースが存在しないと i18n.t は翻訳を行わないので、 fallbackLng を en-US にしておくことで、テスト環境下では用意されていない言語に変更した状態で i18n.t が呼び出されても翻訳結果が得られるようになる。


言語設定とは関係ないが、 i18next の ajax リクエストによる翻訳リソースダウンロードの仕組みを使わず、直接翻訳リソースをセットする場合は、 customLoad よりも resStore を使ったほうがよい。 customLoad は ajax リクエストをカスタマイズする用途向けにある機能だ。

ajax リクエストを送信せず loadComplete に翻訳リソースを渡すのも resStore を使って翻訳リソースをセットするのも最終的には同じ結果となるが、テスト環境では ajax リクエストは利用できないことが多いだろうから、 resStore を使ったほうが意図が明確になる。 resStore は以後の初期化処理をスキップするので無駄がなくより高速でもある(もっとも、実行速度の差は数ミリ秒程度に過ぎないだろう)

実は件の落ちるテストの調査を始めた当初は customLoad を使っていることが原因ではないかと疑い、 resStore に置き換えればなおるはずだと思っていた。 i18next.js のソースもその仮説に基づいて読み込み、デモ用のコードまで書いたものの、問題のアプリケーションのテストは pass せず、検証しなおした結果 lngfallbackLng をセットする必要があるという結論に達した。