Nextjs:next-intl(i18n)
2026年3月14日Nextjs16以降のCache Componentsが有効だと警告が出る
対策:https://next-intl.dev/docs/routing/setup#static-rendering
この対策はルーティングありの場合には有効だが、ルーティング無しだと警告が消えない
インストール
https://next-intl.dev/docs/getting-started/app-router
pnpm add next-intl2つの実装方法: with i18n routing = ルーティングあり / without i18n routing = ルーティングなし
ルーティングあり(URLに
/jaや/enが表示)は、ルーティングなしと比べると組み込むのが簡単i18nという単語はnext-intlに固有ではない。i18n = internationalization(国際化)
ルーティングあり
defaultLocale
// routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["en", "ja"],
defaultLocale: "en",
});routing.tsに書くdefaultLocaleは「この言語をデフォルトに」という意味ではなく、「ブラウザの優先言語(accept-language)がlocales配列に含まれていない場合の代替」という意味でのデフォルト例:
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en-US;q=0.7,en;q=0.6だと、ブラウザの優先言語の3番目にrouting.tsのlocales配列に含まれる ja がヒットするので、localeは ja になるAccept-Language: zh-CN,zh;q=0.9だとlocales配列でヒットしないのでdefaultLocaleの en になる
*ルーティングなしの場合、このような内部的な最適化は無いので自分で行う必要がある
優先言語の取得
(知識として)
クライアント:
useEffect(() => {
const lang = navigator.languages[0];navigator.languagesでブラウザの優先言語の配列を取得(['ja', 'en-US', 'en'])
サーバー:
export default function middleware(request: NextRequest) {
const acceptLangs = request.headers.get("accept-language");acceptLangsの中身:[ ja,en-US;q=0.9,en;q=0.8 ](1に近いほど優先)数値が設定されていないものは1扱い(上記だと ja )
ja-JP や en-US という文字列自体はブラウザ間で違いはない。
proxy.ts
docsのproxy.ts はnext-intl の処理だけをmiddlewareにする構成なので、他のmiddleware処理もできるようにする
import createMiddleware from "next-intl/middleware";
import { routing } from "./next-intl/i18n/routing";
import { NextRequest } from "next/server";
const intlMiddleware = createMiddleware(routing);
export default async function middleware(req: NextRequest) {
return intlMiddleware(req);
}ルーティング無し
intl-provider.tsx:
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
export const LocaleProvider = async ({
children,
}: {
children: React.ReactNode;
}) => {
const locale = await getLocale();
const messages = await getMessages();
return (
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
);
};現状
Cache Componentsが有効の場合、layout.tsxのexport defaultにasyncを含めないようするに必要があるので、NextIntlProviderは別のファイルに分けて<Suspense>で囲い警告が出ないようにする。
request.ts:
import { getRequestConfig } from "next-intl/server";
import { cookies } from "next/headers";
export default getRequestConfig(async () => {
const store = await cookies();
const locale = store.get("locale")?.value || "ja";
const messages = (await import(`./messages/${locale}.json`)).default;
return {
locale,
messages,
};
});paramsからlocaleを取得できないので、ルーティングありで書いていた
({requestLocale}}は機能しない。cookieからlocaleを取得するように変更する。
proxy.ts :
createMiddleware()があると、ルート("/")が404になるので外す
初回リクエスト時のlocaleセット
ルーティングなしの場合、localeのcookieへのセットを自分で行う必要がある。
クライアント(useEffect)側でやると、default-localeと異なる言語設定のユーザーは初回リクエスト時に一瞬違う言語がUIに表示されるので、UXが悪くなる。
サーバーサイド(middleware)で初回cookieのセットは行う方がいい。
以下はjaとenだけ用意し、enに該当しない場合はjaに流すケース
クライアント:
"use client";
import { Locale, useLocale } from "next-intl";
import { useEffect } from "react";
export default function LocaleSwitcher({
changeLocale,
}: {
changeLocale: (locale: Locale) => void;
}) {
const locale = useLocale();
const nextLocale = locale === "ja" ? "en" : "ja";
useEffect(() => {
const langs = navigator.languages.map((lang) => lang.split("-")[0]);
let prefLang: "ja" | "en" = "ja";
if (langs.includes("en")) {
prefLang = "en";
}
document.cookie = `locale=${prefLang}; path=/; max-age=31536000`;
}, []);
return (
<Button
variant={"link"}
className="flex items-center gap-1"
onClick={() => changeLocaleAction(nextLocale)}
>
<EarthIcon size={14} strokeWidth={0.75} />
<p className="text-xs">{nextLocale.toUpperCase()}</p>
</Button>
);
}サーバー:
import { NextRequest, NextResponse } from "next/server";
export function middleware(req: NextRequest) {
const res = NextResponse.next();
// セットされていない場合
if (!req.cookies.get("locale")) {
const acceptLang = req.headers.get("accept-language") || "";
const langs = acceptLang.split(",").map(lang => lang.split(";")[0].split("-")[0]);
const prefLang: "ja" | "en" = langs.includes("en") ? "en" : "ja";
res.cookies.set({
name: "locale",
value: prefLang,
path: "/",
maxAge: 60 * 60 * 24 * 365,
});
}
return res;
}localeの変更
"use server";
import { Locale } from "next-intl";
import { cookies } from "next/headers";
export const changeLocaleAction = async (locale: Locale) => {
const store = await cookies();
store.set("locale", locale);
};クライアント側で
document.cookieの値を変更しても、cookieの値が変わるだけでページの内容はリロードするまで変わらない。buttonのonClick等でserverActonを呼び出すことでRSCが再取得されるようにする。
localeに応じたmetadata
layout.tsx :
export const generateMetadata = async () => {
const t = await getTranslations("metadata");
return {
title: t("title"),
description: t("description"),
...
};
};jsonファイルの分割
request.ts:
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: {
home: {
...(await import(`./messages/${locale}.json`)).default,
},
test: {
...(await import(`./messages/${locale}/test.json`)).default,
},
},
};
});以下のようにしても機能するが、jsonファイル間でキーが被る可能性がある
messages: {
...(await import(`./messages/${locale}.json`)).default,
...(await import(`./messages/${locale}/test.json`)).default,
},page.tsx:
const Page = () => {
const t = useTranslations("test");
return (
<div>
{t("title")}
<hr />
{t("metadata.description")}
</div>
);
};Link
NextjsのLink(next/link)ではなくnext-intlのLinkを使うとlocaleが自動付与される。
import Link from "next/link";
const Footer = async () => {
const locale = await getLocale();
return (
<Link href={`/${locale}/terms`}>terms</Link>|NextjsのLinkはlocaleの取得プロセスが必要
next-intlのLink:
import { Link } from "@/i18n/routing";
const Footer = () => {
return (
<Link href={`/terms`}>terms</Link>|ルーティングなしの場合:
i18nのLinkを使うとlocaleが含まれたURLになるので表示がずれるため、NextjsのLinkを使う。
NextjsのLinkを使えばいいので必要はないが、
routing.tsにlocalePrefix: "never",の行を足すとi18nのLinkを使ってもURLにlocaleが含まれなくなる。
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
defaultLocale: "ja",
locales: ["en", "ja"],
localePrefix: "never",
});