返回文章列表
frontend2025年10月15日约 12 分钟阅读

前端国际化(i18n)- React

主要叙述React方向目前的多语言方案

方案一: 原生 JS

命令式编程

核心:独立语言配置文件 + 全局工具函数 + 语言切换逻辑,适配纯静态页面

命令式编程的缺点太普遍,这里就不重复说明,仅仅介绍方法作为一个演变过程的了解

  1. 第一步:创建多语言配置文件(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" }
};
  1. 第二步:封装多语言核心工具类(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;
  1. 第三步:页面使用 + 语言切换
<!-- 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>

声明式编程

  1. 改造语言包
// lang/zh-CN.js
export default {
  welcome: "欢迎",
  button: "提交"
};
  1. 创建 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;
};
  1. 组件里使用
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>
  );
}

✅ 优点

  1. 响应式切换:
    • 切换语言时,不需要刷新浏览器,页面瞬间变化。这是 SPA (单页应用) 的标准体验。
  2. 开发体验极佳:
    • 代码里直接写 {locale.home.title},有 TypeScript 甚至还能有智能提示。
    • 完全符合 React 的声明式编程思维,心智负担小。
  3. 强大的插值能力:
    • 处理 "Welcome, {name}" 这种带参数的文案非常简单,写个简单的 replace 函数即可,或者直接在组件里拼接。
  4. 组件化支持:
    • 可以轻松翻译组件的 Props,比如 <Input placeholder={locale.searchPlaceholder} />,这是原生 DOM 方案很难做到的。

❌ 缺点

  1. 渲染性能开销:
    • 因为语言 Context 通常包裹在最顶层 (<App>)。一旦语言切换,整个 React 组件树都会重新渲染。
    • 注:对于绝大多数应用,这个开销是可以忽略不计的,除非你的页面极其庞大且没有做任何 memo 优化。
  2. 包体积问题(需优化):
    • 如果直接 import zh from './zh-CN',无论你用不用,所有语言包都会被打包进主 JS 里。
    • 解决方案:需要配合 React.lazy 或动态 import() 实现语言包的按需加载。

方案二: React + react-i18next

核心:基于 i18next 核心引擎,提供专属 Hooks,支持懒加载、SSR,适配 React 函数组件,步骤如下

  1. 安装依赖
npm install i18next react-i18next i18next-http-backend --save
# i18next-http-backend:支持远程加载配置文件,可选
  1. 初始化 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;
  1. 全局引入(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 />);
  1. 组件内使用
// 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。切换到英文时,再下载英文包。首屏体积大幅减小。

✅ 优点

  1. 生态霸主:i18next 是 JS 界最成熟的国际化库,几乎没有它解不了的 bug。
  2. 性能优化完备:
    • 按需加载:不用一次性把 10MB 的翻译文件全加载进来。
    • Memoization:内部做了缓存优化,比手写的 Context 性能更好。
    • SSR 支持:完美支持 Next.js / Remix 等服务端渲染框架。
  3. 功能极其强大:
    • 复数处理:自动处理英文的单复数(apple vs apples)。
    • 上下文:支持基于上下文的翻译(比如“行”:行走的行 vs 银行的行)。
    • 嵌套引用:一个翻译里可以引用另一个翻译 key。
  4. 安全:自带 XSS 防护,默认转义 HTML 字符。

❌ 缺点

  1. 配置繁琐:相比于手写一个简单的 Context,i18next 需要引入 2-3 个包,配置一大堆参数(backend, detector, interpolation...)。
  2. 包体积:虽然支持懒加载语言包,但 i18next 核心库本身有一定体积(约 15-20KB gzipped),对于追求极致轻量的微型项目来说有点重。
  3. TypeScript 支持略繁琐:要想获得完美的类型提示(键入 t(' 时自动提示 user.welcome),需要写额外的 d.ts 类型声明文件来覆盖默认类型,新手容易卡住。

方案三: React + react-intl (FormatJS)

核心:雅虎(Yahoo!)开源,基于 Web 标准(ICU 语法),强调数据格式化(日期、货币),采用组件化风格,适合对国际化标准要求极高的金融 / B端项目。

  1. 安装依赖
npm install react-intl --save
  1. 全局配置与注入(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 />);
  1. 组件内使用

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>
  );
}

✅ 优点

  1. 标准化:严格遵循 ICU 国际标准语法,处理复数、性别等复杂语法极其精准。
  2. 格式化能力最强:内置了对日期、时间、数字、货币、百分比的专业格式化,这是 i18next 相对较弱的地方(虽然 i18next 也有插件,但不如这个原生)。
  3. 代码即文档:鼓励在代码中写 defaultMessage,看代码就能知道这里原本显示的文案,不用去翻 json 文件。
  4. 工具链完善:自动提取消息的 CLI 工具非常成熟,适合多人协作的大型项目。

❌ 缺点

  1. 代码啰嗦:大量使用 <FormattedMessage /> 组件,导致 JSX 结构变得很深、很重,不如 t('') 函数简洁。
  2. 包体积:由于包含了大量 Polyfill(为了兼容旧浏览器对 Intl API 的支持),体积相对较大。
目录 · 收起