Next.js 16 Page Router 国际化 🌐

Next.js 16 Page Router 国际化 🌐

引言

在现代 Web 应用开发中,国际化(i18n)已经成为一个必备功能。传统的 Next.js 国际化方案通常采用 URL 前缀方式(如 /en/page/zh-CN/page),这种方式虽然实现简单,但存在一些明显的问题:

  1. URL 频繁变更:用户切换语言时,页面 URL 会发生变化
  2. SEO 分散:相同内容分散在不同 URL 下,影响搜索引擎优化
  3. 用户体验不佳:复制链接时需要考虑语言前缀

那么,有没有一种方式可以在不改变 URL 的情况下实现国际化呢?答案是肯定的!本文将详细介绍我在 Next.js 16 项目中实现的无 URL 变更的国际化方案,采用浏览器缓存 + Cookie 机制管理语言切换,保持 URL 稳定的同时提供流畅的国际化体验。

技术栈

技术 版本 用途
Next.js 16.0.7 前端框架
React 19.2.0 UI 库
TypeScript 5.5.4 类型系统
next-i18next 15.4.3 国际化核心库
react-i18next 16.3.5 React 国际化集成
js-cookie 3.0.5 Cookie 管理
ahooks 3.9.6 React Hooks 工具库

项目结构

python 复制代码
src/
├── components/            # 组件目录
│   └── I18nLngSelector.tsx  # 语言选择器组件
├── i18n/                 # 国际化配置目录
│   ├── hooks/            # 自定义 Hooks
│   │   └── useI18n.ts    # 语言切换钩子
│   ├── locales/          # 翻译资源文件
│   │   ├── en/           # 英文翻译
│   │   │   ├── common.json       # 通用翻译
│   │   │   └── index_page.json   # 首页翻译
│   │   └── zh-CN/        # 中文翻译
│   │       ├── common.json       # 通用翻译
│   │       └── index_page.json   # 首页翻译
│   ├── type.ts           # TypeScript 类型定义
│   └── i18next.d.ts      # 类型声明文件
└── pages/                # 页面目录
    ├── _app.tsx          # 应用入口(语言初始化)
    └── index.tsx         # 首页

核心实现

1. next-i18next 配置

首先,我们需要配置 next-i18next,创建 next-i18next.config.js 文件:

javascript 复制代码
// next-i18next.config.js
// @ts-check

/**
 * @type {import('next-i18next').UserConfig}
 */
module.exports = {
  // 开发环境下启用调试模式
  debug: process.env.NODE_ENV === 'development',
  // 国际化配置
  i18n: {
    // 默认语言
    defaultLocale: 'zh-CN',
    // 支持的语言列表
    locales: ['zh-CN', 'en'],
    // 禁用自动语言检测,使用自定义逻辑
    localeDetection: false,
  },
  // 语言资源文件路径
  localePath: './src/i18n/locales',
  // 开发环境下在预渲染时重新加载语言资源
  reloadOnPrerender: process.env.NODE_ENV === 'development',
}

配置说明

  • debug: true:开发环境下启用调试模式,便于开发调试
  • defaultLocale: 'zh-CN':设置默认语言为中文
  • locales: ['zh-CN', 'en']:配置支持的语言列表
  • localeDetection: false:禁用自动语言检测,使用自定义的语言检测和切换逻辑
  • localePath: './src/i18n/locales':指定语言资源文件的存放路径

2. 自定义语言切换钩子

核心逻辑在于自定义的 useI18nLng 钩子,它负责处理语言的存储、切换和初始化:

typescript 复制代码
// src/i18n/hooks/useI18n.ts
import { useTranslation } from 'next-i18next';
import { LangEnum } from '@/i18n/type';
import Cookies from "js-cookie";

// 语言存储的键名
const LANG_KEY = 'NEXT_LOCALE';

/**
 * 检查当前是否在 iframe 中
 */
const isInIframe = () => {
  try {
    return window.self !== window.top;
  } catch (e) {
    return true; // 发生异常时默认认为在 iframe 中
  }
};

/**
 * 设置语言到存储中
 */
const setLang = (value: string) => {
  if (isInIframe()) {
    // 在 iframe 中只使用 localStorage
    localStorage.setItem(LANG_KEY, value);
  } else {
    // 不在 iframe 中,同时使用 Cookie 和 localStorage
    Cookies.set(LANG_KEY, value, { expires: 30 }); // Cookie 有效期30天
    localStorage.setItem(LANG_KEY, value);
  }
};

