金松果 發表於 2025-3-11 15:16:00

Next.js lingui.js 多语言自动提取翻译键 - ats-node

<h2>组件依赖</h2>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">{
    "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",
    }
}
</pre>
</div>
<p>  </p>
<h2>脚本代码</h2>
<p>scripts/transform-lingui-ast.js</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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')),
    ,
    false
)
}

// 创建t(i18n)表达式
function createTExpression(text) {
return t.taggedTemplateExpression(
    t.callExpression(t.identifier('t'), ),
    t.templateLiteral(
      ,
      []
    )
)
}

// 创建msg表达式
function createMsgExpression(text) {
return t.taggedTemplateExpression(
    t.identifier('msg'),
    t.templateLiteral(
      ,
      []
    )
)
}

// 检查是否是React组件声明
function isReactComponentDeclaration(path) {
// 函数声明组件
if (
    path.type === 'FunctionDeclaration' &amp;&amp;
    path.node.id &amp;&amp;
    /^/.test(path.node.id.name)
) {
    return true
}

// 箭头函数组件
if (
    path.type === 'VariableDeclarator' &amp;&amp;
    path.node.id &amp;&amp;
    /^/.test(path.node.id.name)
) {
    return true
}

// forwardRef组件
if (
    path.type === 'CallExpression' &amp;&amp;
    path.node.callee.name === 'forwardRef' &amp;&amp;
    path.parent.type === 'VariableDeclarator' &amp;&amp;
    /^/.test(path.parent.id.name)
) {
    return true
}

// 类组件
if (
    path.type === 'ClassDeclaration' &amp;&amp;
    path.node.superClass &amp;&amp;
    ((path.node.superClass.type === 'MemberExpression' &amp;&amp;
      path.node.superClass.object.name === 'React' &amp;&amp;
      path.node.superClass.property.name === 'Component') ||
      (path.node.superClass.type === 'Identifier' &amp;&amp;
      path.node.superClass.name === 'Component'))
) {
    return true
}

return false
}

// 检查是否在React组件内部
function isInsideReactComponent(path) {
let currentPath = path
while (currentPath) {
    // 检查函数声明
    if (
      currentPath.node.type === 'FunctionDeclaration' &amp;&amp;
      currentPath.node.id &amp;&amp;
      /^/.test(currentPath.node.id.name)
    ) {
      return true
    }
    // 检查箭头函数组件
    if (
      currentPath.node.type === 'VariableDeclarator' &amp;&amp;
      currentPath.node.id &amp;&amp;
      /^/.test(currentPath.node.id.name)
    ) {
      return true
    }
    // 检查forwardRef组件
    if (
      currentPath.node.type === 'ArrowFunctionExpression' &amp;&amp;
      currentPath.parent?.type === 'CallExpression' &amp;&amp;
      currentPath.parent.callee?.name === 'forwardRef'
    ) {
      return true
    }
    // 检查类组件
    if (
      currentPath.node.type === 'ClassDeclaration' &amp;&amp;
      currentPath.node.superClass &amp;&amp;
      ((currentPath.node.superClass.type === 'MemberExpression' &amp;&amp;
      currentPath.node.superClass.object.name === 'React' &amp;&amp;
      currentPath.node.superClass.property.name === 'Component') ||
      (currentPath.node.superClass.type === 'Identifier' &amp;&amp;
          currentPath.node.superClass.name === 'Component'))
    ) {
      return true
    }
    currentPath = currentPath.parentPath
}
return false
}

// 创建导入声明
function createImportDeclaration(specifiers, source) {
return t.importDeclaration(
    specifiers.map((name) =&gt;
      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' &amp;&amp;
          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 &amp;&amp;
            value.type === 'StringLiteral' &amp;&amp;
            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) &amp;&amp; text.length &gt; 0) {
            const parent = path.parent
            if (
            parent.type === 'JSXElement' &amp;&amp;
            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) =&gt; p.isImportDeclaration()) ||
            path.findParent(
            (p) =&gt;
                p.isJSXElement() &amp;&amp; p.node.openingElement.name.name === 'Trans'
            )
          ) {
            if (path.findParent(
            (p) =&gt;
                p.isJSXElement() &amp;&amp; p.node.openingElement.name.name === 'Trans'
            )) {
            needsLinguiMacro = true// 如果在Trans标签内,也需要确保导入
            }
            return
          }

          const isInComponent = isInsideReactComponent(path)

          if (isInComponent &amp;&amp; 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' &amp;&amp;
          path.parent.callee?.name === 'forwardRef' &amp;&amp;
          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) =&gt;
            t.isVariableDeclaration(node) &amp;&amp;
            node.declarations.some(
                (dec) =&gt;
                  dec.init?.type === 'CallExpression' &amp;&amp;
                  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' &amp;&amp;
          path.node.arguments.length &gt; 0 &amp;&amp;
          t.isArrowFunctionExpression(path.node.arguments)
      ) {
          const arrowFunction = path.node.arguments
          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) =&gt;
                t.isVariableDeclaration(node) &amp;&amp;
                node.declarations.some(
                  (dec) =&gt;
                  dec.init?.type === 'CallExpression' &amp;&amp;
                  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 &gt; 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) =&gt; {
      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

if (!targetPath) {
console.error('请提供文件或文件夹路径!')
console.log('使用方法: node transform-lingui-ast.js &lt;文件或文件夹路径&gt;')
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)
}
</pre>
</div>
<p>  </p>
<h2>如何使用</h2>
<p>执行下面命令,脚本会自动对项目中的中文进行特定标签的包裹</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">node scripts/transform-lingui-ast.js
</pre>
</div>
<p>  </p>

</div>
<div id="MySignature" role="contentinfo">
    愿你走出半生,归来仍是少年<br><br>
来源:https://www.cnblogs.com/yz-blog/p/18765118
頁: [1]
查看完整版本: Next.js lingui.js 多语言自动提取翻译键 - ats-node