Next.js 16 Page Router 国际化 🌐
引言
在现代 Web 应用开发中,国际化(i18n)已经成为一个必备功能。传统的 Next.js 国际化方案通常采用 URL 前缀方式(如 /en/page 或 /zh-CN/page),这种方式虽然实现简单,但存在一些明显的问题:
- URL 频繁变更:用户切换语言时,页面 URL 会发生变化
- SEO 分散:相同内容分散在不同 URL 下,影响搜索引擎优化
- 用户体验不佳:复制链接时需要考虑语言前缀
那么,有没有一种方式可以在不改变 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 工具库 |
项目结构
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 文件:
// 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 钩子,它负责处理语言的存储、切换和初始化:
// 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

最低0.47元/天 解锁文章

1313

被折叠的 条评论
为什么被折叠?