/**
 * 从存储中获取语言
 */
const getLang = () => {
  return localStorage.getItem(LANG_KEY) || Cookies.get(LANG_KEY);
};

/**
 * 自定义 i18n 语言切换钩子
 */
export const useI18nLng = () => {
  // 获取 i18n 实例
  const { i18n } = useTranslation();
  
  // 语言映射表,确保语言代码的一致性
  const languageMap: Record<string, string> = {
    'zh-CN': LangEnum.zh_CN,
    en: LangEnum.en,
  };

  /**
   * 切换语言的方法
   */
  const onChangeLng = async (lng: string) => {
    // 确保语言代码的正确性
    const lang = languageMap[lng] || 'en';
    const prevLang = getLang();

    // 将语言保存到存储中
    setLang(lang);

    // 调用 i18n 实例切换语言
    await i18n?.changeLanguage?.(lang);

    // 如果没有资源包且语言发生了变化,则刷新页面
    if (!i18n?.hasResourceBundle?.(lang, 'common') && prevLang !== lang) {
      window?.location?.reload?.();
    }
  };

  /**
   * 设置用户默认语言
   */
  const setUserDefaultLng = (forceGetDefaultLng: boolean = false) => {
    // 确保在浏览器环境中运行
    if (!navigator || !localStorage) return;

    // 如果已经有存储的语言且不是强制获取,则使用存储的语言
    if (getLang() && !forceGetDefaultLng) return onChangeLng(getLang() as string);

    // 获取浏览器语言并映射到支持的语言
    const lang = languageMap[navigator.language] || 'en';

    // 切换到获取的语言
    return onChangeLng(lang);
  };

  // 返回钩子方法
  return {
    onChangeLng,  // 语言切换方法
    setUserDefaultLng // 设置默认语言方法
  };
};

核心亮点

  • 双重存储机制:同时使用 localStorage 和 Cookie 存储语言选择,确保在不同场景下都能正确获取
  • iframe 兼容性:检测是否在 iframe 中运行,针对性处理存储方式
  • 智能语言切换:切换语言时先检查是否有资源包,避免因资源缺失导致的错误
  • 浏览器语言检测:首次访问时根据浏览器语言自动设置默认语言

3. 应用入口初始化

_app.tsx 中实现默认语言的初始化,确保页面刷新后能保持用户的语言选择:

typescript 复制代码
// src/pages/_app.tsx
// 导入应用组件类型定义
import type { AppProps } from 'next/app'
// 导入 i18n 应用包装组件
import { appWithTranslation } from 'next-i18next'
// 导入自定义的 i18n 语言钩子
import { useI18nLng } from '@/i18n/hooks/useI18n'
// 导入 React 副作用钩子
import { useEffect } from 'react'

/**
 * 主应用组件,所有页面的容器组件
 * @param Component 当前渲染的页面组件
 * @param pageProps 页面属性和初始数据
 */
const MyApp = ({ Component, pageProps }: AppProps) => {
  // 获取设置默认语言的方法
  const { setUserDefaultLng } = useI18nLng()

  // 组件挂载时设置默认语言
  useEffect(() => {
    setUserDefaultLng()
  }, [])

  // 渲染当前页面组件
  return <Component {...pageProps} />
}

// 使用 i18n 包装应用组件,提供国际化功能
export default appWithTranslation(MyApp)

初始化流程

  1. 应用启动时,组件挂载
  2. 调用 setUserDefaultLng() 方法
  3. 检查是否有存储的语言设置
  4. 如果有,使用存储的语言;如果没有,根据浏览器语言设置默认语言
  5. 确保用户每次访问时都能看到自己选择的语言

4. 语言选择器组件

创建一个语言选择器组件,让用户可以方便地切换语言:

typescript 复制代码
// src/components/I18nLngSelector.tsx
// 导入自定义的 i18n 语言钩子
import { useI18nLng } from '@/i18n/hooks/useI18n';
// 导入 i18n 翻译钩子
import { useTranslation } from 'next-i18next';
// 导入 React 记忆化钩子
import { useMemo } from 'react';
// 导入语言映射表
import { langMap } from '@/i18n/type';

/**
 * 语言选择器组件
 * 提供UI界面让用户切换应用语言
 */
