言語を切り替える
テーマを切り替える

Next.js 国際化完全ガイド:next-intl ベストプラクティス

Next.js 国際化完全ガイド:next-intl ベストプラクティス

昨年、多言語対応が必要な Next.js プロジェクトを引き継いだとき、設定ファイルを見て少し混乱しました。
ドキュメントを読み込んでようやく分かったのは、App Router と以前の Pages Router では、国際化のアプローチが全く異なるということです。試行錯誤の末、ようやく next-intl を使った理想的な構成に辿り着きましたが、そこに至るまでには多くの落とし穴がありました。

この記事では、Next.js の国際化ソリューション、特に App Router 環境下で next-intl を使っていかに優雅に多言語対応を実現するかについて解説します。

なぜ next-intl なのか?

「Next.js には国際化機能が組み込まれているのでは?」と思うかもしれません。確かに、Pages Router 時代には標準で i18n ルーティング機能がありましたが、App Router ではその機能が削除されました。

公式の推奨は「サードパーティライブラリを使用すること」です。そして、その中で最も人気がある選択肢の一つが next-intl です。

next-intl のメリット:

  • App Router ネイティブ対応 - App Router のために設計されており、統合が非常にスムーズです。
  • 型安全性 - TypeScript と組み合わせることで、翻訳キーの型チェックが可能です。
  • 柔軟なルーティング - サブパス、ドメイン、Cookie など、多様な言語切り替え戦略に対応。
  • 強力な機能 - 複数形、日付・数値のフォーマット、リッチテキストなどを標準サポート。
  • 高パフォーマンス - Server Components に対応しており、静的レンダリングも可能です。

他のライブラリと比較しても、ドキュメントが非常に丁寧に書かれており、導入のハードルが低いのも魅力です。

基本設定:ゼロからのスタート

1. インストール

まずはパッケージをインストールします:

npm install next-intl
# または
pnpm add next-intl
# または
yarn add next-intl

2. 翻訳ファイルの作成

プロジェクトのルート(または src 下)に messages フォルダを作成し、言語ごとの JSON ファイルを配置します:

messages/
├── en.json
├── ja.json
└── zh.json

messages/ja.json:

{
  "HomePage": {
    "title": "私のウェブサイトへようこそ",
    "description": "これは多言語対応の Next.js アプリケーションです"
  },
  "Navigation": {
    "home": "ホーム",
    "about": "当サイトについて",
    "contact": "お問い合わせ"
  }
}

messages/en.json:

{
  "HomePage": {
    "title": "Welcome to My Website",
    "description": "This is a multilingual Next.js application"
  },
  "Navigation": {
    "home": "Home",
    "about": "About",
    "contact": "Contact Us"
  }
}

このようにページやコンポーネントごとにネストして管理すると便利です。

3. i18n.ts の設定

i18n.ts(または src/i18n.ts)を作成し、翻訳ファイルの読み込み設定を行います:

import { getRequestConfig } from 'next-intl/server';

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`./messages/${locale}.json`)).default
}));

この設定により、URL から抽出された locale に基づいて適切な翻訳ファイルが読み込まれます。

4. ミドルウェアの作成

ルートディレクトリに middleware.ts を作成します。これが多言語ルーティングの要となります:

import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  // サポートする言語コードのリスト
  locales: ['en', 'ja', 'zh'],

  // デフォルト言語
  defaultLocale: 'ja',

  // URL にデフォルト言語のプレフィックスを表示するか
  localePrefix: 'as-needed'
});

export const config = {
  // api, _next などの内部パスを除外してマッチさせる
  matcher: ['/', '/(ja|en|zh)/:path*', '/((?!api|_next|_next/static|_next/image|favicon.ico).*)']
};

