Reactのreact-i18nextで画面の言語を切り替える方法

​​2026年10月24日​React​i18next,react-i18next,TypeScript

react-i18next では、画面の文言を言語ごとの訳文として用意し、コンポーネントでは useTranslation の t で取り出します。言語を切り替えるときは i18n.changeLanguage('en') を呼ぶだけです。useTranslation を使っているコンポーネントが描画し直され、表示が新しい言語に変わります。

筆者は検証環境で、日本語と英語の2つの言語を持つ小さな画面を作り、切り替えボタン、訳がないときの代わりの言語、選んだ言語の保存、TypeScript でのキーの型チェックを確かめました。

補足

検証環境は Ubuntu 24.04、Node.js 22.22.0、Chromium 141 です。使ったのは react-i18next 17.1.4、i18next 26.5.6、i18next-browser-languagedetector 8.2.1、react / react-dom 19.3.1、typescript 7.0.2 です。型チェックは tsc で、ブラウザでの確認は、esbuild で変換したコード(開発用のビルド)を Chromium で動かし、Playwright で操作して行いました。アプリは StrictMode で包んでいません。訳文を別のファイルからネットワーク越しに読み込む方法(http-backend など)、Trans コンポーネント、Next.js などのサーバー側の描画は試していません。

訳文を用意して、i18next を初期化する

まず、言語ごとの訳文を、同じキーで用意します。検証では TypeScript のオブジェクトにしました。英語の訳文には、わざと nap を入れていません。

ts
// src/locales/ja.ts
export const ja = {
  title: '猫の手帳',
  greeting: 'こんにちは、{{name}}さん',
  meals_other: 'ごはん {{count}} 回',
  language: '言語',
  nap: 'お昼寝中',
} as const;
ts
// src/locales/en.ts
export const en = {
  title: 'Cat Notebook',
  greeting: 'Hello, {{name}}',
  meals_one: '{{count}} meal',
  meals_other: '{{count}} meals',
  language: 'Language',
  // nap はまだ英訳していない
} as const;

次に、i18next を初期化するファイルを作ります。公式のクイックスタートと同じく、i18n.use(initReactI18next) で登録して、react-i18next から使えるようにします。

ts
// src/i18n.ts
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import { ja } from './locales/ja';
import { en } from './locales/en';

export const resources = {
  ja: { translation: ja },
  en: { translation: en },
} as const;

i18n.on('languageChanged', (lng) => {
  document.documentElement.lang = lng;
});

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    supportedLngs: ['ja', 'en'],
    fallbackLng: 'ja',
    detection: {
      order: ['localStorage', 'navigator'],
      caches: ['localStorage'],
    },
    interpolation: { escapeValue: false },
  });

export default i18n;

LanguageDetector は、最初に表示する言語を決め、選んだ言語を保存するためのプラグインです(後の節で説明します)。プラグインを使わないなら、lng: 'ja' のように最初の言語を書きます。クイックスタートには、言語を検出するプラグインを使うときは lng を書かないように、とあります。escapeValue: false は、React が表示する時点でエスケープするため、i18next 側ではエスケープしない設定です。

このファイルは、アプリの入り口(main.tsx)で読み込んでおきます。

useTranslation で訳文を表示し、changeLanguage で切り替える

コンポーネントでは、useTranslation から t と i18n を受け取ります。t にキーを渡すと、今の言語の訳文が返ります。次のコードは、検証に使った App.tsx から、確認用の data-testid などを除いたものです。

tsx
import { useTranslation } from 'react-i18next';

const languages = [
  { code: 'ja', label: '日本語' },
  { code: 'en', label: 'English' },
] as const;

function LanguageSwitcher() {
  const { t, i18n } = useTranslation();
  return (
    <div>
      <span>{t('language')}</span>
      {languages.map(({ code, label }) => (
        <button
          key={code}
          aria-pressed={i18n.resolvedLanguage === code}
          onClick={() => i18n.changeLanguage(code)}
        >
          {label}
        </button>
      ))}
    </div>
  );
}

function CatCard({ name, meals }: { name: string; meals: number }) {
  const { t } = useTranslation();
  return (
    <section>
      <p>{t('greeting', { name })}</p>
      <p>{t('meals', { count: meals })}</p>
      <p>{t('nap')}</p>
    </section>
  );
}

