真.i18n自动化翻译

背景

懒,不想因为文案的问题复制,所以做一个全自动翻译脚本(插件)

前置

想要的功能

  1. 开发者无感,不用做任何和翻译有关的工作,开过过程中只需要将文案写到标签中
  2. 不影响现存的文案

思路

  1. 通过husky,将脚本写在pre-commit
  2. 通过git diff,获取发生变动的文件
  3. 对文件做一层过滤, 只对.vue、js文件中的文案进行翻译
  4. fs读取发生变动的文件的内容,最好将其解析成ast
  5. 遍历行数提取待翻译的文本,需要过滤掉注释中的文案,将需要翻译的文案回写到源语言json中
  6. 将每个文件内容中的文案替换成对应的i18n键值
  7. 读取源语言json,和目标语言json对比找出需要翻译的文案,文案调用第三方翻译接口对进行翻译(不要用公开的,容易挂)
  8. 统一将新翻译的文案注入json中

准备

  1. 一个第三方接口
  2. npm i husky
  3. 框架接入i18n

代码

javascript 复制代码
const fs = require('fs')
const crypto = require('crypto')
const path = require('path')
const { execSync } = require('child_process')
const fetch = require('node-fetch').default

const apiUrl = '自己的接口'
const scriptDirectory = __dirname
const projectRootDir = path.resolve(scriptDirectory, '../..')
const sourceFilePath = path.join(projectRootDir, './src/assets/lang/json/zh-CN.json')
const targetFilePath = path.join(projectRootDir, './src/assets/lang/json/en-US.json')
const source = require(sourceFilePath)
const target = require(targetFilePath)

function md5(text) {
  return crypto.createHash('md5').update(text).digest('hex')
}

function containsChinese(text) {
  return /[\u4e00-\u9fa5]/.test(text)
}

