i18n 居然还能玩出花(i18n汉化模组怎么用)

boyanx2周前技术教程2

众所周知,前端项目想接入国际化,基本就是 i18n 这个方案。

吐槽一下,i18n 的官网居然不支持国际化,仿佛买剪刀来拆包装盒,发现剪刀外面也有包装

i18n 的使用方式如下:

public/
│ └─ locales/
│ ├── en.json # 英文语言文件
│ └── zh.json # 中文语言文件
// en.json
{
"title": "Welcome to the i18n Demo",
"description": "This is a simple i18n demo in React."
}
// zh.json
{
"title": "欢迎来到 i18n 演示",
"description": "这是一个简单的 React i18n 演示。"
}
const App = () => {
const { t } = useTranslation();
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">{t("title")}</h1>
<p className="mb-2">{t("description")}</p>
</div>
);
};

但是这样有几个缺点:

  1. 中文和英文被分开,无法第一时间对应
  2. 要起很多变量名, title/description 等
  3. 看到变量名有时候也想不起来这里原本是什么文案,需要二次搜索
  4. 假如变量名书写错误,网页会直接显示变量名而非 英文/中文 名
  5. 冗余很多,比如上述的 title 要写三遍,不管是改还是写都很麻烦,尤其是需要翻译的东西很多的时候,是真痛苦吧
  6. 默认就要额外加载 json

如果你项目中遇到了这些痛点,不妨继续看下去

设想

稍加思索一下,如果将架构改造成中英文键值对形式,变量名直接使用中文,也许更简单好用

public/
│ └─ locales/
│ ├── default.json # 语言文件
{
"欢迎来到 i18n 演示": "Welcome to the i18n Demo",
"这是一个简单的 React i18n 演示。": "This is a simple i18n demo in React."
}
const App = () => {
const { t } = useTranslation();
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">{t("欢迎来到 i18n 演示")}</h1>
<p className="mb-2">{t("这是一个简单的 React i18n 演示。")}</p>
</div>
);
};

这种方案有一下优势:

  1. 不需要多个文件来回切换,在一个文件中写中英文
  2. 中英文对照明确
  3. 在国际化失败的情况下仍然正常能显示中文而非变量名
  4. 不需要起乱七八糟的变量名
  5. 改造原有代码非常快捷,只需要套一层 t 函数即可

方案调研

那么好,要怎么实现才能达成这种效果呢?我本来想自己写个脚本将键值对转化为对应的 json,但是咨询过 ai 后,发现有更简单的方案。

我们可以在 i18n 上找到init 函数文档,其负责初始化工作


所以要做的就是在 init 中打入自己的逻辑。

受益于 i18n 漂亮的设计,我们可以在 parse 处理掉大部分转化工作,参考如下代码

import i18n, { InitOptions } from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
const i18nConfig: InitOptions = {
supportedLngs: ['zh', 'en'],
load: 'languageOnly',
backend: {
loadPath: (lng: string) => {
if (lng.includes('zh')) {
return ''; // 中文的时候不加载 json,直接让他显示变量名
}
return '/locales/{{ns}}.json';
},
parse: (data: string) => {
const parsed = JSON.parse(data) as Record<string, string>;
const lang = i18n.language;
const resolveTranslations = (translations: Record<string, string>) => {
return Object.keys(translations).reduce(
(acc, key) => {
if (lang.includes('zh')) {
acc[key] = key; // 中文显示键
} else {
acc[key] = translations[key]; // 英文显示值
}
return acc;
},
{} as Record<string, string>,
);
};
return resolveTranslations(parsed);
},
},
};
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init(i18nConfig);

实现

这样的国际化写起来极其方便且不易出错,贴个项目中的图


而且通过设定了加载路径,让中文去请求空路径,实现取消掉中文的 json 请求

可以观察到,我们在中文情况下完全不会去加载 json



当切换成英文时,才会进行额外的资源加载


快捷提取

最后还有一个扫描函数,可以提取 目标文件夹 中的所有文件的 t函数

