老项目再也不怕找汉字国际化了

45 阅读3分钟

能够批量找出项目所有汉字,并替换对应语言的语法格式。减轻开发人员痛苦查找汉字过程。

1.vue2,vue3国际化

// 解析vue国际化项目
const fs = require('fs');
const path = require('path')
const babel = require('@babel/core');
let traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default;
const compiler = require('@vue/compiler-sfc');

// 源代码目录
const startPath = '../agents-desktop/src'
const excludeFiles = ['.ts', '.vue', '.js']
// 所有文本数组
let textArr = []
// 预设
const presets = [
  '@vue/babel-preset-jsx',
  [
    "@babel/preset-env",
    {
      "useBuiltIns": "entry",
      "corejs": "3.22"
    }
  ],
  "@babel/preset-typescript"
]

function findFiles(dir) {
  const files = fs.readdirSync(dir);
  const result = [];

  for (let i = 0; i < files.length; i++) {
    const filePath = path.join(dir, files[i]);
    const stat = fs.statSync(filePath);

    if (stat.isDirectory()) {
      // 判断当前目录是否否存在,存在则创建
      const dirPath = path.resolve(__dirname, filePath.replace('..\\agents-desktop\\', './').replace(/\\/, '/'))
      !fs.existsSync(dirPath) && fs.mkdirSync(dirPath)
      // 如果是目录,递归查找
      result.push(...findFiles(filePath));
    } else {
      // 如果是文件,将文件路径添加到结果数组中
      if (excludeFiles.includes(path.extname(filePath)))
        result.push(filePath);
    }
  }

  return result;
}

// 得到所有匹配到的文件
const files = findFiles(startPath);
files.forEach(a => {
  readFile(a)
})

