为 React 应用添加国际化支持,意味着将所有面向用户的字符串外部化到各语言文件中,并通过翻译层来渲染,而不是在 JSX 中硬编码文本。
如果你曾经发布过一个版本,导致原始的 t('main.header') 出现在用户屏幕上,或者看过一段德语字符串撑破了在英语下看起来完全正常的按钮,你就已经明白:配置本身并不是难点。接线工作花一个下午就能搞定,但针对特定语言环境的边界情况却要耗掉整个迭代周期的其余时间。生产级的标准方案是 react-i18next,即 i18next 框架的 React 绑定层。建议将 react-i18next 作为统一标准:它基于 Hooks 设计,支持命名空间和懒加载,兼容服务端渲染,并依托最大的 i18next 插件生态系统。只有当你明确需要使用 ICU 消息语法时,才考虑选用 react-intl。本指南涵盖当前正确的配置方式,以及在生产环境中最常踩坑的五个问题:插值、复数形式、语言环境感知的数字/日期格式化、从右到左布局,以及 SSR。
核心要点
- 在 i18next 配置中设置
interpolation.escapeValue: false,因为 React 在渲染前已经对值进行了转义;如果保留 i18next 的转义功能,会导致字符串被二次转义。 - 在当前版本的 i18next 中,复数键使用 CLDR/Intl 后缀(
_zero、_one、_two、_few、_many、_other),旧版的_plural后缀属于旧 JSON v3 格式;用于选择复数形式的变量必须命名为count。 - 数字和日期的格式化取决于地区,而不仅仅是语言,因此需要使用完整的语言区域标识符(如
en-US、ar-EG),并通过 i18next 的 Intl 格式化器以{{value, number}}和{{date, datetime}}的形式进行格式化。 - 使用
i18next-http-backend配合loadPath从 JSON 文件加载翻译内容;通过require()内联翻译会将所有语言打包进主 bundle,彻底破坏懒加载机制。 - 在 Next.js 上,不要手动实现 SSR 国际化:
next-i18nextv16 在一个包中同时支持 App Router 和 Pages Router 的接入。
如何配置 react-i18next?
安装核心框架、React 绑定层,以及两个负责语言检测和文件加载的插件。整个配置由四个包承担,各司其职:
| 包 | 版本线 | 用途 |
|---|---|---|
i18next | 26.x | 核心引擎:键查找、插值、复数形式、格式化 |
react-i18next | 17.x | React 绑定层:useTranslation、Trans |
i18next-browser-languagedetector | 8.x | 检测用户语言 |
i18next-http-backend | 4.x | 通过 HTTP 加载翻译 JSON 文件 |
需要注意一点:i18next-http-backend v4 要求原生 fetch 支持。Node ≥ 18、所有现代浏览器、Deno 和 Bun 均默认提供 fetch。在较旧的运行时环境中,需要提供 ponyfill,或继续使用 v3。
npm install i18next react-i18next i18next-browser-languagedetector i18next-http-backend
创建 src/i18n.ts 并进行一次性初始化:
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'es', 'ar'],
load: 'languageOnly',
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
interpolation: { escapeValue: false },
});
export default i18n;
设置 interpolation.escapeValue: false 是因为 React 在渲染前已经对值进行了转义;保留 i18next 的转义功能会导致字符串被二次转义。loadPath 的配置至关重要:通过 require() 内联资源(这是 Create React App / Webpack 时代的模式)会将所有语言打包进主 bundle,从而破坏懒加载机制。在入口文件中,于渲染之前导入一次该配置:在 main.tsx 中添加 import './i18n';。
Discover how at OpenReplay.com.
使用 useTranslation Hook 外部化字符串
翻译文件存放在 public/locales/<lng>/translation.json 路径下,按语言分目录管理,组件通过 useTranslation Hook 提供的 t 函数读取翻译内容。将每一处硬编码字符串替换为键查找调用。
{ "main": { "header": "Welcome to the app!" } }
import { useTranslation } from 'react-i18next';
export default function Header() {
const { t } = useTranslation();
return <h1>{t('main.header')}</h1>;
}
嵌套键(如 main.header)和命名空间用于组织大量字符串。对于包含内联标记或链接的文案,直接调用 t() 会破坏 JSX 结构。此时应改用 Trans 组件,它能将 React 元素插值到已翻译的句子中,同时将标记保留在组件内,而非 JSON 文件中。
<Trans i18nKey="main.docs" components={{ docsLink: <a href="https://react.i18next.com/" /> }} />
如何切换和检测语言?
通过 i18n.changeLanguage(lng) 切换当前语言;所有使用 useTranslation 的组件会自动重新渲染。语言切换器只需是调用该方法的按钮或 <select> 元素:
const { i18n } = useTranslation();
<select
value={i18n.resolvedLanguage}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="en">English</option>
<option value="ar">العربية</option>
</select>
语言检测由语言检测插件负责,它按固定顺序检查各来源:查询字符串(?lng=en)、Cookie、localStorage、浏览器 navigator,最后是 <html lang> 属性,并在找到第一个受支持的匹配项时停止。它会将解析出的语言缓存到 localStorage,因此回访用户的语言选择会被保留,手动调用 changeLanguage 也会同步更新该缓存。
五个常见踩坑点
大多数国际化 Bug 都发生在正常路径之外。以下是那些能通过本地 QA 测试、却只在用户实际语言环境中才会暴露的故障模式。
插值。 使用 {{var}} 语法注入动态值,并将其作为第二个参数传入:针对 "Hello, {{name}}" 调用 t('greeting', { name })。React 的转义机制加上 escapeValue: false 的配置,可确保此操作的 XSS 安全性。
复数形式。 英语需要两种复数形式,阿拉伯语需要六种,这正是为什么永远不应该手写 if (count === 1) 的原因。将 count 传给 t(),让 Intl.PluralRules 自动选择对应的键。使用 CLDR 后缀定义各种形式:_zero、_one、_two、_few、_many、_other。该变量必须命名为 count。
{
"messages_one": "You have one message",
"messages_other": "You have {{count}} new messages"
}
旧版 _plural 后缀属于遗留的 JSON v3 格式。i18next 在引入 JSON v4 格式时统一了复数后缀,使其与 Intl API 保持一致。自 v24 起,Intl API 成为强制依赖:如果运行时不支持 Intl.PluralRules,必须提供 polyfill,因为旧版 v3 复数处理的回退机制已被移除,compatibilityJSON 也不再接受 'v3'。
数字和日期格式化。 使用 i18next 内置的 Intl 格式化器进行格式化:{{value, number}} 和 {{date, datetime}},支持类似 {{value, number(style: percent)}} 的选项。由于格式化取决于地区,需使用完整的语言区域标识符(如 en-US、ar-EG),以确保数字和日期顺序在不同浏览器间保持一致。
从右到左布局。 对于 RTL 语言,在每次语言切换时通过 i18n.dir() 设置文档方向,从而使整个布局重新流式排列,无需为每个组件单独编写 CSS:
useEffect(() => {
const apply = (lng: string) => {
document.documentElement.lang = lng;
document.documentElement.dir = i18n.dir(lng);
};
i18n.on('languageChanged', apply);
return () => i18n.off('languageChanged', apply);
}, [i18n]);
在 languageChanged 事件处理器内部读取 i18n.dir(),而不是在切换过程中同步读取:调用 changeLanguage() 后,i18next.language 只有在资源加载完成后才会反映新的语言。
SSR。 不要在 Next.js 上手动实现服务端国际化。next-i18next v16 是对 i18next 和 react-i18next 的轻量封装,负责处理 Next.js 特有的接入细节:中间件、服务端/客户端分离以及资源注水。它同时支持 App Router(Server Components、Client Components、中间件)和 Pages Router,为 Server Components 提供 getT(),为 Client Components 提供 useT()。请将客户端组件树包裹在 <Suspense> 中,而不是假设 window 对象一定存在。这些特定语言环境的缺陷——例如原始键 main.header 被直接渲染给用户、RTL 内边距导致文字被裁剪,或回退语言的文本泄漏到已翻译的页面中——正是那些能通过默认语言环境 QA 测试、却只在你用目标语言环境观看真实会话时才会暴露的问题,这也正是会话回放工具发挥价值的地方。
通过命名空间和键提取实现规模化
随着字符串数量增长,将翻译内容拆分到命名空间中,并通过 useTranslation('dashboard') 按路由按需加载,使每个页面只获取自己的 JSON 文件,保持 bundle 体积精简。当字符串遍布整个代码库时,可以引入自动化工具:i18next-cli 是官方的一体化命令行工具,支持键提取、代码检查、语言文件同步和类型生成;当真正开始本地化工作时,Lokalise、Phrase 或 Crowdin 等翻译管理系统可以协调翻译人员的协作。
至此,你已经掌握了正确的 react-i18next 配置方式,以及进阶问题的完整路线图。完成配置接入、外部化字符串,在 bundle 增大时引入命名空间,在服务端渲染时使用 next-i18next。安装时请在 npm 上核对各包的确切版本,因为 i18next 的核心包和绑定层更新频繁。
常见问题
i18next 和 react-i18next 有什么区别?
i18next 是核心框架,负责实际的翻译逻辑:键查找、插值、复数形式和格式化。react-i18next 是构建在其上的 React 绑定层,提供 useTranslation 等 Hook、Trans 组件,以及语言切换时的自动重新渲染能力。两者都需要安装:i18next 负责实际工作,react-i18next 将其连接到你的组件。react-i18next 要求兼容的现代 i18next peer 版本,因此需要保持两者的主版本号兼容。
为什么我的翻译键显示为原始文本而不是翻译后的字符串?
原始键(如 main.header)被直接渲染给用户,意味着查找失败,几乎总是因为该语言或命名空间对应的 JSON 文件从未成功加载。常见原因包括:loadPath 与实际文件路径不匹配、命名空间未注册、i18n 配置在渲染前未导入,或键在对应语言文件中不存在。请检查网络面板中是否有对 locales 路径的请求失败,并确认键存在于正确的语言文件中。
在 i18next 中是否还需要使用 _plural 后缀来定义复数键?
不需要。_plural 后缀属于遗留的 JSON v3 格式。当前版本的 i18next 使用与 Intl.PluralRules 匹配的 CLDR/Intl 单词后缀:_zero、_one、_two、_few、_many 和 _other。英语使用两种形式(_one 和 _other),而阿拉伯语使用全部六种。用于选择复数形式的变量必须命名为 count 且必须存在,因为缺少 count 时没有回退机制。如果 Intl.PluralRules 不可用,必须提供 polyfill:自 v24 起,旧版 v3 复数处理的回退机制已被移除,compatibilityJSON 也不再接受 v3。
翻译内容必须存储在 JSON 文件中,还是可以在配置中内联?
可以通过 resources 选项内联翻译,但对于任何超出简单示例的应用,都应该使用 i18next-http-backend 配合 loadPath(如 /locales/{{lng}}/{{ns}}.json)从 JSON 文件加载。通过 require() 内联所有语言会将全部翻译打包进主 bundle,破坏懒加载机制,导致用户下载他们从未使用的语言字符串。基于文件的加载方式仅按需获取当前激活的语言和命名空间。需要注意,i18next-http-backend v4 要求原生 fetch 支持,即 Node 18 或更高版本。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k