今天,让我来分享一下自己对于 React i18n 的理解,并使用核心 lib: i18next和 react-i18next从零开始配置解决方案。
react-i18next版本为:16.1.5,因此相关文档需要看 LeGACY v9 的部分:https://react.i18next.com/legacy-v9/step-by-step-guide
前者是一个与框架无关的国际化库,无论 nodejs 和浏览器端都可以在工程化中使用。后者是 React 的适配层,且是基于 i18next的绑定层,有了它我们就可以在 React中轻松添加 i18n 支持。
项目仓库:AaronConlon/react-i18n-best-practice
我的 i18n 方案实现了以下若干需求:
先看看项目目录结构:
.
├── node_modules
├── package.json
├── README.md
├── rsbuild.config.ts
├── src
│ ├── App.tsx
│ ├── env.d.ts
│ ├── i18n.ts
│ ├── index.tsx
│ ├── routes
│ │ ├── __root.tsx
│ │ ├── about.tsx
│ │ └── index.tsx
│ ├── routeTree.gen.ts
│ ├── styles.css
│ └── utils
├── tsconfig.json
└── yarn.lock
首先创建 src/i18n.ts初始化文件:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
const resources = {
// 语言
en: {
// 默认命名空间
translation: {
"Welcome to React": "Welcome to React and react-i18next",
},
},
fr: {
translation: {
"Welcome to React": "Bienvenue à React et react-i18next",
},
},
};
i18n.use(initReactI18next).init({
resources,
// 默认语言
lng: "en",
// 备用语言
fallbackLng: "en",
// 插值方式
interpolation: {
// 不转义 html 标签,避免 XSS 攻击
escapeValue: false,
},
});
export default i18n;
并且在 src/index.tsx中引入:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
// 导入,初始化 i18n
import "./i18n";
const rootEl = document.getElementById("root");
if (rootEl) {
const root = ReactDOM.createRoot(rootEl);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
然后看看 routes下的几个路由文件:
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
const RootLayout = () => {
const { i18n } = useTranslation();
return (
<>
<div className="header">
<Link to="/" className="header-link">
Home
</Link>{" "}
<Link to="/about" className="header-link">
About
</Link>
{/* language selector */}
<select
className="language-selector"
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="en">English</option>
<option value="fr">French</option>
</select>
</div>
<Outlet />
</>
);
};
export const Route = createRootRoute({ component: RootLayout });
通过 useTranslation hook 可以得到 i18n实例,这个实例具有当前语言属性和修改语言的方法。
接下来看 src/routes/index.tsx的内容:
import { createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/")({
component: RouteComponent,
});
function RouteComponent() {
const { t } = useTranslation();
return <div className="content">{t("Welcome to React")}</div>;
}
还是核心 hook:useTranslation,通过 t函数传入翻译资源的字段来读取翻译的结果。
如上所示是 react-i18next 最常规的使用方法,还有几种写法和在 react 组件外获取翻译内容,有兴趣可以去看文档
现在,在 /路由下点击语言切换即可看到效果:
英语:

法语

