react-i18next を使った i18n 対応の始め方
React における i18n 対応ライブラリの候補
- https://github.com/lingui/js-lingui
- https://github.com/formatjs/react-intl
- https://github.com/i18next/react-i18next
人気という点では 2 つ目の react-intl
が最もありそうですが、今回は react-i18next
を選びました。
理由は t('resource name')
で公式のサンプルも書かれていて、 Rails での国際化の時の気持ちを思い出したぐらいの感じです。
最初は react-intl は hooks 対応が無いのかと思ったのですが、README や document の浅い所になかっただけで、API のページを見るとちゃんとありました。
今回のコード
GitHub - dany1468/react-i18next_sample
上記に create-react-app から作成したサンプルを置いてあります。
また、以下は今回の対応内容に登場するファイル群です。
. ├── package.json ├── public │ └── locales │ └── ja │ └── app.json ├── src │ ├── App.i18n.test.tsx │ ├── App.test.tsx │ ├── App.tsx │ ├── Fakei18n.ts │ ├── i18n.ts │ ├── index.tsx │ └── setupTests.ts ├── tsconfig.json └── yarn.lock
今回の対応内容
- リソースファイルは XHR を使って、実行時に public から取得する
- リソースファイルの JSON はネストさせることで
.
の separator でリソースをコードから指定できる - 上記に対応したテストを書ける
リソースファイルは XHR を使って、実行時に public から取得する
リソースファイルをどう扱うかはいくつか方法があり、backend plugin という形で提供されています。(もちろんコードに直接記述することもできます)
Plugins and Utils - i18next documentation
今回は xhr backend を利用します。
GitHub - i18next/i18next-xhr-backend: backend layer for i18next using browsers xhr
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import XHR from 'i18next-xhr-backend'; i18n.use(initReactI18next) .use(XHR) .init({ lng: 'ja', defaultNS: 'app', backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' } }); export default i18n;
backend.loadPath
に取得するパスを設定します。上記であれば、create-react-app の場合には public/locales
以下が対応します。
指定できるオプションに関しては以下を参照してください。
- Configuration Options - i18next documentation
- i18next instance - react-i18next documentation
- React 用に追加で提供されている部分はこちら
- GitHub - i18next/i18next-xhr-backend: backend layer for i18next using browsers xhr
- xhr backend 用に追加で提供されている部分はこちら
namespace は別リソースファイルで
上記の通り、言語だけでなく、namespace でもリソースファイルを分離しています。
こうすることで、画面別にリソースファイルを分離させることもできます。
ロード中はどうなる?
XHR での取得なので、場合によって長い待ちが発生してしまいます。
上述の React 用の設定オプションの中に useSuspense
があるのですが、こちらがデフォルトで true になっているので、Suspense
を追加することで、ロード時の代替表示を指定できます。
ReactDOM.render( <React.StrictMode> <Suspense fallback={<h1>Loading...</h1>}> <App /> </Suspense> </React.StrictMode>, document.getElementById('root') );
リソースファイルの JSON はネストさせることで .
の separator でリソースをコードから指定できる
ここに関しては、特に何か追加したという訳ではありません。よって、以下で keySeparator
の設定を追加していますが、 .
はデフォルトなので本来無くても問題ありません。
i18n.use(initReactI18next)
.use(XHR)
.init({
lng: 'ja',
defaultNS: 'app',
+ keySeparator: '.',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
});
この機能があることで、ネストした JSON に対して .
でアクセスできるようになります。
Essentials - i18next documentation
上記に対応したテストを書ける
テストに関しては以下の 2 つのパターンを想定します。
- 実際に変換された後の文字列には興味がなく、キーだけがテストできればいい
- 多くの場合はこれで良いのだと思います
- Interpolation を利用しているなど、実際にどう埋め込まれるかも確認したい
キーのみテストする場合
Testing - react-i18next documentation
上記に HOC の場合の jest.mock
のサンプルがありますが、今回は useTranslation
hook を使った場合です。
create-react-app であれば、最初から setupJest.js
があるので、以下を書き足します。
jest.mock('react-i18next', () => ({ useTranslation: () => ({ t: key => key }) }));
これでテスト対象の Component を render(<App />)
した場合には、キー名が埋め込まれた状態になります。
どう文字列として展開されるかもテストする場合
こちらも上述のテストのドキュメントにテスト用の設定をどうするかというのが書かれています。今回は以下のように Fake の設定を作成しました。
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; i18n.use(initReactI18next).init({ resources: {}, ns: ['app'], lng: 'ja', react: { useSuspense: false }, debug: false // 調査したい時は true }); export default i18n;
ポイントは resources: {}
と空を指定している部分と、 react.useSuspense: false
です。
1 点目は、テスト用のリソースは別途テストの setup で入れるのですが、一先ず枠が必要になるものです。
2 点目は、useSuspense
を切っておかないと、 render(<Suspense ...
のように、 テスト時にも Suspense で囲わなければいけなくなるためです。
これで、実際のテスト時には、以下のように Fake のリソースを設定して、その通りに render されているかを確認できます。
import i18n from './Fakei18n'; jest.unmock('react-i18next'); const testingResources = { hello: 'こんばんは {{target}}' }; i18n.addResourceBundle('ja', 'app', testingResources); // 略 expect(helloElement.textContent).toBe('こんばんは React i18n');
最後に
まだ、production で利用する際の設定や、最適化に関しては調べきれておらず、引き続き見てみたいと思っています。