背景
某天接到需求,需要将会议室系统(pc、移动端)支持国际化;项目从前期就没有规划国际化,因此项目内的所有中文都直接编码在代码块内
TODO
需要将代码内的中文解耦到 src/locales/zh-CN.ts 里面
问题
工作量大、容易出错
方案
1、到一个代码文件内复制中文,到 zh-CN.ts 创建对应的映射
2、比如如下代码, 手动复制如下文本 组件名已经存在,请重新输入
if (fs.existsSync(dir)) {
return {
code: 50002,
msg: '组件名已经存在,请重新输入'
}
}
3、手动创建对象的key value、自己想英文名字
export default { "pages.SpeedSchedule.repeatInput": "组件名已经存在,请重新输入" };
存在问题:一个文件内存在大量的中文文字,单个操作繁琐,工作量大、容易出错、起英文名字困难
思考优化方案:为了解决以上问题,进行一步步优化
1、如何解决大量、单个操作繁琐的问题?
可以在vscode内以文件夹维度进行【正则】搜索中文并且全部复制出来;然后批量再二次处理
粘贴如下然后统一处理,这样将单个操作统一批量操作,如果你的vscode的快捷键用的6的话很快就能够处理完,这样能够提高我们的效率。
再次思考? 虽然这种方式能够简化我们的操作,但是毕竟我们是程序员,肯定不会止步于此,我们还要去解决 工作量大、容易出错、起英文名字困难 问题。
我们最终想要的效果是:通过脚本、指定文件路径、自动生成 src/locales/zh-CN.ts 那么我们需要解决哪些问题?
1、通过NodeJS的fs模块,读取文件 2、处理文件,将中文全部提取出来、并且拿到我们想要的格式;并且要注意 key 是需要翻译的,可能我们还需要一个将中文转变英文的库;
读取文件
fs.readFile('./src/pages/SpeedSchedule/index.tsx', 'utf-8', (err, data) => {
if (err) {
console.error(err);
return;
}
});
在一块代码块中输出连续的中文片段,可以使用正则表达式中的分组功能来实现
let pattern = /([\u4e00-\u9fa5]+)/g;
let match = data.match(pattern);
deleteComment 删除注释的中文
const deleteComment = (code) => {
// 删除注释
let pattern = /\/\/.*/g;
let result = code.replace(pattern, '');
return result;
};
问题优化,我们发现 【数据加载失败】【请刷新重试】2个片段被分别了,正确的应该是 [数据加载失败,请刷新重试。优化代码如下:
// let pattern = /([\u4e00-\u9fa5]+)/g;
let pattern = /([\u4e00-\u9fa5]+(?=[^,\u4e00-\u9fa5])|[\u4e00-\u9fa5]+,?[\u4e00-\u9fa5]*)/g;
去除重复
let uniqueData = [...new Set(data)];
好了,当我们拿到如下数据的时候,要构建我们的目标数据结构
构建我们的数据结构的时候,key 需要是英语,所以我们需要一个库来转义 pinyin-pro.cn/
{
"ShuJuJiaZaiShiBaiQingShuaXinZhongShi": "数据加载失败,请刷新重试",
"BuNengXuanZeJinTianZhiQianDeRiQi": "不能选择今天之前的日期",
"QingShaoHou": "请稍候",
"XuanZeRongNaRenShu": "选择容纳人数",
"BuXianRenShu": "不限人数",
"Ren": "人",
"HuiYiYaoQiu": "会议要求",
"YiZhanYong": "已占用",
"KongXian": "空闲",
"YiXuanZe": "已选择"
}
但是我们要考虑一个问题,防止重复,因此我们加一下路径
obj[`SpeedSchedule.${getPinyin(element)}`] = element;
{
'SpeedSchedule.ShuJuJiaZaiShiBaiQingShuaXinZhongShi': '数据加载失败,请刷新重试',
'SpeedSchedule.BuNengXuanZeJinTianZhiQianDeRiQi': '不能选择今天之前的日期',
'SpeedSchedule.QingShaoHou': '请稍候',
'SpeedSchedule.XuanZeRongNaRenShu': '选择容纳人数',
'SpeedSchedule.BuXianRenShu': '不限人数',
'SpeedSchedule.Ren': '人',
'SpeedSchedule.HuiYiYaoQiu': '会议要求',
'SpeedSchedule.YiZhanYong': '已占用',
'SpeedSchedule.KongXian': '空闲',
'SpeedSchedule.YiXuanZe': '已选择'
}
然后把它追加到 zh-CN.ts 对象上
// 读取现有的 zh-CN.ts
fs.readFile('./src/locales/zh-CN.ts', 'utf-8', (err, data) => {
if (err) {
console.error(err);
return;
}
let objRegex = /export default (\{[\s\S]*\})/;
let match = data.match(objRegex);
if (match) {
let objString = match[1];
let fileObj = eval(`(${objString})`);
fileObj = { ...fileObj, ...obj };
let newObjString = JSON.stringify(fileObj);
data = data.replace(objRegex, `export default ${newObjString}`);
}
console.log('faith=============zh-CN3333333333333333333', data);
fs.writeFileSync('./src/locales/zh-CN1.ts', data);
});
添加功能,目前我将将一个文件夹的所有 .tsx 后缀的文件都生成 zh-CN.ts 对象 ,遍历一个路径下的所有以 .tsx 为后缀的文件
// 遍历一个路径下的所有以 .tsx 为后缀的文件
function findTSXFiles(dir) {
let results = [];
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(findTSXFiles(filePath));
} else if (path.extname(file) === '.tsx') {
results.push(filePath);
}
}
return results;
}
然后我们拿到每一个文件的路径之后,在循环调用我们刚才 readAndWrite方法追加到 zh-CN.ts
当前我们路径是写死在代码中的,我们需要通过控制台输入我们想要生成的路径
最终效果
export default { 'AuthRouter.YongHuRenZhengShiBai': '用户认证失败', 'AuthRouter.RenZheng': '认证', 'AuthRouter.QiYeWeiXinTiaoZhuanDaoRenZhengYeMian': '企业微信跳转到认证页面', 'AuthRouter.YongHuRenZhengChuCuo': '用户认证出错', 'AuthRouter.RenZhengZhong': '认证中', 'Loading.tsx.JiaZaiZhong': '加载中', 'components.AppointmentHelp.QingKongZhuYi': '清空注意', 'components.AppointmentHelp.QieHuanRiQiJiangQingKongYiXuanHuiYiShi': '切换日期将清空已选会议室', 'components.AppointmentHelp.ShiFouJiXu': '是否继续', 'components.AppointmentHelp.QueRen': '确认', 'components.AppointmentHelp.QuXiao': '取消', 'components.AppointmentHelp.QieHuanShiJianJiangQingKongYiXuanHuiYiShi': '切换时间将清空已选会议室', 'components.AppointmentHelp.ChaoChuHuiYiZuiDa': '超出会议最大', 'components.AppointmentHelp.FenZhongShiChang': '分钟时长', 'components.AppointmentHelp.SuoXuanShiJianBuNengXiaoYuDangQianShiJian': '所选时间不能小于当前时间', 'components.AppointmentHelp.YiDaDaoKeXuanHuiYiShiZuiDaShuLiang': '已达到可选会议室最大数量', 'components.AppointmentHelp.NinYiJingXuanZeGuoGaiHuiYiShi': '您已经选择过该会议室', 'components.AppointmentHelp.HuiYiShiZaiSuoXuanShiJianDuanNeiCunZaiShiJianChongTu': '会议室在所选时间段内存在时间冲突', 'components.AppointmentHelp.QingXuanZeCanHuiRen': '请选择参会人', 'components.AppointmentHelp.QingXuanZeHuiYiShiJian': '请选择会议时间', 'components.AppointmentHelp.SuoXuanShiDuanNeiCunZaiBeiZhanYongDeHuiYiShi': '所选时段内存在被占用的会议室', 'components.AppointmentHelp.XuanZeBanGongLou': '选择办公楼', 'components.AppointmentHelp.XuanZeHuiYiShi': '选择会议室', 'components.AppointmentHelp.BuXianRenShu': '不限人数', 'components.AppointmentHelp.Ren': '人', 'components.AppointmentHelp.HuiYiYuYueXiaoZhuShou': '会议预约小助手', 'components.AppointmentHelp.QingShaoHou': '请稍候', 'components.AppointmentHelp.CanHuiRen': '参会人', 'components.AppointmentHelp.PiLiangTianJia': '批量添加', 'components.AppointmentHelp.CanHuiRenXiaoBiaoTi': '参会人小标题', 'components.AppointmentHelp.ShuRuXingMingHuoYouXiang': '输入姓名或邮箱', 'components.AppointmentHelp.CanHuiRenXingXiang': '参会人行项', 'components.AppointmentHelp.BiXuCanHui': '必需参会', 'components.AppointmentHelp.XuanZeCanHui': '选择参会', 'components.AppointmentHelp.HuiYiShiXiaoBiaoTi': '会议室小标题', 'components.AppointmentHelp.HuiYiShi': '会议室', 'components.AppointmentHelp.HuiYiShiShaiXuanQi': '会议室筛选器', 'components.AppointmentHelp.HuiYiShiYiXuanZeXiang': '会议室已选择项', 'components.AppointmentHelp.HuiYiShiDaiXuanZeXiang': '会议室待选择项', 'components.AppointmentHelp.YouCeBuFen': '右侧部分', 'components.AppointmentHelp.RiQiQieHuanZhong': '日期切换中', 'components.AppointmentHelp.YouCeBiaoGeBuFen': '右侧表格部分', 'components.AppointmentHelp.HuiYiShiXiaoBiaoTiDuiYing': '会议室小标题对应', 'components.AppointmentHelp.KongXing': '空行', 'components.AppointmentInfo.BaoQian': '抱歉', 'components.AppointmentInfo.TuPianWuFaXianShi': '图片无法显示', 'components.AppointmentInfo.QingLianXiXiangGuanRenYuan': '请联系相关人员', 'components.AppointmentInfo.QueRenYaoQuXiaoBenCiHuiYiMa': '确认要取消本次会议吗', 'components.AppointmentInfo.QueRenYaoQuXiaoSuoYouShiDuanDeZheGeHuiYiMa': '确认要取消所有时段的这个会议吗', 'components.AppointmentInfo.QueRenYaoQuXiaoZheGeHuiYiMa': '确认要取消这个会议吗', 'components.AppointmentInfo.ShiFouQuXiao': '是否取消', 'components.AppointmentInfo.DeHuiYi': '的会议', 'components.AppointmentInfo.HuiYiXiangQing': '会议详情', 'components.AppointmentInfo.ZaiCiYuYue': '再次预约', 'components.AppointmentInfo.BianJi': '编辑', 'components.AppointmentInfo.QuXiaoHuiYi': '取消会议', 'components.AppointmentInfo.HuiFu': '回复', 'components.AppointmentInfo.JieShou': '接受', 'components.AppointmentInfo.ZanDing': '暂定', 'components.AppointmentInfo.JuJue': '拒绝', 'components.AppointmentInfo.ZuZhiRen': '组织人', 'components.AppointmentInfo.ZuZhiZhe': '组织者', 'components.AppointmentInfo.BiXuCanHuiRen': '必需参会人', 'components.AppointmentInfo.ShouQi': '收起', 'components.AppointmentInfo.ZhanKai': '展开', 'components.AppointmentInfo.KeXuanCanHuiRen': '可选参会人', 'components.AppointmentInfo.CanHuiRenJieShouJuJueQingKuang': '参会人接受拒绝情况', 'components.AppointmentInfo.WuXiangYing': '无响应', 'components.AppointmentInfo.HuiYiShiJian': '会议时间', 'components.AppointmentInfo.HuiYiShi': '会议室', 'components.AppointmentInfo.Ren': '人', 'components.AppointmentInfo.WeiZhiZhiYin': '位置指引', 'components.AppointmentInfo.HuiYi': '会议', 'components.AppointmentInfo.HuiYiMiMa': '会议密码', 'components.AppointmentInfo.ChongFu': '重复', 'components.AppointmentInfo.HuiYiTiXing': '会议提醒', 'components.AppointmentInfo.MiaoShu': '描述', 'components.AppointmentMeeting.Wu': '五', 'components.AppointmentMeeting.ShiJianFaShengShi': '事件发生时', 'components.AppointmentMeeting.FenZhongQian': '分钟前', 'components.AppointmentMeeting.XiaoShiQian': '小时前', 'components.AppointmentMeeting.TianQian': '天前', 'components.AppointmentMeeting.YiZhouQian': '一周前', 'components.AppointmentMeeting.ShiJianRiQi': '事件日期',
.............
主要代码
{
"private": true,
"version": "1.0.9",
"type": "module",
"scripts": {
"generate_locales": "node ./.generate_locales/index.js",
},
"dependencies": {
"inquirer": "^9.1.4",
"pinyin-pro": "^3.13.0",
}
}
.generate_locales/index.js
import fs from 'fs';
import inquirer from 'inquirer';
import path from 'path';
import pkg from 'pinyin-pro';
const { pinyin } = pkg;
const questions = [
{
name: 'filePath',
message: 'Please input the you want filePath:',
},
];
const prompt = () => {
return new Promise((resolve) => {
inquirer.prompt(questions).then((res) => {
resolve(res);
});
});
};
function mustInluceComponent(res) {
console.log(`❌❌❌ ${res.msg}`);
}
const firstWordBig = (word) => {
if (!word) return;
let firstLetter = word[0];
let uppercaseFirstLetter = firstLetter.toUpperCase();
let newWord = uppercaseFirstLetter + word.slice(1);
return newWord;
};
const getPinyin = (text) => {
let englishText = pinyin(text.replace(/,/g, ''), { toneType: 'none' });
let resultzPinyin = '';
englishText.split(' ').forEach((word) => {
resultzPinyin += firstWordBig(word) || '';
});
return resultzPinyin;
};
async function createComponent() {
let res = await verificationInput();
if (res?.code && res?.code === 50001) {
console.log('非法输入');
return;
}
readFile(res);
// codeFc[res.code](res);
}
const deleteComment = (code) => {
// 删除注释
let pattern = /\/\/.*/g;
let result = code.replace(pattern, '');
return result;
};
const extractChinese = (data) => {
let pattern = /([\u4e00-\u9fa5]+(?=[^,\u4e00-\u9fa5])|[\u4e00-\u9fa5]+,?[\u4e00-\u9fa5]*)/g;
let match = data.match(pattern);
return match;
};
// 遍历一个路径下的所有以 .tsx 为后缀的文件
function findTSXFiles(dir) {
let results = [];
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(findTSXFiles(filePath));
} else if (path.extname(file) === '.tsx') {
results.push(filePath);
}
}
return results;
}
const buildDataStructure = (uniqueData, localesPreName) => {
let obj = {};
uniqueData.forEach((element) => {
obj[`${localesPreName}.${getPinyin(element)}`] = element;
});
// 读取现有的 zh-CN.ts
let data = fs.readFileSync('./src/locales/zh-CN.ts', 'utf-8');
let objRegex = /export default (\{[\s\S]*\})/;
let match = data.match(objRegex);
if (match) {
let objString = match[1];
let fileObj = eval(`(${objString})`);
fileObj = { ...fileObj, ...obj };
let newObjString = JSON.stringify(fileObj);
data = data.replace(objRegex, `export default ${newObjString}`);
}
fs.writeFileSync('./src/locales/zh-CN.ts', data);
};
const readFile = (filePath) => {
try {
fs.statSync(filePath);
console.log('progress: 20%, 路径存在');
// 读取路径下所有 .tsx为后缀的文件路径
const allTSXFiles = findTSXFiles(filePath);
console.log(allTSXFiles);
allTSXFiles.forEach((element) => {
readAndWrite(element);
});
console.log('👏👏👏success! 👏👏👏');
} catch (error) {
console.log('路径不存在,请检查', error);
}
};
const readAndWrite = (file) => {
// 读取文件
let data = fs.readFileSync(file, 'utf-8');
let localesPreName = file
.replace('/index.tsx', '')
.replace(/\//g, '.')
.replace(/src\.\./g, '')
.replace(/src\./g, '');
data = deleteComment(data);
data = extractChinese(data);
let uniqueData = [...new Set(data)];
buildDataStructure(uniqueData, localesPreName);
};
async function verificationInput() {
let res = await prompt();
const { filePath } = res;
if (!filePath) {
return {
code: 50001,
msg: 'You must include filePath',
};
}
// const dir = `./src/components`;
console.log('faith=============filePathfilePath', filePath);
const dir = filePath;
return dir;
}
createComponent();