国际化
2026年9月6日
i18n(internationalization)= i + 中间 18 个字母 + n。Next 没有绑死某家库,常见做法是 URL 带语言前缀:/zh/home、/en/home,再用 Proxy 把裸路径补上前缀。
完整示例可以对照社区仓库,这里只记一套能跑通的骨架。官方指南:Internationalization。
locale
语言用 ISO 639-1:en、zh、ja、ko。地区用 ISO 3166-1:US、CN。拼起来是 zh-CN、en-US。项目里也可以只做到语言一级 zh / en。
浏览器会带 Accept-Language: zh-CN,zh;q=0.9,en;q=0.8。q 是权重,省略视为 1。解析后按权重排序,再和站点支持的列表做匹配。
解析与匹配
pnpm add negotiator @formatjs/intl-localematcher
bash
export const locales = ["zh", "en", "ja", "ko"] as const;
export const defaultLocale = "zh";
ts
src/proxy.ts(Next 16;15 仍叫 middleware):
import { NextRequest, NextResponse } from "next/server";
import Negotiator from "negotiator";
import { match } from "@formatjs/intl-localematcher";
import { defaultLocale, locales } from "@/dictionaries";
export function proxy(req: NextRequest) {
const { pathname } = req.nextUrl;
if (pathname === "/") return NextResponse.next();
if (locales.some((l) => pathname.startsWith(`/${l}`))) {
return NextResponse.next();
}
const languages = new Negotiator({
headers: { "accept-language": req.headers.get("accept-language") ?? "" },
}).languages();
const lang = match(languages, [...locales], defaultLocale);
req.nextUrl.pathname = `/${lang}${pathname}`;
return NextResponse.redirect(req.nextUrl);
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
ts
访问 /home 且浏览器偏中文时,跳到 /zh/home。
字典
export type Dictionary = {
title: string;
description: string;
};
export function getDictionary(locale: string): Promise<Dictionary> {
return import(`./${locale}.json`).then((m) => m.default);
}
ts
zh.json / en.json 各一份文案。页面:
import { getDictionary } from "@/dictionaries";
export default async function Home({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const t = await getDictionary(lang);
return <h1>{t.title}</h1>;
}
tsx
路由是 app/[lang]/home/page.tsx。
切换语言
"use client";
import { locales } from "@/dictionaries";
import { usePathname, useRouter } from "next/navigation";
export default function SwitchI18n({ lang }: { lang: string }) {
const pathname = usePathname();
const router = useRouter();
return (
<select
value={lang}
onChange={(e) => {
const next = pathname.replace(`/${lang}`, `/${e.target.value}`);
router.replace(next);
}}
>
{locales.map((l) => (
<option key={l} value={l}>
{l}
</option>
))}
</select>
);
}
tsx
SEO 上每语言一页,补 hreflang / 对应 sitemap。接口翻译是另一层,别和 UI 字典搅在一起。
下一篇:next.config 里真正常用的那些项。