32blog.comをnext-intl v4で日英対応したとき、ドキュメントに書いてない落とし穴を何個も踏んだ。
「動いているように見えるけど実は壊れている」というパターンが多く、気づくまでに時間がかかった。この記事では実際に踏んだ落とし穴を全部まとめて、解決策とともに公開する。
落とし穴1: ミドルウェアのmatcherパターンが間違っている
最初にハマった落とし穴がこれだ。ミドルウェアの matcher パターンが不完全だと、一部のリクエストでロケール検出が走らず / にアクセスすると /ja/ にリダイレクトされない。
よくある間違いパターン:
// src/middleware.ts
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
このパターンは一見正しそうに見えるが、.png や .svg などの静的ファイルまでミドルウェアが処理しようとする。
正しいパターン:
// src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: [
"/",
"/(ja|en)/:path*",
"/((?!api|_next|_vercel|.*\\..*).*)"],
};
.*\\..* の部分がポイントだ。ドットを含むパス(= 拡張子あり = 静的ファイル)を除外している。
落とし穴2: next-intlのLinkとNext.jsのLinkを混在させる
コンポーネント内でNext.jsの Link を使い続けると、言語切替が正しく動かない。
素のNext.js Linkを使う問題:
// これは言語プレフィックスが付かない
import Link from "next/link";
export function ArticleCard({ slug, category }) {
return (
<Link href={`/${category}/${slug}`}>
記事を読む
</Link>
);
}
日本語ユーザーが /ja/ ページにいる状態でこのリンクをクリックすると、/nextjs/hydration-error-fix に飛ぶ。/ja/ プレフィックスが消える。
next-intlのLinkを使う修正:
// next-intlのLinkは自動でロケールプレフィックスを付ける
import { Link } from "@/i18n/routing";
export function ArticleCard({ slug, category }) {
return (
<Link href={`/${category}/${slug}`}>
記事を読む
</Link>
);
}
同様に、useRouter や redirect も next-intl のものを使う必要がある。
// 素のNext.jsのhooksではロケールが失われる
import { useRouter } from "next/navigation";
// next-intlのhooksを使う
import { useRouter, redirect } from "@/i18n/routing";
落とし穴3: Server ComponentでuseTranslationsを使おうとする
next-intlはServer ComponentとClient Componentで使う関数が異なる。
Server Componentで間違えるパターン:
// "use client" がない = Server Component
import { useTranslations } from "next-intl";
export default async function HomePage() {
// エラー: useTranslations is not supported in Server Components
const t = useTranslations("home");
return <h1>{t("title")}</h1>;
}
Server Componentでの正しい書き方:
import { getTranslations } from "next-intl/server";
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// getTranslations はawait必須
const t = await getTranslations({ locale, namespace: "home" });
return <h1>{t("title")}</h1>;
}
使い分けの早見表:
| Server Component | Client Component | |
|---|---|---|
| 翻訳取得 | getTranslations (await必須) | useTranslations |
| ロケール取得 | getLocale | useLocale |
| メッセージ取得 | getMessages | useMessages |
落とし穴4: 動的ルートでgenerateStaticParamsにロケールを含め忘れる
多言語サイトで generateStaticParams を使う場合、ロケールのパラメータも含める必要がある。
ロケールなしで生成する(英語記事が404になる):
// app/[locale]/[category]/[slug]/page.tsx
// ロケールが"ja"のみ生成される
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
// locale がない!
category: post.category,
slug: post.slug,
}));
}
ロケールを含めて全言語版を生成する:
export async function generateStaticParams() {
const locales = ["ja", "en"] as const;
const paths = [];
for (const locale of locales) {
const posts = await getPostsByLocale(locale);
for (const post of posts) {
paths.push({
locale,
category: post.category,
slug: post.slug,
});
}
}
return paths;
}
英語記事を追加したのに /en/nextjs/... が404になる場合、これが原因であることが多い。
落とし穴5: hreflangのx-defaultが設定されていない
SEOでよくやらかすのが x-default の設定漏れだ。
x-defaultがない(Googleが言語判定できない):
export async function generateMetadata({ params }) {
const { locale, category, slug } = await params;
return {
alternates: {
languages: {
"ja": `https://32blog.com/ja/${category}/${slug}`,
"en": `https://32blog.com/en/${category}/${slug}`,
// x-defaultがない!
},
},
};
}
x-defaultを追加した正しい設定:
export async function generateMetadata({ params }) {
const { locale, category, slug } = await params;
const baseUrl = "https://32blog.com";
return {
alternates: {
canonical: `${baseUrl}/${locale}/${category}/${slug}`,
languages: {
"ja": `${baseUrl}/ja/${category}/${slug}`,
"en": `${baseUrl}/en/${category}/${slug}`,
// 言語が判定できないユーザーをデフォルト言語へ
"x-default": `${baseUrl}/ja/${category}/${slug}`,
},
},
};
}
x-default はどの言語にも当てはまらないユーザーが来たときにリダイレクトされるURLだ。設定しないとGoogleが「このサイトは多言語対応だが、デフォルトがわからない」と判断してしまう。
落とし穴6: notFoundのロケール対応
notFound() を呼び出すと、Next.jsの標準 not-found.tsx が表示される。しかし app/not-found.tsx は [locale] セグメントの外なので、翻訳が効かない。
app/
├── not-found.tsx ← [locale]の外 → 翻訳できない
└── [locale]/
└── not-found.tsx ← ここに置くと翻訳が効く
app/not-found.tsx の問題:
// next-intlが提供するtransを使えない
export default function NotFound() {
return (
<div>
<h1>404 - ページが見つかりません</h1>
{/* 英語ユーザーにも日本語が表示される */}
</div>
);
}
app/[locale]/not-found.tsx に置く修正:
import { getTranslations } from "next-intl/server";
export default async function NotFound() {
const t = await getTranslations("notFound");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("description")}</p>
</div>
);
}
not-found.tsx はServer Componentなので useTranslations ではなく getTranslations を使う必要がある。notFound() を [locale] セグメント内から呼び出した場合は app/[locale]/not-found.tsx が表示されるが、ミドルウェアが処理する前の404は app/not-found.tsx が使われる。両方用意しておくのが安全だ。
落とし穴7: クライアントコンポーネントのlocaleが古い
言語切替直後に、Client Componentが古いロケールを参照し続けることがある。
depsにlocaleがない(切替後も古い表示が残る):
"use client";
import { useLocale } from "next-intl";
import { useMemo } from "react";
export function LocalizedDate({ date }: { date: Date }) {
const locale = useLocale();
const formatted = useMemo(
() => date.toLocaleDateString(locale),
[] // localeがdepsにない
);
return <time>{formatted}</time>;
}
depsにlocaleを含める修正:
"use client";
import { useLocale } from "next-intl";
import { useMemo } from "react";
export function LocalizedDate({ date }: { date: Date }) {
const locale = useLocale();
const formatted = useMemo(
() => date.toLocaleDateString(locale),
[date, locale] // localeを含める
);
return <time>{formatted}</time>;
}
useMemo や useCallback の依存配列に locale を含め忘れると、言語切替後も古いロケールでフォーマットされたデータが表示される。
まとめ
next-intlで実際に踏んだ落とし穴をまとめる。
| 落とし穴 | 症状 | 修正 |
|---|---|---|
| matcherパターンが不完全 | ルートへのリダイレクトが動かない | 正しいmatcherパターンに修正 |
| Next.jsのLinkを使う | 言語切替でプレフィックスが消える | next-intlのLinkを使う |
| Server ComponentでuseTranslations | ランタイムエラー | getTranslations (await)に変更 |
| generateStaticParamsにlocaleなし | 英語記事が404 | ロケール×記事の全組み合わせを生成 |
| x-defaultがない | SEOで言語判定が不完全 | hreflangにx-defaultを追加 |
| not-foundのロケール対応漏れ | 404ページが翻訳されない | app/[locale]/not-found.tsx に配置 |
| useMemoのdepsにlocaleなし | 言語切替後も古い表示 | depsに locale を追加 |
next-intlは設定さえ正しければ非常に快適に使えるライブラリだ。ただ、Next.jsの標準APIとの混在(特にLinkとuseRouter)が最も見落としやすいので、プロジェクト初期にESLintで縛っておくのが一番の予防策だ。