bmc-i18n-extract-cli
Version:
这是一款能够自动将代码里的中文转成i18n国际化标记的命令行工具。当然,你也可以用它实现将中文语言包自动翻译成其他语言。适用于vue2、vue3和react
451 lines • 17.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const compiler_sfc_1 = require("@vue/compiler-sfc");
const htmlparser2 = __importStar(require("htmlparser2"));
const prettier_1 = __importDefault(require("prettier"));
const mustache_1 = __importDefault(require("mustache"));
const ejs_1 = __importDefault(require("ejs"));
const includeChinese_1 = require("./utils/includeChinese");
const log_1 = __importDefault(require("./utils/log"));
const transformJs_1 = __importDefault(require("./transformJs"));
const parse_1 = require("./parse");
const collector_1 = __importDefault(require("./collector"));
const constants_1 = require("./utils/constants");
const stateManager_1 = __importDefault(require("./utils/stateManager"));
const error_logger_1 = __importDefault(require("./utils/error-logger"));
const COMMENT_TYPE = '!';
function parseJsSyntax(source, rule) {
// html属性有可能是{xx:xx}这种对象形式,直接解析会报错,需要特殊处理。
// 先处理成temp = {xx:xx} 让babel解析,解析完再还原成{xx:xx}
let isObjectStruct = false;
if (source.startsWith('{') && source.endsWith('}')) {
isObjectStruct = true;
source = `temp=${source}`;
}
const { code } = (0, transformJs_1.default)(source, {
rule: {
...rule,
functionName: rule.functionNameInTemplate,
caller: '',
importDeclaration: '',
},
parse: (0, parse_1.initParse)(),
});
let stylizedCode = prettier_1.default.format(code, {
singleQuote: true,
semi: false,
parser: 'babel',
});
// pretter格式化后有时会多出分号
if (stylizedCode.startsWith(';')) {
stylizedCode = stylizedCode.slice(1);
}
if (isObjectStruct) {
stylizedCode = stylizedCode.replace('temp = ', '');
}
return stylizedCode.endsWith('\n') ? stylizedCode.slice(0, stylizedCode.length - 1) : stylizedCode;
}
// 判断表达式是否已经转换成i18n
function hasTransformed(code, functionNameInTemplate) {
return new RegExp(`\\${functionNameInTemplate}\\(.*\\)`, 'g').test(code);
}
// TODO: 需要优化,传参方式太挫
function parseTextNode(text, rule, getReplaceValue, customizeKey) {
let str = '';
let tokens = [];
try {
tokens = mustache_1.default.parse(text);
}
catch (err) {
error_logger_1.default.reportTemplateError(text, err);
return text;
}
for (const token of tokens) {
const type = token[0];
const value = token[1];
if ((0, includeChinese_1.includeChinese)(value)) {
if (type === 'text') {
const translationKey = collector_1.default.add(value, customizeKey);
str += `{{${getReplaceValue(translationKey)}}}`;
}
else if (type === 'name') {
const source = parseJsSyntax(value, rule);
str += `{{${source}}}`;
}
else if (type === COMMENT_TYPE) {
// 形如{{!xxxx}}这种形式,在mustache里属于注释语法
const source = parseJsSyntax(`!${value}`, rule);
str += `{{${source}}}`;
}
}
else {
if (type === 'text') {
str += value;
}
else if (type === 'name') {
str += `{{${value}}}`;
}
else if (type === COMMENT_TYPE) {
// 形如{{!xxxx}}这种形式,在mustache里属于注释语法
str += `{{!${value}}}`;
}
}
}
return str;
}
function handleTemplate(code, rule) {
let htmlString = '';
const { functionNameInTemplate, customizeKey } = rule;
function getReplaceValue(translationKey) {
// 表达式结构 $t('xx')
return `${functionNameInTemplate}('${translationKey}')`;
}
function parseIgnoredTagAttribute(attributes) {
let attrs = '';
for (const key in attributes) {
const attrValue = attributes[key];
if (attrValue === undefined) {
attrs += ` ${key} `;
}
else {
attrs += ` ${key}="${attrValue}" `;
}
}
return attrs;
}
function parseTagAttribute(attributes) {
let attrs = '';
for (const key in attributes) {
const attrValue = attributes[key];
const isVueDirective = key.startsWith(':') || key.startsWith('@') || key.startsWith('v-');
if (attrValue === undefined) {
attrs += ` ${key} `;
}
else if ((0, includeChinese_1.includeChinese)(attrValue) && isVueDirective) {
const source = parseJsSyntax(attrValue, rule);
// 处理属性类似于:xx="'xx'",这种属性值不是js表达式的情况。attrValue === source即属性值不是js表达式
// !hasTransformed()是为了排除,类似:xx="$t('xx')"这种已经转化过的情况。这种情况不需要二次处理
if (attrValue === source && !hasTransformed(source, functionNameInTemplate !== null && functionNameInTemplate !== void 0 ? functionNameInTemplate : '')) {
const translationKey = collector_1.default.add(removeQuotes(attrValue), customizeKey);
const expression = getReplaceValue(translationKey);
attrs += ` ${key}="${expression}" `;
}
else {
attrs += ` ${key}="${source}" `;
}
}
else if ((0, includeChinese_1.includeChinese)(attrValue) && !isVueDirective) {
const translationKey = collector_1.default.add(attrValue, (key, path) => {
// 属性里的$t('')转成$t(``),并把双引号转成单引号
key = key.replace(/'/g, '`').replace(/"/g, "'");
return customizeKey(key, path);
});
const expression = getReplaceValue(translationKey);
attrs += ` :${key}="${expression}" `;
}
else if (attrValue === '') {
// 这里key=''是因为之后还会被pretttier处理一遍,所以写死单引号没什么影响
attrs += `${key}='' `;
}
else {
attrs += ` ${key}="${attrValue}" `;
}
}
return attrs;
}
// 转义特殊字符
function escapeSpecialChar(text) {
text = text.replace(/ /g, ' ');
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
text = text.replace(/"/g, '"');
text = text.replace(/&/g, '&');
return text;
}
let shouldIgnore = false; // 是否忽略提取
let textNodeCache = ''; // 缓存当前文本节点内容
let attrsCache = {}; // 缓存当前标签的属性
const ignoreTags = []; // 记录忽略提取的标签名
const parser = new htmlparser2.Parser({
onopentag(tagName) {
// 处理文本节点没有被标签包裹的情况
// 如果这个标签没被忽略提取,那么就进行文本节点解析
if (!shouldIgnore) {
const text = parseTextNode(textNodeCache, rule, getReplaceValue, customizeKey);
htmlString += text;
textNodeCache = '';
}
let attrs = '';
const attributes = attrsCache;
if (shouldIgnore) {
ignoreTags.push(tagName);
attrs = parseIgnoredTagAttribute(attributes);
// 重置属性缓存
attrsCache = {};
htmlString += `<${tagName} ${attrs}>`;
return;
}
attrs = parseTagAttribute(attributes);
// 重置属性缓存
attrsCache = {};
htmlString += `<${tagName} ${attrs}>`;
},
onattribute(name, value, quote) {
if (value) {
attrsCache[name] = value;
}
else {
if (quote === undefined) {
attrsCache[name] = undefined;
}
else {
attrsCache[name] = value;
}
}
},
ontext(text) {
text = escapeSpecialChar(text);
if (shouldIgnore) {
htmlString += text;
return;
}
textNodeCache += text;
},
onclosetag(tagName, isImplied) {
// 处理文本被标签包裹的情况
// 如果这个标签没被忽略提取,那么就进行文本节点解析
if (!shouldIgnore) {
const text = parseTextNode(textNodeCache, rule, getReplaceValue, customizeKey);
htmlString += text;
textNodeCache = '';
}
// 判断是否可以取消忽略提取
if (ignoreTags.length === 0) {
shouldIgnore = false;
}
else {
if (ignoreTags[ignoreTags.length - 1] === tagName) {
ignoreTags.pop();
if (ignoreTags.length === 0) {
shouldIgnore = false;
}
}
}
// 如果是自闭合标签
if (isImplied) {
htmlString = htmlString.slice(0, htmlString.length - 2) + '/>';
return;
}
htmlString += `</${tagName}>`;
},
oncomment(comment) {
// 如果注释前有文本节点,就拼接
const text = parseTextNode(textNodeCache, rule, getReplaceValue, customizeKey);
htmlString += text;
textNodeCache = '';
if (comment.includes(constants_1.IGNORE_REMARK)) {
shouldIgnore = true;
}
htmlString += `<!--${comment}-->`;
},
}, {
lowerCaseTags: false,
recognizeSelfClosing: true,
lowerCaseAttributeNames: false,
decodeEntities: false,
});
parser.write(code);
parser.end();
return htmlString;
}
function getComponentDecoratorPosition(source) {
return source.indexOf('@Component');
}
function getExportDefaultPosition(source) {
return source.indexOf('export default');
}
function handleScript(source, rule) {
const lang = stateManager_1.default.getVueScriptLang().toLowerCase();
if (['ts', 'typescript', 'tsx'].includes(lang)) {
// 如果vue用了ts,按@Component装饰器进行分割
const startIndex = getComponentDecoratorPosition(source);
return combineVueScript(source.slice(0, startIndex), source.slice(startIndex), rule);
}
else {
const startIndex = getExportDefaultPosition(source);
return combineVueScript(source.slice(0, startIndex), source.slice(startIndex), rule);
}
}
function combineVueScript(nonComponentCode, componentCode, rule) {
const transformOptions = {
rule: {
...rule,
functionName: rule.functionNameInScript,
},
isJsInVue: true,
parse: (0, parse_1.initParse)(),
};
const lang = stateManager_1.default.getVueScriptLang().toLowerCase();
const scriptContext = {};
const transformedNonComponentCode = (0, transformJs_1.default)(nonComponentCode, {
...transformOptions,
rule: ['ts', 'typescript', 'tsx'].includes(lang)
? stateManager_1.default.getToolConfig().rules.ts
: stateManager_1.default.getToolConfig().rules.js,
}, scriptContext).code;
const transformedComponentCode = (0, transformJs_1.default)(componentCode, transformOptions, scriptContext).code;
if (transformedNonComponentCode) {
return '\n' + transformedNonComponentCode + '\n' + transformedComponentCode + '\n';
}
else {
return transformedComponentCode + '\n';
}
}
function mergeCode(tagOrder, tagMap) {
const sourceCode = tagOrder.reduce((code, tagName) => {
return code + tagMap[tagName];
}, '');
return sourceCode;
}
function removeQuotes(value) {
if (['"', "'"].includes(value.charAt(0)) && ['"', "'"].includes(value.charAt(value.length - 1))) {
value = value.substring(1, value.length - 1);
}
return value;
}
function getWrapperTemplate(sfcBlock) {
const { type, lang, attrs } = sfcBlock;
let template = `<${type}`;
if (lang) {
template += ` lang="${lang}"`;
}
if (sfcBlock.setup) {
template += ` setup`;
}
if (sfcBlock.scoped) {
template += ` scoped`;
}
for (const attr in attrs) {
if (!['lang', 'scoped', 'setup'].includes(attr)) {
if (attrs[attr] === true) {
template += attr;
}
else {
template += ` ${attr}="${attrs[attr]}"`;
}
}
}
template += `><%- code %></${type}>`;
return template;
}
function generateSource(sfcBlock, handler, rule) {
const wrapperTemplate = getWrapperTemplate(sfcBlock);
let source;
try {
source = handler(sfcBlock.content, rule);
}
catch (err) {
source = sfcBlock.content;
error_logger_1.default.reportFileError(err.message);
}
return ejs_1.default.render(wrapperTemplate, {
code: source,
});
}
function removeSnippet(source, sfcBlock) {
return sfcBlock ? source.replace(sfcBlock.content, '') : source;
}
// 提取文件头注释
// TODO: 这里投机取巧了一下,把标签内容清空再匹配注释。避免匹配错了。后期有好的方案再替换
function getFileComment(descriptor) {
const { template, script, scriptSetup, styles } = descriptor;
let source = descriptor.source;
source = removeSnippet(source, template);
source = removeSnippet(source, script);
source = removeSnippet(source, scriptSetup);
if (styles) {
for (const style of styles) {
source = removeSnippet(source, style);
}
}
const result = source.match(/<!--[\s\S]*?-->/);
return result ? result[0] : '';
}
function transformVue(code, options) {
const { rule, filePath } = options;
const { descriptor, errors } = (0, compiler_sfc_1.parse)(code);
if (errors.length > 0) {
const line = errors[0].loc.start.line;
log_1.default.error(`源文件${filePath}第${line}行附近解析出现错误:`, errors[0].toString());
return {
code,
};
}
const { template, script, scriptSetup, styles } = descriptor;
let templateCode = '';
let scriptCode = '';
let stylesCode = '';
const fileComment = getFileComment(descriptor);
if (template) {
templateCode = generateSource(template, handleTemplate, rule);
}
if (script) {
stateManager_1.default.setVueScriptLang(script.lang);
scriptCode = generateSource(script, handleScript, rule);
}
if (scriptSetup) {
stateManager_1.default.setVueScriptLang(scriptSetup === null || scriptSetup === void 0 ? void 0 : scriptSetup.lang);
scriptCode = generateSource(scriptSetup, handleScript, rule);
}
if (styles) {
for (const style of styles) {
const wrapperTemplate = getWrapperTemplate(style);
const source = style.content;
stylesCode +=
ejs_1.default.render(wrapperTemplate, {
code: source,
}) + '\n';
}
}
const tagMap = {
template: templateCode,
script: scriptCode,
style: stylesCode,
};
const tagOrder = stateManager_1.default.getToolConfig().rules.vue.tagOrder;
code = mergeCode(tagOrder, tagMap);
if (fileComment) {
code = fileComment + code;
}
return {
code,
};
}
exports.default = transformVue;
//# sourceMappingURL=transformVue.js.map