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,
};
/**
* 切换语言的方法
*/
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 中实现默认语言的初始化,确保页面刷新后能保持用户的语言选择:
// 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)
初始化流程:
- 应用启动时,组件挂载
- 调用
setUserDefaultLng() 方法
- 检查是否有存储的语言设置
- 如果有,使用存储的语言;如果没有,根据浏览器语言设置默认语言
- 确保用户每次访问时都能看到自己选择的语言
4. 语言选择器组件
创建一个语言选择器组件,让用户可以方便地切换语言:
// 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 类型:
// 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)[];
类型安全优势:
- 避免拼写错误:使用枚举和类型定义确保语言代码的正确性
- 智能提示:在使用翻译键时提供自动补全
- 类型检查:在编译时就能发现翻译资源的错误使用
翻译资源文件示例
中文翻译
// src/i18n/locales/zh-CN/common.json
{
"change-locale": "切换到 \"{{changeTo}}\" 语言",
"welcome": "欢迎使用 Next.js 国际化方案"
}
英文翻译
// src/i18n/locales/en/common.json
{
"change-locale": "Change locale to \"{{changeTo}}\"",
"welcome": "Welcome to Next.js i18n Solution"
}
首页翻译资源
// src/i18n/locales/zh-CN/index_page.json
{
"title": "next-i18next 示例"
}
// src/i18n/locales/en/index_page.json
{
"title": "next-i18next example"
}
翻译资源管理
为了更好地管理翻译资源,我们可以创建一个 resources.ts 文件来集中导入和导出所有翻译资源:
// 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 的类型定义:
// 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 中配置:
{
"i18n-ally.localesPaths": "src/i18n/locales",
"i18n-ally.enableNamespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespace}.json"
}
插件优势:
- 实时预览:在编辑器中直接看到翻译结果
- 自动补全:输入翻译键时提供智能提示
- 错误检查:检测缺失的翻译键和格式错误
- 批量操作:方便地管理和同步翻译资源
国际化功能使用指南
1. 在页面中使用翻译
在 Next.js 页面中,我们可以使用 useTranslation 钩子来获取翻译函数:
// src/pages/index.tsx
import { useTranslation } from 'next-i18next'
export default function Page() {
// 获取翻译函数
const { t } = useTranslation()
return (
<>
{/* 使用翻译 */}
<h1>{t('welcome')}</h1>
</>
)
}
2. 多命名空间处理
对于大型项目,我们可以使用多个命名空间来组织翻译资源。例如,首页使用 index_page 命名空间:
// 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. 服务端翻译属性获取
为了确保服务端渲染时能正确获取翻译资源,我们需要在页面中定义 getStaticProps 或 getServerSideProps 函数:
// 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'])),
},
})
服务端翻译工具函数实现:
// 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. 变量插值的使用
翻译资源支持变量插值,我们可以在翻译字符串中使用占位符:
// 翻译资源文件
{
"change-locale": "切换到 \"{{changeTo}}\" 语言"
}
在组件中使用:
const { t } = useTranslation()
// 带变量的翻译调用
<p>{t('common:change-locale', { changeTo: 'English' })}</p>
5. 完整使用示例
// 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. 语言切换流程
- 用户点击语言选择器
- 调用
onChangeLng 方法
- 将选择的语言保存到 localStorage 和 Cookie 中
- 调用 i18n 实例的
changeLanguage 方法切换语言
- 检查是否有对应的语言资源包
- 如果没有资源包且语言发生了变化,则刷新页面确保资源加载
3. 默认语言设置
- 应用启动时,调用
setUserDefaultLng 方法
- 检查是否有存储的语言设置
- 如果有,使用存储的语言
- 如果没有,根据浏览器语言自动设置默认语言
- 确保用户每次访问时都能看到一致的语言界面
注意事项和最佳实践
-
Cookie 依赖:确保服务器环境支持 Cookie,以便在服务端渲染时获取用户的语言偏好
-
服务端渲染:在服务端渲染时,需要从 Cookie 中获取语言设置,确保首次渲染的语言正确
-
缓存策略:注意语言切换后的缓存处理,避免出现缓存导致的语言不一致问题
-
多命名空间管理:对于大型项目,建议使用多命名空间管理翻译资源,提高可维护性
-
类型安全:充分利用 TypeScript 的类型系统,确保翻译资源的正确使用
-
开发工具:使用 i18n-ally 等工具提升开发体验,减少手动编写翻译的错误
总结和展望
本文详细介绍了在 Next.js 16 项目中实现无 URL 变更的国际化方案,主要包括:
-
核心实现:使用 next-i18next 作为基础,自定义语言切换钩子处理语言存储和切换逻辑
-
创新点:采用浏览器缓存 + Cookie 机制管理语言切换,不需要 URL 前缀,保持 URL 稳定
-
用户体验:实现了语言设置的持久化,确保页面刷新后不会丢失用户的语言选择
-
开发体验:使用 TypeScript 提供类型安全,结合 i18n-ally 插件提升开发效率
这个国际化方案解决了传统 URL 前缀方式的问题,提供了更好的用户体验和 SEO 效果。未来可以考虑:
- 支持更多语言的动态加载
- 实现翻译资源的自动同步和管理
- 提供更多的语言切换动画和交互效果
希望本文的实现方案能够帮助到正在寻找 Next.js 国际化解决方案的开发者们,也欢迎大家提出宝贵的意见和建议!
项目地址
GitHub 仓库
如果觉得这篇文章对你有帮助,欢迎点赞、评论和分享!👍
#Next.js #国际化 #i18n #前端开发 #TypeScript