チュートリアル//25 読了時間

Next.js App Routerでi18nを設定する — 2026年完全ガイド

Eray Gündoğmuş
共有

複数言語に対応したNext.jsアプリケーションを開発しているなら、設定ファイルの混乱、壊れたミドルウェアチェーン、server componentsで読み込みを拒否する翻訳に直面したことがあるでしょう。このガイドでは、@better-i18n/nextを使用してNext.js 15 App Routerでinternationalization(i18n)を設定する方法を、ゼロから30分以内に本番環境に対応した状態にするまで、順を追って解説します。

Next.js App RouterでのI18nが異なる理由

Pages RouterでI18nを使用したことがある方は、知識の大部分をリセットする必要があります。App Routerはゲームのルールを変えました:

  • Server Componentsがデフォルトになりました。useTranslationsのようなReact hooksは使えません — サーバー側でgetTranslationsを使う必要があります。
  • Middlewarenext.config.jsi18n設定(Next.js 13+で廃止)の代わりにlocale検出とルーティングを担います。
  • **ISR(Incremental Static Regeneration)**を使って、翻訳をエッジでキャッシュしバックグラウンドで再検証できます — 翻訳が変わっても完全な再ビルドは不要です。
  • Streamingにより、翻訳が非同期に読み込まれる間もレイアウトを即座にレンダリングできます。

多くのi18nライブラリはこのアーキテクチャへの適応に苦労しました。@better-i18n/nextはまさにこのアーキテクチャのために設計されており、ISRキャッシュを備えたグローバルCDNから翻訳を配信し、コンポーザブルなミドルウェアAPIを提供します。

I18nが重要な理由

インターネットユーザーの60%以上が、母国語でブラウジングすることを好みます。Next.jsで構築されたSaaS製品、Eコマースストア、コンテンツプラットフォームにとって、ローカライゼーションは「あったらいい機能」ではなく、成長のための乗数効果をもたらすものです。適切に実装されたi18nは以下を改善します:

  • SEOランキングhreflangタグとローカライズされたURLを通じて英語圏以外の市場で効果的に。堅固なlocalization SEO strategyにより、翻訳されたすべてのページの価値が倍増します。
  • コンバージョン率 — ユーザーが自分の言語でコンテンツを見ると70%以上向上
  • ユーザー定着率 — ネイティブな体験を通じて

デベロッパーファーストのツールへの移行により、i18nの採用も加速しています。Why developer-first localization wins in 2026では、翻訳者中心のワークフローから、ここで構築するようなコードネイティブな設定へとチームを移行させる広範な力について解説しています。

モバイル開発のバックグラウンドをお持ちの方は、ここのパターンがReact Native Expo localizationとは異なることに注意してください — App Routerはバンドルされたlocaleファイルではなく、サーバー側のメッセージ読み込みとISRキャッシュを使用します。これは重要なアーキテクチャの違いです。コードに入る前にinternationalizationの用語を幅広く理解したい方には、localization and internationalisation fundamentalsが良い入門書となります。

構築するもの

このガイドの最後には、Next.jsアプリが以下を備えます:

  1. URL、Cookie、ブラウザヘッダーからの自動locale検出
  2. ISRキャッシュによるサーバー側の翻訳読み込み
  3. ページリロードなしのクライアント側即時locale切り替え
  4. hreflangとcanonical URLによるSEO最適化されたルーティング
  5. namespaceスコープによる型安全な翻訳

1. インストール

@better-i18n/nextとpeer dependenciesをインストールします:

npm install @better-i18n/next next-intl

@better-i18n/nextはpeer dependencyとしてNext.js 15+とnext-intl 4+が必要です。next-intlの実証済みリクエスト処理を基盤に、CDNによる翻訳配信、自動locale検出、コンポーザブルなミドルウェアAPIを追加しています。

2. I18n設定ファイルを作成する

アプリの他の部分が参照する中央設定ファイルを作成します。これがi18n設定の単一の信頼できる情報源です。

// i18n/config.ts
import { createI18n } from "@better-i18n/next";

export const i18n = createI18n({
  project: "acme/dashboard",       // Better i18nプロジェクト識別子
  defaultLocale: "en",             // フォールバックlocale
  localePrefix: "as-needed",       // URL戦略(セクション6参照)
  timeZone: "UTC",                 // 一貫した日時フォーマット
});

createI18n関数は必要なすべてを含むオブジェクトを返します:

