Next.js 支持配置路由和内容渲染,以适应多种语言。让网站适应不同 locale,既包括翻译内容,也就是本地化,也包括国际化路由。
术语
Locale:一组语言和格式偏好的标识,通常包含用户首选语言,也可能包含所在地区。例如:
en-US:美国使用的英语。nl-NL:荷兰使用的荷兰语。nl:不指定地区的荷兰语。
路由概览
建议根据浏览器中的用户语言偏好选择 locale。用户更改首选语言后,发送给应用程序的 Accept-Language 请求头也会变化。
例如,利用以下库,根据传入 Request 的请求头、计划支持的 locale 和默认 locale,决定使用哪种语言。文件:proxy.js。
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
let headers = { 'accept-language': 'en-US,en;q=0.5' }
let languages = new Negotiator({ headers }).languages()
let locales = ['en-US', 'nl-NL', 'nl']
let defaultLocale = 'en-US'
match(languages, locales, defaultLocale) // -> 'en-US'
国际化路由可以采用子路径,例如 /fr/products,或域名,例如 my-site.fr/products。获得 locale 后,就可以在 Proxy 中进行对应重定向。文件:proxy.js。
import { NextResponse } from "next/server";
let locales = ['en-US', 'nl-NL', 'nl']
// Get the preferred locale, similar to the above or using a library
function getLocale(request) { ... }
export function proxy(request) {
// Check if there is any supported locale in the pathname
const { pathname } = request.nextUrl
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return
// Redirect if there is no locale
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(request.nextUrl)
}
export const config = {
matcher: [
// Skip all internal paths (_next)
'/((?!_next).*)',
// Optional: only run on root (/) URL
// '/'
],
}
最后,确保 app/ 中的所有特殊文件都嵌套在 app/[lang] 下。这样 Next.js 路由器便能动态处理不同 locale,并将 lang 参数传给每个布局与页面。例如,TypeScript 版本的 app/[lang]/page.tsx:
// You now have access to the current locale
// e.g. /en-US/products -> `lang` is "en-US"
export default async function Page({ params }: PageProps<'/[lang]'>) {
const { lang } = await params
return ...
}
JavaScript 版本的 app/[lang]/page.js:
// You now have access to the current locale
// e.g. /en-US/products -> `lang` is "en-US"
export default async function Page({ params }) {
const { lang } = await params
return ...
}
提示:PageProps 和 LayoutProps 是全局可用的 TypeScript 辅助类型,为路由参数提供强类型支持。详情见 PageProps 与 LayoutProps。
根布局也可以放在新目录中,例如 app/[lang]/layout.js。
本地化
根据用户首选 locale 改变显示内容,即本地化,并非 Next.js 独有。下面的模式同样适用于其他 Web 应用。
假设应用要支持英语与荷兰语,可以维护两份“字典”,将键映射到本地化字符串。例如,dictionaries/en.json:
{
"products": {
"cart": "Add to Cart"
}
}
dictionaries/nl.json:
{
"products": {
"cart": "Toevoegen aan Winkelwagen"
}
}
然后创建 getDictionary,加载所请求 locale 的翻译。TypeScript 版本,app/[lang]/dictionaries.ts:
import 'server-only'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export type Locale = keyof typeof dictionaries
export const hasLocale = (locale: string): locale is Locale =>
locale in dictionaries
export const getDictionary = async (locale: Locale) => dictionaries[locale]()
JavaScript 版本,app/[lang]/dictionaries.js:
import 'server-only'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export const hasLocale = (locale) => locale in dictionaries
export const getDictionary = async (locale) => dictionaries[locale]()
有了当前语言,就可以在布局或页面中获取相应字典。
lang 的类型是 string,使用 hasLocale 可以将类型缩小到应用支持的 locale。它还确保缺少翻译时返回 404,而不是产生运行时错误。
TypeScript 版本,app/[lang]/page.tsx:
import { notFound } from 'next/navigation'
import { getDictionary, hasLocale } from './dictionaries'
export default async function Page({ params }: PageProps<'/[lang]'>) {
const { lang } = await params
if (!hasLocale(lang)) notFound()
const dict = await getDictionary(lang)
return <button>{dict.products.cart}</button> // Add to Cart
}
JavaScript 版本,app/[lang]/page.js:
import { notFound } from 'next/navigation'
import { getDictionary, hasLocale } from './dictionaries'
export default async function Page({ params }) {
const { lang } = await params
if (!hasLocale(lang)) notFound()
const dict = await getDictionary(lang)
return <button>{dict.products.cart}</button> // Add to Cart
}
app/ 下的布局和页面默认都是 服务器组件,因此不必担心翻译文件大小增加客户端 JavaScript 包的体积。上述代码只在服务器运行,浏览器收到的只有生成的 HTML。
在整个应用中共享 locale
除了直接接收参数的页面,共享数据获取工具或深层嵌套组件等位置也经常需要 locale。无需逐层传递 lang,可以直接通过 next/root-params 读取。
next/root-params 为根布局之上的每个动态路由段导出 getter。由于全部路由都嵌套在 app/[lang] 下,lang 是根参数,任意服务器组件或服务器端工具都可以调用它。把 locale 查询移进 getDictionary,调用者就不再需要传入 lang。
TypeScript 版本,app/[lang]/dictionaries.ts:
import { lang } from 'next/root-params'
import { notFound } from 'next/navigation'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export type Locale = keyof typeof dictionaries
export const hasLocale = (locale: string): locale is Locale =>
locale in dictionaries
export const getDictionary = async () => {
const locale = await lang()
if (!hasLocale(locale)) notFound()
return dictionaries[locale]()
}
JavaScript 版本,app/[lang]/dictionaries.js:
import { lang } from 'next/root-params'
import { notFound } from 'next/navigation'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export const hasLocale = (locale) => locale in dictionaries
export const getDictionary = async () => {
const locale = await lang()
if (!hasLocale(locale)) notFound()
return dictionaries[locale]()
}
提示:导入 next/root-params 的文件不需要额外写 import 'server-only'。如果客户端组件使用了该导入,构建时就会失败。
页面和组件随后无需传参,直接调用 getDictionary(),locale 会在函数内部解析。
TypeScript 版本,app/[lang]/page.tsx:
import { getDictionary } from './dictionaries'
export default async function Page() {
const dict = await getDictionary()
return <button>{dict.products.cart}</button> // Add to Cart
}
JavaScript 版本,app/[lang]/page.js:
import { getDictionary } from './dictionaries'
export default async function Page() {
const dict = await getDictionary()
return <button>{dict.products.cart}</button> // Add to Cart
}
提示:根参数 getter 可在服务器组件和服务器端工具中运行,但不能在客户端组件、Server Actions 或 Route Handlers 中运行。完整 API 及缓存行为见 next/root-params。
静态渲染
要为一组 locale 生成静态路由,可以在任意页面或布局中使用 generateStaticParams。也可以全局配置,例如放在根布局中。
TypeScript 版本,app/[lang]/layout.tsx:
export async function generateStaticParams() {
return [{ lang: 'en-US' }, { lang: 'de' }]
}
export default async function RootLayout({
children,
params,
}: LayoutProps<'/[lang]'>) {
return (
<html lang={(await params).lang}>
<body>{children}</body>
</html>
)
}
JavaScript 版本,app/[lang]/layout.js:
export async function generateStaticParams() {
return [{ lang: 'en-US' }, { lang: 'de' }]
}
export default async function RootLayout({ children, params }) {
return (
<html lang={(await params).lang}>
<body>{children}</body>
</html>
)
}
相关资源
- 最小化 i18n 路由与翻译示例
- next-intl
- next-international
- next-i18n-router
- paraglide-next
- lingui
- tolgee
- next-intlayer
- gt-next
相关 API:root-params 模块提供根级路由参数的访问能力。
原文:Internationalization。作者/维护者:Vercel 与 Next.js 文档贡献者。本文为原文的中文译文;代码保留原文内容。











暂无评论内容