async function translate(data, languageCode) {
  let sign = 'bwcode.'
  sign += data.map(item => item.fieldName).sort().join('.')
  sign = md5(sign)
  const bodyRequest = {
    sign,
    languageCode,
    translateList: data,
  }
  try {
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(bodyRequest),
    })
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`)
    }
    const resJSON = await response.json()
    if (resJSON.code !== 0) {
      throw new Error(`result error! Status: ${JSON.stringify(resJSON)}`)
    }
    return resJSON.data.translateList
  } catch (error) {
    console.error('Error during translation request:', error.message)
  }
}

// 暂时只翻译英语,后面有需要再拓展
async function startTranslate(data) {
  const res = await translate(data, 'en-US')
  // 转成对象
  const fileContent = target
  res.map(item => {
    const key = item.fieldName.split('-')
    if (key.length === 2) {
      if (!fileContent[key[0]]) {
        fileContent[key[0]] = {}
      }
      fileContent[key[0]][key[1]] = item.translateContent
    } else {
      fileContent[key[0]] = item.translateContent
    }
  })
  fs.writeFileSync(targetFilePath, JSON.stringify(fileContent, null, 2), 'utf-8')
}

// 获取在 Git 中修改的文件列表
function getModifiedFiles() {
  try {
    // const result = execSync('git diff --name-only --cached', { encoding: 'utf-8' })  // 获取暂存区的修改
    const result = execSync('git diff --name-only', { encoding: 'utf-8' })              // 获取工作区的修改
    return result.split('\n').filter(item => item.includes('views'))
  } catch (error) {
    console.error('Error getting modified files:', error)
    return []
  }
}

function extractTemplateChinese(node) {
  // 在线ast tree解析:https://astexplorer.net/
  if (node.children && node.children.length) {
    // console.log('node parent ------------------', node)
    node.children.forEach(item => {
      extractTemplateChinese(item)
    })
  } else if (containsChinese(node.value)) {
    const regex = /[\u4e00-\u9fa5\s]+/
    const content = node.value.match(regex)
    source[content] = content
    const newValue = node.value.replace(regex, match => `{{ $t('${match}') }}`)
    node.value = newValue
  }
}

function scanAndReplace(fileDirectory) {
  const filePath = path.join(projectRootDir, `/${fileDirectory}`)
  const pageContent = fs.readFileSync(filePath, 'utf8')
  // vue2好像不支持ast,没有相关的ast库,所以还是直接用文本替换吧
  // extractTemplateChinese(ast.templateBody)
  // 匹配非注释的中文
  const translationRegex = /[\u4e00-\u9fa5\s]+/g
  // todo: js文件、template标签、scripts标签中的文案替换格式会不一样的
  const translateData = pageContent.match(translationRegex)
  const replacedPageContent = pageContent.replace(translationRegex, match => `{{ $t('${match}') }}`)
  if (translateData && translateData.length) {
    // 将替换后的内容写回文件
    fs.writeFileSync(filePath, replacedPageContent)
    // 记录文案,等所有文件扫描完毕后再回填数据
    translateData.forEach(item => { source[item] = item })
  }
}

function main() {
  console.log('-- start translate --')
  // 获取git diff的文件,寻找需要翻译的文案,并将文案提取出来新增到json后,文案替换成i18n格式
  // 提取文案的过程最好用ast的方法,否则很难判断哪些中文是需要提取,哪些是注释
  // 然而vue2的库太少了,要自己写,后面升级到vue3再完善这个自动提取的过程吧。目前要翻译什么文案还是手动去提取吧
  // const modifiedFiles = getModifiedFiles()
  // console.log('need translate file:', modifiedFiles)
  // modifiedFiles.forEach(item => {
  //   // 暂时只对.vue文件进行翻译
  //   if (item.includes('.vue')) scanAndReplace(item)
  // })

  // 读取json,批量进行翻译(需要过滤掉已经翻译的文案)
  const flat = []
  // const sourceEntries = Object.entries(source)
  // const targetEntries = Object.entries(target)
  const translate = {}
  for (const key in source) {
    if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) {
      translate[key] = source[key]
    }
  }
  const translateEntries = Object.entries(translate)
  translateEntries.forEach(([sourceKey, sourceValue]) => {
    flat.push({ fieldName: sourceKey, content: sourceValue })
    // if (typeof sourceValue === 'object' && sourceValue !== null) {
    //   const entriesChild = Object.entries(sourceValue)
    //   entriesChild.forEach(([entriesChildKey, entriesChildValue]) => {
    //     flat.push({
    //       fieldName: sourceKey+'-'+entriesChildKey,
    //       content: entriesChildValue,
    //     })
    //   })
    // } else {
    //   flat.push({ fieldName: sourceKey, content: sourceValue })
    // }
  })
  if (flat && flat.length) startTranslate(flat)
  else console.log('no translate data')
  console.log('-- end translate --')
}

main()

后续

  1. 因为vue2支持的ast转化库太少了,没找到合适的,需要自己写,懒得写了,所以2~6步跳过,代码上面也有,无非就是递归遍历ast树,替换文案,再转回字符串会写到文件中
  2. 感觉写成webpack/vite的插件会更好。有空在做吧。
相关推荐
前端付豪1 小时前
1、震惊!99% 前端都没搞懂的 JavaScript 类型细节
前端·javascript·面试
朝与暮1 小时前
js符号(Symbol)
前端·javascript
大怪v2 小时前
前端:人工智能?我也会啊!来个花活,😎😎😎“自动驾驶”整起!
前端·javascript·算法
遂心_4 小时前
为什么 '1'.toString() 可以调用?深入理解 JavaScript 包装对象机制
前端·javascript
王同学QaQ4 小时前
Vue3对接UE,通过MQTT完成通讯
javascript·vue.js
程序员鱼皮5 小时前
刚刚 Java 25 炸裂发布!让 Java 再次伟大
java·javascript·计算机·程序员·编程·开发·代码
Asort5 小时前
JavaScript 从零开始(五):运算符和表达式——从零开始掌握算术、比较与逻辑运算
前端·javascript
一枚前端小能手5 小时前
🚀 缓存用错了网站更慢?前端缓存策略的5个致命误区
前端·javascript
艾小码5 小时前
为什么你的页面会闪烁?useLayoutEffect和useEffect的区别藏在这里!
前端·javascript·react.js
艾小码5 小时前
告别Vue混入的坑!Composition API让我效率翻倍的3个秘密
前端·javascript·vue.js