プロパティ役割
i18n.configデフォルト値が適用された正規化された設定
i18n.requestConfigApp Router用next-intlリクエスト設定
i18n.middlewareレガシーミドルウェア(next-intlベース)
i18n.betterMiddleware()認証コールバック付きモダンコンポーザブルミドルウェア
i18n.getLocales()CDNから利用可能なlocaleを取得
i18n.getMessages()ISRキャッシュでlocaleの翻訳を取得

設定オプション

I18nConfigインターフェースはコア設定をNext.js固有のオプションで拡張します:

interface I18nConfig {
  project: string;                    // "org/project"形式
  defaultLocale: string;              // 例:"en"
  localePrefix?: "as-needed" | "always" | "never";
  cookieName?: string;                // デフォルト:"locale"
  manifestRevalidateSeconds?: number; // manifest用ISR(デフォルト:3600)
  messagesRevalidateSeconds?: number; // 翻訳用ISR(デフォルト:30)
  timeZone?: string;                  // IANAタイムゾーン識別子
  storage?: TranslationStorage;       // オフラインフォールバックストレージ
  staticData?: Record<string, Messages>; // バンドルされたフォールバック翻訳
  fetchTimeout?: number;              // CDNタイムアウトms(デフォルト:10000)
  retryCount?: number;                // リトライ回数(デフォルト:1)
}

3. ミドルウェアを設定する

ミドルウェアはlocale検出とURLルーティングを管理します。すべてのリクエストで実行され、ユーザーの優先言語を検出し、URLの構造がlocaleプレフィックス戦略と一致することを確認します。

シンプルな設定

ほとんどのアプリでは1行で十分です:

// middleware.ts
import { i18n } from "./i18n/config";

export default i18n.betterMiddleware();

export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)`"],
};

認証との組み合わせ(Clerkスタイルのコールバック)

i18nと認証を組み合わせる必要がある場合、betterMiddlewareは検出されたlocaleとi18nレスポンスへのアクセスを提供するコールバックを受け付けます:

// middleware.ts
import { NextResponse } from "next/server";
import { i18n } from "./i18n/config";

export default i18n.betterMiddleware(async (request, { locale, response }) => {
  const isProtected = request.nextUrl.pathname.includes("/dashboard");
  const isLoggedIn = request.cookies.get("session")?.value;

  if (isProtected && !isLoggedIn) {
    return NextResponse.redirect(
      new URL(`/${locale}/login`, request.url)
    );
  }

  // 何も返さない = i18nレスポンスが使用される(ヘッダーが保持される!)
});

export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)`"],
};

このパターンは非推奨のcomposeMiddlewareアプローチを置き換えます。コールバックは完全に解決されたlocaleと、すべてのi18nヘッダーがすでに設定されたレスポンスを受け取るため、ヘッダーの競合を気にせず認証ロジックに集中できます。

Locale検出の仕組み

ミドルウェアは優先度チェーンを使用してユーザーのlocaleを検出します:

  1. URLパス/fr/aboutfrに解決される
  2. CookielocaleCookie(前回の訪問時に設定)
  3. ブラウザヘッダーAccept-Languageヘッダー
  4. デフォルトdefaultLocaleにフォールバック

利用可能なlocaleはリクエストごとにBetter i18n CDNから取得されます(メモリにキャッシュ)。新しいlocaleが検出されると、将来の訪問のためにCookieが自動的に設定されます。

4. リクエスト設定をセットアップする

リクエスト設定はnext-intlに各リクエストの翻訳の読み込み方法を指示します。Better i18nはISRキャッシュでCDNからメッセージを取得することでこれを処理します。

// i18n/request.ts
import { i18n } from "./config";

export default i18n.requestConfig;

内部では、requestConfigは各サーバーリクエストで以下を実行します:

  1. ミドルウェアヘッダーからlocaleを解決(またはCookie、次にデフォルトにフォールバック)
  2. Next.js ISR再検証でCDNから翻訳を取得
  3. ハイドレーションの不一致を防ぐためタイムゾーンを解決
  4. { locale, messages, timeZone }をnext-intlに返す

ISR戦略により、翻訳はサーバーでキャッシュされバックグラウンドで再検証されます — manifestデータはデフォルトで3600秒(1時間)ごとに、翻訳メッセージは30秒ごとに再検証されます。これはmanifestRevalidateSecondsmessagesRevalidateSeconds設定オプションで調整できます。

5. コンポーネントで翻訳を使用する

Server Components

server componentsでは、next-intlのgetTranslations関数を使用します:

// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";

export default async function HomePage() {
  const t = await getTranslations("home");

  return (
    <main>
      <h1>{t("title")}</h1>
      <p>{t("description")}</p>
    </main>
  );
}

Client Components