切り替えボタンは、i18n.changeLanguage(code) を呼ぶだけです。どのボタンを押された状態にするかは、i18n.resolvedLanguage と比べて決めています。公式の API リファレンスでも、言語の切り替えボタンには resolvedLanguage が使えると説明されています。

ブラウザの言語を日本語にした Chromium で開き、「English」を押した結果です(見出しは、2つのコンポーネントを並べた App で t('title') を表示しています。検証の画面には、後の節で使う行も入っています)。

検証環境のChromiumで、上に日本語の画面(「猫の手帳」「こんにちは、たまさん」「ごはん 1 回」「お昼寝中」)、下に「English」を押した後の画面(「Cat Notebook」「Hello, たま」「1 meal」「3 meals」、「お昼寝中」はそのまま)を縦に並べた画像
「English」を押す前と後の画面

見出し、あいさつ、ごはんの回数、ボタンの横の「言語」が英語に変わりました。押されている側を示す aria-pressed も、日本語から English に移っています。<html> の lang 属性も ja から en に変わりました。src/i18n.ts で languageChanged のイベントを受け取り、lang を書き換えているためです。このイベントは初期化のときにも起きていて、最初の表示から lang が ja になっていました。

useTranslation を使うコンポーネントが描画し直されるのは、react-i18next の設定の bindI18n が、既定で languageChanged になっているためです。

言語によって、複数形のキーの数が違う

t('meals', { count: meals }) のように count を渡すと、i18next は数に合わせて meals_one や meals_other を選びます。どの接尾辞を使うかは、ブラウザや Node.js にある Intl.PluralRules の区分に従います(公式ドキュメントの複数形の説明)。

Node.js で区分を出すと、日本語は other だけ、英語は one と other の2つでした。そのため、日本語の訳文は meals_other だけで足ります。0、1、2 を渡した結果です。

text
# Intl.PluralRules ja: ["other"]
# Intl.PluralRules en: ["one","other"]
# ja: ごはん 0 回 / ごはん 1 回 / ごはん 2 回
# en: 0 meals / 1 meal / 2 meals

訳がないキーは、fallbackLng の言語で表示される

英語の訳文には nap を入れていません。英語に切り替えた後も、この行は「お昼寝中」のまま表示されました。fallbackLng: 'ja' にしているので、英語に訳がないキーは日本語で表示されます。

fallbackLng を書かないと、既定値の dev(開発者向けの言語)が使われます。公式ドキュメントのフォールバックの説明では、本番では実在の言語を指定するようにと書かれています。

ポイント

fallbackLng には、訳文がいちばんそろっている言語を指定します。訳し忘れたキーがあっても、キーの名前(nap)がそのまま画面に出ずに済みます。Node.js で fallbackLng を書かずに英語で t('nap') を呼ぶと、nap がそのまま返りました。

「en-US」のような言語コードは supportedLngs でそろえる

ブラウザが伝える言語は en-US のように地域が付いていることがあります。訳文は ja と en しか用意していません。この状態で、Node.js で changeLanguage('en-US') と changeLanguage('fr') を呼び、supportedLngs の有無で比べました。

text
# --- supportedLngs: undefined
# changeLanguage('en-US') -> language=en-US resolvedLanguage=en languages=["en-US","en","ja"] title=Cat Notebook
# changeLanguage('fr') -> language=fr resolvedLanguage=ja languages=["fr","ja"] title=猫の手帳
# --- supportedLngs: ["ja","en"]
# changeLanguage('en-US') -> language=en resolvedLanguage=en languages=["en","ja"] title=Cat Notebook
# changeLanguage('fr') -> language=ja resolvedLanguage=ja languages=["ja"] title=猫の手帳

supportedLngs がないと、i18n.language は渡したとおりの en-US や fr になります。このとき i18n.language をボタンの言語コードと比べると、en-US は ja とも en とも一致しません。訳文が実際に使われている言語は resolvedLanguage に入るので、ボタンの判定はこちらを使います。supportedLngs を書くと、en-US は en に、用意していない fr は fallbackLng の ja になりました。

選んだ言語を保存して、次に開いたときも使う