function readFile(filePath) {
  let newCode = ''
  const extname = path.extname(filePath)
  const isVue = extname === '.vue';
  const code = fs.readFileSync(filePath, 'utf-8')
  if(!code){
    return
  }
  if (isVue) {
    // 将代码转化成ast
    const result = compiler.parse(code);
    const templateContent = result.descriptor.template?.ast;
    let content = result.descriptor.template?.content;
    // 遍历template中的节点
    const templateTextArr = []
    function transformAttributes(children = []) {
      children.forEach(node => {
        // 遍历属性
        node.props && node.props.length && node.props.forEach(a => {
          // 属性
          if (a.type === 6) {
            // 属性文本
            // 文本节点
            const regex = /[\u4e00-\u9fff]+/g;
            const value = a.value?.content
            if (!value || !regex.test(value)) {
              return
            }
            const scoreCode = getSoucre(`const a = "${value}"`, isTemplate = true).replace('const a = ', '')
            templateTextArr.push({
              name: a.name,
              matchText: a.loc.source,
              value: `:${a.name}="${scoreCode.substring(0, scoreCode.length - 1)}"`
            })

          } else if (a.type === 7) {
            // 遍历
            const value = a.exp?.content
            const regex = /[\u4e00-\u9fff]+/g;
            // 匹配汉字
            if (!value || !regex.test(value)) {
              return
            }
            const scoreCode = getSoucre(`const a = ${value}`, isTemplate = true).replace('const a = ', '')
            templateTextArr.push({
              name: a.rawName,
              matchText: a.loc.source,
              len: a.loc.source.length,
              value: `${a.rawName}="${scoreCode.substring(0, scoreCode.length - 1)}"`
            })
          }

        })
        if (node.type === 2) {
          // 文本节点
          const regex = /[\u4e00-\u9fff]+/g;
          const value = node.content
          // 匹配汉字
          if (!regex.test(value)) {
            return
          }
          templateTextArr.push({
            matchText: node.loc.source,
            length: value.length,
            code: value
          });
          // 可以入库
          textArr.push(value);
          textArr = [...new Set(textArr)]
        } else if (node.type === 5) {
          const value = node.content.content
          const regex = /[\u4e00-\u9fff]+/g;
          // 匹配汉字
          if (!regex.test(value)) {
            return
          }
          const scoreCode = getSoucre(`${value}`, isTemplate = true)
          templateTextArr.push({
            matchText: node.loc.source,
            length: value.length,
            value: `{{${scoreCode.substring(0, scoreCode.length - 1)}}}`
          });
        }
        node.children && node.children.length && transformAttributes(node.children)
      })
    }
    // 应用修改
    content && transformAttributes(templateContent.children)
    let arr = templateTextArr.sort((a, b) => {
      return a.value || a.length > b.length ? -1 : 1
    })
    arr.forEach(a => {
      if (a.value) {
        content = content.replace(a.matchText, a.value)
      } else {
        content = content.replace(a.matchText, `{{$t('${textArr.indexOf(a.code)}')}}`)
      }
    })
    // 将修改后的AST转换回代码
    // 针对vue文件中script部分
    const scriptContent = result.descriptor.script?.content || result.descriptor.scriptSetup?.content || ''
    // 拼接文档
    function mergeContent(template, script, scriptConfig = {}, styles) {
      const { attrs = {}, setup } = scriptConfig
      delete attrs.setup
      let styleContent = ''
      styles.forEach(a => {
        styleContent += `<style ${a.scoped ? 'scoped' : ''} lang=${a.lang}>${a.content}</style>\n`
      })
      return `<template>
      ${template}
    </template>
    <script ${setup ? 'setup' : ''}  ${Object.entries(attrs).reduce((a, b) => (a + (`${b[0]}="${b[1]}" `)), '')}>
    import locale from '@/i18n/index.ts';\n
    ${script}
    </script>
    ${styleContent}
    `
    }
    const newScriptCode = getSoucre(scriptContent, false, result.descriptor?.script?.setup || result.descriptor?.scriptSetup?.setup || false);
    newCode = mergeContent(content, newScriptCode, result.descriptor.script || result.descriptor.scriptSetup || {}, result.descriptor.styles)
  }else{
    // 除了.vue文件之外的处理方式
    const appendText = `import locale from '@/i18n/index.ts';\n`
    newCode = `${appendText}${getSoucre(code,false)}`
  }

  function getSoucre(scriptContent, isTemplate = false , isSetup = false) {
    const ast = babel.parse(scriptContent, {
      filename: filePath,
      ast: true,
      presets,
      plugins: [["transform-vue-jsx", {
        "functional": false
      }],"@babel/plugin-transform-typescript"],
    })
    traverse(ast, {
      StringLiteral(path) {
        const value = path.node.value
        // 匹配汉字
        const regex = /[\u4e00-\u9fff]+/g;
        if (regex.test(value)) {
          if(path.parent?.callee?.object?.name === 'console' && path.parent?.callee?.property?.name === 'log'){
            return 
          }
          textArr.push(value)
          textArr = [...new Set(textArr)]
          if(isVue){
            path.node.value = `<tt>${isTemplate ? '$' : 'locale.global.'}t('${textArr.indexOf(value)}')<tt>`
          }else{
            path.node.value = `<tt>locale.global.t('${textArr.indexOf(value)}')<tt>`
          }
        }
      },
      TemplateElement(path){
        const regex = /[\u4e00-\u9fff]+/g;
        const value = path.node.value.raw
        if(!value || !regex.test(value)){
            return 
        }
        textArr.push(value)
        textArr = [...new Set(textArr)]
        if(isVue){
            path.node.value.raw = `\${${isTemplate ? '$' : 'locale.global.'}t('${textArr.indexOf(value)}')}`
        }else{
            path.node.value.raw = `\${${`locale.global.t('${textArr.indexOf(value)}')`}\}`
        }
      }
    })
    // script代码部分
    const scriptCode = generate(ast, {}).code;
    const regex1 = /("<tt>|'<tt>|<tt>"|<tt>')/g;
    
    return scriptCode.replace(regex1, '')
  }
  fs.writeFileSync(`.${filePath.replace('..\\agents-desktop', '').replace(/\\/g, '/')}`, newCode)
  // 针对.ts
}

// readFile('./AddTask.vue')
const obj = {}
textArr.forEach((a, b) => {
  obj[b] = a
})
fs.writeFileSync('./agents-desktop.json', JSON.stringify(obj))

2.react项目国际化

// 解析react项目

const fs = require('fs');
const path = require('path')
const t = require('@babel/types')
const babel = require('@babel/core');
let traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
// 预设
const presets = [
  "@babel/preset-react",
  [
    "@babel/preset-env",
    {
      "useBuiltIns": "entry",
      "corejs": "3.22"
    }
  ],
  "@babel/preset-typescript"
]
// 文本内容数组
let textList = [];

// 示例:读取文件并输出处理后的AST
const dirPath = '../iflygpt-finetune-webapp-front-master/frontend/webfront/src/';

function findFiles(dir) {
  const files = fs.readdirSync(dir);

  let newDir = dir.replace('..\\iflygpt-finetune-webapp-front-master\\frontend\\webfront\\', '\\').replace(/\\/g, '/')
  if (dirPath !== dir) {
    const dirPath = path.join(__dirname, newDir)
    !fs.existsSync(dirPath) && fs.mkdirSync(dirPath)
  }
  const result = [];

  for (let i = 0; i < files.length; i++) {
    const filePath = path.join(dir, files[i]);
    const stat = fs.statSync(filePath);

    if (stat.isDirectory()) {
      // 如果是目录,递归查找
      result.push(...findFiles(filePath));
    } else {
      // 如果是文件,将文件路径添加到结果数组中
      if (['.ts', '.tsx'].includes(path.extname(filePath)))
        result.push(filePath);
    }
  }

  return result;
}

function readFile(filePath) {
  const len = textList.length;
  let index = 0;
  // 读取并解析TSX文件
  const code = fs.readFileSync(filePath, 'utf-8')
  const extname = path.extname(filePath)
  const isTs = extname === '.ts'
  // 每个文件前面先引入
  const appendText = `
  import { FormattedMessage, useIntl } from 'react-intl';\n
  `
  const appendText1 = `import locale from "@/locales/index.js";\n
  
  `
  const ast = babel.parseSync(code, {
    ast: true,
    filename: 'file.tsx',
    presets: presets,
    plugins: ["react-intl", "@babel/plugin-transform-react-jsx", "@babel/plugin-transform-typescript"],
  })
  traverse(ast, {
    enter(path) {
      const value = path.node.value;
        // 匹配汉字
      const regex = /[\u4e00-\u9fff]/g;
      if (path.node.type === 'StringLiteral' && regex.test(value)) {
        textList.push(path.node.value)
        textList = [...new Set(textList)]
        if(isTs){
          path.node.value = `<tt>locale[${textList.indexOf(value)}]<tt>`
          return 
        }
        const parent = path.parent;
        // 表达式
        if (
          t.isExpressionStatement(parent) ||
          (t.isCallExpression(parent) && parent.arguments.includes(path.node))
        ) {
          path.node.value = `<tt>intl.formatMessage({id:'${textList.indexOf(value)}'})<tt>`
        }else{
          path.node.value = `<tt>{intl.formatMessage({id:'${textList.indexOf(value)}'})}<tt>`
        }
        
      } else if (path.node.type === 'JSXText' && regex.test(value)) {
        textList.push(path.node.value)
        textList = [...new Set(textList)]
        if(isTs){
          path.node.value = `<tt>locale[${textList.indexOf(value)}]<tt>`
          return 
        }
        path.node.value = `<FormattedMessage id={'${textList.indexOf(value)}'} />`
      }
    },
    CallExpression(path) {
      if (
        path.node.callee.name === 'useState' && index === 0
      ) {
      index++
        // 创建一个变量声明的节点
        const variableDeclaration = t.variableDeclaration('const', [
          t.variableDeclarator(
            t.identifier('intl'), // 变量名
            t.callExpression(       // 函数调用
              t.identifier('useIntl '), // 函数名
              []                            // 参数列表
            )
          )
        ]);
        path.parentPath.insertAfter(variableDeclaration)
      }

    }
  })
  const result = generate(ast, {}).code;
  const regex1 = /("<tt>|'<tt>|<tt>"|<tt>')/g
  const newLength = textList.length
  const newCode = `${isTs ? appendText1 : ''}${newLength === len && !isTs ? "" : appendText}${result.replace(regex1,"")}`
  let a = filePath.replace('..\\iflygpt-finetune-webapp-front-master\\frontend\\webfront\\','\\').replace(/\\/g,'/')
  fs.writeFileSync(`.${a}`,newCode)
  // fs.writeFileSync(`./new.tsx`,newCode)
}

// readFile('./test.tsx')
const fileList = findFiles(dirPath)
fileList.forEach(a=>{
  readFile(a)
})

const obj = {}

textList.forEach((a,b)=>{
   obj[b] = a
})
fs.writeFileSync('./index.json',JSON.stringify(obj))

  1. 依赖
"devDependencies": {
    "@babel/cli": "^7.24.5",
    "@babel/core": "^7.24.5",
    "@babel/plugin-transform-typescript": "^7.24.5",
    "@babel/preset-env": "^7.24.5",
    "@babel/types": "^7.24.5",
    "@vue/babel-plugin-jsx": "^1.2.2",
    "@vue/babel-preset-app": "^5.0.8",
    "@vue/cli-plugin-babel": "^5.0.8",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^9.1.3",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-vue-jsx": "^3.7.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-vue": "^2.0.2"
  },
  "dependencies": {
    "@babel/generator": "^7.24.5",
    "@babel/parser": "^7.24.5",
    "@babel/plugin-transform-react-jsx": "^7.23.4",
    "@babel/plugin-transform-runtime": "^7.24.3",
    "@babel/preset-react": "^7.24.1",
    "@babel/preset-typescript": "^7.24.1",
    "@babel/traverse": "^7.24.5",
    "@vue/compiler-core": "^3.4.27",
    "@vue/compiler-dom": "^3.4.27",
    "@vue/compiler-sfc": "^3.4.27",
    "babel-plugin-react-intl": "^8.2.25",
    "react-intl": "^6.6.6",
    "vue-template-compiler": "^2.7.16"
  }