5
0
热心网友
{ "scripts": { "i18n:extract": "lingui extract" }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", "**/*.{js,jsx,tsx,ts,less,md,json}": [ "prettier --write" ], "**/*.{weapp,jpg,png}": "node scripts/compress-images.mjs" }, "dependencies": { "@lingui/core": "^4.0.0-next.3", "@lingui/macro": "^4.0.0-next.3", "@lingui/react": "^4.0.0-next.3", }, "devDependencies": { "@lingui/cli": "^4.0.0-next.3", "@lingui/loader": "^4.0.0-next.3", "@lingui/swc-plugin": "4.0.3", "@babel/parser": "^7.26.3", "@babel/traverse": "^7.26.4", "@babel/generator": "^7.26.3", "@babel/types": "^7.26.3", } }
scripts/transform-lingui-ast.js
const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const generate = require('@babel/generator').default const t = require('@babel/types') const fs = require('fs') const path = require('path') // 判断是否为中文字符 function containsChinese(str) { return /[\u4e00-\u9fa5]/.test(str) } // 创建Trans元素 function createTransElement(text) { return t.jsxElement( t.jsxOpeningElement(t.jsxIdentifier('Trans'), [], false), t.jsxClosingElement(t.jsxIdentifier('Trans')), [t.jsxText(text)], false ) } // 创建t(i18n)表达式 function createTExpression(text) { return t.taggedTemplateExpression( t.callExpression(t.identifier('t'), [t.identifier('i18n')]), t.templateLiteral( [t.templateElement({ raw: text, cooked: text }, true)], [] ) ) } // 创建msg表达式 function createMsgExpression(text) { return t.taggedTemplateExpression( t.identifier('msg'), t.templateLiteral( [t.templateElement({ raw: text, cooked: text }, true)], [] ) ) } // 检查是否是React组件声明 function isReactComponentDeclaration(path) { // 函数声明组件 if ( path.type === 'FunctionDeclaration' && path.node.id && /^[A-Z]/.test(path.node.id.name) ) { return true } // 箭头函数组件 if ( path.type === 'VariableDeclarator' && path.node.id && /^[A-Z]/.test(path.node.id.name) ) { return true } // forwardRef组件 if ( path.type === 'CallExpression' && path.node.callee.name === 'forwardRef' && path.parent.type === 'VariableDeclarator' && /^[A-Z]/.test(path.parent.id.name) ) { return true } // 类组件 if ( path.type === 'ClassDeclaration' && path.node.superClass && ((path.node.superClass.type === 'MemberExpression' && path.node.superClass.object.name === 'React' && path.node.superClass.property.name === 'Component') || (path.node.superClass.type === 'Identifier' && path.node.superClass.name === 'Component')) ) { return true } return false } // 检查是否在React组件内部 function isInsideReactComponent(path) { let currentPath = path while (currentPath) { // 检查函数声明 if ( currentPath.node.type === 'FunctionDeclaration' && currentPath.node.id && /^[A-Z]/.test(currentPath.node.id.name) ) { return true } // 检查箭头函数组件 if ( currentPath.node.type === 'VariableDeclarator' && currentPath.node.id && /^[A-Z]/.test(currentPath.node.id.name) ) { return true } // 检查forwardRef组件 if ( currentPath.node.type === 'ArrowFunctionExpression' && currentPath.parent?.type === 'CallExpression' && currentPath.parent.callee?.name === 'forwardRef' ) { return true } // 检查类组件 if ( currentPath.node.type === 'ClassDeclaration' && currentPath.node.superClass && ((currentPath.node.superClass.type === 'MemberExpression' && currentPath.node.superClass.object.name === 'React' && currentPath.node.superClass.property.name === 'Component') || (currentPath.node.superClass.type === 'Identifier' && currentPath.node.superClass.name === 'Component')) ) { return true } currentPath = currentPath.parentPath } return false } // 创建导入声明 function createImportDeclaration(specifiers, source) { return t.importDeclaration( specifiers.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)) ), t.stringLiteral(source) ) } // 创建useLingui hook声明 function createUseLinguiDeclaration() { return t.variableDeclaration('const', [ t.variableDeclarator( t.objectPattern([ t.objectProperty( t.identifier('i18n'), t.identifier('i18n'), false, true ) ]), t.callExpression(t.identifier('useLingui'), []) ) ]) } // 处理单个文件 function transformFile(filePath) { try { // 检查文件是否为 TypeScript/React 文件 if (!/\.(tsx?|jsx?)$/.test(filePath)) { return } console.log('处理文件:', filePath) const sourceCode = fs.readFileSync(filePath, 'utf-8') // 解析代码生成 AST const ast = parser.parse(sourceCode, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'], tokens: true, attachComment: true }) // 用于跟踪需要添加的导入和hooks let needsLinguiMacro = false let needsLinguiReact = false let componentsNeedingUseLingui = new Set() let hasExistingLinguiImports = false // 第一次遍历:检查现有的导入 traverse(ast, { ImportDeclaration(path) { const source = path.node.source.value if (source === '@lingui/macro' || source === '@lingui/react') { hasExistingLinguiImports = true // 移除现有的导入,稍后会重新添加 path.remove() } } }) // 转换JSX中的中文内容 traverse(ast, { // 处理组件声明 'FunctionDeclaration|VariableDeclarator|CallExpression'(path) { if (!isReactComponentDeclaration(path)) return let componentName if (path.type === 'FunctionDeclaration') { componentName = path.node.id.name } else if (path.type === 'VariableDeclarator') { componentName = path.node.id.name } else if ( path.type === 'CallExpression' && path.node.callee.name === 'forwardRef' ) { // 获取forwardRef组件的名称 if (path.parent?.type === 'VariableDeclarator') { componentName = path.parent.id.name } } if (componentName) { componentsNeedingUseLingui.add(componentName) needsLinguiReact = true } }, // 处理JSX属性中的中文 JSXAttribute: { exit(path) { if (!isInsideReactComponent(path)) return const value = path.node.value if ( value && value.type === 'StringLiteral' && containsChinese(value.value) ) { path.node.value = t.jsxExpressionContainer( createTExpression(value.value) ) needsLinguiMacro = true needsLinguiReact = true } } }, // 处理JSX文本中的中文 JSXText: { exit(path) { if (!isInsideReactComponent(path)) return const text = path.node.value.trim() if (containsChinese(text) && text.length > 0) { const parent = path.parent if ( parent.type === 'JSXElement' && parent.openingElement.name.name === 'Trans' ) { needsLinguiMacro = true // 即使已经是Trans标签,也需要确保导入 return } path.replaceWith(createTransElement(path.node.value)) needsLinguiMacro = true } } }, // 检查是否使用了Trans组件 JSXElement(path) { if (path.node.openingElement.name.name === 'Trans') { needsLinguiMacro = true } }, // 处理字符串字面量 StringLiteral: { exit(path) { if ( !path.node.value || !containsChinese(path.node.value) || path.findParent((p) => p.isImportDeclaration()) || path.findParent( (p) => p.isJSXElement() && p.node.openingElement.name.name === 'Trans' ) ) { if (path.findParent( (p) => p.isJSXElement() && p.node.openingElement.name.name === 'Trans' )) { needsLinguiMacro = true // 如果在Trans标签内,也需要确保导入 } return } const isInComponent = isInsideReactComponent(path) if (isInComponent && path.parent.type === 'JSXAttribute') { return } if (isInComponent) { path.replaceWith(createTExpression(path.node.value)) needsLinguiMacro = true needsLinguiReact = true } else { path.replaceWith(createMsgExpression(path.node.value)) needsLinguiMacro = true } } } }) // 第三次遍历:添加useLingui hook到需要的组件 traverse(ast, { 'FunctionDeclaration|ArrowFunctionExpression'(path) { let componentName let isForwardRef = false // 处理普通函数组件 if (path.node.type === 'FunctionDeclaration') { componentName = path.node.id?.name } // 处理箭头函数组件 else if (path.parent?.type === 'VariableDeclarator') { componentName = path.parent.id?.name } // 处理forwardRef组件 else if ( path.parent?.type === 'CallExpression' && path.parent.callee?.name === 'forwardRef' && path.parent.parent?.type === 'VariableDeclarator' ) { componentName = path.parent.parent.id?.name isForwardRef = true } if (!componentName || !componentsNeedingUseLingui.has(componentName)) return const body = path.node.body if (t.isBlockStatement(body)) { // 检查是否已经有useLingui声明 const hasUseLingui = body.body.some( (node) => t.isVariableDeclaration(node) && node.declarations.some( (dec) => dec.init?.type === 'CallExpression' && dec.init.callee.name === 'useLingui' ) ) if (!hasUseLingui) { body.body.unshift(createUseLinguiDeclaration()) } } else if (t.isJSXElement(body)) { // 如果直接返回JSX,需要包装在代码块中 path.node.body = t.blockStatement([ createUseLinguiDeclaration(), t.returnStatement(body) ]) } }, // 专门处理forwardRef的箭头函数组件 CallExpression(path) { if ( path.node.callee.name === 'forwardRef' && path.node.arguments.length > 0 && t.isArrowFunctionExpression(path.node.arguments[0]) ) { const arrowFunction = path.node.arguments[0] const componentName = path.parent?.id?.name if (!componentName || !componentsNeedingUseLingui.has(componentName)) return const body = arrowFunction.body if (t.isBlockStatement(body)) { // 检查是否已经有useLingui声明 const hasUseLingui = body.body.some( (node) => t.isVariableDeclaration(node) && node.declarations.some( (dec) => dec.init?.type === 'CallExpression' && dec.init.callee.name === 'useLingui' ) ) if (!hasUseLingui) { body.body.unshift(createUseLinguiDeclaration()) } } else if (t.isJSXElement(body)) { // 如果直接返回JSX,需要包装在代码块中 arrowFunction.body = t.blockStatement([ createUseLinguiDeclaration(), t.returnStatement(body) ]) } } } }) // 添加必要的导入语句 const imports = [] if (needsLinguiMacro) { imports.push( createImportDeclaration( ['Trans', 'msg', 't', 'Plural'], '@lingui/macro' ) ) } if (needsLinguiReact) { imports.push(createImportDeclaration(['useLingui'], '@lingui/react')) } // 在文件开头添加导入语句 if (imports.length > 0) { ast.program.body.unshift(...imports) } // 生成转换后的代码 const output = generate(ast, { retainLines: true, compact: 'auto', concise: false, jsescOption: { minimal: true }, sourceMaps: false, comments: true }) // 直接覆盖源文件 fs.writeFileSync(filePath, output.code) console.log('已更新文件:', filePath) } catch (error) { console.error(`处理文件 ${filePath} 时出错:`, error) } } // 处理文件夹 function transformDirectory(dirPath) { try { const files = fs.readdirSync(dirPath) files.forEach((file) => { const fullPath = path.join(dirPath, file) const stat = fs.statSync(fullPath) if (stat.isDirectory()) { // 递归处理子文件夹 transformDirectory(fullPath) } else { // 处理文件 transformFile(fullPath) } }) } catch (error) { console.error(`处理文件夹 ${dirPath} 时出错:`, error) } } // 获取命令行参数 const targetPath = process.argv[2] if (!targetPath) { console.error('请提供文件或文件夹路径!') console.log('使用方法: node transform-lingui-ast.js <文件或文件夹路径>') process.exit(1) } // 检查路径是否存在 if (!fs.existsSync(targetPath)) { console.error('指定的路径不存在!') process.exit(1) } // 判断是文件还是文件夹 const stat = fs.statSync(targetPath) if (stat.isDirectory()) { transformDirectory(targetPath) } else { transformFile(targetPath) }
执行下面命令,脚本会自动对项目中的中文进行特定标签的包裹
node scripts/transform-lingui-ast.js
使用道具 舉報
本版積分規則 發表回覆 回帖並轉播 回帖後跳轉到最後一頁
相关侵权、举报、投诉及建议等,请发 E-mail:qiongdian@foxmail.com
Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.