Next.js i18n
Next.js アプリを複数言語に対応させるための構造ガイド:フォルダレイアウトの要件、不適切な場合に何が壊れるか、そして避けるべきこと。
最初にライブラリを選択する
App Router を使用する新しいプロジェクトでは、デフォルトで next-intl を選択してください。ビルドプラグイン、マクロステップ、ロケールごとのインスタンス管理は不要で、メッセージはソースロケールから型付けされたプレーンな JSON です。
| ライブラリ | 使用するタイミング | コスト |
|---|---|---|
| next-intl (デフォルト) | 新しい App Router プロジェクト、任意のチームサイズ | なし — プレーン JSON、コード生成不要 |
| @lingui/react | コンパイル時の抽出と小さなランタイムが必要な場合、または React/Vue/Astro 間で統一された i18n レイヤーが必要な場合 | SWC または Babel マクロプラグイン、正確なバージョン固定が必要 |
| next-i18next | チームがすでに i18next を知っている場合、または Pages Router のコードを移行する場合 | リクエストごとのインスタンス、getT/useT の分割 |
Next.js 組み込み i18n | Pages Router のみ。App Router では絶対に使用しない | 該当なし |
Next.js の i18n: { locales, defaultLocale } 設定は Pages Router のみ です。App Router では機能しません。App Router でのロケールルーティングは [locale] セグメントとプロキシを使用します。エージェントに App Router プロジェクトで組み込み i18n を「有効化」させないでください。
バンドルコストやライブラリごとのトレードオフを含む完全な比較は、references/library-comparison.md を参照してください。
標準的なアーキテクチャ
静的ディクショナリは src/ の外 に配置し、バンドルとインポートパスが明確になるようにし、すべてのロケール関連の仕組みを1つのフォルダにまとめます。
messages/ # 純粋な静的ディクショナリ — /src の外に配置
├── en.json # ソースオブトゥルース;他のすべてのロケールはこれをミラーリング
├── de.json
└── ja.json
src/
├── i18n/
│ ├── routing.ts # defineRouting: ロケール + defaultLocale
│ ├── request.ts # getRequestConfig: ロケール解決、メッセージ読み込み
│ └── navigation.ts # createNavigation: ロケール対応 Link/redirect
├── proxy.ts # ロケール検出 + リダイレクト (Next 16; 以前は middleware.ts)
└── app/
├── [locale]/ # ローカライズされたアプリ全体をラップ
│ ├── layout.tsx # ロケールの検証、<html lang>、generateStaticParams
│ └── page.tsx
└── not-found.tsx # グローバル 404 は [locale] の外に配置
この構造を機能させるルール:
[locale]はルートの動的セグメントです。 ローカライズされたすべてのものはこの下に配置されます。ルートレベルのルート(not-found.tsx、error.tsx、api/)は外側に残し、無条件に i18n フックを呼び出してはいけません。呼び出すと実行時にクラッシュします。- 1つのソースロケール。
messages/en.jsonがマスターです。他のすべてのファイルはその形状を完全にミラーリングする必要があります。 - すべてのロケール知識は
src/i18n/に。 コンポーネントは@/i18n/navigationからLinkをインポートし、next/linkからは絶対にインポートしません。言語を追加する場所は1か所です。
セットアップ(新規プロジェクト)
以下の順序で実行してください。順序が重要です。proxy.ts は routing.ts に依存し、レイアウトは request.ts に依存します。
next-intlをインストールし、next.config.tsをcreateNextIntlPlugin()でラップします。src/i18n/routing.tsを作成し、defineRoutingを設定します。src/proxy.tsを作成し、createMiddleware(routing)とデフォルトのマッチャーをエクスポートします。src/i18n/request.tsを作成し、ロケールを解決するgetRequestConfigを設定します。app/**をapp/[locale]/**の下に移動し、generateStaticParamsを追加してパラメータを検証します。src/i18n/navigation.tsを作成し、createNavigation(routing)を設定します。messages/en.jsonを作成し、ハードコードされた文字列をuseTranslationsに置き換えます。<html lang>を設定し、ローカライズされたメタデータと hreflang を追加します。
各ステップの正確なファイル内容は、references/next-intl-setup.md を参照してください。
落とし穴
i18n コードを書く前にこのセクションを読んでください。これらは何時間も費やす原因となる失敗です。
setRequestLocaleはレガシーです。 Next.js 16.3+ では、getRequestConfig内でnext/root-paramsを使用してロケールを読み取ってください。ほとんどのガイド、チュートリアル、LLM のトレーニングデータは依然としてsetRequestLocale(params.locale)を教えています。これはまだ動作し、後方互換性があると文書化されていますが、古い API です。Next < 16.3 でのみ使用してください。どちらの場合でもgenerateStaticParamsは依然として必要です。paramsは Next 15+ で Promise です。const {locale} = await params。params.localeを実行時エラーではなく型エラーとして扱ってください。- ロケールを検証しないと、未知の値がレンダリングされます。
hasLocale(routing.locales, locale)とnotFound()を使用してください。これがないと、/xx/aboutが偽の URL で英語のページをレンダリングします。 - Next 16 では
middleware.tsではなくproxy.tsです。middleware.tsは非推奨です。プロキシは Node.js のみで実行され、Edge ランタイムはサポートされません。Lingui と next-i18next は Edge セーフなプロキシを宣伝していますが、Edge が必要な場合は、無料の勝利ではなく、真のトレードオフがあります。 - 翻訳された文字列を連結しないでください。
"You have " + count + " items"は日本語やアラビア語では非文法的です。ICU を使用してください:"{count, plural, =1 {One item} other {# items}}"。 - メッセージ内で
+やテンプレートリテラルを使って補間しないでください。 名前付きプレースホルダー({name})を使用して、語順を変更できるようにしてください。 - 日付と数値はハイドレーションの不一致を引き起こします。 サーバーとブラウザでの
new Date()はタイムゾーンとクロックが異なります。getRequestConfigでtimeZoneを設定するか、相対時間の場合はクライアントのみでレンダリングしてください。症状:「テキストコンテンツがサーバーレンダリングされた HTML と一致しません」。 - フォーマットはロケールに依存します。 明示的なロケールなしの
toLocaleString()は、リクエストされた ロケールではなく、ランタイム ロケールでフォーマットします。useFormatter()/getFormatter()を使用してください。 getRequestConfigはリクエストごとに実行され、React によってキャッシュされます。 リクエストごとに変化する状態(カート、ユーザー設定)をここに配置しないでください。ロケールごとの設定のみを配置してください。- サーバーアクションとルートハンドラにはルートパラメータがありません。
await locale()はサーバーコンポーネントでのみ機能します。アクションとハンドラでは、ロケールを明示的に引数として渡してください。
既存アプリの改造
これは設定変更ではなく、URL に影響を与える移行として扱ってください。[locale] プレフィックスを追加すると、既存のすべての URL の形状が変わります。
以下の順序で計画してください:
- 文字列を棚卸しする —
scripts/find-untranslated.shを実行して、ハードコードされた JSX テキストを見つけます。 - コーディング前にルーティング戦略を選択します。 ロケールプレフィックス常時(
/en/about、/de/about)がデフォルトで、曖昧さがなく、hreflang もクリーンです。localePrefix: 'as-needed'はデフォルトロケールをベアパスで提供します — より良い URL ですが、SEO シグナルは弱くなります。今決めてください;後で切り替えると完全な再移行になります。 - コンポーネントに触れる前にルーティングを立ち上げます。
proxy.ts+[locale]を英語のみのメッセージで動作させます。 - ルートを
[locale]/の下に移動します — これは大規模な機械的なgit mvです。git log --followが有用であり続けるように、1つのコミットで行います。 - 文字列を名前空間ごとに置き換えます。ファイルごとではなく、名前空間ごとに。1つの名前空間をエンドツーエンドで翻訳して、キーの形状が増殖する前に証明します。
- 変更されたすべての URL に対してリダイレクトを追加します。
/about→/en/about(または検出されたロケールに 307)。これがないと、既存のすべてのランキングが低下します。必須です。 - hreflang + ローカライズされたメタデータは、すべてのロケールがルートを持った後にのみ追加します。 そうしないと、404 を指す代替 URL を発行することになります。
リダイレクトとロールバックの注意事項を含む完全な移行シーケンスは、references/migration.md を参照してください。
避けるべきこと
messages/をsrc/の中に配置しないでください。 ソースツリー内のサーバー専用ディクショナリは、クライアントに何が送られるかを曖昧にします。- ローカライズされたルートで
next/linkからLinkをインポートしないでください。 ロケールプレフィックスが失われ、静かに間違った言語にリンクします。 - href でロケールプレフィックスを手書きしないでください(
href={/${locale}/about})。@/i18n/navigationのLinkを使用してください。これはプレフィックス付き、as-needed、ドメインルーティングを処理します。 - 1つのフラットなキー名前空間を使用しないでください。 機能/ページごとにグループ化してください。フラットなキーは衝突し、検索不可能です。
- ビルド時に自動翻訳をソースオブトゥルースとして使用しないでください。 機械出力はドラフトです;レビューされずにコミットすると、エラーが永続化され、追跡不可能になります。
- ロケールを
routing.localesに追加して完了としないでください。 翻訳されたmessages/xx.jsonがないと、すべてのページがキーをレンダリングするか、静かにフォールバックします。 - ルーティング戦略を混在させないでください。
as-neededとプレフィックス付き URL を指すhreflangの組み合わせは、自己責任の SEO バグです。
参考資料
必要に応じてオンデマンドで読んでください:
references/library-comparison.md— next-intl、Lingui、next-i18next の選択;バンドルコスト、移行トリガー、ライブラリごとのセットアップ形状。references/next-intl-setup.md— 標準セットアップのすべてのファイル、コピー&ペースト可能。references/messages-and-keys.md— 名前空間構造、キー命名、ICU 構文、日付/数値/リストのフォーマット、型付きメッセージ。references/seo.md— hreflang、<html lang>、ローカライズされたgenerateMetadata、canonical、サイトマップ代替、ロケールスイッチャーの SEO ルール。references/migration.md— 既存アプリの改造、リダイレクト、ロールバック。
スクリプト
scripts/find-untranslated.sh— プロジェクト内のハードコードされた可能性が高いユーザー向け文字列をフラグ付けし、作業を開始する前に規模を測定できるようにします。改造のステップ 1 として実行します。scripts/run-trigger-eval.sh— このスキルのdescriptionが適切なプロンプトで発動するかを測定します。説明を変更するときに使用します。
説明のテスト
description は、エージェントがこのスキルをロードする前に確認する唯一のものです。これが発動しない場合、以下のものはすべて無意味です。
bash scripts/run-trigger-eval.sh --dry-run # 計画を表示、API 呼び出しなし
bash scripts/run-trigger-eval.sh --split train # 12 クエリ x 3 実行
bash scripts/run-trigger-eval.sh --split validation --out report.json
クエリセット:evals/trigger-queries.json — 12 の発動すべき、8 の発動すべきでない、60/40 に分割して固定の train と validation セットにします。
説明を編集する際は:
- train セットのみを使用して失敗を修正します。最後に validation セットに一度だけ触れて、変更が汎化されているかを確認します — train が改善され validation が低下した場合、過学習です。
- 発動すべき失敗は説明が狭すぎることを意味します:クエリのキーワードではなく、クエリが表す 概念 を追加します。
- 発動すべきでない失敗は説明が広すぎることを意味します:スキルがカバーしない内容を記述します。
- 編集前に再実行します。発動は非決定的であり、1回の実行では何も証明できません。
失敗したクエリからキーワードを追加しないでください — そうすると説明がキーワードリストのようになり、実際のプロンプトと一致しなくなります。