首页 / Next.js 16 入门教程 / 国际化 i18n

Next.js 16 入门教程

国际化 i18n

本教程共 42 篇 · 第 31 篇 · 更新于 2026-07-30 · 约 7 分钟阅读

Next.jsNext.js 16 入门教程国际化i18n多语言本地化

31. 国际化 i18n

本节目标:学会在 Next.js 中实现多语言支持,包括路由设计、翻译管理和语言自动检测。

基本概念

国际化(Internationalization,简称 i18n)包含两个方面:

  • 路由国际化:不同语言用不同 URL 路径(如 /en/about/zh/about
  • 内容本地化:根据用户语言显示对应翻译

Locale 是标识一组语言偏好的字符串,比如:

  • en-US:美式英语
  • zh-CN:简体中文
  • nl:荷兰语(不指定地区)

路由设计

Next.js 推荐用动态路由段 [lang] 来处理多语言:

app/
├── [lang]/
│   ├── layout.tsx
│   ├── page.tsx
│   └── about/
│       └── page.tsx

所有 app/ 下的特殊文件都要放在 [lang] 目录内,这样 Next.js 路由器就能动态处理不同语言。

// app/[lang]/page.tsx
export default async function Page({ params }) {
  const { lang } = await params
  // lang 就是当前语言,比如 "en" 或 "zh"
  return <h1>当前语言: {lang}</h1>
}

语言检测

推荐根据浏览器的 Accept-Language 头来判断用户偏好。可以用 negotiator@formatjs/intl-localematcher 这两个库:

// lib/locale.js
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

export function getLocale(request) {
  const headers = { 'accept-language': request.headers.get('accept-language') || '' }
  const languages = new Negotiator({ headers }).languages()
  const locales = ['en-US', 'zh-CN', 'ja']
  const defaultLocale = 'en-US'

  return match(languages, locales, defaultLocale)
}

然后在 proxy.ts 中做重定向:

// proxy.ts
import { NextResponse } from 'next/server'

const locales = ['en-US', 'zh-CN', 'ja']

export function proxy(request) {
  const { pathname } = request.nextUrl

  // 检查路径是否已包含语言前缀
  const hasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (hasLocale) return

  // 没有语言前缀,检测并重定向
  const locale = getLocale(request)
  request.nextUrl.pathname = `/${locale}${pathname}`
  return NextResponse.redirect(request.nextUrl)
}

翻译管理

字典文件

为每种语言创建独立的 JSON 文件:

// dictionaries/en.json
{
  "products": {
    "cart": "Add to Cart",
    "title": "Our Products"
  }
}
// dictionaries/zh.json
{
  "products": {
    "cart": "加入购物车",
    "title": "我们的产品"
  }
}

加载字典

// app/[lang]/dictionaries.ts
import 'server-only'

const dictionaries = {
  en: () => import('./dictionaries/en.json').then(m => m.default),
  zh: () => import('./dictionaries/zh.json').then(m => m.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]()

注意:这里用了 import 'server-only' 确保代码只在服务端运行,翻译文件不会打包到客户端。

在页面中使用

// app/[lang]/page.tsx
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>
}

hasLocale 函数有两个作用:一是类型收窄,二是遇到不支持的语言时返回 404。

静态生成

如果语言组合有限,可以用 generateStaticParams 预生成所有语言版本:

// app/[lang]/layout.tsx
export function generateStaticParams() {
  return [
    { lang: 'en-US' },
    { lang: 'zh-CN' },
    { lang: 'ja' },
  ]
}

export default async function RootLayout({ children, params }) {
  const { lang } = await params
  return (
    <html lang={lang}>
      <body>{children}</body>
    </html>
  )
}

这样构建时会为每种语言生成静态页面,访问速度更快。

语言切换

语言切换本质上就是路由跳转。做一个简单的语言选择器:

'use client'

import { useRouter, usePathname } from 'next/navigation'

export function LanguageSwitcher() {
  const router = useRouter()
  const pathname = usePathname()

  const switchLocale = (locale: string) => {
    // 替换路径中的语言前缀
    const newPath = pathname.replace(/^\/[a-z]{2}(-[A-Z]{2})?/, `/${locale}`)
    router.push(newPath)
  }

  return (
    <select onChange={e => switchLocale(e.target.value)} defaultValue="en-US">
      <option value="en-US">English</option>
      <option value="zh-CN">中文</option>
      <option value="ja">日本語</option>
    </select>
  )
}

第三方库推荐

如果项目比较复杂,可以考虑这些专门做国际化的库:

  • next-intl:功能最全,支持复数、日期格式化、数字格式化
  • next-international:轻量级,API 简洁
  • paraglide-next:基于消息 ID 的类型安全方案
  • lingui:成熟的 React 国际化方案

注意事项

  1. HTML lang 属性<html lang={lang} 对 SEO 和屏幕阅读器都很重要
  2. 文本方向:阿拉伯语、希伯来语是从右到左(RTL),需要额外处理
  3. 日期和数字:不同地区的格式不同,用 Intl.DateTimeFormatIntl.NumberFormat
  4. 翻译键命名:用嵌套结构(如 products.cart)比扁平结构更易维护

国际化不是一次性工作。随着产品迭代,翻译内容会不断增加,提前设计好结构能省很多事。