Next.js 国际化:路由、本地化与静态渲染

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>
  )
}

相关资源

相关 API:root-params 模块提供根级路由参数的访问能力。


原文:Internationalization。作者/维护者:Vercel 与 Next.js 文档贡献者。本文为原文的中文译文;代码保留原文内容。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容