2024.6.11更新单元测试
2024.7.25封装成npm
最近有一个功能,将json文件里的内容抽取到一个xlxs中,然后维护xlxs文件。当要更新json文件时,就更新xlxs的内容并把它传回json中。这个脚本主要使用NodeJS写。
以下是完成此功能时做的一些笔记。
前期工作和依赖
js文件中要使用import,因此需要在package.json中设置:"type": "module"
依赖:
import xlsx from 'xlsx';
import path, { dirname } from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import md5 from 'blueimp-md5';
相关文档:
API 参考 | SheetJS 中文网 (nodejs.cn)
xlsx - npm (npmjs.com)
笔记
获取此文件目录名:
const __dirname = dirname(fileURLToPath(import.meta.url));
组合文件名:
const outPath = path.join(__dirname, './fileName.xlsx');
读取一个目录下的所有文件名:
const files = fs.readdirSync(dirName);
读取一个文件:
const enJson = fs.readFileSync(fileName, 'utf8');
将二维数组转换为xlxs工作表:
const sheet = xlsx.utils.aoa_to_sheet(sheetData);
将工作表添加到工作簿:
xlsx.utils.book_append_sheet(workBook, sheet, sheetName);
将工作表输出到xlsx文件中:
xlsx.writeFile(workBook, outPath, { bookType: 'xlsx' });
将数组输出到xlxs:data
是数组。
const sheet = xlsx.utils.json_to_sheet(data); //将 JS 对象数组转换为工作表
const workBook= xlsx.utils.book_new();// 创建一个工作簿对象
xlsx.utils.book_append_sheet(workBook, sheet, 'sheetName'); // 将工作表添加到工作簿:
xlsx.writeFile(workBook, path, {
bookType: 'xlsx',
}); // 输出到xlsx
功能
维护一个xlsx文档,里面包含国际化的所有翻译,如下:第一行 为 语言
en | ko | ja | fr |
---|---|---|---|
英文翻译 | 韩文翻译 | 日文翻译 | 法文翻译 |
有英文json翻译文件如下:src/locales/en/fileName1.json
其中key值为i18n的标记,value值为对应的翻译。
{
"inviter": "Inviter",
"worth": "Worth ${{value}}",
"countDownTips": "Rewards Countdown"
}
默认语言为英语,我们需要以它为例子生成对应的其他语言文件,如生成:src/locales/fr/fileName1.json
{
"inviter": "法文翻译1",
"worth": "法文翻译2",
"countDownTips": "法文翻译3"
}
所有的翻译都在xlsx中。因此我们需要读取xlsx文件,遍历src/locales/en
下的对应文件(参数pageNames
),将对应翻译生成到对应文件夹。若无对应翻译,则用英文兜底。
中途使用md5加密后的en翻译为key,将对应xlsx文件中的翻译(那一行)保存到set中,是因为xlsx中没有i18n标记,只有各语言翻译。
将xlsx翻译文件输出到locales/[language]/[fileName]
的功能封装成一个函数,参数如下:
读入的en文件夹上级为pagePath
参数:
pageNames
:要维护的文件名列表,如 invite 对应locale/en/invite
filePath
:xlsx文件路径
pagePath
:locale文件路径
option
:选项,包括
-outputKey
:输出为json的某一个key值的value,没有就不填(这个功能的代码很死板,像是硬编码,需求如此,先写着!)
-languageArray
:传入一个数组,包含要求的语言,没有就不填,会默认xlsx里的所有文件
参数填错了就会报错,这个代码的健壮性并不强,只是一个加快工作效率的工具 / 练手代码。 所以建议严格按照参数要求调用函数。不要做那种:“没有outputKey应该不传,但我就传个空串”,可能会报奇怪的错误!
代码
// 入口
import xlsx from "xlsx";
import md5 from "blueimp-md5";
import fs from "node:fs";
import path from "node:path";
// 删去前后空格 特判str为undefined
const trim = (str) => (str || "").replace(/^\s+|\s+$/g, "");
// 生成一个set,key是en的翻译,value是对应row
function transSheetToMap(sheetData) {
const map = new Map();
for (const row of sheetData) {
const key = md5(trim(row.en));
map.set(key, row); // 加密后en的value为key,整个row为value
}
return map;
}
/*
读入的en文件夹上级为pagePath
pageNames:要维护的文件名列表,如 invite 对应locale/en/invite
filePath:xlsx文件路径
pagePath:locale文件路径
option:选项,包括
- outputKey:输出为json的某一个key值的value,没有就不填
- languageArray:传入一个数组,包含要求的语言缩写,没有就不填,会默认xlsx里的所有文件
*/
function xlsxToJson(pageNames, filePath, pagePath, option) {
let outputKey = undefined;
if (option.outputKey) {
outputKey = option.outputKey;
}
const workBook = xlsx.readFile(filePath);
const firstWorksheet = workBook.SheetNames[0]; // 全都放在第一个sheet中
const sheet = workBook.Sheets[firstWorksheet];
const sheetData = xlsx.utils.sheet_to_json(sheet); // 数组,每个项是对象,key为语言value为翻译
const languages = option.languageArray
? option.languageArray
: Object.keys(sheetData[0]); // 所有语言
const dictions = transSheetToMap(sheetData);
for (const pageName of pageNames) {
// 英文文件
const pageEnJson = JSON.parse(
fs.readFileSync(
path.join(pagePath, "en", `${pageName}.json`),
"utf8"
)
);
// key 为 i18n标记
const pageEnKeys = outputKey
? Object.keys(pageEnJson[outputKey])
: Object.keys(pageEnJson);
for (const language of languages) {
// 深拷贝英文样本
const translatedJson = JSON.parse(JSON.stringify(pageEnJson));
// translatedJson生成为对应语言,en兜底
for (const key of pageEnKeys) {
const translatedKey = outputKey
? md5(trim(pageEnJson[outputKey][key]))
: md5(trim(pageEnJson[key]));
const translation = dictions.get(translatedKey);
if (outputKey) {
translatedJson[outputKey][key] =
translation[language] || translation["en"];
} else {
translatedJson[key] =
translation[language] || translation["en"];
}
}
const filePath = path.join(pagePath, language, `${pageName}.json`);
// 若文件夹不存在则创建
if (!fs.existsSync(path.join(pagePath, language))) {
fs.mkdirSync(path.join(pagePath, language));
}
fs.writeFileSync(filePath, JSON.stringify(translatedJson, null, 4));
}
}
}
export default xlsxToJson;
xlsxToJson(
["invite", "player"],
"C:/Users/somePath/i18n-all.xlsx",
"C:/Users/somePath/locales",
{
outputKey: "key",
languageArray: ["de"],
}
);
输出
调用:要翻译的文件为invite
,输出到json文件的key
属性中,只翻译到de
德文。
xlsxToJson(
["invite"],
"C:/Users/somePath/i18n-all.xlsx",
"C:/Users/somePath/locales",
{
outputKey: "key",
languageArray: ["de"],
}
);
对应英文json如下:(有outputKey一定要填!没有的话一定不填!不然输出会很奇怪。)
// locales/en/invite.json
{
"key": {
"ogDesc": "Download Now!",
"inviter": "Inviter"
}
}
结果输出locales/de/invite.json
:
单元测试
参考资料:
如何开始对 JavaScript 代码进行单元测试 (freecodecamp.org)
javascript - Jest - SyntaxError: 无法在模块外使用 import 语句 - SegmentFault 思否
文件index.test.js
import xlsxToJson from "../index.js";
import fs from "node:fs";
import path from "node:path";
// import xlsxToJson from "../index."; 不加.js会报错
const filePath =
"somePath1",
localesPath =
"somePath2",
pageNames = ["invite"];
// 有key
xlsxToJson(pageNames, filePath, localesPath, {
outputKey: "key",
// 只翻译两种语言,其中de正确翻译,ja存在空翻译和空串(英文兜底)
languageArray: ["de", "ja"],
});
// 无key
xlsxToJson(["invite_noKey"], filePath, localesPath, {
languageArray: ["de", "ja"],
});
const pageDeJson = JSON.parse(
fs.readFileSync(
path.join(localesPath, "de", `${pageNames[0]}.json`),
"utf-8"
)
);
const pageJaJson = JSON.parse(
fs.readFileSync(
path.join(localesPath, "ja", `${pageNames[0]}.json`),
"utf-8"
)
);
const pageEnJson = JSON.parse(
fs.readFileSync(
path.join(localesPath, "en", `${pageNames[0]}.json`),
"utf-8"
)
);
// 翻译正确
test("translate de right", () => {
expect(pageDeJson.key.inviter).toBe("Einladender");
});
// 翻译为空格(非空串)
test("translate ja space", () => {
expect(pageJaJson.key.inviter).toBe(" ");
});
// 英文兜底
test("no translation to en", () => {
expect(pageJaJson.key.Download).toBe(pageEnJson.key.Download);
});
// 无key翻译正确
test("translate de right_noKey", () => {
expect(
JSON.parse(
fs.readFileSync(
path.join(localesPath, "de", "invite_noKey.json"),
"utf-8"
)
).inviter
).toBe("Einladender");
});
// 是否创建文件夹
test("is create dir", () => {
expect(fs.existsSync(path.join(localesPath, "de"))).toBe(true);
});
单元测试覆盖率是96.77%,显示第84行没有覆盖到,具体内容是:
// 若文件夹不存在则创建
if (!fs.existsSync(path.join(pagePath, language))) {
fs.mkdirSync(path.join(pagePath, language));
}
如果没创建文件夹的话,以下代码是读取不到文件的。
fs.readFileSync(
path.join(localesPath, "de", `${pageNames[0]}.json`),
"utf-8"
)
就当作是100%吧!
封装成npm包
xlsx-to-locale-json
https://www.npmjs.com/package/xlsx-to-locale-json
把npm的源换成官方的:
npm config set registry https://registry.npmjs.org
在项目命令行登录:
npm login
出现这个后:
npm publish
即可。
不一定维护。