Use Cases (更新: 2026/6/2)

Claude Codeでi18n実装: Next.js多言語対応の実務手順

Claude CodeでNext.jsのi18nを実装する手順を、next-intl設定、翻訳漏れ検査、失敗例まで実務目線で解説。

Claude Codeでi18n実装: Next.js多言語対応の実務手順

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 setupNext.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を既定言語、endeを追加言語にします。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/linknext/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に何でも入れると一見便利ですが、後から不要キーが消せなくなります。ページ固有の文言はHomePagePricingPageのように分け、ボタンやナビゲーションだけを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.jsonlint:i18nへ登録し、CIで実行するところまで依頼するとよいです。

実務で使う3つ以上のユースケース

1つ目はSaaSの料金ページです。価格、税表示、通貨、契約期間の表現が国によって変わります。翻訳キーだけでなく、Intl.NumberFormatや決済サービス側の通貨設定も確認します。たとえば日本語では「月額3,000円」、英語では「$20 per month」のように、文の組み立て順そのものが変わります。

2つ目はドキュメントサイトです。英語の技術語をすべて日本語化すると検索意図から外れることがあります。middlewareproxylocalenamespaceのような語は初出で「ルーティングを横取りする処理」「言語コード」「翻訳のまとまり」と言い換え、以降は原語も残す方が読みやすいです。Claude Codeには「専門用語は初出だけ補足して」と明示します。

3つ目は管理画面です。管理画面では見た目の翻訳より、操作ミスを防ぐ文言が重要です。「削除」「無効化」「招待を取り消す」のような危険操作は、短く翻訳しすぎると意味が変わります。ボタン、確認ダイアログ、トースト通知まで同じキー体系に入れて、レビュー対象にします。

4つ目を挙げるなら、ブログやメディアの記事です。本文を翻訳するだけでなく、title、description、canonical、alternate language links、OGP画像の扱いを確認します。ClaudeCodeLabのように記事数が多いサイトでは、CLAUDE.mdベストプラクティスに「公開記事は10言語で更新する」「updatedDateを揃える」といったルールを書いておくと、Claude Codeの作業が安定します。

失敗例と落とし穴

落とし穴起きる問題対策
URL設計を後回しにする公開後にリダイレクト地獄になる最初に/[locale]、サブドメイン、ドメイン分けを決める
翻訳キー名が抽象的title2text3が増えてレビュー不能になる画面名と意味で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の導入相談から現在のリポジトリ構成と公開手順を共有してください。

#Claude Code #i18n #国際化 #多言語対応 #Next.js
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。