next-intl v4は、Next.js App Routerに多言語対応を追加するためのライブラリだ。npm install next-intl でインストールし、i18n/routing.ts でロケール設定、ミドルウェアで言語検出、Server Componentでは getTranslations、Client Componentでは useTranslations で翻訳テキストを表示する。
32blog.comは日本語・英語・スペイン語の3言語に対応している。URLは /ja/nextjs/slug、/en/nextjs/slug、/es/nextjs/slug のプレフィックス方式だ。
この記事では、ルーティング設定からコンポーネントでの使い方、言語切替ボタン、SEOのhreflang設定まで、実際に32blog.comで動かしている構成を全部公開する。
next-intlとはどんなライブラリか?
Next.jsの多言語対応は標準で next.config.js の i18n オプションがある(Pages Routerのみ)。しかしApp Routerではこのオプションが廃止されている。App Routerで多言語対応するには自力でロケールベースのルーティングを組むか、next-intlのようなライブラリを使う必要がある。
next-intlはApp Router専用の多言語ライブラリで、以下の機能を提供する。
- URLプレフィックスによる言語ルーティング(
/ja/...、/en/...) - Server ComponentとClient Component両方で使える翻訳フック
- TypeScript対応(翻訳キーの型チェック)
- SEO対応(hreflang自動設定のユーティリティ)
インストールとファイル構成
npm install next-intl
32blog.comのファイル構成はこうなっている。
src/
├── app/
│ ├── [locale]/ # 言語プレフィックス
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── [category]/
│ │ └── [slug]/
│ │ └── page.tsx
│ └── api/
├── i18n/
│ ├── routing.ts # ルーティング設定
│ └── request.ts # リクエスト設定
├── messages/
│ ├── ja.json # 日本語翻訳
│ ├── en.json # 英語翻訳
│ └── es.json # スペイン語翻訳
└── middleware.ts # ロケール検出・リダイレクト
ルーティング設定
まず src/i18n/routing.ts でサポートする言語とデフォルト言語を定義する。設定オプションの詳細はnext-intlのルーティングドキュメントを参照。
// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["ja", "en", "es"],
defaultLocale: "ja",
// "always" にすると /ja/...、/en/...、/es/... すべてにプレフィックスが付く
localePrefix: "always",
});
次に middleware.ts を作る。これがリクエストを見てロケールを検出し、適切なURLにリダイレクトする役割を担う。設定の詳細はミドルウェアドキュメントにある。
// src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
// ミドルウェアを適用するパス(静的ファイルや内部APIは除外)
matcher: [
"/",
"/(ja|en|es)/:path*",
"/((?!api|_next|_vercel|.*\\..*).*)",
],
};
リクエスト設定
src/i18n/request.ts ではServer Componentでのロケール取得方法を定義する。
// src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
// ロケールを取得(URLから)
let locale = await requestLocale;
// サポートされていないロケールの場合はデフォルトにフォールバック
if (!locale || !routing.locales.includes(locale as "ja" | "en" | "es")) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});
翻訳ファイルの作成
messages/ja.json に翻訳テキストを定義する。
{
"nav": {
"home": "ホーム",
"about": "このブログについて",
"articles": "記事一覧"
},
"home": {
"hero": {
"title": "ググって最後にたどり着くページ",
"subtitle": "エラー解決・技術Tips・実装例を実体験ベースで書いています"
},
"latestArticles": "新着記事",
"allArticles": "すべての記事を見る"
},
"article": {
"publishedAt": "公開日",
"updatedAt": "更新日",
"readingTime": "{minutes}分で読めます",
"tableOfContents": "目次",
"relatedArticles": "関連記事"
}
}
{
"nav": {
"home": "Home",
"about": "About",
"articles": "Articles"
},
"home": {
"hero": {
"title": "The last page you land on after Googling",
"subtitle": "Error fixes, tech tips, and real-world implementations based on actual experience"
},
"latestArticles": "Latest Articles",
"allArticles": "View all articles"
},
"article": {
"publishedAt": "Published",
"updatedAt": "Updated",
"readingTime": "{minutes} min read",
"tableOfContents": "Table of Contents",
"relatedArticles": "Related Articles"
}
}
レイアウトへの組み込み
app/[locale]/layout.tsx でnext-intlのプロバイダーを設定する。
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// サポートされていないロケールは404
if (!routing.locales.includes(locale as "ja" | "en" | "es")) {
notFound();
}
// Server Componentから翻訳メッセージを取得
const messages = await getMessages();
return (
<html lang={locale}>
<body>
{/* Client Componentで翻訳を使うためのプロバイダー */}
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Server Componentでの翻訳使用
// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";
// Server Componentでは getTranslations を使う
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "home" });
return (
<main>
<section>
<h1>{t("hero.title")}</h1>
<p>{t("hero.subtitle")}</p>
</section>
</main>
);
}
Client Componentでの翻訳使用
// components/NavMenu.tsx
"use client";
import { useTranslations } from "next-intl";
import { Link } from "@/i18n/routing";
export function NavMenu() {
// Client ComponentではuseTranslationsを使う
const t = useTranslations("nav");
return (
<nav>
{/* next-intlのLinkは自動でロケールプレフィックスを付ける */}
<Link href="/">{t("home")}</Link>
<Link href="/about">{t("about")}</Link>
</nav>
);
}
next-intlが提供する Link コンポーネントを使うと、現在のロケールが自動でURLに付与される。/about と書いても、日本語ユーザーには /ja/about に、英語ユーザーには /en/about にリンクが張られる。
言語切替ボタンの実装
ユーザーが言語を切り替えられるスイッチャーを作る。
// components/LocaleSwitcher.tsx
"use client";
import { useLocale } from "next-intl";
import { useRouter, usePathname } from "@/i18n/routing";
export function LocaleSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const switchLocale = (newLocale: string) => {
// 現在のパスを維持したまま言語を切り替える
router.replace(pathname, { locale: newLocale });
};
return (
<div>
<button
onClick={() => switchLocale("ja")}
aria-current={locale === "ja" ? "true" : undefined}
>
日本語
</button>
<button
onClick={() => switchLocale("en")}
aria-current={locale === "en" ? "true" : undefined}
>
English
</button>
</div>
);
}
router.replace にロケールオプションを渡すだけで、同じパスの別言語版に遷移できる。例えば /ja/nextjs/hydration-error-fix を見ていた状態でEnglishボタンを押すと /en/nextjs/hydration-error-fix に移動する。
TypeScriptでの型チェック設定
next-intl v4ではAppConfig インターフェースへのモジュール拡張でTypeScriptの型チェックを設定できる。翻訳キーの打ち間違いをビルド時に検出できる。
// global.d.ts
import { routing } from "@/i18n/routing";
import en from "../messages/en.json";
declare module "next-intl" {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof en;
}
}
Locale を登録すると、ロケール文字列が厳密に型チェックされる(routing.ts にない "fr" などを書くとコンパイルエラー)。Messages を登録すると翻訳キーの補完と型チェックが有効になる。
// ❌ キーが存在しないのでTypeScriptエラー
const title = t("nav.hme"); // Error: Argument of type '"hme"' is not assignable...
// ✅ 正しいキー
const title = t("nav.home"); // OK
Locale 型を登録しておくと、next-intlの hasLocale() ヘルパーで未知の文字列を安全にナローイングできる。
import { hasLocale } from "next-intl";
// narrowing後、localeは "ja" | "en" | "es" 型になる
if (hasLocale(routing.locales, locale)) {
// TypeScriptはlocaleが有効だと認識する
}
SEO(hreflang)の設定
多言語サイトではSEOのために hreflang タグを各ページに設定する必要がある。next-intlはNext.jsのメタデータAPIと組み合わせてこれを簡単に実現できる。
// app/[locale]/[category]/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; category: string; slug: string }>;
}): Promise<Metadata> {
const { locale, category, slug } = await params;
const post = await getPost(category, slug, locale);
const baseUrl = "https://32blog.com";
return {
title: post.title,
description: post.description,
alternates: {
canonical: `${baseUrl}/${locale}/${category}/${slug}`,
languages: {
// hreflang設定
"ja": `${baseUrl}/ja/${category}/${slug}`,
"en": `${baseUrl}/en/${category}/${slug}`,
"es": `${baseUrl}/es/${category}/${slug}`,
"x-default": `${baseUrl}/ja/${category}/${slug}`,
},
},
};
}
これで各ページに <link rel="alternate" hreflang="ja" href="..."> タグが自動生成される。Googleは複数言語のページが同じコンテンツの翻訳版であることを正しく認識できる。
FAQ
next-intlはNext.js 16で使える?
使える。next-intl v4はNext.js 13〜16をサポートしている。ただしNext.js 16の use cache ディレクティブは getTranslations() が内部で headers() を読むため、まだシームレスに動かない。既存の setRequestLocale パターンは引き続き動作する。
getTranslations と useTranslations の違いは?
getTranslations はServer Component用の非同期関数。useTranslations はClient Component用のReact hook。間違ったコンテキストで使うとランタイムエラーになる。
URLプレフィックスなしで使える?
使える。routing.ts で localePrefix: "as-needed" を設定すると、デフォルトロケール以外のみプレフィックスが付く。localePrefix: "never" にすればドメインやCookieベースの検出に切り替えられる。
後から言語を追加するにはどうすればいい?
routing.ts にロケールを追加し、messages/{locale}.json ファイルを作成し、ミドルウェアのmatcher正規表現を更新するだけ。コンポーネントの変更は不要で、next-intlが自動的に認識する。
next-intlはクライアントバンドルサイズに影響する?
next-intl自体は軽量(クライアントランタイムはgzip後 約2KB)。バンドルへの主な影響は NextIntlClientProvider に渡す翻訳JSONのサイズだ。namespaceごとにメッセージを分割すれば軽減できる。
多言語サイトのSEOはどう対応する?
next-intlはNext.jsのメタデータAPIと組み合わせてhreflangユーティリティを提供する。ロケールプレフィックス付きURLと適切なcanonicalタグを設定すれば、Googleは各ページの全言語バージョンを正しく関連付ける。
next-intlとnext-i18nextはどっちがいい?
next-i18nextはPages Router向けでi18nextのラッパー。App Routerプロジェクトでは、Server Componentsをネイティブサポートするnext-intlの方が適している。
まとめ
next-intl v4の設定ポイントをまとめる。
| ファイル | 役割 |
|---|---|
i18n/routing.ts | サポートロケールとデフォルトロケールを定義 |
middleware.ts | リクエストのロケール検出とリダイレクト |
i18n/request.ts | Server Componentでのメッセージロードを設定 |
messages/ja.json | 日本語翻訳テキスト |
app/[locale]/layout.tsx | NextIntlClientProviderの設定 |
global.d.ts | AppConfig を登録して型安全なキーを実現 |
Server Componentでは getTranslations、Client Componentでは useTranslations と使い分ける点が最初は混乱しやすい。URLリンクはnext-intlの Link を使えば自動でロケールが付くので、素のNext.jsの Link と使い分けることも忘れずに。
基本が動いたら、next-intlとi18nの落とし穴もチェックしてほしい。ロケール依存のフォーマットで起きるhydrationミスマッチや、スケールで破綻する翻訳キー設計のパターンをまとめている。Server Componentsのレンダリングモデルをより深く理解したいなら、Next.js SSR完全ガイドも参考になる。
32blog.comではこの構成で数千のページビューを捌いている。App Routerのnext-intlは安定して動いている。