UNPKG

@c-sheep/i18n-extract-cli

Version:

这是一款能够自动将代码里的中文转成i18n国际化标记的命令行工具。当然,你也可以用它实现将中文语言包自动翻译成其他语言。适用于vue2、vue3和react

380 lines (379 loc) 19 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@babel/core"); const traverse_1 = __importDefault(require("@babel/traverse")); const generator_1 = __importDefault(require("@babel/generator")); const template_1 = __importDefault(require("@babel/template")); const isEmpty_1 = __importDefault(require("lodash/isEmpty")); const collector_1 = __importDefault(require("../shared/collector")); const includeChinese_1 = require("../shared/includeChinese"); const assertType_1 = require("../shared/assertType"); const constants_1 = require("../shared/constants"); const stateManager_1 = __importDefault(require("../shared/stateManager")); const comblie_inject_1 = require("../shared/comblie-inject"); function getObjectExpression(obj) { const ObjectPropertyArr = []; Object.keys(obj).forEach((k) => { const tempValue = obj[k]; let newValue; if ((0, assertType_1.isObject)(tempValue)) { newValue = tempValue.value; } else { newValue = core_1.types.identifier(tempValue); } ObjectPropertyArr.push(core_1.types.objectProperty(core_1.types.identifier(k), newValue)); }); const ast = core_1.types.objectExpression(ObjectPropertyArr); return ast; } // 判断节点是否是props属性的默认值 function isPropNode(path) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const objWithProps = (_d = (_c = (_b = (_a = path.parentPath) === null || _a === void 0 ? void 0 : _a.parentPath) === null || _b === void 0 ? void 0 : _b.parentPath) === null || _c === void 0 ? void 0 : _c.parentPath) === null || _d === void 0 ? void 0 : _d.parent; const rootNode = (_k = (_j = (_h = (_g = (_f = (_e = path.parentPath) === null || _e === void 0 ? void 0 : _e.parentPath) === null || _f === void 0 ? void 0 : _f.parentPath) === null || _g === void 0 ? void 0 : _g.parentPath) === null || _h === void 0 ? void 0 : _h.parentPath) === null || _j === void 0 ? void 0 : _j.parentPath) === null || _k === void 0 ? void 0 : _k.parent; let isMeetProp = false; let isMeetKey = false; let isMeetContainer = false; // 属性是否包含在props结构里 if (objWithProps && objWithProps.type === "ObjectProperty" && objWithProps.key.type === "Identifier" && objWithProps.key.name === "props") { isMeetProp = true; } // 对应key是否是default if (path.parent && path.parent.type === "ObjectProperty" && path.parent.key.type === "Identifier" && path.parent.key.name === "default") { isMeetKey = true; } // 遍历到指定层数后是否是导出声明 if (rootNode && rootNode.type === "ExportDefaultDeclaration") { isMeetContainer = true; } return isMeetProp && isMeetKey && isMeetContainer; } function getStringLiteral(value) { return Object.assign(core_1.types.stringLiteral(value), { extra: { raw: `'${value}'`, rawValue: value, }, }); } function nodeToCode(node) { return (0, generator_1.default)(node).code; } // 允许往react函数组件中加入自定义代码 function insertSnippets(node, snippets) { if (node.body.type === "BlockStatement" && snippets) { const returnStatement = node.body.body.find((node) => node.type === "ReturnStatement"); if (returnStatement) { // TODO: 这里判断是否包含snippet,不严谨,推荐节点判断 const arg = returnStatement.argument; const statements = template_1.default.statements(snippets)(); const source = nodeToCode(node.body).replace(/[\n\s]/g, ""); for (let i = 0; i < statements.length; i++) { const statement = statements[i]; const snippet = nodeToCode(statement).replace(/[\n\s]/g, ""); // 只插入不存在的snippet if (!source.includes(snippet)) { pushStatement(node, arg, statement); } } } } } function pushStatement(node, arg, statement) { // 函数是否是react函数组件 // 情况1: 返回的三元表达式包含JSXElement // 情况2: 直接返回了JSXElement const argType = arg === null || arg === void 0 ? void 0 : arg.type; const code = nodeToCode(node); if (argType === "ConditionalExpression" && (arg.consequent.type === "JSXElement" || arg.alternate.type === "JSXElement")) { if ((0, includeChinese_1.includeChinese)(code) && node.body.type === "BlockStatement") { node.body.body.unshift(statement); } } else if (argType === "JSXElement" && node.body.type === "BlockStatement") { node.body.body.unshift(statement); } } function transformJs(code, options) { const config = stateManager_1.default.getToolConfig(); const defaultFormat = (a) => `{${a}}`; const jsonVarFormat = config.jsonVarFormat || defaultFormat; // console.log(config, 'config'); const { rule } = options; const { caller, functionName, customizeKey, importDeclaration, functionSnippets, forceImport, } = rule; let hasImportI18n = false; // 文件是否导入过i18n let hasTransformed = false; // 文件里是否存在中文转换,有的话才有必要导入i18n function getCallExpression(identifier, quote = "'") { const callerName = caller ? caller + "." : ""; const expression = `${callerName}${functionName}(${quote}${identifier}${quote})`; return expression; } function getReplaceValue(translationKey, params) { if (!functionName) { throw new Error("functionName is required"); } // 表达式结构 obj.fn('xx',{xx:xx}) let expression; // i18n标记有参数的情况 if (params) { const keyLiteral = getStringLiteral(translationKey); if (caller) { return core_1.types.callExpression(core_1.types.memberExpression(core_1.types.identifier(caller), core_1.types.identifier(functionName)), [keyLiteral, getObjectExpression(params)]); } else { return core_1.types.callExpression(core_1.types.identifier(functionName), [ keyLiteral, getObjectExpression(params), ]); } } else { // i18n标记没参数的情况 expression = getCallExpression(translationKey); return template_1.default.expression(expression)(); } } function transformAST(code, options) { function getTraverseOptions() { return { enter(path) { const leadingComments = path.node.leadingComments; if (leadingComments) { // 是否跳过翻译 let isSkipTransform = false; leadingComments.every((comment) => { if (comment.value.includes(constants_1.IGNORE_REMARK)) { isSkipTransform = true; return false; } return true; }); if (isSkipTransform) { path.skip(); } } }, StringLiteral(path) { // raw可以拿到未转义的原始文本。例如\u4E00,用raw获取时是'\u4E00'。用value获取的是'一' const value = path.node.extra ? path.node.extra.raw.slice(1, -1) : path.node.value; if ((0, comblie_inject_1.checkStringLiteralIsSkip)(path)) { path.skip(); return; } // 处理vue props里的中文 if ((0, includeChinese_1.includeChinese)(value) && options.isJsInVue && isPropNode(path)) { const translationKey = collector_1.default.add(value, customizeKey); const expression = `function() { return ${getCallExpression(translationKey)} }`; path.replaceWith(template_1.default.expression(expression)()); path.skip(); return; } if ((0, includeChinese_1.includeChinese)(value)) { hasTransformed = true; const translationKey = collector_1.default.add(value, customizeKey); path.replaceWith(getReplaceValue(translationKey)); } path.skip(); }, TemplateLiteral(path) { const { node } = path; const templateMembers = [...node.quasis, ...node.expressions]; templateMembers.sort((a, b) => a.start - b.start); const shouldReplace = node.quasis.some((node) => (0, includeChinese_1.includeChinese)(node.value.raw)); if (shouldReplace) { let value = ""; let slotIndex = 1; const params = {}; templateMembers.forEach(function (node) { if (node.type === "Identifier") { value += jsonVarFormat(node.name); params[node.name] = node.name; } else if (node.type === "TemplateElement") { value += node.value.raw.replace(/[\r\n]/g, ""); // 用raw防止字符串中出现 /n } else if (node.type === "MemberExpression") { const key = `slot${slotIndex++}`; value += jsonVarFormat(key); params[key] = { isAstNode: true, value: node, }; } else { // 处理${}内容为表达式的情况。例如`测试${a + b}`,把 a+b 这个语法树作为params的值, 并自定义params的键为slot加数字的形式 const key = `slot${slotIndex++}`; value += jsonVarFormat(key); const expression = (0, generator_1.default)(node).code; const tempAst = transformAST(expression, options); const expressionAst = tempAst.program.body[0].expression; params[key] = { isAstNode: true, value: expressionAst, }; } }); hasTransformed = true; const translationKey = collector_1.default.add(value, customizeKey); const slotParams = (0, isEmpty_1.default)(params) ? undefined : params; path.replaceWith(getReplaceValue(translationKey, slotParams)); } }, JSXText(path) { const value = path.node.value; if ((0, includeChinese_1.includeChinese)(value)) { hasTransformed = true; const translationKey = collector_1.default.add(value.trim(), customizeKey); path.replaceWith(core_1.types.jSXExpressionContainer(getReplaceValue(translationKey))); } path.skip(); }, JSXAttribute(path) { var _a; const node = path.node; const valueType = (_a = node.value) === null || _a === void 0 ? void 0 : _a.type; if (valueType === "StringLiteral" && node.value && (0, includeChinese_1.includeChinese)(node.value.value)) { const value = node.value.value; const translationKey = collector_1.default.add(value, customizeKey); const jsxIdentifier = core_1.types.jsxIdentifier(node.name.name); const jsxContainer = core_1.types.jSXExpressionContainer(getReplaceValue(translationKey)); hasTransformed = true; path.replaceWith(core_1.types.jsxAttribute(jsxIdentifier, jsxContainer)); path.skip(); } }, CallExpression(path) { const { node } = path; const callee = node.callee; // 根据全局配置,跳过不需要提取的函数 const globalRule = stateManager_1.default.getToolConfig().globalRule; const code = nodeToCode(node); globalRule.ignoreMethods.forEach((ignoreRule) => { if (code.startsWith(ignoreRule)) { path.skip(); return; } }); // 跳过console.log的提取 if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "console") { path.skip(); return; } // 无调用对象的情况,例如$t('xx') if (callee.type === "Identifier" && callee.name === functionName) { path.skip(); return; } // 有调用对象的情况,例如this.$t('xx')、i18n.$t('xx) if (callee.type === "MemberExpression") { if (callee.property && callee.property.type === "Identifier") { if (callee.property.name === functionName) { // 处理形如i18n.$t('xx)的情况 if (callee.object.type === "Identifier" && callee.object.name === caller) { path.skip(); return; } // 处理形如this.$t('xx')的情况 if (callee.object.type === "ThisExpression" && caller === "this") { path.skip(); return; } } } } }, ImportDeclaration(path) { const res = importDeclaration.match(/from ["'](.*)["']/); const packageName = res ? res[1] : ""; if (path.node.source.value === packageName) { hasImportI18n = true; } if (!hasImportI18n && hasTransformed) { const importAst = template_1.default.statements(importDeclaration)(); const program = path.parent; importAst.forEach((statement) => { program.body.unshift(statement); }); hasImportI18n = true; } }, ArrowFunctionExpression(path) { const { node } = path; // 函数组件必须在代码最外层 if (path.parentPath.scope.block.type !== "Program") { return; } // 允许往react函数组件中加入自定义代码 insertSnippets(node, functionSnippets); }, FunctionExpression(path) { const { node } = path; // 函数组件必须在代码最外层 if (path.parentPath.scope.block.type !== "Program") { return; } // 允许往react函数组件中加入自定义代码 insertSnippets(node, functionSnippets); }, ObjectProperty(path) { if (core_1.types.isStringLiteral(path.node.key)) { if ((0, includeChinese_1.includeChinese)(path.node.key.value)) { hasTransformed = true; const translationKey = collector_1.default.add(path.node.key.value, customizeKey); path.replaceWith(core_1.types.objectProperty(getReplaceValue(translationKey), path.node.value, true, path.node.shorthand, path.node.decorators)); } } }, ObjectMethod(path) { if (core_1.types.isStringLiteral(path.node.key)) { if ((0, includeChinese_1.includeChinese)(path.node.key.value)) { hasTransformed = true; const translationKey = collector_1.default.add(path.node.key.value, customizeKey); path.replaceWith(core_1.types.objectMethod(path.node.kind, getReplaceValue(translationKey), path.node.params, path.node.body, true, path.node.generator, path.node.async)); } } }, }; } const ast = options.parse(code); (0, traverse_1.default)(ast, getTraverseOptions()); return ast; } const ast = transformAST(code, options); const result = (0, generator_1.default)(ast, { compact: false, retainLines: true, }); // 文件里没有出现任何导入语句的情况 if (!hasImportI18n && hasTransformed) { result.code = `${importDeclaration}\n${result.code}`; } // 有forceImport时,即使没发生中文提取,也要在文件里加入i18n导入语句 if (!hasImportI18n && !hasTransformed && forceImport) { result.code = `${importDeclaration}\n${result.code}`; } return result; } exports.default = transformJs;