本节摘要:多语言不是"把文案翻译一下"这么简单——还要处理 URL 结构、语言切换、日期货币格式与 SEO。本节讲清 Next.js 的国际化方案:路由级语言前缀、字典管理、动态切换,以及多语言 SEO 的注意事项。
阅读完本节,你应当能够:
"你好" → "Hello" 只是第一步。真正的国际化要处理:
/en/about、/zh/about(或子域名 en.example.com);2026/08/15 vs Aug 15, 2026,货币 ¥100 vs $100;直觉类比:国际化像"同一家餐厅的多语言菜单"——菜(功能)一样,但每本菜单(语言版)有自己的封面(URL)、排版(格式)、内容(翻译)。
💡 关键直觉:Next.js 推荐的国际化是"URL 前缀"方案——
/zh/about与/en/about是两个独立路由,各自服务端渲染,SEO 友好、可被独立收录。语言信息在 URL 里,而不是只在 Cookie 里。
app/ ├── [lang]/ # 语言段 │ ├── layout.tsx # 语言布局(设置 lang 属性) │ ├── page.tsx # /zh、/en │ ├── about/page.tsx # /zh/about、/en/about │ └── blog/[slug]/page.tsx └── layout.tsx # 根布局
// dictionaries.ts const dictionaries = { zh: () => import("./dictionaries/zh.json").then((m) => m.default), en: () => import("./dictionaries/en.json").then((m) => m.default), }; export async function getDictionary(lang: "zh" | "en") { return dictionaries[lang](); }
// dictionaries/zh.json { "home": { "title": "首页", "welcome": "欢迎使用" }, "common": { "login": "登录", "logout": "登出" } }
// dictionaries/en.json { "home": { "title": "Home", "welcome": "Welcome" }, "common": { "login": "Login", "logout": "Logout" } }
// app/[lang]/page.tsx import { getDictionary } from "@/dictionaries"; export default async function HomePage({ params, }: { params: Promise<{ lang: "zh" | "en" }>; }) { const { lang } = await params; const dict = await getDictionary(lang); return ( <main> <h1>{dict.home.title}</h1> <p>{dict.home.welcome}</p> </main> ); }
// components/LangSwitcher.tsx(客户端) "use client"; import { usePathname } from "next/navigation"; import Link from "next/link"; export function LangSwitcher({ lang }: { lang: "zh" | "en" }) { const pathname = usePathname(); const target = lang === "zh" ? "en" : "zh"; // 替换 URL 中的语言段 const newPath = pathname.replace(/^\/(zh|en)/, `/${target}`); return <Link href={newPath}>切换至 {target === "zh" ? "中文" : "English"}</Link>; }
// utils/format.ts export function formatDate(date: Date, lang: "zh" | "en") { return new Intl.DateTimeFormat(lang === "zh" ? "zh-CN" : "en-US", { year: "numeric", month: "long", day: "numeric", }).format(date); } export function formatCurrency(amount: number, lang: "zh" | "en") { return new Intl.NumberFormat(lang === "zh" ? "zh-CN" : "en-US", { style: "currency", currency: lang === "zh" ? "CNY" : "USD", }).format(amount); }
// app/[lang]/layout.tsx import type { Metadata } from "next"; export async function generateMetadata({ params, }: { params: Promise<{ lang: "zh" | "en" }>; }): Promise<Metadata> { const { lang } = await params; return { title: lang === "zh" ? "我的网站" : "My Website", alternates: { languages: { "zh-CN": "/zh", "en-US": "/en", }, }, }; }
hreflang 的价值:告诉搜索引擎"同一内容的多语言版本关系",避免重复内容惩罚。
// app/[lang]/layout.tsx export default function LangLayout({ children, params, }: { children: React.ReactNode; params: Promise<{ lang: "zh" | "en" }>; }) { return <html lang={(await params).lang}>{children}</html>; }
lang 属性:屏幕阅读器、浏览器翻译、SEO 都依赖它,必须正确设置。
| 误区 | 现象 | 正解 |
|---|---|---|
| 只翻译文案不处理 URL | 无独立语言页 | 用 [lang] 路由段 |
| 忘记 lang 属性 | 无障碍/SEO 受损 | 布局里设置 html lang |
| 字典 key 不一致 | 翻译缺失 | 两语言字典保持同构 |
| 客户端写死文案 | 切换无效 | 字典从服务器组件传入 |
| 忽略 hreflang | 多语言被当重复内容 | generateMetadata 配 alternates |
app/ ├── [lang]/ │ ├── layout.tsx # 设置 lang + 语言切换器 │ ├── page.tsx # 首页(读字典) │ └── about/page.tsx # 关于页 ├── dictionaries/ │ ├── zh.json │ └── en.json └── layout.tsx # 根布局
// app/[lang]/layout.tsx import { LangSwitcher } from "@/components/LangSwitcher"; export default async function LangLayout({ children, params, }: { children: React.ReactNode; params: Promise<{ lang: "zh" | "en" }>; }) { const { lang } = await params; return ( <html lang={lang}> <body> <header> <LangSwitcher lang={lang} /> </header> <main>{children}</main> </body> </html> ); }
访问 /zh 看到中文、/en 看到英文,点切换器在两种语言间跳转(URL 变化、页面刷新)——URL 前缀方案的多语言站点跑通了。后续可扩展:默认语言重定向(/ → /zh)、更多语言、服务端检测浏览器语言。
URL 前缀(/zh、/en)决定语言 → 加载对应字典 → 渲染 + 正确 lang 属性 + hreflang SEO。
一句话:国际化的本质是"语言是路由的一部分"——每个语言版本都是独立页面、独立 SEO、独立可访问,字典只是内容的载体。
问:国际化一定要用 URL 前缀吗?
Next.js 官方推荐 URL 前缀(/zh、/en),因为:SEO 友好(独立 URL)、可分享(链接带语言)、无状态(刷新不丢语言)。Cookie 方案省 URL 但 SEO 差。
问:默认语言怎么处理根路径?
中间件把 / 重定向到默认语言(如 /zh):读取浏览器语言(Accept-Language)或 Cookie 决定。未匹配语言时回退默认。
问:客户端组件的文案怎么国际化?
客户端组件不能直接用服务器字典(跨边界)。方案:字典作为 props 从服务器组件传入,或用客户端 i18n 库(如 i18next)。简单场景 props 传入最省事。
问:图片/SEO 也需要国际化吗?
需要:每个语言的 og 图片、title 描述都独立;hreflang 关联语言版本;alt 文本按语言翻译。
问:日期货币格式怎么处理?
用 Intl API(Intl.DateTimeFormat、Intl.NumberFormat),按语言传 locale——比手动拼接格式更规范、自动适配不同地区的习惯。
国际化的本质是"语言是路由的一部分"——app/[lang]/ 组织页面,字典保持同构,Intl API 处理格式,hreflang 关联 SEO。记住:URL 前缀方案让每个语言版本独立可收录、可分享、可访问。从第一天就考虑 i18n,比上线后重构便宜十倍。
把练习项目改造成双语站:目录重构为 app/[lang]/,建 dictionaries/zh.json 与 en.json(保持同构),首页与关于页从字典读文案,语言切换器替换 URL 语言段,根布局设置 html lang。
进阶练习:加默认语言重定向(中间件把 / 指向浏览器语言的版本)、日期本地化(Intl API 按语言格式化)、generateMetadata 配 hreflang。改完用 /zh 与 /en 分别访问验证——每个语言版本都是独立可访问、可收录的页面,这就是 URL 前缀方案的价值。
能解释为什么 URL 前缀方案比 Cookie 方案更适合 SEO 吗?能说出字典文件保持同构的原因吗?能说出 hreflang 的作用吗?答对即掌握国际化核心。