Next.js 16 + next-intl App Router 国际化实现指南

Next.js 16 + next-intl App Router 国际化实现指南

引言

✨ 随着全球化的发展,为网站添加国际化支持已经成为现代前端开发的标配。本文将基于真实项目代码,详细介绍如何在 Next.js 16 项目中使用 next-intl 实现基于 App Router 的国际化功能。

技术栈

  • Next.js: 16.0.5 (App Router)
  • next-intl: 4.5.7 (国际化核心库)
  • TypeScript: 类型安全保障

一、项目国际化架构概述

App Router 路由方案

本项目采用 Next.js 13 及以上版本引入的 App Router 架构,通过动态路由 [locale] 实现国际化。这种方案的优势在于:

  • 支持 SEO 友好的语言前缀路由(如 /zh/page/en/page
  • 提供服务端组件和客户端组件的灵活组合
  • 内置数据流和布局系统,简化国际化实现

项目目录结构

项目采用清晰的模块化结构,将国际化相关代码集中在 src/i18n 目录下,便于维护和扩展:

复制代码
src/
├── app/
│   ├── [locale]/           # 动态语言路由
│   │   ├── layout.tsx      # 语言特定布局
│   │   └── page.tsx        # 语言特定页面
│   ├── layout.tsx          # 根布局
│   └── page.tsx            # 根页面(重定向用)
├── components/
│   └── LocaleSwitcher.tsx  # 语言切换组件
└── i18n/                   # 国际化核心目录
    ├── config.ts           # 语言配置
    ├── navigation.ts       # 导航配置
    ├── request.ts          # 翻译加载配置
    ├── routing.ts          # 路由配置
    └── messages/           # 翻译文件目录
        ├── en/             # 英文翻译
        │   ├── index_page.json
        │   └── locale_switcher.json
        └── zh/             # 中文翻译
            ├── index_page.json
            └── locale_switcher.json

二、核心配置文件

1. 语言配置 (config.ts)

定义支持的语言和默认语言:

typescript 复制代码
// src/i18n/config.ts
export type Locale = (typeof locales)[number];
export const locales = ['zh', 'en'] as const; // 支持的语言:中文、英文
export const defaultLocale: Locale = 'zh'; // 默认语言:中文

2. 路由配置 (routing.ts)

配置国际化路由规则:

typescript 复制代码
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
import { defaultLocale, locales } from './config';

export const routing = defineRouting({
  locales,
  defaultLocale
});

3. 导航配置 (navigation.ts)

创建支持国际化的导航工具:

typescript 复制代码
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';

// 创建国际化导航工具集
export const { Link, getPathname, redirect, usePathname, useRouter } = 
  createNavigation(routing);

4. 翻译加载配置 (request.ts)

实现动态加载翻译文件的核心逻辑:

typescript 复制代码
// src/i18n/request.ts
import { hasLocale } from 'next-intl';
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

/**
 * 翻译模块配置接口
 */
export interface TranslationModuleConfig {
  namespace: string; // 翻译键的命名空间
  fileName: string; // 文件名
}

// 定义所有翻译模块
const TRANSLATION_MODULES: readonly TranslationModuleConfig[] = [
  { namespace: 'index_page', fileName: 'index_page' },
  { namespace: 'locale_switcher', fileName: 'locale_switcher' },
];

// 动态加载单个翻译模块
const importTranslationModule = async (
  locale: string,
  moduleConfig: TranslationModuleConfig
) => {
  try {
    const modulePath = `./messages/${locale}/${moduleConfig.fileName}.json`;
    const importedModule = await import(modulePath);
    
    return {
      namespace: moduleConfig.namespace,
      content: importedModule.default,
      success: true,
    };
  } catch (error) {
    console.error(
      `[i18n] Failed to load module "${moduleConfig.fileName}" for locale "${locale}":`,
      error
    );
    
    return {
      namespace: moduleConfig.namespace,
      content: {},
      success: false,
      error,
    };
  }
};

// 加载指定语言的所有翻译消息
const loadMessages = async (locale: string) => {
  // 并行导入所有翻译模块以提高性能
  const moduleLoadPromises = TRANSLATION_MODULES.map(moduleConfig =>
    importTranslationModule(locale, moduleConfig)
  );
  
  const loadResults = await Promise.all(moduleLoadPromises);
  
  // 合并所有成功加载的模块内容
  return loadResults.reduce((acc, result) => {
    if (result.success) {
      acc[result.namespace] = result.content;
    }
    return acc;
  }, {});
};

// 验证语言代码是否有效
const validateLocale = (locale: string | undefined): string => {
  if (typeof locale !== 'string') {
    console.warn(`[i18n] Invalid locale type: ${typeof locale}, falling back to default`);
    return routing.defaultLocale;
  }
  
  if (!hasLocale(routing.locales, locale)) {
    console.warn(
      `[i18n] Locale "${locale}" not supported, falling back to default (${routing.defaultLocale})`
    );
    return routing.defaultLocale;
  }
  
  return locale;
};

// 导出请求配置
export default getRequestConfig(async ({ requestLocale }) => {
  const requestedLocale = await requestLocale;
  const validatedLocale = validateLocale(requestedLocale);
  
  const messages = await loadMessages(validatedLocale);
  
  return {
    locale: validatedLocale,
    messages,
  };
});

三、Next.js 配置

1. 修改 next.config.ts

typescript 复制代码
// next.config.ts
import { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const nextConfig: NextConfig = {
  // 其他配置
};

const withNextIntl = createNextIntlPlugin();

export default withNextIntl(nextConfig);

四、翻译文件结构

项目采用模块化的翻译文件结构,每个功能模块对应独立的翻译文件:

json 复制代码
// src/i18n/messages/zh/index_page.json
{
  "description": "这是一个基本示例,演示了如何使用 <code>next-intl</code> 与 Next.js App Router 配合使用。尝试在右上角切换语言,看看内容如何变化。",
  "title": "next-intl 示例"
}
json 复制代码
// src/i18n/messages/en/index_page.json
{
  "description": "This is a basic example that demonstrates the usage of <code>next-intl</code> with the Next.js App Router. Try changing the locale in the top right corner and see how the content changes.",
  "title": "next-intl example"
}
json 复制代码
// src/i18n/messages/zh/locale_switcher.json
{
  "label": "切换语言",
  "locale": {
    "zh": "中文",
    "en": "English"
  }
}
json 复制代码
// src/i18n/messages/en/locale_switcher.json
{
  "label": "Switch language",
  "locale": {
    "zh": "Chinese",
    "en": "English"
  }
}

五、App Router 国际化实现

1. 动态语言路由布局 ([locale]/layout.tsx)

typescript 复制代码
// src/app/[locale]/layout.tsx
import { Inter } from "next/font/google"
import { hasLocale, NextIntlClientProvider } from "next-intl"
import { routing } from "@/i18n/routing";
import { notFound } from "next/navigation";
import { setRequestLocale } from "next-intl/server";
import { Locale } from "@/i18n/config";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
})

