国际化

2026年9月6日

i18n(internationalization)= i + 中间 18 个字母 + n。Next 没有绑死某家库,常见做法是 URL 带语言前缀:/zh/home/en/home,再用 Proxy 把裸路径补上前缀。

完整示例可以对照社区仓库,这里只记一套能跑通的骨架。官方指南:Internationalization


locale

语言用 ISO 639-1enzhjako。地区用 ISO 3166-1USCN。拼起来是 zh-CNen-US。项目里也可以只做到语言一级 zh / en

浏览器会带 Accept-Language: zh-CN,zh;q=0.9,en;q=0.8q 是权重,省略视为 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 里真正常用的那些项。


参考文档