const I18nLngSelector = () => {
  // 获取 i18n 实例
  const { i18n } = useTranslation();
  // 获取语言切换方法
  const { onChangeLng } = useI18nLng();

  // 记忆化处理语言列表,避免重复计算
  const list = useMemo(() => {
    return Object.entries(langMap).map(([key, lang]) => ({
      label: lang.label, // 显示标签
      value: key         // 语言代码值
    }));
  }, []);

  return (
    // 语言选择下拉框
    <select 
      value={i18n.language} // 当前选中的语言
      onChange={(e) => onChangeLng(e.target.value)} // 语言变更处理
    >
      {/* 渲染语言选项列表 */}
      {list.map((item) => (
        <option key={item.value} value={item.value}>
          {item.label}
        </option>
      ))}
    </select>
  );
};

// 导出语言选择器组件
export default I18nLngSelector;

组件特点

  • 使用原生 select 元素,简洁高效
  • 绑定当前语言状态,确保 UI 与实际语言一致
  • 调用自定义的 onChangeLng 方法处理语言切换
  • 支持多语言显示语言选项

5. 类型定义

为了提供更好的类型安全,我们需要定义相关的 TypeScript 类型:

typescript 复制代码
// src/i18n/type.ts
// 导入语言资源文件
import { resources } from "./resources";

/**
 * 国际化字符串类型定义
 * 要求必须提供中文,英文为可选
 */
export type I18nStringType = {
  'zh-CN': string; // 中文简体
  en?: string;     // 英文(可选)
};

/**
 * 语言枚举类型
 * 定义支持的语言代码
 */
export enum LangEnum {
  'zh_CN' = 'zh-CN', // 中文简体
  'en' = 'en'        // 英文
}

/**
 * 语言类型,基于LangEnum的字符串类型
 */
export type localeType = `${LangEnum}`;

/**
 * 支持的语言列表常量
 */
export const LocaleList = ['en', 'zh-CN'] as const;

/**
 * 语言映射表,用于UI显示
 */
export const langMap = {
  [LangEnum.en]: {
    label: 'English(US)', // 英文显示名称
  },
  [LangEnum.zh_CN]: {
    label: '简体中文',     // 中文显示名称
  }
};

/**
 * 国际化命名空间类型,基于resources的类型
 */
export type I18nNamespaces = typeof resources;

/**
 * 国际化命名空间数组类型
 */
export type I18nNsType = (keyof I18nNamespaces)[];

类型安全优势

  • 避免拼写错误:使用枚举和类型定义确保语言代码的正确性
  • 智能提示:在使用翻译键时提供自动补全
  • 类型检查:在编译时就能发现翻译资源的错误使用

翻译资源文件示例

中文翻译

json 复制代码
// src/i18n/locales/zh-CN/common.json
{
  "change-locale": "切换到 \"{{changeTo}}\" 语言",
  "welcome": "欢迎使用 Next.js 国际化方案"
}

英文翻译

json 复制代码
// src/i18n/locales/en/common.json
{
  "change-locale": "Change locale to \"{{changeTo}}\"",
  "welcome": "Welcome to Next.js i18n Solution"
}

首页翻译资源

json 复制代码
// src/i18n/locales/zh-CN/index_page.json
{
  "title": "next-i18next 示例"
}
json 复制代码
// src/i18n/locales/en/index_page.json
{
  "title": "next-i18next example"
}

翻译资源管理

为了更好地管理翻译资源,我们可以创建一个 resources.ts 文件来集中导入和导出所有翻译资源:

typescript 复制代码
// src/i18n/resources.ts
// 导入英文的通用语言资源
import common from './locales/en/common.json';
// 导入英文的首页语言资源
import indexPage from "./locales/en/index_page.json";

/**
 * 语言资源导出
 * 定义应用中使用的所有国际化命名空间
 */
export const resources = {
  common,         // 通用语言资源
  'index_page': indexPage,  // 首页语言资源
} as const;

类型定义增强

为了提供更好的 TypeScript 类型支持,我们可以创建 i18next.d.ts 文件来扩展 i18next 的类型定义:

typescript 复制代码
// src/i18n/i18next.d.ts
/**
 * If you want to enable locale keys typechecking and enhance IDE experience.
 *
 * Requires `resolveJsonModule:true` in your tsconfig.json.
 *
 * @link https://www.i18next.com/overview/typescript
 */
import 'i18next'

// resources.ts file is generated with `npm run toc`
import { I18nNamespaces } from './type'

declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'common'
    resources: I18nNamespaces
  }
}

