Next.js 项目中 i18n 多语言实现详解

前端184 次阅读24 分钟

在现代 Web 开发中,国际化(i18n)已成为构建全球化应用不可或缺的一部分。

Next.js 相关架构下,一般可以基于 next-intl 的国际化实现方式,本文将从代码结构、配置方式到实际使用场景,全面介绍 i18n的使用,包括语言配置、翻译消息加载、路由支持、组件使用等核心知识点。

一、i18n 实现基础

1.1 使用 next-intl 库实现国际化

next-intl 库来实现国际化功能。该库为 Next.js 提供了完整的国际化解决方案,包括翻译、日期格式化、数字格式化等功能。

1.2 安装与依赖管理

项目使用 pnpm 作为包管理器,安装 next-intl 及其相关依赖非常简单:

pnpm add next-intl

1.3 语言配置文件

语言配置主要定义在 /src/config/website.tsx 文件中,包含默认语言和所有支持的语言列表:

// /src/config/website.tsx
export const websiteConfig = {
  i18n: {
    defaultLocale: 'en',
    locales: {
      en: { flag: '🇺🇸', name: 'English' },
      zh: { flag: '🇨🇳', name: '中文' }
    }
  }
};

此配置不仅用于控制应用的默认语言,还被多个组件引用,如语言切换器、页面标题生成等。

二、翻译消息文件

2.1 翻译消息文件结构

翻译消息存储在 /messages/<locale>.json 文件中,按模块划分,例如 "Common"、"PricingPage" 等:

{
  "Common": {
    "login": "Log in",
    "logout": "Log out",
    "language": "Switch language"
  },
  "PricingPage": {
    "title": "Pricing",
    "description": "Choose the plan that works best for you"
  }
}

2.2 在客户端组件中使用翻译

在客户端组件中,使用 useTranslations 钩子获取翻译内容:

import { useTranslations } from 'next-intl';

export default function DashboardPage() {
  const t = useTranslations();
  return (
    <h1>{t('Dashboard.dashboard.title')}</h1>
  );
}

2.3 在服务端组件中使用翻译

在服务器端组件中,使用 getTranslations 获取翻译内容:

export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
  const { locale } = await params;
  const pt = await getTranslations({ locale, namespace: 'AuthPage.login' });
  return constructMetadata({
    title: pt('title'),
  });
}

三、路由国际化配置

3.1 动态路由 **\[locale\]** 支持

Next.js 的 App Router 支持通过动态路由 \[locale\] 来处理不同语言版本的页面请求。路由配置文件位于 /src/i18n/routing.ts,定义了支持的语言列表和默认语言:

// /src/i18n/routing.ts
export const routing = defineRouting({
  locales: LOCALES,
  defaultLocale: DEFAULT_LOCALE,
  localeDetection: false,
  localeCookie: {
    name: LOCALE_COOKIE_NAME,
  },
  localePrefix: 'as-needed',
});

设置 localeDetection: false 禁用浏览器自动检测语言的功能。用户语言偏好通过 cookie (NEXT\_LOCALE) 记忆。

四、语言切换组件与导航链接

4.1 语言切换组件

可以实现多个语言切换组件,如 LocaleSwitcherLocaleSelector,允许用户手动切换语言:

// /src/components/layout/locale-switcher.tsx
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/router';

export default function LocaleSwitcher() {
  const router = useRouter();
  const { locales } = router;
  const t = useTranslations('Common');

  return (
    <select
      value={router.locale}
      onChange={(e) => router.push(router.asPath, router.asPath, { locale: e.target.value })}
    >
      {locales?.map((locale) => (
        <option key={locale} value={locale}>
          {t(`language.${locale}`)}
        </option>
      ))}
    </select>
  );
}

4.2 带翻译的导航链接

使用自定义的 LocaleLink 组件生成带语言前缀的链接:

import { LocaleLink } from '@/i18n/navigation';

<LocaleLink href="/dashboard">Dashboard</LocaleLink>

五、TypeScript 类型定义与类型安全

为了确保类型安全并提供更好的开发体验,项目中使用全局类型定义文件 global.d.ts 扩展 AppConfig 接口,提供 LocaleMessages 类型定义:

// global.d.ts
import type { routing } from '@/i18n/routing';
import type messages from './messages/en.json';

declare module 'next-intl' {
  interface AppConfig {
    Locale: (typeof routing.locales)[number];
    Messages: typeof messages;
  }
}

