🐙

Next.js App routerをnext-intlで多言語対応(i18n)実装

2025/02/05に公開

はじめに

Next.jsの多言語化ライブラリーといったらi18nextはとても有名で、 たくさんの人に使われています、もちろん僕も使っていますが、最近、Next.js専用のライブラリーを探してみて、とある成長のはやいライブラリーを発見しましたので、共有したいと思います。

https://npmtrends.com/next-i18next-vs-next-intl

next‑intlとは

next‑intlは、Next.jsの従来のPages Routerだけではなく、最新のApp Routerとの連携にも対応しているi18nライブラリーです。

主な特徴

  • 動的な言語ルーティング
    トップレベルに[locale]セグメントを設けることで、/enや/jaなどのプレフィックスを自動で扱います。
  • シンプルなナビゲーションAPI
    Next.jsのルーティングに合わせた独自API(例えば、createNavigation)を提供し、言語切替時もシームレスなナビゲーションが実現できます。
  • サーバーコンポーネント対応
    サーバー側で翻訳メッセージを取得し、クライアントに渡す仕組みを簡単に実装可能です。

より詳しい使用例や公式ドキュメントについては、next‑intlの公式サイトを参照してください。

Next.js15とnext-intlで多言語対応実装

next-intlを使って多言語対応サイトを作成していきます。

実行環境

Next.jsプロジェクトの初期化

  • 新規プロジェクト作成

    pnpm create next-app@latest
    
  • next-intlのインストール

    pnpm add next-intl
    

フォルダー構造

├── messages
│   ├── en.json (1)
│   └── ja.json (1)
├── next.config.mjs (2)
├── i18n
│   ├── routing.ts (3)
│   └── request.ts (5)
├── middleware.ts (4)
└── app
└── [locale]
    ├── layout.tsx (6)
    └── page.tsx (7)

翻訳メッセージの用意

各言語ごとに翻訳ファイルを作成します。

messages/ja.json
{
	"title": "こんにちは世界",
	"description": {
		"author": "山田太郎",
		"date": "2025-01-01",
		"content": "これはテストメッセージです"
	},
	"language": {
		"toggle": "言語を切り替える",
		"current": "現在の言語: {locale}"
	}
}

i18nファイルを用意する

i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';

// 利用可能な言語とデフォルト言語を設定
export const routing = defineRouting({
  locales: ['en', 'ja'],
  defaultLocale: 'ja'
});

// ナビゲーション用のユーティリティを作成
export const { 
  Link, 
  redirect, 
  usePathname, 
  useRouter, 
  getPathname 
} = createNavigation(routing);
i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { hasLocale } from 'next-intl';
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
  };
});

next.config.tsとmiddleware.tsを設定する

next.config.ts
import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';

// next-intlのプラグインを初期化
const withNextIntl = createNextIntlPlugin();

const nextConfig: NextConfig = {};

// next-intlプラグインを適用
export default withNextIntl(nextConfig);
middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from '@/i18n/routing';

export default createMiddleware(routing);

export const config = {
  // ルートパスと言語プレフィックス付きのパスに対してミドルウェアを適用
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};

layout.tsxに設定する

app/[locale]/layout.tsx
import type { Metadata } from "next";
import { setRequestLocale } from "next-intl/server";
import { NextIntlClientProvider, hasLocale } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
import "@/app/globals.css";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default async function RootLayout({
  children,
  params,
}: Readonly<{
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}>) {
  const { locale } = await params;
  
  // 無効な言語の場合は404ページを表示
  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  // SSG対応
  setRequestLocale(locale);
  // 言語ファイルの読み込み
  const messages = await getMessages();
  
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

server componentでi18nを実装する(SSG対応も含む)

app/[locale]/page.tsx
import { ToggleLanguage } from "@/components/toggle-language";
import { routing } from "@/i18n/routing";
import { getTranslations, setRequestLocale } from "next-intl/server";

// SSG対応
export function generateStaticParams() {
  return routing.locales.map(locale => ({ locale }));
}

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

  return {
    title: t("title"),
  };
}

export default async function HomePage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  setRequestLocale(locale);
  const t = await getTranslations({ locale });

  return (
    <div>
      <div className="flex justify-end">
        <ToggleLanguage />
      </div>
      <h1>{t("title")}</h1>
      <p>{t("description.author")}</p>
      <p>{t("description.date")}</p>
      <p>{t("description.content")}</p>
    </div>
  );
}

言語の入れ替え機能を実装する

components/toggle-language.tsx
"use client";

import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useLocale } from "next-intl";
import { routing } from "@/i18n/routing";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";

export const ToggleLanguage = () => {
  const t = useTranslations("language");
  const locale = useLocale();
  const router = useRouter();

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">{t("toggle")}</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel>{t("current", { locale })}</DropdownMenuLabel>
        {routing.locales.map(loc => (
          <DropdownMenuItem
            disabled={loc === locale}
            key={loc}
            onClick={() => router.push(`/${loc}`)}
          >
            {loc}
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
};

画面検証

日本語の画面

英語の画面

最後に

今回紹介した実装例は基本的な機能のみですが、next-intlはより高度な機能(例:数値のフォーマット、日付の変換、複数形対応など)も簡単に追加することができます。
また、App RouterとPages Routerの両方に対応しており、プロジェクトの要件に応じて柔軟に選択できます。
興味のある方は、公式ドキュメントを参考に、ぜひチャレンジしてみてください。
国際化対応は、グローバルなユーザー体験の向上に直結する重要な要素となります。
最後まで読んでいただきありがとうございました!

参考リンク

next-intl公式サイト

Discussion