开发体验优化:i18n-ally 插件

为了提升国际化开发体验,我们可以使用 i18n-ally 插件,它提供了实时翻译预览、自动补全、错误检查等功能。

.vscode/settings.json 中配置:

json 复制代码
{
  "i18n-ally.localesPaths": "src/i18n/locales",
  "i18n-ally.enableNamespace": true,
  "i18n-ally.pathMatcher": "{locale}/{namespace}.json"
}

插件优势

  • 实时预览:在编辑器中直接看到翻译结果
  • 自动补全:输入翻译键时提供智能提示
  • 错误检查:检测缺失的翻译键和格式错误
  • 批量操作:方便地管理和同步翻译资源

国际化功能使用指南

1. 在页面中使用翻译

在 Next.js 页面中,我们可以使用 useTranslation 钩子来获取翻译函数:

typescript 复制代码
// src/pages/index.tsx
import { useTranslation } from 'next-i18next'

export default function Page() {
  // 获取翻译函数
  const { t } = useTranslation()
  
  return (
    <>
      {/* 使用翻译 */}
      <h1>{t('welcome')}</h1>
    </>
  )
}

2. 多命名空间处理

对于大型项目,我们可以使用多个命名空间来组织翻译资源。例如,首页使用 index_page 命名空间:

typescript 复制代码
// src/pages/index.tsx
import { useTranslation } from 'next-i18next'

export default function Page() {
  // 获取翻译函数
  const { t } = useTranslation()
  
  return (
    <>
      {/* 使用index_page命名空间的翻译 */}
      <h1>{t('title', { ns: 'index_page' })}</h1>
    </>
  )
}

3. 服务端翻译属性获取

为了确保服务端渲染时能正确获取翻译资源,我们需要在页面中定义 getStaticPropsgetServerSideProps 函数:

typescript 复制代码
// src/pages/index.tsx
// 导入静态属性类型定义
import { GetStaticProps } from 'next'
// 导入自定义的服务端翻译属性获取函数
import { serviceSideProps } from '@/i18n/utils'

export const getStaticProps: GetStaticProps = async (context) => ({
  props: {
    // 获取服务端翻译属性,包含common和index_page命名空间
    ...(await serviceSideProps(context, ['common', 'index_page'])),
  },
})

服务端翻译工具函数实现:

typescript 复制代码
// src/i18n/utils.ts
// 导入国际化命名空间类型
import { type I18nNsType } from '@/i18n/type';
// 导入服务端翻译函数
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

/**
 * 获取服务端翻译属性的自定义函数
 * @param content 上下文对象,包含请求、响应等信息
 * @param ns 需要加载的国际化命名空间数组
 * @returns 包含翻译资源的属性对象
 */
export const serviceSideProps = async (content: any, ns: I18nNsType = []) => {
  // 从 Cookie 或上下文获取当前语言
  const lang = content.req?.cookies?.NEXT_LOCALE || content.locale;
  // 如果有 Cookie 中的语言,则不需要额外语言,否则使用上下文中的所有语言
  const extraLng = content.req?.cookies?.NEXT_LOCALE ? undefined : content.locales;

  // 从 Cookie 获取设备尺寸信息
  const deviceSize = content.req?.cookies?.NEXT_DEVICE_SIZE || null;

  return {
    // 获取服务端翻译资源,默认包含 common 命名空间
    ...(await serverSideTranslations(lang, ['common', ...ns], undefined, extraLng)),
    // 传递设备尺寸信息
    deviceSize
  };
};

4. 变量插值的使用

翻译资源支持变量插值,我们可以在翻译字符串中使用占位符:

json 复制代码
// 翻译资源文件
{
  "change-locale": "切换到 \"{{changeTo}}\" 语言"
}

在组件中使用:

typescript 复制代码
const { t } = useTranslation()

// 带变量的翻译调用
<p>{t('common:change-locale', { changeTo: 'English' })}</p>

5. 完整使用示例

typescript 复制代码
// src/pages/index.tsx
// 导入语言选择器组件
import I18nLngSelector from '@/components/I18nLngSelector'
// 导入自定义的服务端翻译属性获取函数
import { serviceSideProps } from '@/i18n/utils'
// 导入静态属性类型定义
import { GetStaticProps } from 'next'
// 导入翻译钩子
import { useTranslation } from 'next-i18next'

/**
 * 首页组件
 */