i18next-browser-languagedetector を使うと、最初に表示する言語をブラウザの情報から決め、選んだ言語を保存できます。インストールした 8.2.1 の既定では、クエリ文字列(?lng=)、Cookie、localStorage、sessionStorage、ブラウザの言語設定、<html> の lang 属性の順に調べ、選んだ言語を localStorage の i18nextLng に保存します。検証では、調べる先を localStorage とブラウザの言語設定の2つに絞りました。

Chromium の言語設定(Playwright の locale)を変えて開き、最初の表示を確かめた結果です。

ブラウザの言語 操作 表示 localStorage の i18nextLng
ja-JP 開いた直後 日本語 ja
ja-JP 「English」を押した後 英語 en
ja-JP そのまま再読み込み 英語 en
en-US 開いた直後 英語 en
fr-FR 開いた直後 日本語 ja

ブラウザの言語が日本語でも、「English」を押した後に再読み込みすると、英語のまま表示されました。保存した言語を、ブラウザの言語設定より先に調べているためです。訳文を用意していないフランス語のブラウザでは、fallbackLng の日本語で表示されました。

このプラグインは、ブラウザの言語設定から決めた言語も保存します。開いた直後の時点で、localStorage に ja や en が入っていました。そのため、ボタンを押していなくても、次に開いたときは保存した言語が使われます。

コンポーネントの外で t を呼ぶと、言語を変えても訳し直されない

コンポーネントの外、たとえばモジュールの一番上で i18n.t を呼ぶと、その時点の言語で1回だけ訳されます。検証では、次の定数を画面に出しました。

tsx
import i18n from './i18n';

// モジュールの読み込み時に1回だけ訳す(言語を切り替えても変わらない)
const titleAtLoad = i18n.t('title');

「English」を押すと見出しは「Cat Notebook」に変わりましたが、titleAtLoad は「猫の手帳」のままでした。英語のまま再読み込みすると、今度は「Cat Notebook」になり、続けて「日本語」を押しても「Cat Notebook」のままです。画面に出す文言は、コンポーネントの中で useTranslation の t を呼んで訳します。

TypeScript で、キーの書き間違いを型エラーにする

i18next の型に、訳文のオブジェクトの型を教えると、t に渡せるキーが決まります。公式ドキュメントの TypeScript の説明にあるとおり、型宣言のファイルに declare module 'i18next' を書き、その中の CustomTypeOptions に resources の型を書き足します。

ts
// src/i18next.d.ts
import 'i18next';
import type { resources } from './i18n';

declare module 'i18next' {
  interface CustomTypeOptions {
    resources: (typeof resources)['ja'];
  }
}

キーの元にしたのは、訳文がすべてそろっている日本語です。この状態で t('titel') と書くと、型チェックでエラーになりました(出力は、先頭の行の前半だけを抜き出しています)。

text
errors/WrongKey.tsx(5,17): error TS2345: Argument of type '["titel"]' is not assignable to parameter of type ...

続きの行には、Did you mean '"title"'? と、正しいキーの候補も出ていました。複数形のキーは meals_other のほか、接尾辞を除いた meals でも渡せるようになっていて、t('meals', { count: meals }) はエラーになりません。

{{name}} を使う訳文では、渡す値にも型が付きます。t('greeting', {}) や、綴りを間違えた t('greeting', { nmae: 'たま' }) は型エラーになりました。ただし、2つ目の引数ごと省いた t('greeting') はエラーになりませんでした。

公式ドキュメントには、t($ => $.title) のように関数でキーを選ぶ書き方(セレクター)の例も載っています。これは CustomTypeOptions の enableSelector を有効にしたときの書き方です。この設定の既定値は false で、インストールした i18next 26.5.6 の型でも false でした。この記事では、文字列でキーを渡す書き方で確かめています。公式ドキュメントには、v27 で文字列のキーへの型の対応を非推奨にする予定(確定ではない)とも書かれています。

言語の切り替えは changeLanguage と resolvedLanguage でつくる

react-i18next で言語を切り替えるときは、i18n.changeLanguage を呼び、画面の文言はコンポーネントの中で useTranslation の t から取り出します。今の言語の判定には resolvedLanguage を使い、訳がないときの言語は fallbackLng で、受け付ける言語は supportedLngs で決めておきます。次に開いたときも同じ言語にしたいなら、言語を検出するプラグインに localStorage へ保存させます。

​Posted by 86