const fs = require('fs');
const path = require('path');
const outputPath = path.join(__dirname, 'output.json');
const tempPath = path.join(__dirname, 'output_temp.json');
const collectedKeys = new Set();
// 匹配 t('xxx') / t("xxx") / t(`xxx`),支持换行
const tRegex = /\bt\s*\(\s*(['"])([\s\S]*?)\1\s*,?\s*\)/g;
/**
* 从文件中提取 t('...') 字符串
*/
function extractStringsFromFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const keys = new Set();
let match;
while ((match = tRegex.exec(content)) !== null) {
const key = match[2].trim(); // 去除多余空格和换行
keys.add(key);
collectedKeys.add(key);
}
if (keys.size > 0) {
const obj = {};
Array.from(keys).forEach((key) => {
obj[key] = '';
});
fs.appendFileSync(tempPath, `${JSON.stringify(obj, null, 2)}\n\n`);
}
}
/**
* 递归遍历目录
*/
function traverseDir(dirPath) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
entries.forEach((entry) => {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
traverseDir(fullPath);
} else if (entry.isFile()) {
extractStringsFromFile(fullPath);
}
});
}
/**
* 读取已有的翻译文件(json)
* @param {string} filePath
* @returns {object}
*/
function readExistingTranslations(filePath) {
if (fs.existsSync(filePath)) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (e) {
console.warn(` 读取已有翻译文件失败: ${e.message}`);
}
}
return {};
}
/**
* 写入最终去重后的 output.json
* @param {string} existingTranslationPath - 可选,已有翻译文件路径
*/
function writeFinalOutput(existingTranslationPath) {
const finalOutput = {};
const existingTranslations = readExistingTranslations(existingTranslationPath);
Array.from(collectedKeys).forEach((key) => {
finalOutput[key] = existingTranslations[key] || '';
});
fs.writeFileSync(outputPath, JSON.stringify(finalOutput, null, 2), 'utf-8');
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
}
/**
* 主程序入口
* @param {string} targetDir - 需要扫描的目录
* @param {string} [existingTranslationPath] - 可选,已有翻译 JSON 文件路径
*/
function main(targetDir, existingTranslationPath) {
fs.writeFileSync(tempPath, '', 'utf-8');
traverseDir(targetDir);
writeFinalOutput(existingTranslationPath);
console.log(' 提取完成,结果已保存至 output.json');
}
// 调用示例(请根据实际替换路径)
const targetDir = 'xxxx';
const existingTranslationPath = 'xxxxxx';
main(targetDir, existingTranslationPath);


如此一来,我们做 i18n 只需要三步:

  1. 给所有需要翻译的地方,使用 t 函数包裹目标文案
  2. node 跑一下这个提取函数(目标文件夹设定为 src 即可),就能获得完整 json
  3. 将 output 文件翻译并应用(翻译这一步可以用 cursor 之类的,巨快)

另外,如果想更新而非新建,可以在提取函数中添加已有翻译 JSON 文件路径,这样就可以完成更新,没有翻译的键会置空,翻译过的键会保留。

使用这个方案,可以大大加速项目中的 i18n 进程~

相关文章

从 Element UI 源码的构建流程来看前端 UI 库设计

作者:前端森林转发链接:https://mp.weixin.qq.com/s/ziDMLDJcvx07aM6xoEyWHQ引言由于业务需要,近期团队要搞一套自己的UI组件库,框架方面还是Vue。而业界...

深资架构师经验分享,Spring源码解析:高级容器的扩展内幕

本篇我们一起来详细探究一下基于 ApplicationContext 的容器初始化和注入过程,至于 ApplicationContext 的使用方式,广大开发者应该是信手拈来,这里还是简单的举例一下:...

Spring Boot 3.4国际化进阶:MessageSource的动态刷新秘籍

背景与痛点Spring Boot 3.4 对国际化(i18n)的支持已非常成熟,但在实际生产环境中,静态的 messages.properties 文件难以满足动态配置需求。例如,多语言文本需频繁更新...

推荐一款经典的.NET后台管理系统(.net 系统)

基于.NET5/.Net7 + vue2.x/vue3.x前后端分离的.net快速开发框架概述本项目适合有一定NetCore和 vue基础的开发人员基于.NET5/.NET7实现的通用权限管理平台(R...

SpringMVC流程及源码分析(springmvc流程和原理)

前言学了一遍SpringMVC以后,想着做一个总结,复习一下。复习写下面的总结的时候才发现,其实自己学得并不彻底、牢固、也没有学全,视频跟书本是要结合起来一起,每一位老师的视频可能提到的东西都不一致,...

前端大屏原理系列:高性能拖拽系统的实现

本文是《前端大屏原理系列》第一篇:高性能拖拽系统的实现。本系列所有技术点,均经过本人开源项目 react-big-screen 实际应用,欢迎 star (*'▽`)ノノ.一、效果演示二、拖拽移动点击...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。