localePrefix のオプション:

  • 'always' - 全ての言語でプレフィックスを表示(/ja/about, /en/about
  • 'as-needed' - デフォルト言語のみプレフィックスを省略(/about, /en/about
  • 'never' - 全ての言語でプレフィックスを省略(ドメインなど他の手段で区別する場合)

URL をシンプルに保つため、私は通常 'as-needed' を推奨しています。

5. ディレクトリ構造の変更

ここが最も重要なステップです。すべてのページを [locale] 動的ルートの下に移動させる必要があります:

変更前:

app/
├── page.tsx
├── about/
│   └── page.tsx
└── layout.tsx

変更後:

app/
├── [locale]/
│   ├── page.tsx
│   ├── about/
│   │   └── page.tsx
│   └── layout.tsx
└── layout.tsx (オプション: グローバル設定用)

6. ルートレイアウトの設定

app/[locale]/layout.tsx でプロバイダーを設定します:

import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';

const locales = ['en', 'ja', 'zh'];

// 静的生成(SSG)のために必要
export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({
  children,
  params: { locale }
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  // 言語がサポートされているか検証
  if (!locales.includes(locale)) {
    notFound();
  }

  // 翻訳データの取得
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

generateStaticParams を定義することで、ビルド時に各言語の静的ページを生成できるようになります。

コンポーネントでの使用方法

設定が完了したら、翻訳機能を使ってみましょう。

Server Component での使用

import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage');

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </div>
  );
}

useTranslations の引数にはネームスペース(JSON のキー)を指定します。指定しない場合は t('HomePage.title') のようにフルパスで記述も可能です。

Client Component での使用

Client Component でも使い方は全く同じです:

'use client';

import { useTranslations } from 'next-intl';

export default function Navigation() {
  const t = useTranslations('Navigation');

  return (
    <nav>
      <a href="/">{t('home')}</a>
      <a href="/about">{t('about')}</a>
      <a href="/contact">{t('contact')}</a>
    </nav>
  );
}

Server/Client を意識せずに同じ API で使えるのが next-intl の素晴らしい点です。

多言語ルーティングの実装

現在の言語の取得

import { useLocale } from 'next-intl';

export default function LanguageInfo() {
  const locale = useLocale();
  return <div>現在の言語: {locale}</div>;
}

言語切り替え機能(Language Switcher)

多くのサイトで必須となる言語切り替えドロップダウンの実装例です:

'use client';

import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';

export default function LanguageSwitcher() {
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  const switchLanguage = (newLocale: string) => {
    // パス名から現在の言語プレフィックスを取り除く処理が必要
    let path = pathname;
    if (pathname.startsWith(`/${locale}`)) {
      path = pathname.substring(locale.length + 1);
    }

    // 新しいパスを構築
    const newPath = newLocale === 'ja' ? path : `/${newLocale}${path}`;
    // ※ 'as-needed' 設定でデフォルト言語が 'ja' の場合
    
    router.push(newPath);
  };

  return (
    <select value={locale} onChange={(e) => switchLanguage(e.target.value)}>
      <option value="ja">日本語</option>
      <option value="en">English</option>
      <option value="zh">中文</option>
    </select>
  );
}

next-intl は言語プレフィックスを自動処理する Link コンポーネントを提供しています:

import { Link } from '@/navigation'; // 設定ファイルからインポート

<Link href="/about">
  {t('about')}
</Link>

これを使うには、navigation.ts を作成します:

import { createSharedPathnamesNavigation } from 'next-intl/navigation';

export const locales = ['en', 'ja', 'zh'] as const;
export const localePrefix = 'as-needed';

export const { Link, redirect, usePathname, useRouter } =
  createSharedPathnamesNavigation({ locales, localePrefix });

この Link を使えば、現在の言語に応じた正しいパス(例:英語なら /en/about、日本語なら /about)へ自動的にリンクされます。

高度な機能

1. 変数の埋め込み

翻訳テキストに変数を挿入できます:

messages/ja.json:

{
  "welcome": "お帰りなさい、{username}さん!",
  "items": "{count}件の新着メッセージがあります"
}

使用法:

const t = useTranslations();

<p>{t('welcome', { username: '田中' })}</p>
<p>{t('items', { count: 5 })}</p>

2. 複数形(Pluralization)

言語によって異なる複数形のルールに対応できます:

messages/en.json:

{
  "messages": {
    "one": "You have {count} message",
    "other": "You have {count} messages"
  }
}

使用法:

t('messages', { count: 1 })  // "You have 1 message"
t('messages', { count: 5 })  // "You have 5 messages"

3. 日付と数値のフォーマット

useFormatter フックを使用します:

import { useFormatter } from 'next-intl';

export default function FormatExample() {
  const format = useFormatter();
  const now = new Date();

  return (
    <div>
      <p>{format.dateTime(now, { dateStyle: 'full' })}</p>
      {/* 日本語:2025年12月25日水曜日 */}
      {/* 英語:Wednesday, December 25, 2025 */}

      <p>{format.number(1234567.89, { style: 'currency', currency: 'JPY' })}</p>
      {/* 日本語:¥1,234,568 */}
      {/* 英語:¥1,234,568 */}
    </div>
  );
}

4. リッチテキスト

HTMLタグやコンポーネントを含んだ翻訳も可能です:

messages/ja.json:

{
  "richText": "<terms>利用規約</terms>と<privacy>プライバシーポリシー</privacy>に同意します"
}

使用法:

<p>
  {t.rich('richText', {
    terms: (chunks) => <a href="/terms">{chunks}</a>,
    privacy: (chunks) => <a href="/privacy">{chunks}</a>
  })}
</p>

翻訳ファイルの管理:ベストプラクティス

プロジェクトが大規模になると、翻訳ファイルの管理が課題になります。

1. 機能ごとの分割

巨大な JSON ファイルを作るより、機能ごとに分割するほうが管理しやすいです:

messages/
├── ja/
│   ├── common.json      # 共通(ボタン、エラーなど)
│   ├── home.json        # トップページ
│   ├── about.json       # アバウトページ
│   └── auth.json        # 認証関連

i18n.ts でこれらを結合します:

import { getRequestConfig } from 'next-intl/server';

export default getRequestConfig(async ({ locale }) => {
  const common = (await import(`./messages/${locale}/common.json`)).default;
  const home = (await import(`./messages/${locale}/home.json`)).default;
  // ...他も同様に

  return {
    messages: {
      common,
      home,
      ...
    }
  };
});

2. TypeScript による型安全性

これが next-intl の最強の機能の一つです。翻訳キーの間違いをコンパイルエラーとして検出できます。

types/global.d.ts:

// 日本語ファイルを型定義のソースとして使用
import ja from './messages/ja.json';

type Messages = typeof ja;

declare global {
  interface IntlMessages extends Messages {}
}

tsconfig.json:

{
  "compilerOptions": {
    "types": ["./types/global.d.ts"] // パスは適宜調整
  }
}

これで t('not.exist') のように存在しないキーを指定すると、エディタ上でエラーが表示されます。

パフォーマンス最適化

翻訳ファイルのコード分割

ページ全体で全ての翻訳を読み込むのではなく、必要な部分だけ読み込むことができます。

// 管理画面コンポーネント
export default function AdminPage() {
  const t = useTranslations('admin'); // admin ネームスペースのみ使用されていることを示唆
  // ...
}

ただし、i18n.ts での結合方法によっては全てのメッセージがバンドルされる可能性があるので、大規模アプリではページごとの動的インポート戦略を検討してください。

静的レンダリング(Static Rendering)

変更頻度の低いページは静的に生成することで、パフォーマンスを最大化できます。

// app/[locale]/about/page.tsx
export const dynamic = 'force-static';

export function generateStaticParams() {
  return [
    { locale: 'ja' },
    { locale: 'en' },
    { locale: 'zh' }
  ];
}

よくある質問とトラブルシューティング

Q: 動的ルート(Dynamic Routes)の言語切り替えはどうすれば?

A: [slug] を含むパスで言語を切り替える場合、現在の slug を保持する必要があります。useParams でパラメータを取得し、新しい言語でのパスを生成する際に埋め込みます。

Q: SEO 対策は?

A: generateMetadataalternates タグを正しく設定することが重要です。

export async function generateMetadata({ params: { locale } }) {
  return {
    alternates: {
      canonical: `https://example.com/${locale}`,
      languages: {
        'ja-JP': 'https://example.com/ja',
        'en-US': 'https://example.com/en',
      }
    }
  };
}

Q: 初回アクセスの言語判定は?

A: next-intl のミドルウェアは自動的に Accept-Language ヘッダーを解析し、適切な言語にリダイレクトします。また、NEXT_LOCALE という Cookie があればそれを優先します。

まとめ

App Router 時代の Next.js 国際化は、最初は少し複雑に感じるかもしれませんが、next-intl を使えば体系的かつ型安全に実装できます。

  1. 基本設定:ミドルウェアと [locale] ディレクトリ構成
  2. 実装:Server/Client 両方で使える useTranslations
  3. ナビゲーション:専用の LinkRouter を活用
  4. 管理:ファイル分割と型定義でメンテナンス性を確保

これらをマスターすれば、世界中のユーザーに快適な体験を提供する準備は完了です。

Next.js next-intl 導入フロー

インストールからミドルウェア設定、ディレクトリ構成までの完全手順

⏱️ Estimated time: 20 min

  1. 1

    Step1: インストール

    `npm install next-intl` を実行してライブラリを追加します。
  2. 2

    Step2: 翻訳ファイルの準備

    `messages` フォルダを作成し、`ja.json`, `en.json` などの言語ファイルを作成します。
  3. 3

    Step3: 設定ファイルの作成

    `i18n.ts` を作成し、ロケールに応じたメッセージのロードロジックを記述します。
  4. 4

    Step4: ミドルウェアの設定

    `middleware.ts` を作成し、ロケールのマッチングとリダイレクトルールを定義します。
  5. 5

    Step5: プロバイダーの配置

    App Router の構造を `app/[locale]/layout.tsx` に変更し、`NextIntlClientProvider` でアプリ全体をラップします。

FAQ

Server Component と Client Component で使い方は違いますか?
いいえ、基本的には同じ `useTranslations` フックを使用できます。ただし、非同期コンポーネント内では `await getTranslations` を使用することも可能です。
翻訳ファイルを追加したら再起動が必要ですか?
開発環境(devモード)では自動的に反映されますが、キャッシュが効いている場合はブラウザのリロードが必要な場合があります。新しい言語を追加した場合はサーバーの再起動が必要です。
一部のページだけ多言語化しないことは可能ですか?
はい、ミドルウェアの `matcher` 設定で特定のパスを除外するか、特定のルートを `[locale]` フォルダの外に配置することで対応可能です。

4 min read · 公開日: 2025年12月25日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事