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 小时前
未来之窗昭和仙君(六十五)Vue与跨地区多部门开发—东方仙盟练气
前端·javascript·vue.js·仙盟创梦ide·东方仙盟·昭和仙君
baidu_247438611 小时前
Android ViewModel定时任务
android·开发语言·javascript
VT.馒头1 小时前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript
有位神秘人2 小时前
Android中Notification的使用详解
android·java·javascript
phltxy2 小时前
Vue 核心特性实战指南:指令、样式绑定、计算属性与侦听器
前端·javascript·vue.js
Byron07073 小时前
Vue 中使用 Tiptap 富文本编辑器的完整指南
前端·javascript·vue.js
Mr Xu_4 小时前
告别硬编码:前端项目中配置驱动的实战优化指南
前端·javascript·数据结构
Byron07075 小时前
从 0 到 1 搭建 Vue 前端工程化体系:提效、提质、降本实战落地
前端·javascript·vue.js
guangzan5 小时前
为博客园注入现代 UI 体验:shadcn 皮肤上线
typescript·tailwindcss·shadcn ui·tona
德育处主任Pro5 小时前
纯前端网格路径规划:PathFinding.js的使用方法
开发语言·前端·javascript