client componentsでは、useTranslationsフックを使用します:

"use client";

import { useTranslations } from "next-intl";

export function WelcomeBanner() {
  const t = useTranslations("home");

  return (
    <section>
      <h2>{t("welcome")}</h2>
      <p>{t("subtitle", { name: "Developer" })}</p>
    </section>
  );
}

Namespaceスコープ

翻訳はnamespaceで整理されます。useTranslations("home")またはgetTranslations("home")を呼び出すと、翻訳ファイルのhomenamespaceにスコープを絞ります:

{
  "home": {
    "title": "Acmeへようこそ",
    "description": "ビジネスに最適なダッシュボード",
    "welcome": "こんにちは、{name}さん!",
    "subtitle": "さっそく始めましょう"
  },
  "auth": {
    "login": "ログイン",
    "logout": "ログアウト"
  }
}

これにより、アプリの異なる部分でのキーの衝突を防ぎ、アプリが成長しても翻訳ファイルを管理しやすい状態に保てます。

6. URLのlocaleプレフィックス戦略

localePrefixオプションは、localeがURLにどのように表示されるかを制御します。アプリに合った戦略を選択してください:

"as-needed"(デフォルト)

デフォルトlocaleにはプレフィックスがありません。他のlocaleにはプレフィックスが付きます。

LocaleURL
en(デフォルト)/about
fr/fr/about
tr/tr/about
createI18n({ localePrefix: "as-needed", defaultLocale: "en" });

"always"

デフォルトを含むすべてのlocaleにプレフィックスが付きます。

LocaleURL
en/en/about
fr/fr/about
tr/tr/about
createI18n({ localePrefix: "always", defaultLocale: "en" });

"never"

URLにlocaleは表示されません。localeはCookieとブラウザヘッダーのみで決定されます。

LocaleURL
すべて/about
createI18n({ localePrefix: "never", defaultLocale: "en" });

"never"を使用する場合、ミドルウェアはnext-intlのURL書き換えを完全にバイパスし、x-middleware-request-x-next-intl-localeヘッダーを通じてlocaleを設定します。リクエスト設定は、ミドルウェアヘッダーが利用できない場合にlocaleCookieの読み取りにフォールバックします。

7. クライアント側のlocale切り替え

Better i18nは、インスタント切り替えかサーバーリフレッシュアプローチかによって、クライアントでのlocale切り替えの2つのアプローチを提供します。

BetterI18nProviderによるインスタント切り替え

ページリロードなしのインスタントlocale切り替えを有効にするため、レイアウトをBetterI18nProviderでラップします:

// app/[locale]/layout.tsx
import { getLocale, getMessages } from "next-intl/server";
import { BetterI18nProvider } from "@better-i18n/next/client";

export default async function LocaleLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const locale = await getLocale();
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <BetterI18nProvider
          locale={locale}
          messages={messages}
          config={{ project: "acme/dashboard", defaultLocale: "en" }}
        >
          {children}
        </BetterI18nProvider>
      </body>
    </html>
  );
}

そして、コンポーネントツリーのどこでもuseSetLocaleフックを使用します:

"use client";

import { useSetLocale } from "@better-i18n/next/client";

export function LanguageSwitcher() {
  const setLocale = useSetLocale();

  return (
    <div>
      <button onClick={() => setLocale("en")}>English</button>
      <button onClick={() => setLocale("fr")}>Français</button>
      <button onClick={() => setLocale("tr")}>Türkçe</button>
    </div>
  );
}

setLocaleが呼ばれると:

  1. 次のナビゲーション時のサーバー側永続化のためにlocaleCookieを設定
  2. クライアントでCDNから新しい翻訳を取得
  3. 新しいlocaleとメッセージでツリー全体を再レンダリング — ページリロードなし

動的言語ピッカーの構築

useManifestLanguagesフックを使用して、Better i18nプロジェクトで設定された言語を自動的に反映する言語ピッカーを構築します:

"use client";

import { useManifestLanguages, useSetLocale } from "@better-i18n/next/client";

export function DynamicLanguagePicker() {
  const { languages, isLoading, error } = useManifestLanguages({
    project: "acme/dashboard",
    defaultLocale: "en",
  });
  const setLocale = useSetLocale();

  if (isLoading) return <div>言語を読み込み中...</div>;
  if (error) return <div>言語の読み込みに失敗しました</div>;

  return (
    <select onChange={(e) => setLocale(e.target.value)}>
      {languages.map((lang) => (
        <option key={lang.code} value={lang.code}>
          {lang.nativeName || lang.name || lang.code}
        </option>
      ))}
    </select>
  );
}

