背景
在开发面向全球用户的网页时,需要:
- 支持多语言(英文、中文等)
- 按环境(生产/测试/开发)加载不同翻译资源
- 语言切换时保持当前页面路径
- 自动检测用户语言偏好
- 动态加载翻译资源,避免打包体积过大
技术选型
采用 i18next + react-i18next 生态:
i18next:核心库react-i18next:React 集成i18next-http-backend:HTTP 后端加载@18n-language-detect:语言检测
核心实现
1. i18n 初始化配置
typescript
// src/i18n/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "@i18n-language-detect";//需npm上找替代包
import HttpBackend from "i18next-http-backend";
import Backend from "./backend";
import { isTestnet, isProd } from "env";
// 根据环境动态生成资源路径
const fullPath = () => {
if (isProd) {
return "i18n-prod.json";
} else if (isTestnet) {
return "i18n-testnet.json";
} else {
return "i18n.json";
}
};
const bundledResources = {};
i18n
.use(Backend) // 自定义后端
.use(LanguageDetector) // 语言检测
.use(initReactI18next) // React 集成
.init({
lng: "en", // 默认语言
fallbackLng: "en", // 回退语言
load: "currentOnly", // 只加载当前语言
backend: {
backends: [HttpBackend],
bundledResources,
backendOptions: [{ loadPath: fullPath() }],
},
ns: ["example"], // 命名空间
defaultNS: "example",
jsonFormat: "v4",
interpolation: {
escapeValue: false, // 不转义 HTML
},
});
export default i18n;
notes:1.通过 fullPath() 按环境返回不同资源路径 2.使用 HTTP 后端,按需加载 3.支持多命名空间管理
2. 自定义 Backend 实现
typescript
// src/i18n/backend.ts
class Backend {
constructor(services, options = {}) {
this.backends = [];
this.type = 'backend';
this.init(services, options);
}
init(services, options = {}, i18nextOptions) {
this.services = services;
this.options = utils.defaults(options, this.options || {}, getDefaults());
// 支持多个后端,按顺序尝试加载
this.options.backends &&
this.options.backends.forEach((b, i) => {
this.backends[i] = this.backends[i] || utils.createClassOnDemand(b);
this.backends[i].init(
services,
(this.options.backendOptions && this.options.backendOptions[i]) || {},
i18nextOptions
);
});
}
read(language, namespace, callback) {
// 按顺序尝试从各个后端加载资源
const loadPosition = (pos) => {
if (pos >= this.backends.length) {
return callback(new Error('non of the backend loaded data;', true));
}
const backend = this.backends[pos];
if (backend.read) {
const resolver = (err, data) => {
if (!err && data && Object.keys(data).length > 0) {
callback(null, data, pos);
// 保存到前面的后端(如本地缓存)
savePosition(pos - 1, data);
} else {
// 尝试下一个后端
loadPosition(pos + 1);
}
};
// 处理 Promise 或回调
const result = backend.read(language, namespace);
if (result && typeof result.then === 'function') {
result.then((data) => resolver(null, data)).catch(resolver);
} else {
resolver(null, result);
}
} else {
loadPosition(pos + 1);
}
};
loadPosition(0);
}
}
优势:支持多后端,可配置多个后端,按顺序尝试;一个失败时自动尝试下一个;同时支持将远程资源保存到本地缓存
3. 路由集成多语言路径
typescript
// src/App.tsx
import { Routes, Route } from "react-router-dom";
import { routesMap } from "./pages/constants";
function App() {
return (
<Routes>
{/* 默认路由(无语言前缀) */}
<Route path="/">
<Route index element={<Home/>} />
<Route path={routesMap.example1} element={<Example1 />} />
{/* ... */}
</Route>
{/* 多语言路由(/:lang/...) */}
<Route path={`/:lang`} element={<Home />} />
<Route path={`/:lang/${routesMap.example1}`} element={<Example1 />} />
{/* ... */}
</Routes>
);
}
4. 语言切换组件
typescript
// src/components/languageChange/index.tsx
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { getLang, setLang } from "@/utils/storageData";
import { routesMap } from "@/pages/constants";
export default function LanguageChange() {
const navigate = useNavigate();
const [t, i18n] = useTranslation("example");
const { lang } = useParams();
const langList = [
{ name: "EN", key: "en" },
{ name: "中文", key: "zh-MY" },
];
// 切换语言并更新 URL
const urlLangChange = (paramLang: any) => {
let currentlang = paramLang;
// 验证语言是否有效
if (!paramLang || !langList.map((item) => item.key).includes(paramLang)) {
currentlang = getLang() || "en";
}
// 提取当前路径(去除语言前缀)
let pathSegments = window.location.pathname.split("/").slice(1).join("/");
if (
window.location.pathname.startsWith("/zh-MY") ||
window.location.pathname.startsWith("/en")
) {
pathSegments = window.location.pathname.split("/").slice(2).join("/");
}
// 验证路径是否在路由表中
let currentPath = pathSegments;
const isPathNotInMap = Object.values(routesMap).includes(pathSegments);
if (!isPathNotInMap) {
currentPath = "";
}
// 更新 URL,保持当前路径
navigate(
`/${currentlang}${currentPath && `/${currentPath}`}${
window.location.search && window.location.search
}`,
{ replace: true }
);
// 切换 i18n 语言
handleLangChange(currentlang);
};
const handleLangChange = (newLang: string) => {
i18n.changeLanguage(newLang, () => {
document?.querySelector("html")?.setAttribute("lang", newLang);
});
setLang(newLang); // 保存到本地存储
};
// 初始化时根据 URL 参数设置语言
useEffect(() => {
urlLangChange(lang);
}, []);
return (
<div className="lang-icon">
{langList.map((item) => (
<p
className={item?.key === getLang() ? "active" : ""}
onClick={() => urlLangChange(item?.key)}
>
{item?.name}
</p>
))}
</div>
);
}
要点:
- URL 同步:切换语言时更新 URL,保持当前路径
- 路径验证:确保路径在路由表中
- 本地存储:保存用户语言偏好
- HTML lang 属性:同步更新
<html lang>
5. 本地存储管理
typescript
// src/utils/storageData.ts
import { storage } from "@storage";//获取本地存储的包,需在npm上找替代包
const LANG_KEY = "LANG_KEY";
export function getLang() {
const lang = storage.get(LANG_KEY) || "en";
return lang;
}
export function setLang(lang: string) {
storage.set(LANG_KEY, lang);
}
6. 在组件中使用翻译
typescript
// 示例:在组件中使用
import { useTranslation } from "react-i18next";
export default function Banner() {
const [t] = useTranslation();
const list = [
{
id: "one",
hint: t("exclusiveEventDesc1"), // 使用翻译
// ...
},
];
return (
<div>
<h1>{t("pageTitle")}</h1>
{/* ... */}
</div>
);
}
技术优势
不仅实现了3种环境的隔离,在性能上,i18n的翻译是按需加载的,减少初始包体积,支持运行时更新翻译资源,而且用户切换语言时保持当前页面
最佳实践
1. 资源文件组织
bash
└── example/
├── en.json # 生产环境英文
├── zh-MY.json # 生产环境中文
├── testnet-en.json # 测试环境英文
└── testnet-zh-MY.json # 测试环境中文
2. 翻译 Key 命名规范
json
{
"metaTitle": "example",
"pageTitle": "Welcome to example",
"chooseLanguage": "Choose Language"
}
完整流程图
markdown
用户访问页面
↓
语言检测器检测语言(浏览器/URL/本地存储)
↓
根据环境加载对应资源路径
↓
HTTP Backend 请求翻译资源
↓
加载成功 → 渲染页面
↓
用户切换语言
↓
更新 i18n 语言 + 更新 URL + 保存到本地存储
↓
重新加载对应语言资源
↓
页面更新
