かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React i18next 国際化対応で言語切替が上手くいかない問題にハマる

React で i18next を使った国際化対応したアプリを開発していて、言語切り替えボタンを押しても言語が切り替わらない問題にハマったのでメモ

環境

  • i18next 23.4.1
  • react-i18next 13.0.3
  • react 18.2.0
  • typescript 5.0.2

setup i18next

$ npm install i18next
$ npm install react-i18next

ユーザーの環境からいい感じに言語設定を取得する i18next-browser-languagedetector というパッケージもあるが今回の例では使用しない

i18next config

i18next を使う設定ファイルを作成する
/src/i18n/config.ts

import i18n from "i18next";
import { initReactI18next } from 'react-i18next';

i18n
  .use(initReactI18next)
  .init({
    debug: true, // for development
    fallbackLng: 'en',
    resources: {
      en: {
        translation: {
          "Learn": "Learn i18next + React"
        },
      },
      ja: {
        translation: {
          "Learn": "i18next + React を学ぶ"
        },
      },
    },
  });

cf.

設定ファイル React のアプリで読み込む

/src/main.tsx

import './i18n/config';

App.tsx など開発中にホットリロードされるコンポーネントで i18next の設定ファイルをインポートしていると i18next: init: i18next is already initialized. You should call init just once! という警告が発生するので変更が入らないファイルでインポートするのが良さそう

🐞 i18next から import した t では changeLanguage で言語を切り替えてもテキストが切り替わらない

import i18next, { changeLanguage, t } from 'i18next';

const langs = {
  en: { nativeName: 'English' },
  ja: { nativeName: '日本語' },
} as const;

function App() {
  return (
    <div>
      <h1>{t('Learn')}</h1>
      <div>
        {Object.keys(langs).map((lang) => (
          <button
            key={lang}
            onClick={() => changeLanguage(lang)}
            disabled={i18next.language === lang}
          >
            {langs[lang as typeof keyof langs].nativeName}
          </button>
        ))}
      </div>
    </div>
  );
}
export default App;

Object.keys(langs).map 内のの箇所で型エラーが発生するが、長くなるので本エントリーでは割愛し as keyof typeof langs で対応

🙅 i18next react

この App コンポーネントではボタンを押した際に changeLanguage(lang) で言語が変更されるが t('Learn') で表示されるテキストは切り替わらない。useState などで state を持たせると state が変更されコンポーネントが再レンダリングされると t('Learn') のテキストも変更される。
つまり i18next から直接 import した t は再レンダリングをしない。(state や props が変更されるわけではないのでそれはそう…)

i18next-reactuseTranslation hook を使う

import { useTranslation } from 'react-i18next';

function App() {
  const {t, i18n: {changeLanguage, resolvedLanguage} } = useTranslation();
  return (
    <div>
      <h1>{t('Learn')}</h1>
      <div>
        {Object.keys(langs).map((lang) => (
          <button
            key={lang}
            onClick={() => changeLanguage(lang)}
            disabled={resolvedLanguage === lang}
          >
            {langs[lang as typeof keyof langs].nativeName}
          </button>
        ))}
      </div>
    </div>
  );
}
export default App;

🙆 i18next react

useTranslation() から取得した t を利用すると changeLanguage() で言語が切り替えられた際にコンポーネントが再レンダリングされテキストが切り替わるようになる
useTranslation() で取得できる i18n は内部的に使われている i18next のオブジェクト

設定で i18n.use(initReactI18next) していたらよしなにやってくれるんだろうと思いこんでてハマりました。ドキュメントのデザインが似ていて気づかなかったけど i18next-react のドキュメントの方にちゃんと書いてあった…
思い込みはエンジニアの敵!!!!反省!!!!!!!

おわり


[参考]