言語リストはリクエスト重複排除機能を内蔵してCDN manifestから取得されます — useManifestLanguagesを呼び出す複数のコンポーネントが単一のネットワークリクエストを共有します。

8. SEO:hreflang、canonical URL、Metadata

多言語Next.jsアプリでは適切なSEO設定が不可欠です。hreflangタグとcanonical URLの設定方法を以下に示します。これがより広範な多言語SEO戦略にどのように適合するかの詳細については、localization SEO strategy guideをご覧ください。

多言語アプリの全体的な情報アーキテクチャを計画する際、multilingual website design guideでは、localeを考慮したナビゲーションパターン、言語間のテキスト拡張、ここで構築する構造に直接影響するRTLスクリプトの考慮事項を扱っています。

hreflangタグの生成

ルートレイアウトまたはページのmetadataに代替言語リンクを追加します:

// app/[locale]/layout.tsx
import { i18n } from "@/i18n/config";
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { locale: string };
}): Promise<Metadata> {
  const { locale } = await params;
  const locales = await i18n.getLocales();

  const languages: Record<string, string> = {};
  for (const loc of locales) {
    languages[loc] = `https://yourdomain.com/${loc}`;
  }
  // 検索エンジン用にx-defaultを追加
  languages["x-default"] = "https://yourdomain.com/en";

  return {
    alternates: {
      canonical: `https://yourdomain.com/${locale}`,
      languages,
    },
  };
}

これによりHTMLに以下が生成されます:

<link rel="alternate" hreflang="en" href="https://yourdomain.com/en" />
<link rel="alternate" hreflang="fr" href="https://yourdomain.com/fr" />
<link rel="alternate" hreflang="tr" href="https://yourdomain.com/tr" />
<link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en" />
<link rel="canonical" href="https://yourdomain.com/en" />

ローカライズされたMetadata

各localeに翻訳されたページタイトルと説明を提供します:

// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { locale: string };
}): Promise<Metadata> {
  const t = await getTranslations("meta");

  return {
    title: t("home.title"),
    description: t("home.description"),
    openGraph: {
      title: t("home.title"),
      description: t("home.description"),
    },
  };
}

9. オフラインフォールバックとレジリエンス

本番アプリはCDN障害を適切に処理する必要があります。@better-i18n/nextはmanifestと翻訳データの両方に対して3段階のフォールバックチェーンを提供します:

  1. メモリキャッシュ — プロセス内TTLキャッシュ(最速)
  2. CDNフェッチ — 設定可能なタイムアウトとリトライ付き
  3. 永続ストレージ — オフライン/機能低下シナリオ向け
  4. 静的データ — 最終手段としてのバンドルされた翻訳
// i18n/config.ts
import { createI18n } from "@better-i18n/next";

export const i18n = createI18n({
  project: "acme/dashboard",
  defaultLocale: "en",
  fetchTimeout: 5000,       // 5秒後にCDNフェッチを中止
  retryCount: 2,            // 失敗時に2回リトライ
  staticData: {             // バンドルされたフォールバック
    en: {
      common: { error: "エラーが発生しました" },
    },
  },
});

CDNに到達できない場合、翻訳は永続ストレージから提供(設定されている場合)、またはバンドルされたstaticDataにフォールバックします。アプリはユーザーに壊れたキーを表示することはありません。

10. MCPを使ったAI翻訳

Better i18nにはAIアシスタントが翻訳を直接管理できるMCP(Model Context Protocol)サーバーが含まれています。翻訳ファイルを手動で書く代わりに、Claude、Cursor、またはMCP対応ツールを使って以下が可能です:

  • createKeys翻訳キーの作成
  • proposeLanguages新しい言語の提案
  • updateKeys既存の翻訳の更新
  • publishTranslationsCDNへの公開

ワークフローの例

  1. Reactコンポーネントを英語の文字列で書く
  2. AIアシスタントに依頼:「ホームページのフランス語とトルコ語の翻訳を追加して」
  3. MCPサーバーがキーを作成し、翻訳を提案して公開
  4. Next.jsアプリが次のISR再検証サイクル(デフォルトで30秒)で新しい翻訳を取得

手動でJSONを編集する必要なし。ファイル間のコピーアンドペーストも不要。翻訳ワークフロー全体がAIコーディングアシスタントを通じて実現します。このワークフローを自分たちのブログコンテンツで実際に使っている様子については、how we use AI to write our own blogをご覧ください。

