日本語だけで作ったアプリを、あとから英語対応する——これがいちばん大変な作業です。文言がJSXの中に散らばっているので、抜き出すだけで丸1日かかります。
先に見ておけば、そうならずに済みます。
文言をコードの外に出す
npm install --save-exact i18next react-i18next設定を1つ置きます。
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
i18n.use(initReactI18next).init({
resources: {
ja: {
translation: {
title: 'ことりずかん',
search_placeholder: '名前やすんでいる場所でさがす',
favorite_one: 'お気に入り{{count}}羽',
favorite_other: 'お気に入り{{count}}羽',
},
},
en: {
translation: {
title: "Shima's Bird Book",
search_placeholder: 'Search by name or place',
favorite_one: '{{count}} favorite bird',
favorite_other: '{{count}} favorite birds',
},
},
},
lng: 'ja',
fallbackLng: 'en',
interpolation: { escapeValue: false }, // Reactが既にエスケープしている
})
export default i18nmain.tsxで読みこめば準備完了です。使う側はこうなります。
import { useTranslation } from 'react-i18next'
function BirdListPage() {
const { t } = useTranslation()
return (
<>
<h1>{t('title')}</h1>
<input placeholder={t('search_placeholder')} />
<p>{t('favorite', { count: favoriteIds.length })}</p>
</>
)
}t('title')が、いまの言語の文言に置きかわります。切りかえは1行です。
const { i18n } = useTranslation()
i18n.changeLanguage('en')実際に切りかえると、こうなりました。
| 項目 | ja | en |
|---|---|---|
t('title') | ことりずかん | Shima’s Bird Book |
t('favorite', { count: 1 }) | お気に入り1羽 | 1 favorite bird |
t('favorite', { count: 3 }) | お気に入り3羽 | 3 favorite birds |
birdとbirdsが自動で切りかわっているのが、_one/_otherという書き方の効果です。
やってはいけない書き方
いちばん多い失敗は、文字列をつなげて文を作ることです。
// ✗ 翻訳できない
<p>{birds.length}{t('birds_unit')}{t('found')}</p>
// ○ 文をまるごと1項目にする
<p>{t('found_birds', { count: birds.length })}</p>{ "found_birds_other": "{{count}}羽みつかりました" }
{ "found_birds_other": "Found {{count}} birds" }語順は言語ごとに違います。日本語は「3羽・みつかりました」、英語は「Found・3・birds」。つなげ方を決め打ちすると、どうやっても訳せない文ができあがります。
日付・数字・通貨
これは翻訳ではなく書式の話で、ブラウザに入っているIntlが答えを持っています。
new Intl.NumberFormat('ja-JP').format(1234567) // 1,234,567
new Intl.NumberFormat('de-DE').format(1234567) // 1.234.567
new Intl.DateTimeFormat('ja-JP').format(new Date()) // 2026/8/7
new Intl.DateTimeFormat('en-US').format(new Date()) // 8/7/2026桁区切りが.の国、日と月が逆の国があります。自分でpadStartして組み立てると、そこで詰みます。ライブラリを入れる前に、まずIntlです。
URLで言語を分ける
画面のボタンだけで切りかえると、URLが同じまま中身が変わります。共有できず、検索エンジンにも1言語ぶんしか認識されません。
/ja/birds/8
/en/birds/8パスで分けるのが定番です。react-routerなら言語をパスの先頭に入れます。
<Route path="/:lang/birds/:id" element={<BirdDetailPage />} />入れるかどうか
| 状況 | 判断 |
|---|---|
| 多言語の予定がない | 入れない。文言はそのまま書いてよい |
| 予定がある・将来ありそう | 早めに入れる。あとからの抜き出しが本当に大変 |
| 日本語だけだが表記ゆれを直したい | 文言をまとめるだけでも効く(t()なしでも定数ファイルで足りる) |
| フレームワークを使っている | Next.jsなど側の仕組みを先に見る(ルーティングと絡むため) |
公式ドキュメントも見てみる
react.i18next.com外部サイトQuick start | react-i18next documentationreact-i18next公式のクイックスタート。useTranslationとTransの使い分けはここが最短です(英語)このレッスンのまとめ
- 文言はコードの外(辞書ファイル)に出す。あとから抜き出すのは本当に大変
useTranslation()のt('キー')で引く。切りかえはi18n.changeLanguage()- 複数形は
_one/_otherでライブラリに任せる。言語ごとに種類の数が違う - 文字列をつなげて文を作らない。語順が言語ごとに違う
- 文の中にリンクや強調が入るときは
<Trans> - 日付・数字・通貨は翻訳ではなく書式。
Intlを使う - 言語はURLで分ける(
/ja//en/)。<html lang>も切りかえる - 予定がなければ入れない。予定があるなら最初から
次は、データが増えても重くならないようにする回です。