如果仅需实现持久化,则简单在 i18n.ts这里直接将硬编码的默认语言改为从 localStorage 或 cookie 里读取即可,如此一来既可以让后端接口返回自动修改用户语言,也可以在修改语言的时候将选择的语言保存到本地。
我个人更喜欢另一种方案:i18next-browser-languagedetector插件,根据用户浏览器的语言来设置默认语言!
首先,安装好插件:
yarn add i18next-browser-languagedetector
然后再修改 i18n.ts初始化逻辑(忽略部分代码):
import detector from "i18next-browser-languagedetector";
i18n
.use(detector)
.use(initReactI18next)
.init({
resources,
// 备用语言
fallbackLng: "en",
// 插值方式
interpolation: {
// 不转义 html 标签,避免 XSS 攻击
escapeValue: false,
},
});
测试的浏览器语言是中文,因此在代码里加上中文相关的 resource 翻译和 select option 即可。
此时,浏览器的 localStorage 里会自动加上 i18nextLng ,其值为 zh。
i18next-browser-languagedetector 选择语言的默认优先级如下:
?lng=LANGUAGE to URL)因此,笔者建议将语言选项的值保存到 cookie。
产品应该减少用户的操作,并且提供一种“顺畅”的用户体验。
企业级项目代码量较为庞大,将 resource 翻译的代码写在 i18n.ts里实在是太过拥挤,因此我们必须将这些翻译资源拆分到不同的模块下,通过 json 的格式保存,再统一导入。
看代码:
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import cn from "./locales/cn.json";
import en from "./locales/en.json";
import fr from "./locales/fr.json";
const resources = {
en: {
translation: en,
},
fr: {
translation: fr,
},
cn: {
translation: cn,
},
};
i18n
.use(detector)
.use(initReactI18next)
.init({
resources,
// 备用语言
fallbackLng: "en",
// 插值方式
interpolation: {
// 不转义 html 标签,避免 XSS 攻击
escapeValue: false,
},
});
export default i18n;
首先,我们将翻译资源统一放在 src/locales下,按语言命名通过 json 导入,然后赋值给 translation(默认命名空间)。
当项目进一步扩大之后,翻译 json 文件将会变得越来越大,无论是查找还是修改都非常麻烦(在一个超大 json 文件中增删改查)。这时候,我们可以将翻译文件分为若干模块:
这时候我们可以在 locales下创建多语言目录,并且在目录内创建若干个命名空间对应的 json文件:
.
├── en
│ ├── common.json
│ ├── dashboard.json
│ └── user.json
├── fr
│ ├── common.json
│ ├── dashboard.json
│ └── user.json
└── zh
├── common.json
├── dashboard.json
└── user.json
举例 zh/common.json:
{
"welcome": "欢迎",
"loading": "加载中...",
"error": "错误",
"success": "成功"
}
在组件中通过命名空间使用:
import { createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/")({
component: RouteComponent,
});
function RouteComponent() {
const { t } = useTranslation(["common", "user", "dashboard"]);
return (
<div className="content">
{/* common namespace */}
<div className="common">{t("common:welcome")}</div>
<div className="common">{t("common:loading")}</div>
<br />
{/* user namespace */}
<div className="user">{t("user:profile")}</div>
{/* dashboard namespace */}
<div className="dashboard">{t("dashboard:overview")}</div>
</div>
);
}
这里的钩子函数参数非常关键:useTranslation(["common", "user", "dashboard"]),不同的参数会设置不同的默认命名空间,当设置了默认命名空间的时候,就可以在 t()函数里缺省命名空间前缀。
例如:
import { createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/")({
component: RouteComponent,
});
function RouteComponent() {
const { t } = useTranslation(["common"]);
return (
<div className="content">
{/* common namespace */}
<div className="common">{t("common:welcome")}</div>
<div className="common">{t("loading")}</div>
<br />
{/* user namespace */}
<div className="user">{t("user:profile")}</div>
{/* dashboard namespace */}
<div className="dashboard">{t("dashboard:overview")}</div>
</div>
);
}
直接写 t("loading")是可以得到翻译结果的,因为指定了 common命名空间。此时如果你写 t("profile"),那么是得不到翻译结果的。
如果不传参给 useTranslation,那么就会从 i18n.ts里的默认命名空间去寻找翻译结果。
函数 t()根据传入的 key 去获取翻译自有一套规则,并且可能会根据版本变化而更新。
与其花时间去折腾其规则,不如创建多个指定命名空间且名字不同的 t函数,亦或者统一在 t()函数传参的时候写明命名空间。
import { createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/")({
component: RouteComponent,
});
function RouteComponent() {
const { t } = useTranslation();
const { t: tUser } = useTranslation("user");
const { t: tDashboard } = useTranslation("dashboard");
return (
<div className="content">
{/* common namespace */}
<div className="common">{t("welcome")}</div>
<div className="common">{t("loading")}</div>
<br />
{/* user namespace */}
<div className="user">{tUser("profile")}</div>
{/* dashboard namespace */}
<div className="dashboard">{tDashboard("overview")}</div>
</div>
);
}
如果你也使用 TypeScript 编写 React 代码,那么我想你会喜欢在使用 t()函数的时候填写 key获得编辑器的自动提示支持。
实现方案如下:
i18n.ts文件src/i18next.d.ts类型声明文件首先来看新的 i18n.ts文件:
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import zhCommon from "./locales/zh/common.json";
import zhDashboard from "./locales/zh/dashboard.json";
import zhUser from "./locales/zh/user.json";
import enCommon from "./locales/en/common.json";
import enDashboard from "./locales/en/dashboard.json";
import enUser from "./locales/en/user.json";
import frCommon from "./locales/fr/common.json";
import frDashboard from "./locales/fr/dashboard.json";
import frUser from "./locales/fr/user.json";
export const defaultNS = "common";
export const resources = {
en: {
common: enCommon,
user: enUser,
dashboard: enDashboard,
},
fr: {
common: frCommon,
user: frUser,
dashboard: frDashboard,
},
zh: {
common: zhCommon,
user: zhUser,
dashboard: zhDashboard,
},
} as const;
i18n
.use(detector)
.use(initReactI18next)
.init({
resources,
ns: ["common", "user", "dashboard"],
defaultNS,
// 备用语言
fallbackLng: "en",
// 插值方式
interpolation: {
// 不转义 html 标签,避免 XSS 攻击
escapeValue: false,
},
});
export default i18n;
变化在于:
as const来让 TypeScript 断言,告诉编译器这个对象缩窄后的类型。再看类型声明文件:i18next.d.ts:
import { defaultNS, resources } from "./i18n";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: {
common: (typeof resources)["en"]["common"];
user: (typeof resources)["en"]["user"];
dashboard: (typeof resources)["en"]["dashboard"];
};
}
}
给 module i18next的 CustomTypeOptions接口声明默认命名空间和 resource 的类型,如此一来就可以在 t()这里自动补全 key了。
直接通过 import语句导入 json文件,在打包的时候会把全部翻译资源打包到代码中去,显著增加 bundle尺寸。
在开始之前,明确自己的场景和需求,回答以下几个问题:
得出答案之后,我们逐一分析以下场景选择的技术栈和实现方案:
通常企业后台对 SEO、首屏加载速度、弱网和离线、强可用都要求不高,选择 i18next-resources-to-backend插件足以。
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next";
i18n
.use(detector)
.use(
resourcesToBackend(
(lang: string, ns: string) => import(`./locales/${lang}/${ns}.json`)
)
)
.use(initReactI18next)
.init({
ns: ["common"],
defaultNS: "common",
// 备用语言
fallbackLng: "en",
// 插值方式
interpolation: {
// 不转义 html 标签,避免 XSS 攻击
escapeValue: false,
},
});
export default i18n;
所有初始化时设置的 ns都会在首次加载直接请求对应的 chunk文件,如上所示我的测试环境会立即请求:
http://localhost:3000/static/js/async/src_locales_en_common_json.js
回过头来看,我们的目标还有一个 React Suspense 特性需要支持。为什么要这个特性?在本地开发的时候资源加载极快,你可能一不小心没注意到翻译的结果会出现闪烁,究其原因在于 i18next-react 在初次渲染的时候还没有获取到翻译的资源,于是会立即渲染对应的 key 的字符串内容。
所以,我们应该在这个阶段提供特定的 UI 来防止闪烁,看看如下代码:
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
const RootLayout = () => {
const { i18n, t } = useTranslation("menu");
return (
<>
<div className="header">
<Link to="/" className="header-link">
{t("home")}
</Link>{" "}
<Link to="/about" className="header-link">
{t("about")}
</Link>
<Link to="/test" className="header-link">
{t("test")}
</Link>
{/* language selector */}
<select
className="language-selector"
value={i18n.language}
onChange={async (e) => {
const newLang = e.target.value;
// save to cookie
// document.cookie = `i18nextLng=${newLang}; path=/`;
await i18n.changeLanguage(newLang);
}}
>
<option value="en">English</option>
<option value="fr">French</option>
<option value="zh">中文</option>
</select>
</div>
<Outlet />
</>
);
};
export const Route = createRootRoute({
component: RootLayout,
pendingComponent: () => (
<div className="loading">Loading at root layout...</div>
),
});
在创建路由这里添加一个 Suspense fallback(pendingComponents),即可在渲染 RootLayout 组件的时候,让内部逻辑触发 Suspense 边界条件,从而渲染 loading 内容。
rsbuild会将这个 json 文件打包成一个独立的 js文件,此外上述配置还加上了 react use suspense 支持:
{
react: {
useSuspense: true,
}
}
现在,初始化加载语言翻译资源会触发 Suspense fallback 了!当然,上述代码仅在初始化的时候会渲染 fallback,初始化完成后再次切换语言将会请求新的语言翻译 chunk,此时不会触发 Suspense fallback,针对这一点我们可以单独使用 useTransition或者单独维护一个 loading 状态,在 Outlet组件下方渲染独立的 Loading Mask 组件(和 pendingComponent 共用)。
独立服务器意味着资源存放在服务器这里,出海应用的用户可能来自不同的国家和地区,这时候使用 CDN 存放 JSON 翻译资源可以加快初始化和更换语言的速度。
使用 i18next-resources-to-backend依然可以做到这一点,不过我们需要将 import语句修改为 fetch远程 json 资源的逻辑。但是这一步细化一下也有一些问题需要处理,例如:缓存控制和版本处理、出错处理。
在这里我们需要考虑一个问题:翻译 JSON 文件是否需要在 Build 阶段构建成 js chunk,亦或是将翻译资源独立出来,存放在 OSS 上,利用 CDN 的能力提速,同时还可以灵活更新 OSS 文件,让应用不需要耗时构建整个应用。
有一个插件能很好地处理这个需求:i18next-http-backend,其核心是:让语言包成为“外部资源”,让国际化系统想加载数据一样加载翻译。
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
i18n
.use(detector)
.use(HttpBackend)
.use(initReactI18next)
.init({
backend: {
loadPath:
import.meta.env.PUBLIC_I18N_PATH + "locales/{{lng}}/{{ns}}.json",
crossOrigin: true,
queryStringParams: {
v: import.meta.env.PUBLIC_I18N_VERSION,
},
},
ns: ["common", "menu"],
defaultNS: "common",
// 备用语言
fallbackLng: "en",
// 插值方式
interpolation: {
// 不转义 html 标签,避免 XSS 攻击
escapeValue: false,
},
react: {
useSuspense: true,
},
});
export default i18n;
其核心在于配置项:
backend: {
loadPath:
import.meta.env.PUBLIC_I18N_PATH + "locales/{{lng}}/{{ns}}.json",
crossOrigin: true,
queryStringParams: {
v: import.meta.env.PUBLIC_I18N_VERSION,
},
}
现在,我们可以将 /locales 目录移动到 public 下
我们通过环境变量去设置翻译文件的来源,在开发模式下读取'/'(public)的翻译文件,在生产环境读取 CDN 地址的资源。
如果项目复杂,我们也可以通过 API 获取用户更多信息,在这里修改其翻译资源的来源和版本等配置。
在用户请求 CDN 资源的时候,指定 queryStringParams 中的对象,通过这个机制来控制缓存,CDN 服务器根据 URL 来区分是否返回缓存,当然浏览器也一样。
我们可以通过一个 API 后端服务来统一下发用户的资源版本号和 CDN 地址(如果有必要的话),这个后端 API 既可以是独立于不同项目的,也可以是这个项目下配合的后端服务。
如果想做灰度测试,或者通过某个用户来测试线上环境的翻译效果,则可以单独给这个用户增加版本,从而触发用户浏览器缓存失效,最终从用户浏览器,经过 CDN 服务器,最终回源到 OSS 去请求最新的文件。
假设开发一个离线应用,亦或是可以读取到载体的文件系统的应用,我们可以将翻译资源通过主动下载的方式保存到用户的设备。
我们可以选择这个插件:i18next-fs-backend
import i18n from 'i18next'
import FsBackend from 'i18next-fs-backend'
import { initReactI18next } from 'react-i18next'
import path from 'path'
import { fileURLToPath } from 'url'
// __dirname 兼容 ESM
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// 语言文件目录(绝对路径)
const localesPath = path.join(__dirname, './locales')
i18n
.use(FsBackend)
.use(initReactI18next)
.init({
backend: {
loadPath: path.join(localesPath, '{{lng}}/{{ns}}.json'),
},
lng: 'zh', // 默认语言
fallbackLng: 'en',
ns: ['common', 'dashboard'],
defaultNS: 'common',
interpolation: {
escapeValue: false, // React 已自动防 XSS
}
})
export default i18n
离线应用的语言 JSON 都从 fs 直接读取,不依赖 HTTP 也不需要 CDN。
如果是内网应用,则可以提供一个服务上传翻译资源,再通过 fs 写入到指定的目录。
这些插件的写法非常相似,切换起来很容易。
假设...
那么,我建议使用这个方案:
import i18n from 'i18next'
import Backend from 'i18next-chained-backend'
import LocalStorageBackend from 'i18next-localstorage-backend'
import HttpBackend from 'i18next-http-backend'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next'
const CDN_PRIMARY = 'https://cdn1.yourcompany.com/locales'
const CDN_SECONDARY = 'https://cdn2.backupcdn.com/locales'
const LOCAL_VERSION = '2025.10.26' // 版本控制:可由 CI/CD 自动注入
i18n
.use(Backend)
.use(initReactI18next)
.init({
lng: 'zh',
fallbackLng: 'en',
ns: ['common', 'menu', 'dashboard'],
defaultNS: 'common',
backend: {
// 1️⃣ 多层后端配置(顺序:缓存 → 主 CDN → 备 CDN → 本地 fallback)
backends: [
LocalStorageBackend,
HttpBackend,
HttpBackend,
resourcesToBackend((lng, ns) => import(`../locales/${lng}/${ns}.json`)),
],
backendOptions: [
// --- LocalStorage 缓存层 ---
{
expirationTime: 7 * 24 * 3600 * 1000, // 缓存 7 天
versions: {
common: LOCAL_VERSION,
menu: LOCAL_VERSION,
dashboard: LOCAL_VERSION,
},
},
// --- 主 CDN ---
{
loadPath: `${CDN_PRIMARY}/{{lng}}/{{ns}}.json`,
crossDomain: true,
queryStringParams: { v: LOCAL_VERSION },
},
// --- 备用 CDN ---
{
loadPath: `${CDN_SECONDARY}/{{lng}}/{{ns}}.json`,
crossDomain: true,
queryStringParams: { v: LOCAL_VERSION },
},
// --- 本地 fallback ---
{}, // resourcesToBackend 不需要额外配置
],
},
interpolation: { escapeValue: false },
react: { useSuspense: true },
})
export default i18n
上述方案的功能如下:
i18next-chained-backend按顺序执行版本的控制和缓存更新可以参考之前提到过的方案(版本服务 API),多 CDN 的目标是让用户先从更稳定、更靠近用户的地方的服务商请求数据。
与此同时,应用也可以考虑支持一个清理换存的服务,让用户主动触发。
开发过程中修改翻译资源容易遗漏某个语言的信息,很有必要添加一个 script 在提交 Git 记录的时候提前检测翻译资源,以免字段没有对齐。
首先,添加相关 scripts 到 package.json 里:
{
"scripts": {
"check:i18n": "node scripts/check-i18n-consistency.js",
"prepare": "husky install"
}
}
创建在根目录的 scripts 目录下编辑 check-i18n-consistency.js:
#!/usr/bin/env node
/**
* 检查 /locales/en/*.json 是否在其他语言下都有对应文件,
* 并确保 JSON key 一致。
*/
import fs from "fs"
import path from "path"
const BASE_LANG = "en"
const LOCALES_DIR = path.resolve("./public/locales")
function getAllLangs() {
return fs
.readdirSync(LOCALES_DIR)
.filter((f) => fs.statSync(path.join(LOCALES_DIR, f)).isDirectory())
}
function loadJson(filePath) {
try {
const content = fs.readFileSync(filePath, "utf8")
return JSON.parse(content)
} catch (e) {
console.error(`❌ 无法解析 JSON 文件:${filePath}`)
process.exit(1)
}
}
function getJsonKeys(obj, prefix = "") {
let keys = []
for (const key in obj) {
const full = prefix ? `${prefix}.${key}` : key
if (typeof obj[key] === "object" && obj[key] !== null) {
keys = keys.concat(getJsonKeys(obj[key], full))
} else {
keys.push(full)
}
}
return keys
}
// ----------------- 主逻辑 -----------------
const langs = getAllLangs()
if (!langs.includes(BASE_LANG)) {
console.error(`❌ 缺少基准语言目录:${BASE_LANG}`)
process.exit(1)
}
const baseFiles = fs
.readdirSync(path.join(LOCALES_DIR, BASE_LANG))
.filter((f) => f.endsWith(".json"))
let hasError = false
for (const lang of langs.filter((l) => l !== BASE_LANG)) {
console.log(`🔍 检查语言:${lang}`)
for (const file of baseFiles) {
const baseFile = path.join(LOCALES_DIR, BASE_LANG, file)
const targetFile = path.join(LOCALES_DIR, lang, file)
if (!fs.existsSync(targetFile)) {
console.error(`❌ 缺少 ${lang}/${file}`)
hasError = true
continue
}
const baseJson = loadJson(baseFile)
const targetJson = loadJson(targetFile)
const baseKeys = getJsonKeys(baseJson)
const targetKeys = getJsonKeys(targetJson)
const missing = baseKeys.filter((k) => !targetKeys.includes(k))
const extra = targetKeys.filter((k) => !baseKeys.includes(k))
if (missing.length || extra.length) {
console.error(`❌ ${lang}/${file} 键不一致:`)
if (missing.length)
console.error(` 缺少:${missing.join(", ")}`)
if (extra.length)
console.error(` 多余:${extra.join(", ")}`)
hasError = true
}
}
}
if (hasError) {
console.error("\n🚫 多语言文件结构不一致,请修复后再提交。")
process.exit(1)
}
console.log("✅ 多语言文件检查通过!")
process.exit(0)
接着安装 husky并初始化:
yarn add -D husky
yarn husky install
创建 pre-commit 钩子:
mkdir -p .husky
touch .husky/pre-commit
chmod +x .husky/pre-commit
husky v9 版本更新之后,配置可能有所不同
编辑 pre-commit:
echo "✨ Running lint-staged and i18n check..."
yarn run check:i18n
上述 sh 脚本会在 git 提交的时候执行 check:i18n 这个命令,我们也可以在其他时候执行以下检查字段有没有对齐。
好了,让我们来检查一下是否顺利。
➜ react-i18n-best-practice git:(main) ✗ yarn check:i18n
yarn run v1.22.22
$ node scripts/check-i18n-consistency.js
🔍 检查语言:fr
🔍 检查语言:zh
✅ 多语言文件检查通过!
✨ Done in 0.96s.
➜ react-i18n-best-practice git:(main) ✗
将 locales/zh/common.json修改为:
{
"welcome": "欢迎",
"loading": "加载中...",
"error": "错误",
"successssss": "成功"
}
再提交 git 让 husky 检查一次:
➜ react-i18n-best-practice git:(main) ✗ gacm "chore: test husky"
✨ Running lint-staged and i18n check...
yarn run v1.22.22
$ node scripts/check-i18n-consistency.js
🔍 检查语言:fr
🔍 检查语言:zh
❌ zh/common.json 键不一致:
缺少:success
多余:successssss
🚫 多语言文件结构不一致,请修复后再提交。
error Command failed with exit code 1.
大功告成!
好了,我的 React SPA 多语言方案大致已经梳理清楚了,如果你也有一些不同的见解,欢迎留言交流。
下一篇分析我将给大家带来 React SPA + Antd 的使用分析,通过其 token 机制制定多主题色系统,最终实现一个我们满意的风格。