@tenado/i18n-cli
Version:
i18n-cli是一个自动国际化脚本,通过执行命令,自动提取代码里面的中文,自动调用百度或谷歌翻译接口,自动将翻译结果以 key-value 形式存入*.json 语言包里
318 lines (308 loc) • 10.3 kB
JavaScript
const fs = require("fs");
const parse5 = require("parse5");
const treeAdapterDefault = require("parse5/lib/tree-adapters/default");
const Serializer = require("parse5/lib/serializer");
const { NAMESPACES: NS } = require("parse5/lib/common/html");
const mustache = require("mustache");
const chalk = require("chalk");
const isChinese = require("../utils/isChinese.js");
const generateKey = require("../utils/generateKey.js");
const regex = require("../utils/regex.js");
const transformJs = require("./transformJs.js");
module.exports = (localData, needTranslate, filePath, sourceCode, options) => {
const { ignoreText, ignoreAttributes, keyShowOrigin } = options ?? {};
const vueTemplateLabelPrefix = "autoi18nprefix";
const vueTemplateLabelSuffix = "autoi18nsuffix";
const treeAdapter = {
...treeAdapterDefault,
};
// 需要忽略的行
let ignoreLines = [];
// 保存映射,在处理完之后替换回来
let keysMap = {};
// 将大写字符转换为小写
const toKebab = (sourceCode) => {
// 如果标签是大写,则替换并缓存
sourceCode = sourceCode.replace(
regex.htmlTagWithUppercaseChar,
(_, $1, $2) => {
// 在key的前后都加上前缀,防止出现Form和FormItem转换报错
const temp = `${vueTemplateLabelPrefix}${$2.toLowerCase()}${vueTemplateLabelSuffix}`;
keysMap[temp] = $2;
return `${$1}${temp}`;
}
);
sourceCode = sourceCode.replace(
regex.htmlAttributeWithUppercaseChar,
($1) => {
const temp = `${vueTemplateLabelPrefix}${$1.toLowerCase()}${vueTemplateLabelSuffix}`;
keysMap[temp] = $1;
return `${temp}`;
}
);
sourceCode = sourceCode.replace(regex.htmlSlotWithUppercaseChar, ($1) => {
const temp = `${vueTemplateLabelPrefix}${$1.toLowerCase()}${vueTemplateLabelSuffix}`;
keysMap[temp] = $1;
return `${temp}`;
});
// 如果属性是大写,则特换并缓存
return sourceCode;
};
// 将自闭和标签替换为成对的标签
const toAutoColse = (sourceCode) => {
sourceCode = sourceCode.replace(regex.htmlAutoCloseTag, ($1, $2) => {
const replaceStr = $1.replace("/>", ">");
return `${replaceStr}</${$2}>`;
});
return sourceCode;
};
// 将代码里的template转换
const toTempalte = (sourceCode) => {
sourceCode = sourceCode.replace(regex.htmlTemplateTag, (_, $1, $2) => {
const temp = `${vueTemplateLabelPrefix}${$2.toLowerCase()}${vueTemplateLabelSuffix}`;
keysMap[temp] = $2;
return `${$1}${temp}`;
});
return sourceCode;
};
// 替换之前转换的标签
const toPascal = (sourceCode) => {
Object.keys(keysMap).forEach((i) => {
const reg = new RegExp(`${i}`, "g");
sourceCode = sourceCode.replace(reg, ($1) => {
const temp = keysMap[$1] || $1;
return `${temp}`;
});
});
return sourceCode;
};
// 转义特殊字符
const toEscapeHtml = (sourceCode) => {
const charMap = {
" ": " ",
"<": "<",
">": ">",
""": '"',
"&": "&",
};
const keys = Object.keys(charMap);
const len = keys?.length;
for (let i = 0; i < len; i++) {
const key = keys[i];
const value = charMap[key];
const reg = new RegExp(key, "g");
sourceCode = sourceCode.replace(reg, value);
}
return sourceCode;
};
// 判断代码是否为js表达式
const isExpression = (code) => {
try {
eval(code);
return true;
} catch (e) {
return false;
}
};
// 判断代码是否为字符串对象
const isObjectString = (str) => {
const objectRegex = /^\s*\{[\s\S]*\}\s*$/;
return objectRegex.test(str);
};
// 格式化代码
sourceCode = toKebab(sourceCode);
sourceCode = toAutoColse(sourceCode);
sourceCode = toTempalte(sourceCode);
const ast = parse5.parse(sourceCode, {
sourceCodeLocationInfo: true,
treeAdapter: treeAdapter,
});
// 转换html内容
const traverseHtml = (ast, localData, needTranslate, options) => {
const { i18nMethod } = options;
const cacheKeyFunc = (key, value) => {
if (!localData[key]) {
needTranslate[key] = value;
options.hasTransform = true;
}
};
const transformJsExp = (sourceCode) => {
let { code, hasTransform } = transformJs(
localData,
needTranslate,
"",
sourceCode,
{ ...options, needImport: false, isWritingFile: false }
);
// 如果是;结尾,则删除
if (code.endsWith(";")) {
code = code.slice(0, -1);
}
if (hasTransform) {
options.hasTransform = true;
}
return code;
};
const traverse = (node) => {
if (node.childNodes) {
node.childNodes.forEach((childNode) => traverse(childNode));
}
// 处理属性
if (node.attrs) {
const startLine = node.sourceCodeLocation?.startLine;
if (ignoreLines.includes(startLine)) return;
node.attrs.forEach((attr) => {
const { name, value } = attr;
if (!isChinese(value) || !value) return;
if (ignoreAttributes.includes(name)) {
const source = value;
attr.value = source;
}
// 如果指令、绑定、事件
else if (
name.startsWith("v-") ||
name.startsWith(":") ||
name.startsWith("@")
) {
const isExp = isExpression(value);
const isObjStr = isObjectString(value);
if (isExp) {
const source = transformJsExp(value);
if (value !== source) {
attr.value = source;
}
}
// 如果是对象字符串,替换里面的汉字,直接解析对象字符串会报错,需要特殊处理
// 先转换为i18nCliTempPrefix = {xx:xx} 让babel解析,解析完再还原成{xx:xx}
else if (isObjStr) {
const tempPrefix = "i18nCliTempPrefix";
let sourceCode = value;
sourceCode = `${tempPrefix}=${sourceCode}`;
sourceCode = transformJsExp(sourceCode);
sourceCode = sourceCode.replace(`${tempPrefix} = `, "");
if (value !== sourceCode) {
attr.value = sourceCode;
}
}
} else {
const key = generateKey(value, options);
cacheKeyFunc(key, value);
const methodName = options.isVueTemplate
? `$${i18nMethod}`
: i18nMethod;
attr.value = `${methodName}('${key}'${
keyShowOrigin ? ",'" + value + "'" : ""
})`;
attr.name = `:${name}`;
}
});
}
// 处理innerText
if (node.nodeName === "#text") {
const nodeValue = node.value;
if (!isChinese(nodeValue) || !nodeValue) return;
const startLine = node.sourceCodeLocation.startLine;
if (ignoreLines.includes(startLine)) return;
let value = "";
let tokens = mustache.parse(node.value) || [];
// tokens格式[['text', '中文', 0, 2]]
for (const token of tokens) {
const tokenType = token[0];
const tokenText = token[1];
if (!isChinese(tokenText)) {
if (tokenType === "text") {
value += tokenText;
} else if (tokenType === "name") {
value += `{{${tokenText}}}`;
}
} else {
if (tokenType === "text") {
const text = tokenText.trim();
const key = generateKey(text, options);
cacheKeyFunc(key, text);
const methodName = options.isVueTemplate
? `$${i18nMethod}`
: i18nMethod;
value += `{{${methodName}('${key}'${
keyShowOrigin ? ",'" + text + "'" : ""
})}}`;
} else if (tokenType === "name") {
value += `{{${transformJsExp(tokenText)}}}`;
}
}
}
if (node.value !== value) {
node.value = value;
}
}
// 获取所有
if (node.nodeName === "#comment") {
if (node.data.includes(ignoreText)) {
const endLine = node?.sourceCodeLocation?.endLine;
ignoreLines.push(endLine + 1);
}
}
};
const html = ast.childNodes.find((nd) => nd.nodeName === "html");
if (html) {
const body = html.childNodes.find((nd) => nd.nodeName === "body");
if (body) {
traverse(body);
}
}
};
options.hasTransform = false;
traverseHtml(ast, localData, needTranslate, options);
// 根据ast生成code
class MySerializer extends Serializer {
_serializeAttributes(node) {
const attrs = this.treeAdapter.getAttrList(node);
for (let i = 0, attrsLength = attrs.length; i < attrsLength; i++) {
const attr = attrs[i];
const value = Serializer.escapeString(attr.value, true);
this.html += " ";
if (!attr.namespace) {
this.html += attr.name;
} else if (attr.namespace === NS.XML) {
this.html += "xml:" + attr.name;
} else if (attr.namespace === NS.XMLNS) {
if (attr.name !== "xmlns") {
this.html += "xmlns:";
}
this.html += attr.name;
} else if (attr.namespace === NS.XLINK) {
this.html += "xlink:" + attr.name;
} else {
this.html += attr.prefix + ":" + attr.name;
}
if (value) {
this.html += '="' + value + '"';
}
}
}
}
const htmlFromAst = (ast, options) => {
const serializer = new MySerializer(ast, options);
return serializer.serialize();
};
let code = htmlFromAst(ast);
code = code.split("<body>")[1].split("</body>")[0];
code = toPascal(code);
code = toEscapeHtml(code);
// 代码填回
if (options.isWritingFile) {
if (options.hasTransform) {
fs.writeFileSync(filePath, code, { encoding: "utf-8" }, (err) => {
if (err) {
console.log(chalk.red(err));
process.exit(2);
}
});
}
} else {
return {
code,
hasTransform: options.hasTransform,
};
}
};