UNPKG

i18-fe-automator

Version:

前端代码提取中文并替换成$t函数

345 lines (344 loc) 17.9 kB
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _Translator_provider, _Translator_targetLocale, _Translator_providerOptions, _Translator_textLengthLimit, _Translator_separator; Object.defineProperty(exports, "__esModule", { value: true }); const fs_extra_1 = __importDefault(require("fs-extra")); const index_1 = require("./translate-utils/src/index"); const getAbsolutePath_1 = require("./utils/getAbsolutePath"); const log_1 = __importDefault(require("./utils/log")); const constants_1 = require("./utils/constants"); const getLang_1 = __importDefault(require("./utils/getLang")); const stateManager_1 = __importDefault(require("./utils/stateManager")); const classifyString_1 = require("./utils/classifyString"); const saveLocaleFile_1 = require("./utils/saveLocaleFile"); const flatObjectDeep_1 = require("./utils/flatObjectDeep"); const spreadObject_1 = require("./utils/spreadObject"); async function translateByGoogle(word, locale, options) { // if (!options.google || !options.google?.proxy) { if (!options.google) { log_1.default.error('翻译失败,当前翻译器为谷歌,请完善google配置参数'); process.exit(1); } try { return await (0, index_1.googleTranslate)(word, 'zh-CN', locale, options.google.proxy || ''); } catch (e) { if (e.name === 'TooManyRequestsError') { log_1.default.error('翻译失败,请求超过谷歌api调用次数限制'); } else { log_1.default.error('谷歌翻译请求出错', e); } return ''; } } async function translateByYoudao(word, locale, options) { var _a, _b; if (!options.youdao || !((_a = options.youdao) === null || _a === void 0 ? void 0 : _a.key) || !((_b = options.youdao) === null || _b === void 0 ? void 0 : _b.secret)) { log_1.default.error('翻译失败,当前翻译器为有道,请完善youdao配置参数'); process.exit(1); } try { return await (0, index_1.youdaoTranslate)(word, 'zh-CN', locale, options.youdao); } catch (e) { log_1.default.error('有道翻译请求出错', e); return ''; } } async function translateByChatGPT(word, locale, options) { var _a; if (!options.chatGPT || !((_a = options.chatGPT) === null || _a === void 0 ? void 0 : _a.key)) { log_1.default.error('翻译失败,当前翻译器为有道,请完善youdao配置参数'); process.exit(1); } try { return await (0, index_1.chatGPTTranslate)(word, 'zh-CN', locale, options.chatGPT); } catch (e) { log_1.default.error('chatGPT请求出错', e); return ''; } } // StringObject可能是:ss: {ss: sss:ss} function convertToCamelCase(str) { if (typeof str !== 'string') { return str; } // 去除标点符号 const cleanedStr = str.replace(/[^\w\s]/gi, ''); // 将字符串按空格分割为单词数组 const words = cleanedStr.split(/\s+/); // 转换为大驼峰格式 const camelCaseStr = words .map((word) => { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }) .join(''); return camelCaseStr; } async function translateByBaidu(word, locale, options) { var _a, _b; if (!options.baidu || !((_a = options.baidu) === null || _a === void 0 ? void 0 : _a.key) || !((_b = options.baidu) === null || _b === void 0 ? void 0 : _b.secret)) { log_1.default.error('翻译失败,当前翻译器为百度,请完善baidu配置参数'); process.exit(1); } try { return (await (0, index_1.baiduTranslate)(word, 'zh', locale, options.baidu)); } catch (e) { log_1.default.error('百度翻译请求出错', e); return ''; } } async function translateByBaiduAI(word, locale, options) { var _a; if (!options.baiduAI || !((_a = options.baiduAI) === null || _a === void 0 ? void 0 : _a.access_token)) { log_1.default.error('翻译失败,当前翻译器为百度AI,请完善baiduAI配置参数(百度智能云-机器翻译)'); process.exit(1); } try { return (await (0, index_1.baiduAITranslate)(word, 'zh', locale, options.baiduAI)); } catch (e) { log_1.default.error('百度翻译请求出错', e); return ''; } } async function default_1(localePath, locales, oldPrimaryLang, options) { if (![constants_1.GOOGLE, constants_1.YOUDAO, constants_1.BAIDU, constants_1.CHAT_GPT, constants_1.BAIDU_AI].includes(options.translator || '')) { log_1.default.error('翻译失败,请确认translator参数是否配置正确'); process.exit(1); } log_1.default.verbose('当前使用的翻译器:', options.translator); const primaryLangPath = (0, getAbsolutePath_1.getAbsolutePath)(process.cwd(), localePath); // 默认是中文的 json,getLang获取object类型 const newPrimaryLang = (0, flatObjectDeep_1.flatObjectDeep)((0, getLang_1.default)(primaryLangPath)); const localeFileType = stateManager_1.default.getToolConfig().localeFileType; let targetContent = {}; for (const targetLocale of locales) { log_1.default.info(`正在翻译${targetLocale}语言包`); const reg = new RegExp(`/[A-Za-z-]+.${localeFileType}`, 'g'); const targetPath = localePath.replace(reg, `/${targetLocale}.${localeFileType}`); const targetLocalePath = (0, getAbsolutePath_1.getAbsolutePath)(process.cwd(), targetPath); let oldTargetLangPack = {}; let newTargetLangPack = {}; // en-US.json : {"新增申请": "New application"} // 如果en-US存在,就把老的en-US拿过来用 if (fs_extra_1.default.existsSync(targetLocalePath)) { oldTargetLangPack = (0, flatObjectDeep_1.flatObjectDeep)((0, getLang_1.default)(targetLocalePath)); } else { fs_extra_1.default.ensureFileSync(targetLocalePath); } const keyList = Object.keys(newPrimaryLang); // ["天才", "你好"] // const willTranslateText = {}; // 每个key就是一个中文 for (const key of keyList) { // 主语言同一个key的value不变,就复用原有的翻译结果 // 现在中文的内容 const oldLang = (0, flatObjectDeep_1.flatObjectDeep)(oldPrimaryLang); // 判断转老的 扁平 // oldLang:老的中文对象,第一次应该是空的 // newPrimaryLang: 有值的中文json // 如果老的中文对象中有值,并且en-US.json中该中文有值 // 则把en-US.json中的英文赋值给newTargetLangPack const isNotChanged = oldLang[key] === newPrimaryLang[key]; if (isNotChanged && oldTargetLangPack[key]) { // {"天才": 'enxx'} newTargetLangPack[key] = oldTargetLangPack[key]; } else { // 老的中文对象没匹配到,则把该中文给willTranslateText对象 // {"天才": "天才"} willTranslateText[key] = newPrimaryLang[key]; } } // 翻译新增键值对内容 const translator = new Translator({ provider: options.translator || constants_1.YOUDAO, targetLocale, providerOptions: options, }); const { incrementalTranslation, incrementalTranslationCamelCase } = await translator.translate(willTranslateText); // newTargetLangPack对象中可能包含未转驼峰的value,需要批量处理 const _newTargetLangPack = {}; Object.entries(newTargetLangPack).forEach(([key, val]) => { _newTargetLangPack[key] = convertToCamelCase(val); }); const otherTargetLangPack = JSON.parse(JSON.stringify({ ...newTargetLangPack, ...incrementalTranslation, })); // 这个是驼峰的key newTargetLangPack = JSON.parse(JSON.stringify({ ..._newTargetLangPack, ...incrementalTranslationCamelCase, })); // console.log('otherTargetLangPack', otherTargetLangPack) // console.log('newTargetLangPack', newTargetLangPack) // 驼峰的key const fileContent = (0, spreadObject_1.spreadObject)(newTargetLangPack); // 非驼峰的key -> 应该要存储到en_US.json的内容 const otherFileContent = (0, spreadObject_1.spreadObject)(otherTargetLangPack); let obj = {}; const getObj = (targetFileContent, newObj) => { newObj = JSON.parse(JSON.stringify(newObj)); Object.keys(targetFileContent).forEach((key) => { // 如果key是字符串,newObj = {x: x} if (typeof targetFileContent[key] === 'string') { const item = targetFileContent[key]; newObj[key] = item; } else { // 如果key是非字符串 newObj[key] = targetFileContent[key]; } }); return newObj; }; // 把英文语言包内容拷贝到目标语言包 if (targetLocale === 'en-US' || targetLocale === 'en') { // 把'en-US'改成original-en-US // const enPath = localePath.replace(/\/[A-Za-z-]+.json/, '/hump-en-US.json') const enPath = targetLocalePath.replace(/\/[A-Za-z-]+.json/, '/hump-en-US.json'); // 全驼峰,无空格的英文key,hump-en-US.json targetContent = getObj(fileContent, obj); // 保存这个全驼峰的文件 -> /hump-en-US.json (0, saveLocaleFile_1.saveLocaleFile)(targetContent, enPath); // log.info(`完成${targetLocale}语言包翻译`) obj = {}; const tempTargetContent = getObj(otherFileContent, obj); (0, saveLocaleFile_1.saveLocaleFile)(tempTargetContent, targetLocalePath); } else { // @TODO: 目前执行不到 (0, saveLocaleFile_1.saveLocaleFile)(obj, targetLocalePath); // log.info(`完成${targetLocale}语言包翻译`) } } return targetContent; } exports.default = default_1; class Translator { constructor({ provider, targetLocale, providerOptions }) { _Translator_provider.set(this, void 0); _Translator_targetLocale.set(this, void 0); _Translator_providerOptions.set(this, void 0); _Translator_textLengthLimit.set(this, 5000); _Translator_separator.set(this, '\n'); // 翻译文本拼接用的分隔符 switch (provider) { case constants_1.YOUDAO: __classPrivateFieldSet(this, _Translator_provider, translateByYoudao, "f"); break; case constants_1.GOOGLE: __classPrivateFieldSet(this, _Translator_provider, translateByGoogle, "f"); break; case constants_1.BAIDU: __classPrivateFieldSet(this, _Translator_textLengthLimit, 2000, "f"); __classPrivateFieldSet(this, _Translator_provider, translateByBaidu, "f"); break; case constants_1.CHAT_GPT: __classPrivateFieldSet(this, _Translator_provider, translateByChatGPT, "f"); break; case constants_1.BAIDU_AI: __classPrivateFieldSet(this, _Translator_textLengthLimit, 2000, "f"); __classPrivateFieldSet(this, _Translator_provider, translateByBaiduAI, "f"); } __classPrivateFieldSet(this, _Translator_targetLocale, targetLocale, "f"); __classPrivateFieldSet(this, _Translator_providerOptions, providerOptions, "f"); } async translate(dictionary) { const allTextArr = Object.keys(dictionary).map((key) => dictionary[key]); let restTextBundleArr = allTextArr; log_1.default.debug(`allTextArr:${allTextArr.length}`); log_1.default.debug(`restTextBundleArr:${restTextBundleArr.length}`); const translationCount = 100; let startIndex = 0; // 翻译后的数据 const result = []; // 每轮循环,先判断100行key-value的字符数量 // 如果字符小于1w,以两倍速度递增,扩大翻译行数,以尽可能翻译更多的行数 // 如果字符大于1w,以两倍倍速度递减,扩小翻译行数,以尽可能翻译更多的行数 // 确定了行数后开始翻译,一直循环到翻译完所有行 while (startIndex < allTextArr.length && restTextBundleArr.length > 0) { const maxTranslationCount = this.getMaxTranslationCount(restTextBundleArr, 0, translationCount); const textBundleArr = allTextArr.slice(startIndex, startIndex + maxTranslationCount); restTextBundleArr = allTextArr.slice(startIndex + maxTranslationCount); startIndex = startIndex + maxTranslationCount; const [res] = await Promise.all([ __classPrivateFieldGet(this, _Translator_provider, "f").call(this, textBundleArr.join(__classPrivateFieldGet(this, _Translator_separator, "f")), // 文本中可能有逗号,为了防止后面分割字符出错,使用\\$代替逗号 __classPrivateFieldGet(this, _Translator_targetLocale, "f"), __classPrivateFieldGet(this, _Translator_providerOptions, "f")), new Promise((resolve) => { log_1.default.success('2000m后执行下一批'); setTimeout(resolve, 2000); }), // 有道翻译接口限制每秒1次请求 ]); let resArr; // @TODO:只考虑了英文的情况 if (typeof res === 'object') { // 百度比较特殊,是dst resArr = res.map((item) => (0, classifyString_1.classifyString)(item.dst)); // resArr = res.map((item) => item.dst.charAt(0).toUpperCase() + item.dst.slice(1)) } else { // 用\n分割 resArr = res.split(__classPrivateFieldGet(this, _Translator_separator, "f")).map((item) => (0, classifyString_1.classifyString)(item)); // .map((item) => item.charAt(0).toUpperCase() + item.slice(1)) } result.push(...resArr); } const incrementalTranslationCamelCase = {}; const incrementalTranslation = {}; Object.keys(dictionary).forEach((key, index) => { // 翻译后有可能字符串前后会多出一个空格,这里做一下过滤 let translatedText = result[index] || ''; if (!dictionary[key].startsWith(' ') && translatedText.startsWith(' ')) { translatedText = translatedText.slice(1); } if (!dictionary[key].endsWith(' ') && translatedText.endsWith(' ')) { translatedText = translatedText.slice(0, -1); } incrementalTranslation[key] = translatedText; incrementalTranslationCamelCase[key] = convertToCamelCase(translatedText); }); return { incrementalTranslation, incrementalTranslationCamelCase, }; } // 递归获取1w字以内的能够翻译的最大行数 getMaxTranslationCount(textArr, allowTranslationCount, tryTranslationCount) { const textNum = textArr.length; const diff = Math.min(tryTranslationCount, textNum); const textBundleArr = textArr.slice(0, diff); const textBundleLength = textBundleArr.join(__classPrivateFieldGet(this, _Translator_separator, "f")).length; // allowTranslationCount > tryTranslationCount 是指待翻译内容一开始就超出限制字数的情况 // allowTranslationCount === textNum 是指待翻译内容一开始就小于限制字数的情况 if (allowTranslationCount > tryTranslationCount || allowTranslationCount === textNum) { return allowTranslationCount; } else if (textBundleLength <= __classPrivateFieldGet(this, _Translator_textLengthLimit, "f")) { return this.getMaxTranslationCount(textArr, diff, tryTranslationCount * 2); } else { const midCount = Math.floor((tryTranslationCount - allowTranslationCount) / 2); return this.getMaxTranslationCount(textArr, allowTranslationCount, midCount); } } } _Translator_provider = new WeakMap(), _Translator_targetLocale = new WeakMap(), _Translator_providerOptions = new WeakMap(), _Translator_textLengthLimit = new WeakMap(), _Translator_separator = new WeakMap();