// 静态生成所有语言的路由
export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

type Props = {
  children: React.ReactNode;
  params: Promise<{
    locale: Locale;
  }>;
};

export default async function RootLayout({ children, params }: Props) {
  const { locale } = await params;
  
  // 验证语言是否支持,不支持则返回404
  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }
  
  // 设置请求的语言
  setRequestLocale(locale);
  
  return (
    <html className={inter.className} suppressHydrationWarning lang={locale}>
      <head />
      <body>
        {/* 提供客户端翻译上下文 */}
        <NextIntlClientProvider>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  )
}

2. 语言特定页面 ([locale]/page.tsx)

typescript 复制代码
// src/app/[locale]/page.tsx
import LocaleSwitcher from "@/components/LocaleSwitcher";
import { useTranslations } from "next-intl";
import { setRequestLocale } from "next-intl/server";
import { use } from "react";
import { Locale } from "@/i18n/config";

type PageProps = {
  params: Promise<{
    locale: Locale;
  }>;
};

export default function IndexPage({params}: PageProps) {
  // 使用React 18的use()钩子处理异步params
  const { locale } = use(params);
  
  // 设置请求语言
  setRequestLocale(locale);
  
  // 获取翻译函数
  const t = useTranslations();
  
  return (
    <>
      {/* 使用翻译键获取翻译内容 */}
      <h1>{t('index_page.title')}</h1>
      
      {/* 语言切换组件 */}
      <LocaleSwitcher/>
    </>
  )
}

3. 根页面重定向 (page.tsx)

typescript 复制代码
// src/app/page.tsx
import { defaultLocale } from '@/i18n/config';
import { redirect } from '@/i18n/navigation';

export default function RootPage() {
  // 重定向到默认语言的首页
  redirect(`/${defaultLocale}`);
}

六、语言切换组件

LocaleSwitcher.tsx

typescript 复制代码
// src/components/LocaleSwitcher.tsx
'use client';

import { useTransition } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter, usePathname } from '@/i18n/navigation';
import { Locale } from '@/i18n/config';
import { routing } from '@/i18n/routing';