翻訳がすべての言語でライブになったら、専用のi18n testing passを実行することをお勧めします — 自動チェックにより、欠落したキー、壊れた補間、複数形のエッジケースをユーザーに届く前に検出できます。文字列にトーンやコンテキストによって変わるニュアンスが必要な場合は、why translation context mattersの記事で、そのコンテキストをAI翻訳者に効果的に提供する方法を解説しています。

すべてをまとめる

Better i18nを使ったNext.js App Routerプロジェクトの完全なファイル構造:

your-app/
  i18n/
    config.ts          # createI18n設定
    request.ts         # next-intlリクエスト設定
  middleware.ts        # Locale検出とルーティング
  app/
    [locale]/
      layout.tsx       # BetterI18nProviderラッパー
      page.tsx         # getTranslationsを使ったserver component
      components/
        LanguageSwitcher.tsx  # クライアント側locale切り替え

クイックリファレンス

// i18n/config.ts
import { createI18n } from "@better-i18n/next";
export const i18n = createI18n({
  project: "acme/dashboard",
  defaultLocale: "en",
  localePrefix: "as-needed",
});

// i18n/request.ts
import { i18n } from "./config";
export default i18n.requestConfig;

// middleware.ts
import { i18n } from "./i18n/config";
export default i18n.betterMiddleware();
export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)`"],
};

よくある落とし穴(と回避方法)

Next.jsでi18nを設定する際に何百ものチームを支援した経験から、最も頻繁に見られる問題をご紹介します:

1. 日時フォーマットでのハイドレーションの不一致

timeZoneを設定せずに日付や時刻をフォーマットすると、サーバーとクライアントで異なる値がレンダリングされ、Reactのハイドレーションエラーが発生することがあります。

解決策createI18n設定で必ずtimeZoneを設定してください:

createI18n({
  project: "acme/dashboard",
  defaultLocale: "en",
  timeZone: "UTC", // サーバー/クライアントの不一致を防ぐ
});

Better i18nはリクエスト設定とBetterI18nProviderでこれを自動的に設定し、指定がない場合はIntl.DateTimeFormat().resolvedOptions().timeZoneにフォールバックします。

2. デフォルトlocaleでのlocaleプレフィックスの欠落

localePrefix: "as-needed"(デフォルト)では、デフォルトlocaleにURLプレフィックスはありません。つまり/aboutは英語を、/fr/aboutはフランス語を提供します。これを忘れてリンクにlocaleセグメントをハードコードすると、デフォルトlocaleが壊れます。

解決策:next-intlのLinkコンポーネントを使用するか、パスを動的に構築します:

// こうする:
<Link href="/about">{t("nav.about")}</Link>

// こうしない:
<a href="/en/about">About</a>

デフォルトのlocaleCookieはdなしのとHeadersでpath: /と設定されますが、domainはありません。アプリがサブドメインをまたぐ場合(例:app.yourdomain.comwww.yourdomain.com)、Cookieは共有されません。

解決策:マルチサブドメイン構成では、localeが常にURLに含まれるlocalePrefix: "always"を使用するか、ミドルウェアコールバックでカスタムCookieドメインを設定します。

4. 静的ファイルを除外しないミドルウェアマッチャー

ミドルウェアマッチャーが広すぎると、画像、フォント、APIルートを含むすべてのリクエストで実行されます。これによりアプリが遅くなり、予期しないlocaleリダイレクトが発生する可能性があります。

解決策:常に推奨のマッチャーパターンを使用します:

export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)`"],
};

これにより/api/*/_next/*、ファイル拡張子を含むパスが除外されます。

まとめ

Next.js App RouterでのI18n設定は苦痛である必要はありません。@better-i18n/nextを使えば以下が手に入ります:

  • 設定不要のlocale検出 — URL、Cookie、ブラウザヘッダーから
  • CDN経由の翻訳 — ISRキャッシュとオフラインフォールバック付き
  • コンポーザブルなミドルウェア — 認証と相性抜群(Clerk、NextAuthなど)
  • クライアント側のインスタントlocale切り替え — ページリロードなし
  • SEO対応 — hreflang、canonical URL、ローカライズされたmetadata付き
  • AI翻訳 — MCPサーバー統合を通じて

セットアップ全体は5つのファイルと50行未満の設定コードで完了します。翻訳はグローバルCDNから提供され、ISRでキャッシュされ、AIアシスタントを通じて管理されます。

準備はできていますか?@better-i18n/nextをインストールして、数分で最初のlocaleを動かしましょう:

npm install @better-i18n/next next-intl

より高度なパターンについては完全なドキュメントを確認するか、GitHubリポジトリを探索してください。


関連リソース

Comments

Loading comments...