Claude Codeでi18n実装: Next.js多言語対応の実務手順
Claude CodeでNext.jsのi18nを実装する手順を、next-intl設定、翻訳漏れ検査、失敗例まで実務目線で解説。
Claude Codeに任せる前に決めること
多言語対応、つまりi18nは「翻訳ファイルを増やす作業」だけではありません。URL設計、言語判定、SEO用メタデータ、日付や通貨の表示、翻訳漏れの検査まで含めて初めて公開できる状態になります。Claude Codeは複数ファイルを読んで編集し、確認コマンドまで回せるので相性は良いのですが、最初の方針が曖昧だと、翻訳キーだけが増えて運用しにくい実装になりがちです。
Masaの案件で一番効果があったのは、Claude Codeに「翻訳して」ではなく「i18nの足場、翻訳キー、検査スクリプト、レビュー観点まで一続きで作って」と依頼することでした。足場とは、アプリが多言語で動くための土台です。先に土台を固定すると、後から英語、ドイツ語、インドネシア語を足しても差分が読みやすくなります。
この記事では、Next.js App Routerとnext-intlを例に、Claude Codeで実務レベルのi18nを作る手順をまとめます。公式情報はClaude Code公式ドキュメント、next-intlのrouting setup、Next.jsのInternationalizationガイドを前提にしています。日付・通貨・複数形はブラウザ標準のIntl APIも確認しておくと、ライブラリ任せにしすぎずに判断できます。
flowchart LR
A[要件整理] --> B[ルーティング]
B --> C[翻訳JSON]
C --> D[画面実装]
D --> E[翻訳漏れ検査]
E --> F[SEOと公開確認]
この順番で進めると、Claude Codeの出力をレビューしやすくなります。特に公開済みサイトでは、URLを変えるだけで検索流入や内部リンクに影響が出ます。多言語化は新機能というより、サイト全体の情報設計を触る作業だと考えてください。
Next.js App Routerとnext-intlの最小構成
ここではjaを既定言語、enとdeを追加言語にします。Next.js 16系の公式ドキュメントでは、従来のmiddleware.tsに相当するファイル名としてproxy.tsが使われています。Next.js 15以前のプロジェクトなら、同じ内容をmiddleware.tsに置く構成が残っている場合があります。Claude Codeには、対象プロジェクトのNext.jsバージョンを確認してから編集させるのが安全です。
まずはディレクトリ構成を固定します。
src/
app/
[locale]/
layout.tsx
page.tsx
i18n/
navigation.ts
request.ts
routing.ts
messages/
ja.json
en.json
de.json
proxy.ts
routing.tsでは、対応言語とURLを一か所に集約します。商品ページやドキュメントページのURLを言語ごとに変えたい場合も、ここに寄せるとレビューしやすくなります。
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['ja', 'en', 'de'],
defaultLocale: 'ja',
pathnames: {
'/': '/',
'/pricing': {
ja: '/pricing',
en: '/pricing',
de: '/preise',
},
'/docs': {
ja: '/docs',
en: '/docs',
de: '/dokumentation',
},
},
});
export type Locale = (typeof routing.locales)[number];
ナビゲーション用のラッパーも作ります。通常のnext/linkやnext/navigationを直接使うより、ロケールを意識したリンク生成に統一できます。
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);
リクエストごとの言語判定はrequest.tsに置きます。存在しないロケールが来たときは既定言語へ落とし、翻訳ファイルを動的に読み込みます。
// src/i18n/request.ts
import { hasLocale } from 'next-intl';
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
proxy.tsでロケール付きURLを処理します。api、_next、画像などの静的ファイルを巻き込まないようにするのがポイントです。
// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};
最後にapp/[locale]/layout.tsxで、ロケール検証、静的生成用のパラメータ、翻訳プロバイダーをまとめます。
// src/app/[locale]/layout.tsx
import { hasLocale, NextIntlClientProvider } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
type Props = {
children: React.ReactNode;
params: Promise<{ locale: string }>;
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
setRequestLocale(locale);
const messages = (await import(`@/messages/${locale}.json`)).default;
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
翻訳キーを増やす前にページで使う
翻訳ファイルは、画面の責務に合わせて名前空間を切ります。commonに何でも入れると一見便利ですが、後から不要キーが消せなくなります。ページ固有の文言はHomePageやPricingPageのように分け、ボタンやナビゲーションだけをcommonに置く方が運用しやすいです。
{
"common": {
"language": {
"label": "表示言語",
"ja": "日本語",
"en": "English",
"de": "Deutsch"
},
"nav": {
"docs": "ドキュメント",
"pricing": "料金"
}
},
"HomePage": {
"title": "チームの知識を多言語で届ける",
"lead": "{count}件の記事を、読者の言語に合わせて表示します。",
"cta": "導入相談をする"
}
}
{
"common": {
"language": {
"label": "Display language",
"ja": "日本語",
"en": "English",
"de": "Deutsch"
},
"nav": {
"docs": "Docs",
"pricing": "Pricing"
}
},
"HomePage": {
"title": "Deliver team knowledge in multiple languages",
"lead": "Show {count} articles in the reader's language.",
"cta": "Book a consultation"
}
}
ページ側ではgetTranslationsを使うと、メタデータ生成やServer Componentでも扱いやすくなります。
// src/app/[locale]/page.tsx
import { getTranslations, setRequestLocale } from 'next-intl/server';
type Props = {
params: Promise<{ locale: string }>;
};
export default async function HomePage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'HomePage' });
return (
<main>
<h1>{t('title')}</h1>
<p>{t('lead', { count: 42 })}</p>
<a href={`/${locale}/pricing`}>{t('cta')}</a>
</main>
);
}
言語切り替えは、現在のパスを保ったままロケールだけ変える実装にします。単純な文字列置換で/enを/jaに変えると、/enquiryのようなパスを誤置換することがあります。
// src/components/LanguageSwitcher.tsx
'use client';
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from '@/i18n/navigation';
const languages = [
{ code: 'ja', label: '日本語' },
{ code: 'en', label: 'English' },
{ code: 'de', label: 'Deutsch' },
] as const;
export function LanguageSwitcher() {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
return (
<div role="radiogroup" aria-label="表示言語">
{languages.map((language) => (
<button
key={language.code}
type="button"
role="radio"
aria-checked={locale === language.code}
onClick={() => router.replace(pathname, { locale: language.code })}
>
{language.label}
</button>
))}
</div>
);
}
翻訳漏れをCIで止める
Claude Codeに翻訳ファイルを増やしてもらう場合、レビューで一番見落としやすいのは「キーはあるが片方の言語だけ抜けている」状態です。次のスクリプトは、基準言語と他言語のキー差分を検出します。scripts/check-translations.mjsとして置けば、node scripts/check-translations.mjsで実行できます。
// scripts/check-translations.mjs
import { readdir, readFile } from 'node:fs/promises';
const messagesDir = new URL('../src/messages/', import.meta.url);
const baseLocale = 'ja';
function flattenKeys(value, prefix = '') {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return [prefix];
}
return Object.entries(value).flatMap(([key, child]) => {
const nextPrefix = prefix ? `${prefix}.${key}` : key;
return flattenKeys(child, nextPrefix);
});
}
async function readMessages(locale) {
const file = new URL(`${locale}.json`, messagesDir);
return JSON.parse(await readFile(file, 'utf8'));
}
const files = await readdir(messagesDir);
const locales = files
.filter((file) => file.endsWith('.json'))
.map((file) => file.replace(/\.json$/, ''));
const baseKeys = new Set(flattenKeys(await readMessages(baseLocale)));
let hasError = false;
for (const locale of locales.filter((item) => item !== baseLocale)) {
const targetKeys = new Set(flattenKeys(await readMessages(locale)));
const missing = [...baseKeys].filter((key) => !targetKeys.has(key));
const extra = [...targetKeys].filter((key) => !baseKeys.has(key));
if (missing.length || extra.length) {
hasError = true;
console.error(`\n${locale}.json has translation key drift`);
if (missing.length) console.error('Missing:', missing.join(', '));
if (extra.length) console.error('Extra:', extra.join(', '));
}
}
if (hasError) {
process.exit(1);
}
console.log(`Translation keys are aligned for ${locales.length} locales.`);
この検査は、翻訳の自然さまでは保証しません。そこは人間のレビューが必要です。ただし、キーの欠落をCIで止められるだけで、公開直前の「英語ページだけボタンが表示されない」という事故はかなり減ります。Claude Codeには、このスクリプトを追加したあとにpackage.jsonのlint:i18nへ登録し、CIで実行するところまで依頼するとよいです。
実務で使う3つ以上のユースケース
1つ目はSaaSの料金ページです。価格、税表示、通貨、契約期間の表現が国によって変わります。翻訳キーだけでなく、Intl.NumberFormatや決済サービス側の通貨設定も確認します。たとえば日本語では「月額3,000円」、英語では「$20 per month」のように、文の組み立て順そのものが変わります。
2つ目はドキュメントサイトです。英語の技術語をすべて日本語化すると検索意図から外れることがあります。middleware、proxy、locale、namespaceのような語は初出で「ルーティングを横取りする処理」「言語コード」「翻訳のまとまり」と言い換え、以降は原語も残す方が読みやすいです。Claude Codeには「専門用語は初出だけ補足して」と明示します。
3つ目は管理画面です。管理画面では見た目の翻訳より、操作ミスを防ぐ文言が重要です。「削除」「無効化」「招待を取り消す」のような危険操作は、短く翻訳しすぎると意味が変わります。ボタン、確認ダイアログ、トースト通知まで同じキー体系に入れて、レビュー対象にします。
4つ目を挙げるなら、ブログやメディアの記事です。本文を翻訳するだけでなく、title、description、canonical、alternate language links、OGP画像の扱いを確認します。ClaudeCodeLabのように記事数が多いサイトでは、CLAUDE.mdベストプラクティスに「公開記事は10言語で更新する」「updatedDateを揃える」といったルールを書いておくと、Claude Codeの作業が安定します。
失敗例と落とし穴
| 落とし穴 | 起きる問題 | 対策 |
|---|---|---|
| URL設計を後回しにする | 公開後にリダイレクト地獄になる | 最初に/[locale]、サブドメイン、ドメイン分けを決める |
| 翻訳キー名が抽象的 | title2やtext3が増えてレビュー不能になる | 画面名と意味でPricingPage.planNameのように命名する |
| フォールバックに頼りすぎる | 翻訳漏れが本番で隠れる | CIでは欠落を失敗扱いにする |
| 日付と通貨を文字列連結する | 語順や桁区切りが崩れる | Intlまたはnext-intlのformatterを使う |
| メタデータを翻訳しない | 検索結果だけ既定言語のままになる | generateMetadataも翻訳対象にする |
| 機械翻訳を無レビューで公開する | 文化的に不自然なCTAになる | CTA、価格、法務文言だけでも人間が読む |
特に危険なのは、Claude Codeに「全言語に翻訳して」とだけ伝えることです。この指示だと、キーの整合性、SEO、文字数、内部リンクまで見てくれるとは限りません。プロンプト改善の基本と同じで、期待する成果物と検査コマンドをセットで渡す必要があります。
Claude Codeへの依頼テンプレート
次のように、対象範囲、使うライブラリ、触ってよいファイル、検査方法を明示します。
Next.js App Routerの既存プロジェクトにi18nを追加してください。
前提:
- next-intlを使う
- 対応言語は ja, en, de
- 既定言語は ja
- URLは /ja, /en, /de のprefix方式
- Next.jsのバージョンを確認し、proxy.tsとmiddleware.tsのどちらが適切か判断する
作業:
1. src/i18n/routing.ts, navigation.ts, request.tsを追加
2. src/proxy.tsまたはsrc/middleware.tsを追加
3. app/[locale]/layout.tsxへロケール検証を追加
4. 既存トップページの文言をmessages/*.jsonへ移動
5. scripts/check-translations.mjsを追加
6. npm scriptsにlint:i18nを追加
制約:
- 既存URLを壊す変更は提案だけにする
- 翻訳キー名は画面名と意味で命名する
- 最後に変更ファイル、実行したコマンド、残ったリスクを報告する
このテンプレートの狙いは、Claude Codeに「実装者」と「レビュー担当」の両方をさせることです。作って終わりではなく、検査スクリプトを足し、コマンドを実行し、残ったリスクを言語化させます。チームで運用するなら、この方針をCLAUDE.mdに書き、翻訳更新のたびに同じ流れを使うのが現実的です。
実際に試した結果と次の一手
この記事で紹介した翻訳キー検査を、手元の検証用メッセージファイルで試したところ、HomePage.ctaを英語側だけ削除したケースを正しく検出できました。一方で、「Book a consultation」が対象国で自然かどうかはスクリプトでは判断できませんでした。つまり、Claude CodeとCIで機械的な漏れを潰し、人間はCTA、価格、法務文言、文化的な違和感に集中するのが一番効率的です。
次に進むなら、翻訳運用のルールをCLAUDE.mdへ追加してください。レビュー観点はClaude Codeレビュー・ワークフローと組み合わせると実務に落とし込みやすいです。チームで多言語サイトの更新フローを整えたい場合は、ClaudeCodeLabの導入相談から現在のリポジトリ構成と公開手順を共有してください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。