UNPKG

cnd-i18n-loader

Version:

cnd-i18n-loader 一键式实现项目的国际化 语言包 处理的过程

413 lines (401 loc) 14.8 kB
require("@colors/colors") const path = require("path") const fs = require("fs") const axios = require("axios") const https = require("https") const agent = new https.Agent({ rejectUnauthorized: false }) const ora = require("ora") const Utils = require("../lib/utils") const { config_file, languages } = require("./const") // 引入ebl请求 const requestEbl = require("./request_ebl") let map = {} // 遍历映射中英文对象 let result = {} // map 中英文对照表,result 翻译结果 let fromData = {} // 需要翻译的中文包 let config = {} // 项目配置文件 let toCode = "" //要翻译的语言 let OPENAI_API_KEY = "" // chatGPT key let translateNum = 0 // 分段翻译时的翻译数 let totalTranslateNum = 0 // 本次语言翻译的总个数 const { Configuration, OpenAIApi } = require("openai") // chatGPT function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } // gpt翻译 const gptTranslate = async (chatGPTData) => { const configuration = new Configuration({ apiKey: OPENAI_API_KEY }) const openai = new OpenAIApi(configuration) const lang = languages[toCode] || toCode try { const response = await openai.createChatCompletion({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: "你是一个善于翻译的人工智能助手." }, { role: "user", content: `I want you to act as an ${lang} translator. Your translated content will be applied on the user interface of the software. I will push a json object in any language and you will detect the json value language, translate json value in ${lang}, ignore json key. Keep the meaning the same, but be as concise as possible. return only json content, the return json not allow trailing commas. My first json is ${JSON.stringify(chatGPTData)}` } ] }) let choice = response.data.choices[0].message.content const startIndex = choice.indexOf("{") const endIndex = choice.lastIndexOf("}") choice = choice.slice(startIndex, endIndex + 1) const trans_result = JSON.parse(choice) Object.keys(chatGPTData).forEach((key) => { const src = chatGPTData[key] || "" //原文 let dst = trans_result[key] || "" // 译文 if (dst !== "" && src !== "") { if (toCode === "en") { dst = Utils.replaceStr(src, dst) } map[src] = dst result[key] = dst translateNum++ } }) console.log(`\n${toCode}语言翻译进度:${translateNum}/${totalTranslateNum}`) await sleep(3000) return true } catch (error) { console.log("\n") console.log("X chatGPT翻译中断".red) console.log(error.message) return false } } const translate = (val, keyList) => { const appid = config.appId const key = config.secret const salt = new Date().getTime() //取当前时间作为随机数 const query = val // 需要搜索的值 const str1 = appid + query + salt + key //秘钥 const sign = Utils.Md5(str1) //md5加密 return new Promise(function (resolve, reject) { axios({ url: "https://fanyi-api.baidu.com/api/trans/vip/translate", params: { q: query, appid: appid, salt: salt, from: "auto", to: toCode, //要翻译的语言 sign: sign }, method: "GET" }) .then((res) => { if (res.data) { const data = res.data if (data.error_code) { console.log("X 百度翻译未成功翻译\n".red) console.log(data) console.log("\n") } else { const trans_result = data.trans_result console.log("√ 这个是翻译的结果\n".green) console.log(trans_result) trans_result.forEach((item, index) => { if (toCode === "en") { let dst = Utils.replaceStr(item.src, item.dst) map[item.src] = dst result[keyList[index]] = dst } else { map[item.src] = item.dst result[keyList[index]] = item.dst } translateNum++ }) console.log( `\n${toCode}语言翻译进度:${translateNum}/${totalTranslateNum}` ) } setTimeout(() => { if (data.error_code) { resolve(false) } else { resolve(true) } }, 1500) } }) .catch((err) => { reject(err) console.log("X 百度翻译出现网络错误\n".red) }) }) } // 中台翻译 const platformTranslate = (val, keyList) => { // 请求参数 const data = { platformCode: "baidu", from: "auto", to: toCode, texts: val // 搜索值 } return new Promise(function (resolve, reject) { requestEbl("ebl.openapi.2024100001", data) .then((res) => { const { contents = [] } = res const url = "https://10.193.106.166/prod-api/system/dict/data/open" // 添加参数 const data = new URLSearchParams() data.append("dictValues", keyList) // 定义头部 const config = { httpsAgent: agent, // 忽略SSL证书验证 headers: { "Content-Type": "application/x-www-form-urlencoded" // 如果你发送的是 JSON 数据,则需要设置这个头 } } axios .post(url, data, config) .then((newRes) => { const newData = newRes.data || {} // 后台翻译得到的数据,如果有内容,则优先使用 contents.forEach((item, index) => { if (toCode === "en") { let dst = Utils.replaceStr(item.src, item.dst) let newDst if (newData[keyList[index]] && newData[keyList[index]].en) { newDst = newData[keyList[index]].en } if (keyList[index]) { map[item.src] = newDst ? newDst : dst result[keyList[index]] = newDst ? newDst : dst } } else { map[item.src] = item.dst result[keyList[index]] = item.dst } translateNum++ }) console.log( `\n${toCode}语言翻译进度:${translateNum}/${totalTranslateNum}` ) }) .catch((error) => { console.log(error, "error") }) setTimeout(() => { resolve(true) }, 1500) }) .catch((err) => { reject(err) }) }) } /** * 分割数组的方式,按照字符串长度来分割 * @param {*} arr 需要分割的数组 * @param {*} maxLength 字符串长度达到多少分割 * @returns */ function splitArrayByCharLength(arr, maxLength) { const result = [] let currentSubArray = [] let currentLength = 0 arr.forEach((item) => { const itemLength = typeof item === "string" ? item.length : 0 // 如果不是字符串,则长度为0(可以根据需要调整) if (currentLength + itemLength <= maxLength) { // 如果当前子数组加上当前项的长度仍然不超过最大长度,则添加到当前子数组 currentSubArray.push(item) currentLength += itemLength } else { // 如果超过最大长度,则将当前子数组添加到结果数组,并开始一个新的子数组 result.push(currentSubArray) currentSubArray = [item] // 新子数组以当前项开始 currentLength = itemLength // 重置当前长度为当前项的长度 } }) // 添加最后一个子数组(如果有的话)到结果数组 if (currentSubArray.length > 0) { result.push(currentSubArray) } return result } module.exports = async function (code, filename) { let processFile = "" const spinner = ora("➤ 正在翻译中,请稍后......".blue) spinner.start() // 检测有无初始化配置文件 if (fs.existsSync(config_file)) { processFile = path.join(process.cwd(), config_file) config = require(processFile) // const needFile = fs.readFileSync(config_file, "utf8") // // 清除注释 // const cleanedData = needFile.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "") // config = cleanedData // ? JSON.parse(cleanedData.replace("export default", "").replace(";", "")) // : {} } else { spinner.fail(`${config_file}文件不存在,请先初始化项目\n`.red) return } let file = config.file // 读取需要翻译的中文文件 if (fs.existsSync(config.dir + file)) { processFile = path.join(process.cwd(), config.dir + file) fromData = require(processFile) // 获取需要翻译的文件内容 // const needFile = fs.readFileSync(config.dir + file, "utf8") // fromData = needFile // ? JSON.parse(needFile.replace("export default", "").replace(";", "")) // : {} } else { spinner.fail(`需要翻译的文件不存在,${config.dir + file}\n`.red) return } let distLangs = config.distLangs || [] if ( !distLangs.includes(code) && // 指令的code 不包含在 distLangs配置项内 file !== code + ".js" // 翻译的文件语种 和 翻译目标语种不同 ) { distLangs.unshift(code) } const distLangsLength = distLangs.length let singleNum = config.singleNum || 1500 // 百度翻译 单次请求最长次数 singleNum = Number(singleNum) singleNum = singleNum > 3900 ? 3900 : singleNum singleNum = singleNum < 600 ? 600 : singleNum const mode = config.mode || "Baidu" // 翻译方式 OPENAI_API_KEY = config.openAiKey // chatGPT key for (let i = 0; i < distLangsLength; i++) { translateNum = 0 spinner.start() toCode = distLangs[i] // 获取需要翻译文件的位置 let filePath = config.dir + toCode if (filename) { file = filename.indexOf(".") > -1 ? filename : `${filename}.js` const pointLocation = file.indexOf(".") const toCodeName = file.substr(0, pointLocation) filePath = config.dir + toCode + "-" + toCodeName } // 已有翻译的数据 let oldData = {} if (fs.existsSync(filePath + ".js")) { processFile = path.join(process.cwd(), filePath + ".js") // const needFile = fs.readFileSync(filePath + ".js", "utf8") // oldData = needFile // ? JSON.parse(needFile.replace("export default", "").replace(";", "")) // : {} oldData = require(processFile) result = oldData } let query = "" // 需要翻译的文字拼接 let translateState = true // 翻译成功与否的状态 let keyList = [] const oldDataArray = Object.keys(oldData) // 通过对比中文json和英文josn,取出未翻译的中文 数组 const fromDataArray = Object.keys(fromData).filter((val) => { return oldDataArray.indexOf(val) === -1 }) oldDataArray.forEach((key) => { map[fromData[key]] = oldData[key] //填充已有的map文件 }) totalTranslateNum = fromDataArray.length // 需要翻译的语言数量 let chatGPTData = {} // 本次翻译的原文 {key:val} // 获取需要翻译的内容数组 const splitArr = fromDataArray.map((v) => { return fromData[v] }) const fromDataArr = splitArrayByCharLength(splitArr, 500) // 判断使用中台的翻译功能 if (mode === "platform") { let num = 0 for (let i = 0; i < fromDataArr.length; i++) { // 给keyList赋值 const length = fromDataArr[i].length keyList = fromDataArray.slice(num, num + length) num += length translateState = await platformTranslate(fromDataArr[i], keyList) } } else { for (let i = 0; i < totalTranslateNum; i++) { const key = fromDataArray[i] if (translateState) { if (mode === "chatGPT") { //chatGPT翻译 if (String(query).length >= 600) { translateState = await gptTranslate(chatGPTData) query = fromData[key] chatGPTData = { key: query } } else { if (i === 0) { query = fromData[key] } else { query += "\n" + fromData[key] } chatGPTData[key] = fromData[key] if (i === totalTranslateNum - 1) { translateState = await gptTranslate(chatGPTData) } } } else if (mode === "Baidu") { // 百度翻译 if (String(query).length >= singleNum) { translateState = await translate(query, keyList) query = fromData[key] keyList = [key] } else { query += "\n" + fromData[key] keyList.push(key) if (i === totalTranslateNum - 1) { translateState = await translate(query, keyList) } } } } } } if (translateState || translateNum > 0) { // translateState 翻译成功写入文件 || translateNum > 0 :分段翻译出现部分失败 const mapFile = path.join(process.cwd(), filePath + ".js.map") if (fs.existsSync(mapFile)) { let oldMap = fs.readFileSync(mapFile, "utf8") try { oldMap = JSON.parse(oldMap) } catch (e) { oldMap = {} } map = { ...map, ...oldMap } } const file = path.join(process.cwd(), filePath + ".js") fs.writeFileSync( file, `module.exports = ${JSON.stringify(result, null, "\t")};`, "utf8" ) fs.writeFileSync(mapFile, JSON.stringify(map, null, "\t"), "utf8") if (translateState && totalTranslateNum === translateNum) { spinner.succeed( `${toCode}语言已翻译完成,本次翻译个数:${translateNum}`.green ) } else { const unCompletedNum = totalTranslateNum - translateNum spinner.succeed( `${toCode}语言已翻译个数:${translateNum},未完成翻译个数:${unCompletedNum}` .green ) } } else { spinner.fail("翻译错误,请勿输入不正确的code".red) } } }