该类型定义文件通常位于项目根目录,并需在 tsconfig.json 中显式包含以确保被识别。

此外,还可以通过 fumadocs-ui 提供的 i18n 工具自动生成 Translations 类型定义,确保类型安全和自动补全支持。

六、新增语言的正确做法

当你需要扩展新的语言或新增翻译字段时,应遵循以下步骤:

  1. 添加新语言 JSON 文件/messages/ 目录,如 es.json
  2. **website.tsx** 中添加该语言配置,提供国旗、名称等元信息。
  3. **routing.ts** 中添加语言支持,同步更新语言列表。
  4. 页面中使用 useTranslations('Namespace.key') 即可生效。

这样可以保证国际化系统的一致性和可维护性。

七、国际化布局与上下文注入机制

7.1 LocaleLayout 组件的作用

在 Next.js 的 App Router 中,\[locale\] 路由下的 LocaleLayout 是整个国际化的根布局组件。它的主要职责包括:

  • 加载当前语言的翻译消息文件(如 /messages/en.json/messages/zh.json
  • 注入国际化上下文,通过 NextIntlClientProvider
  • 设置 HTML 标签的语言属性,确保 SEO 和无障碍支持
  • 包裹页面内容,使所有子组件都能基于正确的语言环境渲染

示例代码片段:

export default async function LocaleLayout({
  children,
  params: Promise<{ locale: string }>
}) {
  // 加载对应语言的消息文件
  const messages = (await import(`@/messages/${locale}.json`)).default;

  return (
    <html suppressHydrationWarning lang={locale}>
      <body {...props}>
        <NextIntlClientProvider messages={messages} locale={locale}>
          <Providers>{children}</Providers>
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

⚠️ 注意:虽然目前项目中未显式导入消息文件,但 NextIntlClientProvider 默认会从 /messages/.json 自动加载翻译消息。

7.2 NextIntlClientProvider 的作用

NextIntlClientProvidernext-intl 提供的核心组件之一,它通过 React Context 将翻译数据传递给所有客户端组件。

主要功能:

功能 描述
翻译注入 messages 注入 React 上下文,使 useTranslations 可用
客户端支持 所有使用 useTranslations 的组件均可访问翻译键值
本地化格式化 支持日期、时间、数字等本地化格式输出

示例用法:

import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage.hero');
  return <h1>{t('title')}</h1>;
}

📌 提示:如果你希望手动控制消息加载方式,可以显式传入 messages 属性给 NextIntlClientProvider。

7.3 整体流程图解

[LocaleLayout]
   ↓
加载 messages/locale.json
   ↓
包裹 <NextIntlClientProvider> 注入翻译数据
   ↓
客户端组件通过 useTranslations 获取翻译内容
   ↓
渲染多语言 UI

4. 最佳实践建议

  • ✅ 新增语言时只需添加 /messages/<new-locale>.json 文件即可生效
  • ✅ 在 website.tsx 中注册语言元信息
  • ✅ 在 routing.ts 中同步更新语言列表
  • ❗ 不建议修改 LocaleLayout 的结构,除非你有特殊需求

通过以上机制,可以实现了简洁、可维护且高效的国际化系统,适合大型 SaaS 应用的多语言需求。

八、翻译消息加载机制详解

8.1 为什么没有显式导入翻译消息?

/src/app/\[locale\]/layout.tsx 中,你不会看到类似 **import messages from '@/messages/zh.json'** 的代码片段,这是因为 next-intl 提供了自动加载翻译消息的能力。

✅ 自动加载机制

NextIntlClientProvider 默认会从 /messages/<locale>.json 路径加载对应语言的消息文件。只要项目结构符合预期(如 /messages/en.json/messages/zh.json 存在),就可以无需手动导入。

示例代码如下:

<NextIntlClientProvider>
  <Providers>{children}</Providers>
</NextIntlClientProvider>

📌 注意:这里并没有传入 messages 属性,表示使用默认的自动加载方式。

⚠️ 手动加载的适用场景

如果你需要自定义路径、延迟加载或调试,可以显式导入并传入 messages 属性:

const messages = (await import(`@/messages/${locale}.json`)).default;

return (
  <html suppressHydrationWarning lang={locale}>
    <body {...props}>
      <NextIntlClientProvider messages={messages} locale={locale}>
        <Providers>{children}</Providers>
      </NextIntlClientProvider>
    </body>
  </html>
);

但目前项目中不推荐这样做,因为结构和配置已完全符合自动加载的要求。

8.2 翻译消息文件路径约定

  • 消息文件必须存放在 /messages/<locale>.json
  • 示例:
    • /messages/en.json
    • /messages/zh.json

💡 建议:新增语言时只需添加对应的 JSON 文件即可生效,无需修改 layout.tsx。

8.3 加载方式对比

方式 是否需要手动导入 使用场景
✅ 自动加载 标准项目结构、消息文件在 /messages/<locale>.json
🔁 手动加载 自定义消息路径、动态加载、服务器渲染等高级用法

8.4 最佳实践建议

  • ✅ 新增语言时只需添加 /messages/<new-locale>.json 文件即可生效
  • ✅ 在 website.tsx 中注册语言元信息
  • ✅ 在 routing.ts 中同步更新语言列表
  • ❗ 不建议修改 LocaleLayout 的结构,除非你有特殊需求

通过以上机制,MkSaaS 模板实现了简洁、可维护且高效的国际化系统,适合大型 SaaS 应用的多语言需求。

九、语言配置的职责划分:website.tsx 与 routing.ts 的作用解析

在 MkSaaS 模板中,国际化(i18n)的语言配置分别定义在两个文件中:

  • website.tsx:用于提供语言元信息和 UI 显示;
  • routing.ts:用于控制路由系统中的多语言行为。

9.1 website.tsx:UI 层语言配置

路径: /src/config/website.tsx

示例代码:

i18n: {
  defaultLocale: 'en',
  locales: {
    en: { flag: '🇺🇸', name: 'English' },
    zh: { flag: '🇨🇳', name: '中文' }
  }
}

作用:

  • 定义网站支持的语言列表及默认语言;
  • 提供语言元信息(如国旗、名称),用于 UI 显示;
  • 被多个组件引用,例如语言切换器、页面标题生成等。

📌 这是面向前端 UI 的语言配置,主要用于控制语言选择器、显示标签等。

9.2 routing.ts:框架层语言路由配置

路径: /src/i18n/routing.ts

示例代码:

export const routing = defineRouting({
  locales: LOCALES,
  defaultLocale: DEFAULT_LOCALE,
  localeDetection: false,
  localeCookie: { name: LOCALE_COOKIE_NAME },
  localePrefix: 'as-needed'
});

作用:

  • 配置 App Router 中的国际化路由规则;
  • 控制 URL 是否添加语言前缀(如 /en/about vs /about);
  • 设置语言检测方式(浏览器自动检测 or cookie);
  • 定义多语言 Cookie 名称(如 NEXT\_LOCALE)。

📌 这是面向 Next.js 路由系统的语言配置,主要用于控制语言感知的路由行为和请求处理。

9.3 为什么不统一到一个地方?

虽然从表面上看将语言配置合并到一个文件中似乎更简洁,但实际原因如下:

维度 website.tsx routing.ts
用途 提供语言元信息(UI 层) 控制路由行为(框架层)
使用者 前端组件、翻译钩子 Next.js App Router、中间件
功能 显示语言选项、翻译内容 多语言 URL、Cookie 存储、路由匹配
是否可合并? ❌ 不建议 ❌ 不建议

✅ 正确做法:同步更新两处配置

当你新增一种语言(如西班牙语 es)时,应执行以下步骤:

1、 在 website.tsx 添加语言配置

i18n: {
  defaultLocale: 'en',
  locales: {
    en: { flag: '🇺🇸', name: 'English' },
    zh: { flag: '🇨🇳', name: '中文' },
    es: { flag: '🇪🇸', name: 'Español' } // 新增语言
  }
}

2、 在 routing.ts 更新语言列表

ts // 自动从 websiteConfig 获取语言列表 export const LOCALES = Object.keys(websiteConfig.i18n.locales); // 包含 ['en', 'zh', 'es']

3、 创建对应语言的消息文件

/messages/es.json

✅ 总结

问题 回答
为什么要分开配置? 因为 website.tsx 提供 UI 元信息,routing.ts 控制路由行为,二者职责不同。
能否统一? 不建议,违反单一职责原则且可能导致类型冲突。
新增语言应怎么做? 在 website.tsx 添加语言元信息,在 routing.ts 同步更新,并创建对应 JSON 消息文件。

这样设计确保了国际化系统的灵活性和可维护性,是符合最佳实践的结构.