方案一: 原生 JS
命令式编程
核心:独立语言配置文件 + 全局工具函数 + 语言切换逻辑,适配纯静态页面
命令式编程的缺点太普遍,这里就不重复说明,仅仅介绍方法作为一个演变过程的了解
- 第一步:创建多语言配置文件(
lang目录下)
// lang/zh-CN.js 中文配置
export default {
button: { submit: "提交", cancel: "取消" },
tip: { success: "操作成功", fail: "操作失败" },
user: { name: "用户名", login: "请登录后操作" }
};
// lang/en-US.js 英文配置
export default {
button: { submit: "Submit", cancel: "Cancel" },
tip: { succes
s: "Operation succeeded", fail: "Operation failed" },
user: { name: "Username", login: "Please log in first" }
};- 第二步:封装多语言核心工具类(
lang/index.js)
import zhCN from "./zh-CN.js";
import enUS from "./en-US.js";
// 全局存储当前语言+配置
const Lang = {
currentLang: "zh-CN", // 默认中文
langConfig: { "zh-CN": zhCN, "en-US": enUS },
// 获取对应key的文案,支持嵌套key(如 button.submit)
get(key) {
const keys = key.split(".");
let value = this.langConfig[this.currentLang];
for (const k of keys) {
value = value?.[k] || key; // 无对应key返回本身,兜底
}
return value;
},
// 切换语言,触发页面重新渲染
changeLang(lang) {
if (!this.langConfig[lang]) return;
this.currentLang = lang;
this.renderPage(); // 重新渲染页面文案
},
// 遍历页面带data-lang-key属性的元素,更新文案
renderPage() {
document.querySelectorAll("[data-lang-key]").forEach(el => {
const key = el.getAttribute("data-lang-key");
el.textContent = this.get(key);
});
}
};
export default Lang;- 第三步:页面使用 + 语言切换
<!-- HTML 结构 -->
<div>
<button onclick="Lang.changeLang('zh-CN')">中文</button>
<button onclick="Lang.changeLang('en-US')">English</button>
<button data-lang-key="button.submit"></button>
<p data-lang-key="tip.success"></p>
</div>
<!-- 引入脚本 -->
<script type="module">
import Lang from "./lang/index.js";
window.Lang = Lang;
// 页面初始化渲染
Lang.renderPage();
</script>声明式编程
- 改造语言包
// lang/zh-CN.js
export default {
welcome: "欢迎",
button: "提交"
};- 创建 React Context
创建一个 useLocale Hook,代替直接命令式编程的 import。
// src/hooks/useLocale.js
import { useState, useContext, createContext } from "react";
import zhCN from "../lang/zh-CN";
import enUS from "../lang/en-US";
// 1. 建立对应关系
const langMap = {
"zh-CN": zhCN,
"en-US": enUS
};
// 2. 创建上下文
const LocaleContext = createContext();
// 3. 提供器组件 (包裹在 App 最外层)
export const LocaleProvider = ({ children }) => {
// 核心:这里用 useState 存当前语言包,这样切换时才会触发 React 重新渲染!
const [langType, setLangType] = useState("zh-CN");
// 动态获取当前的语言对象
const locale = langMap[langType];
const changeLang = (type) => {
setLangType(type);
};
return (
<LocaleContext.Provider value={{ locale, changeLang, langType }}>
{children}
</LocaleContext.Provider>
);
};
// 4. 导出 Hook
export const useLocale = () => {
const context = useContext(LocaleContext);
if (!context) throw new Error("useLocale must be used within LocaleProvider");
return context;
};- 组件里使用
import { useLocale } from "./hooks/useLocale";
export default function HomePage() {
// 这里拿到的 locale 是响应式的!
const { locale, changeLang } = useLocale();
return (
<div>
{/* 像对象一样直接点出来 */}
<h1>{locale.welcome}</h1>
<button>{locale.button}</button>
{/* 点击切换,不需要刷新页面,React 自动重新渲染 */}
<button onClick={() => changeLang("zh-CN")}>中文</button>
<button onClick={() => changeLang("en-US")}>English</button>
</div>
);
}✅ 优点
- 响应式切换:
- 切换语言时,不需要刷新浏览器,页面瞬间变化。这是 SPA (单页应用) 的标准体验。
- 开发体验极佳:
- 代码里直接写
{locale.home.title},有 TypeScript 甚至还能有智能提示。 - 完全符合 React 的声明式编程思维,心智负担小。
- 代码里直接写
- 强大的插值能力:
- 处理
"Welcome, {name}"这种带参数的文案非常简单,写个简单的 replace 函数即可,或者直接在组件里拼接。
- 处理
- 组件化支持:
- 可以轻松翻译组件的 Props,比如
<Input placeholder={locale.searchPlaceholder} />,这是原生 DOM 方案很难做到的。
- 可以轻松翻译组件的 Props,比如
❌ 缺点
- 渲染性能开销:
- 因为语言 Context 通常包裹在最顶层 (
<App>)。一旦语言切换,整个 React 组件树都会重新渲染。 - 注:对于绝大多数应用,这个开销是可以忽略不计的,除非你的页面极其庞大且没有做任何 memo 优化。
- 因为语言 Context 通常包裹在最顶层 (
- 包体积问题(需优化):
- 如果直接 import zh from './zh-CN',无论你用不用,所有语言包都会被打包进主 JS 里。
- 解决方案:需要配合 React.lazy 或动态 import() 实现语言包的按需加载。
方案二: React + react-i18next
核心:基于 i18next 核心引擎,提供专属 Hooks,支持懒加载、SSR,适配 React 函数组件,步骤如下
- 安装依赖
npm install i18next react-i18next i18next-http-backend --save
# i18next-http-backend:支持远程加载配置文件,可选- 初始化 i18n 配置(
src/i18n.js)
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend"; // 按需加载配置文件
// 本地语言配置(也可放独立JSON文件,通过Backend加载)
const resources = {
"zh-CN": {
translation: {
button: { submit: "提交", cancel: "取消" },
tip: { success: "操作成功" },
user: { welcome: "欢迎{{name}}登录" } // 占位符语法
}
},
"en-US": {
translation: {
button: { submit: "Submit", cancel: "Cancel" },
tip: { success: "Operation succeeded" },
user: { welcome: "Welcome {{name}} to log in" }
}
}
};
i18n
.use(Backend) // 启用远程加载(可选,小型项目可省略)
.use(initReactI18next) // 绑定react-i18next
.init({
resources,
lng: "zh-CN", // 默认语言
fallbackLng: "zh-CN", // 兜底语言
interpolation: {
escapeValue: false // React自带XSS防护,关闭即可
}
});
export default i18n;- 全局引入(
src/index.js)
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./i18n"; // 引入i18n配置,无需挂载,自动生效
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);- 组件内使用
// App.jsx
import { useTranslation } from "react-i18next";
function App() {
// 1. 获取 hook
// t: 翻译函数
// i18n: 实例对象,用于切换语言
const { t, i18n } = useTranslation();
// 切换语言
const handleSwitch = (lang) => {
i18n.changeLanguage(lang);
};
return (
<div className="p-4 border rounded">
{/* 基础用法:直接翻译 */}
<h2>{t('tip.success')}</h2>
{/* 插值用法:传递变量 */}
{/* 对应配置: "welcome": "欢迎 {{name}} 登录" */}
<p>{t('user.welcome', { name: '张三' })}</p>
{/* 组件插值 (高级用法) */}
{/* 场景:文案里夹杂着加粗、链接 */}
<Trans i18nKey="user.agreement">
我已阅读并同意 <strong>用户协议</strong>
</Trans>
{/* 切换按钮 */}
<div className="mt-4 gap-2 flex">
<button onClick={() => handleSwitch('zh-CN')}>中文</button>
<button onClick={() => handleSwitch('en-US')}>English</button>
</div>
</div>
);
);
}
export default App;工程化进阶
如果做的是大型项目,不要把所有翻译都塞进一个 resources 对象里。
命名空间 (Namespaces) —— 文件拆分
i18next 支持把翻译文件按模块拆分(如 common.json, header.json, settings.json)。
// 组件里只加载自己需要的模块,提升性能
const { t } = useTranslation('settings');
t('title'); // 会去 settings.json 里找懒加载 (Lazy Loading) —— 极致性能
结合 i18next-http-backend,我们不需要在首屏把所有语言包(比如几十种语言)都打包进 JS 里。
// i18n.js 修改
i18n
.use(Backend)
.init({
// 不需要在这里写 resources 对象了!
// 告诉它去哪里加载静态文件
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
// ...
});- 效果:用户访问页面时,浏览器会在 Network 里发起一个请求去下载
/locales/zh-CN/translation.json。切换到英文时,再下载英文包。首屏体积大幅减小。
✅ 优点
- 生态霸主:i18next 是 JS 界最成熟的国际化库,几乎没有它解不了的 bug。
- 性能优化完备:
- 按需加载:不用一次性把 10MB 的翻译文件全加载进来。
- Memoization:内部做了缓存优化,比手写的 Context 性能更好。
- SSR 支持:完美支持 Next.js / Remix 等服务端渲染框架。
- 功能极其强大:
- 复数处理:自动处理英文的单复数(apple vs apples)。
- 上下文:支持基于上下文的翻译(比如“行”:行走的行 vs 银行的行)。
- 嵌套引用:一个翻译里可以引用另一个翻译 key。
- 安全:自带 XSS 防护,默认转义 HTML 字符。
❌ 缺点
- 配置繁琐:相比于手写一个简单的 Context,i18next 需要引入 2-3 个包,配置一大堆参数(backend, detector, interpolation...)。
- 包体积:虽然支持懒加载语言包,但 i18next 核心库本身有一定体积(约 15-20KB gzipped),对于追求极致轻量的微型项目来说有点重。
- TypeScript 支持略繁琐:要想获得完美的类型提示(键入 t(' 时自动提示 user.welcome),需要写额外的 d.ts 类型声明文件来覆盖默认类型,新手容易卡住。
方案三: React + react-intl (FormatJS)
核心:雅虎(Yahoo!)开源,基于 Web 标准(ICU 语法),强调数据格式化(日期、货币),采用组件化风格,适合对国际化标准要求极高的金融 / B端项目。
- 安装依赖
npm install react-intl --save- 全局配置与注入(src/main.jsx)
不同于 i18next 的单例模式,react-intl 采用标准的 React Context Provider 模式。
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
import { IntlProvider } from 'react-intl';
import App from './App';
// 1. 引入语言包(实际项目中通常按需加载)
import zhCN from './locales/zh-CN.json';
import enUS from './locales/en-US.json';
const messages = {
'zh-CN': zhCN,
'en-US': enUS,
};
const Root = () => {
const [locale, setLocale] = useState('zh-CN');
return (
// 2. 使用 IntlProvider 包裹应用
// key={locale} 是一个小技巧:强迫 Provider 在语言切换时彻底重新渲染,避免更新延迟
<IntlProvider locale={locale} messages={messages[locale]} key={locale}>
<App changeLanguage={setLocale} />
</IntlProvider>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(<Root />);- 组件内使用
react-intl 提供了两种风格:组件式 和 Hook 式
import React from 'react';
import { FormattedMessage, FormattedDate, FormattedNumber, useIntl } from 'react-intl';
export default function Dashboard({ changeLanguage }) {
// Hook 方式:用于无法使用组件的地方(如 placeholder, title 属性)
const intl = useIntl();
return (
<div className="p-4">
{/* 1. 核心特性:组件式翻译 */}
{/* id 对应 json 中的 key,defaultMessage 是兜底文案 */}
<h1>
<FormattedMessage id="dashboard.title" defaultMessage="仪表盘" />
</h1>
{/* 2. 核心特性:强大的数据格式化 (日期/货币) */}
<p>
当前时间:
<FormattedDate
value={new Date()}
year="numeric"
month="long"
day="2-digit"
/>
{/* 中文下显示:2024年1月21日 */}
{/* 英文下显示:January 21, 2024 */}
</p>
<p>
账户余额:
<FormattedNumber
value={9999.99}
style="currency"
currency="USD"
/>
{/* 自动处理千分位和货币符号:$9,999.99 */}
</p>
{/* 3. 属性翻译 (使用 Hook) */}
<input
placeholder={intl.formatMessage({ id: 'search.placeholder', defaultMessage: '搜索...' })}
/>
{/* 切换按钮 */}
<div className="mt-4">
<button onClick={() => changeLanguage('zh-CN')}>中文</button>
<button onClick={() => changeLanguage('en-US')}>English</button>
</div>
</div>
);
}工程化进阶
自动化提取 — 「一键初始化」
react-intl (FormatJS) 生态最大的亮点是它的 CLI 工具。它鼓励你在代码里写 defaultMessage,然后通过工具自动提取出 json 文件,防止漏翻。
# 安装 CLI
npm install --save-dev @formatjs/cli
# package.json 添加脚本
# "extract": "formatjs extract 'src/**/*.js*' --out-file lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'"- 效果:开发时直接写
<FormattedMessage defaultMessage="你好" />,运行脚本后,自动生成 id 和翻译文件,大大降低维护成本。
按需加载 - 「对比react-i18next理解」
默认的 import zhCN from './zh-CN.json' 会将所有语言包打包进主 bundle.js。如果项目有 10 种语言,用户只看中文,却被迫下载了其他 9 种语言的资源。
解决方案:利用 Vite/Webpack 的 Dynamic Import (import()) 能力,将语言包拆分成独立的 Chunk,点击切换时才通过网络请求加载。
1. 第一步:重构目录结构
将语言包放在 src 目录下(而不是 public),这样 Vite 才能对 JSON 文件进行打包、压缩和 Hash 处理。
src/
locales/
zh-CN.json
en-US.json
i18n/
loader.js <-- 新增:专门负责动态加载2. 第二步:编写异步加载器 (src/i18n/loader.js)
我们需要封装一个函数,根据传入的 locale 字符串,动态 import 对应的文件。
// src/i18n/loader.js
// 定义支持的语言列表(白名单)
const localeMap = {
'zh-CN': () => import('../locales/zh-CN.json'),
'en-US': () => import('../locales/en-US.json'),
};
export const loadLocaleData = async (locale) => {
// 1. 检查语言是否支持,不支持则回退到默认
const loader = localeMap[locale] || localeMap['zh-CN'];
try {
// 2. 动态加载 (Vite 会自动将这些 json 分割成单独的 js chunk)
const module = await loader();
// 3. 返回 JSON 内容 (注意: 动态 import 得到的是 Module,数据在 default 属性里)
return module.default;
} catch (error) {
console.error(`无法加载语言包: ${locale}`, error);
return {};
}
};3. 第三步:改造入口组件 (支持异步状态)
注:IntlProvider 不能在 messages 为空时渲染,所以我们需要处理 Loading 状态。
// src/App.jsx 或 src/main.jsx
import React, { useState, useEffect } from 'react';
import { IntlProvider } from 'react-intl';
import { loadLocaleData } from './i18n/loader';
import Dashboard from './Dashboard';
export default function App() {
const [locale, setLocale] = useState('zh-CN');
const [messages, setMessages] = useState(null); // 初始为空
useEffect(() => {
// 切换语言时,触发异步加载
loadLocaleData(locale).then((data) => {
setMessages(data);
});
}, [locale]);
// 1. 如果消息还没加载回来,显示 Loading (防止页面闪烁或报错)
if (!messages) {
return <div className="p-4 text-gray-500">Language Loading...</div>;
}
// 2. 加载完成后,渲染应用
return (
<IntlProvider locale={locale} messages={messages} key={locale}>
<Dashboard changeLanguage={setLocale} />
</IntlProvider>
);
}✅ 优点
- 标准化:严格遵循 ICU 国际标准语法,处理复数、性别等复杂语法极其精准。
- 格式化能力最强:内置了对日期、时间、数字、货币、百分比的专业格式化,这是 i18next 相对较弱的地方(虽然 i18next 也有插件,但不如这个原生)。
- 代码即文档:鼓励在代码中写 defaultMessage,看代码就能知道这里原本显示的文案,不用去翻 json 文件。
- 工具链完善:自动提取消息的 CLI 工具非常成熟,适合多人协作的大型项目。
❌ 缺点
- 代码啰嗦:大量使用
<FormattedMessage />组件,导致 JSX 结构变得很深、很重,不如t('')函数简洁。 - 包体积:由于包含了大量 Polyfill(为了兼容旧浏览器对 Intl API 的支持),体积相对较大。