export default function LocaleSwitcher() {
  const locale = useLocale() as Locale;
  const t = useTranslations('locale_switcher');
  const [isPending, startTransition] = useTransition();
  const router = useRouter();
  const pathname = usePathname();
  const items = routing.locales;

  function onChange(value: Locale) {
    // 使用React的并发特性处理路由切换
    startTransition(() => {
      router.replace(
        { pathname },
        { locale: value }
      );
    });
  }

  return (
    <div style={{ position: 'absolute', top: '20px', right: '20px' }}>
      <select 
        value={locale}
        onChange={(e) => onChange(e.target.value as Locale)}
        disabled={isPending}
      >
        {items.map((cur) => (
          <option key={cur} value={cur}>
            {t('locale', { locale: cur })}
          </option>
        ))}
      </select>
    </div>
  );
}

七、实现原理与流程

1. 路由处理流程

  1. 用户访问 / → 重定向到默认语言 /zh
  2. 用户访问 /en → 加载英文版本
  3. generateStaticParams 预生成所有语言的路由页面

2. 翻译加载流程

  1. 服务端接收请求,解析语言参数
  2. 通过 request.ts 动态加载对应语言的翻译模块
  3. 通过 NextIntlClientProvider 将翻译消息传递给客户端
  4. 客户端组件通过 useTranslations() 获取翻译内容

3. 语言切换流程

  1. 用户点击语言切换按钮
  2. 触发 startTransition 开始路由转换
  3. 使用 useRouter().replace() 切换到对应语言的相同路径
  4. 服务端重新加载对应语言的翻译内容
  5. 客户端更新界面显示新语言内容

八、技术亮点与最佳实践

1. 类型安全

  • 使用 TypeScript 确保语言代码和翻译键的类型安全
  • Locale 类型基于实际支持的语言数组自动生成

2. 性能优化

  • 翻译模块并行加载,提高加载效率
  • 使用 generateStaticParams 预生成静态页面
  • Next.js 的自动静态优化机制

3. 可维护性

  • 模块化的翻译文件结构,便于管理
  • 集中的语言和路由配置,便于扩展
  • 清晰的代码组织,遵循 Next.js 最佳实践

4. 用户体验

  • SEO 友好的语言前缀路由
  • 平滑的语言切换过渡
  • 支持浏览器语言检测(由 next-intl 自动处理)

九、扩展建议

1. 添加更多语言支持

  1. config.ts 中添加新语言代码
  2. 创建对应的翻译文件目录和内容
  3. 在语言切换组件中自动支持新语言

2. 优化翻译加载

  • 考虑使用 CDN 托管翻译文件
  • 实现翻译内容的缓存机制
  • 支持按需加载大型翻译模块

3. 增强翻译功能

  • 添加日期、数字的本地化格式化
  • 支持复数形式和性别中立表达
  • 实现动态翻译内容的更新机制

总结

🎉 本项目成功实现了基于 Next.js App Router 的国际化方案,通过 next-intl 库提供了优雅、高效、类型安全的多语言支持。

核心实现包括:

  • 基于动态路由 [locale] 的国际化路由
  • 模块化的翻译文件管理
  • 服务端与客户端的翻译上下文传递
  • 平滑的语言切换体验

这种方案充分利用了 Next.js App Router 的特性,为现代 Web 应用提供了高质量的国际化解决方案。希望这篇指南能帮助你理解和实现自己的 Next.js 国际化项目! 🫶

相关推荐
有意义1 小时前
this 不是你想的 this:从作用域迷失到调用栈掌控
javascript·面试·ecmascript 6
风止何安啊1 小时前
别被 JS 骗了!终极指南:JS 类型转换真相大揭秘
前端·javascript·面试
拉不动的猪1 小时前
深入理解 Vue keep-alive:缓存本质、触发条件与生命周期对比
前端·javascript·vue.js
over6972 小时前
深入理解 JavaScript 原型链与继承机制:从 instanceof 到多种继承模式
前端·javascript·面试
烂不烂问厨房2 小时前
前端实现docx与pdf预览
前端·javascript·pdf
GDAL2 小时前
Vue3 Computed 深入讲解(聚焦 Vue3 特性)
前端·javascript·vue.js
Moment2 小时前
半年时间使用 Tiptap 开发一个和飞书差不多效果的协同文档 😍😍😍
前端·javascript·后端
前端加油站2 小时前
记一个前端导出excel受限问题
前端·javascript
坐吃山猪2 小时前
Electron02-Hello
开发语言·javascript·ecmascript