Reactのreact-i18nextで画面の言語を切り替える方法
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 などのサーバー側の描画は試していません。
- 1. 訳文を用意して、i18next を初期化する
- 2. useTranslation で訳文を表示し、changeLanguage で切り替える
- 3. 言語によって、複数形のキーの数が違う
- 4. 訳がないキーは、fallbackLng の言語で表示される
- 5. 「en-US」のような言語コードは supportedLngs でそろえる
- 6. 選んだ言語を保存して、次に開いたときも使う
- 7. コンポーネントの外で t を呼ぶと、言語を変えても訳し直されない
- 8. TypeScript で、キーの書き間違いを型エラーにする
- 9. 言語の切り替えは changeLanguage と resolvedLanguage でつくる
訳文を用意して、i18next を初期化する
まず、言語ごとの訳文を、同じキーで用意します。検証では TypeScript のオブジェクトにしました。英語の訳文には、わざと nap を入れていません。
// src/locales/ja.ts
export const ja = {
title: '猫の手帳',
greeting: 'こんにちは、{{name}}さん',
meals_other: 'ごはん {{count}} 回',
language: '言語',
nap: 'お昼寝中',
} as const;
// 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 から使えるようにします。
// 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 などを除いたものです。
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') を表示しています。検証の画面には、後の節で使う行も入っています)。

見出し、あいさつ、ごはんの回数、ボタンの横の「言語」が英語に変わりました。押されている側を示す 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 を渡した結果です。
# 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 の有無で比べました。
# --- 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回だけ訳されます。検証では、次の定数を画面に出しました。
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 の型を書き足します。
// src/i18next.d.ts
import 'i18next';
import type { resources } from './i18n';
declare module 'i18next' {
interface CustomTypeOptions {
resources: (typeof resources)['ja'];
}
}
キーの元にしたのは、訳文がすべてそろっている日本語です。この状態で t('titel') と書くと、型チェックでエラーになりました(出力は、先頭の行の前半だけを抜き出しています)。
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 へ保存させます。