export default function Page() {
  // 获取翻译函数
  const { t } = useTranslation()
  
  return (
    <>
      {/* 翻译后的标题,使用index_page命名空间 */}
      <h1>{t('title', { ns: 'index_page' })}</h1>
      {/* 语言选择器 */}
      <I18nLngSelector />
    </>
  )
}

/**
 * 静态属性生成函数
 * 用于在构建时获取翻译资源
 */
export const getStaticProps: GetStaticProps = async (context) => ({
  props: {
    // 获取服务端翻译属性,包含common和index_page命名空间
    ...(await serviceSideProps(context, ['common', 'index_page'])),
  },
})

实现原理总结

1. 语言存储机制

采用浏览器缓存 + Cookie的双重存储机制:

  • localStorage:用于客户端持久化存储用户的语言选择
  • Cookie:用于服务端渲染时获取用户的语言偏好

2. 语言切换流程

  1. 用户点击语言选择器
  2. 调用 onChangeLng 方法
  3. 将选择的语言保存到 localStorage 和 Cookie 中
  4. 调用 i18n 实例的 changeLanguage 方法切换语言
  5. 检查是否有对应的语言资源包
  6. 如果没有资源包且语言发生了变化,则刷新页面确保资源加载

3. 默认语言设置

  1. 应用启动时,调用 setUserDefaultLng 方法
  2. 检查是否有存储的语言设置
  3. 如果有,使用存储的语言
  4. 如果没有,根据浏览器语言自动设置默认语言
  5. 确保用户每次访问时都能看到一致的语言界面

注意事项和最佳实践

  1. Cookie 依赖:确保服务器环境支持 Cookie,以便在服务端渲染时获取用户的语言偏好

  2. 服务端渲染:在服务端渲染时,需要从 Cookie 中获取语言设置,确保首次渲染的语言正确

  3. 缓存策略:注意语言切换后的缓存处理,避免出现缓存导致的语言不一致问题

  4. 多命名空间管理:对于大型项目,建议使用多命名空间管理翻译资源,提高可维护性

  5. 类型安全:充分利用 TypeScript 的类型系统,确保翻译资源的正确使用

  6. 开发工具:使用 i18n-ally 等工具提升开发体验,减少手动编写翻译的错误

总结和展望

本文详细介绍了在 Next.js 16 项目中实现无 URL 变更的国际化方案,主要包括:

  1. 核心实现:使用 next-i18next 作为基础,自定义语言切换钩子处理语言存储和切换逻辑

  2. 创新点:采用浏览器缓存 + Cookie 机制管理语言切换,不需要 URL 前缀,保持 URL 稳定

  3. 用户体验:实现了语言设置的持久化,确保页面刷新后不会丢失用户的语言选择

  4. 开发体验:使用 TypeScript 提供类型安全,结合 i18n-ally 插件提升开发效率

这个国际化方案解决了传统 URL 前缀方式的问题,提供了更好的用户体验和 SEO 效果。未来可以考虑:

  • 支持更多语言的动态加载
  • 实现翻译资源的自动同步和管理
  • 提供更多的语言切换动画和交互效果

希望本文的实现方案能够帮助到正在寻找 Next.js 国际化解决方案的开发者们,也欢迎大家提出宝贵的意见和建议!

项目地址

GitHub 仓库


如果觉得这篇文章对你有帮助,欢迎点赞、评论和分享!👍

#Next.js #国际化 #i18n #前端开发 #TypeScript

相关推荐
suke6 小时前
紧急高危:Next.js 曝出 CVSS 10.0 级 RCE 漏洞,请立即修复!
前端·程序员·next.js
锈儿海老师8 小时前
深入探究 React 史上最大安全漏洞
前端·react.js·next.js
dorisrv1 天前
Next.js Page Router + Chakra UI 实现优雅的主题切换 🌓
next.js
FanetheDivine2 天前
Next.js 学习笔记5 使用心得
react.js·next.js
七淮7 天前
Next.js SEO 优化完整方案
前端·next.js
孟祥_成都8 天前
nextjs 16 基础完全指南!(一) - 初步安装
前端·next.js
山依尽11 天前
如何将一个 React SPA 项目迁移到 Next.js 服务端渲染
前端·next.js
人工智能训练13 天前
前端框架选型破局指南:Vue、React、Next.js 从差异到落地全解析
运维·javascript·人工智能·前端框架·vue·react·next.js
米诺zuo21 天前
nextjs文件路由、路由组
前端·next.js