真.i18n自动化翻译
背景
懒,不想因为文案的问题复制,所以做一个全自动翻译脚本(插件)
前置
想要的功能
- 开发者无感,不用做任何和翻译有关的工作,开过过程中只需要将文案写到标签中
- 不影响现存的文案
思路
- 通过husky,将脚本写在pre-commit
- 通过git diff,获取发生变动的文件
- 对文件做一层过滤, 只对.vue、js文件中的文案进行翻译
- fs读取发生变动的文件的内容,最好将其解析成ast
- 遍历行数提取待翻译的文本,需要过滤掉注释中的文案,将需要翻译的文案回写到源语言json中
- 将每个文件内容中的文案替换成对应的i18n键值
- 读取源语言json,和目标语言json对比找出需要翻译的文案,文案调用第三方翻译接口对进行翻译(不要用公开的,容易挂)
- 统一将新翻译的文案注入json中
准备
- 一个第三方接口
- npm i husky
- 框架接入i18n
代码
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()
后续
- 因为vue2支持的ast转化库太少了,没找到合适的,需要自己写,懒得写了,所以2~6步跳过,代码上面也有,无非就是递归遍历ast树,替换文案,再转回字符串会写到文件中
- 感觉写成webpack/vite的插件会更好。有空在做吧。
作者:濷褚餾㨉㺭
来源:juejin.cn/post/7316357622847782931
来源:juejin.cn/post/7316357622847782931