UNPKG

bmc-i18n-extract-cli

Version:

这是一款能够自动将代码里的中文转成i18n国际化标记的命令行工具。当然,你也可以用它实现将中文语言包自动翻译成其他语言。适用于vue2、vue3和react

465 lines 20.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_extra_1 = __importDefault(require("fs-extra")); const chalk_1 = __importDefault(require("chalk")); const inquirer_1 = __importDefault(require("inquirer")); const path_1 = __importDefault(require("path")); const prettier_1 = __importDefault(require("prettier")); const cli_progress_1 = __importDefault(require("cli-progress")); const glob_1 = __importDefault(require("glob")); const merge_1 = __importDefault(require("lodash/merge")); const cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); const isArray_1 = __importDefault(require("lodash/isArray")); const slash_1 = __importDefault(require("slash")); const transform_1 = __importDefault(require("./transform")); const log_1 = __importDefault(require("./utils/log")); const getAbsolutePath_1 = require("./utils/getAbsolutePath"); const collector_1 = __importDefault(require("./collector")); const translate_1 = __importDefault(require("./translate")); const getLang_1 = __importDefault(require("./utils/getLang")); const constants_1 = require("./utils/constants"); const stateManager_1 = __importDefault(require("./utils/stateManager")); const exportExcel_1 = __importDefault(require("./exportExcel")); const initConfig_1 = require("./utils/initConfig"); const saveLocaleFile_1 = require("./utils/saveLocaleFile"); const assertType_1 = require("./utils/assertType"); const error_logger_1 = __importDefault(require("./utils/error-logger")); const isDirectory_1 = __importDefault(require("./utils/isDirectory")); const openaiKeyMapper_1 = require("./utils/openaiKeyMapper"); const https_1 = __importDefault(require("https")); const http_1 = __importDefault(require("http")); function resolvePathFrom(inputPath) { const currentDir = process.cwd(); return path_1.default.resolve(currentDir, inputPath); } function getPathFromInput(input, exclude) { const resolvePath = resolvePathFrom(input); if ((0, isDirectory_1.default)(resolvePath)) { const base = (0, slash_1.default)(resolvePath); const pattern = `${base}/**/*.{cjs,mjs,js,ts,tsx,jsx,vue}`; const ignore = Array.isArray(exclude) ? exclude.map((p) => (0, slash_1.default)(p)) : []; const paths = glob_1.default .sync(pattern, { ignore, }) .filter((file) => fs_extra_1.default.statSync(file).isFile()); return paths; } else { return [resolvePath]; } } function getSourceFilePaths(input, exclude) { const filePaths = []; if ((0, isArray_1.default)(input)) { input.forEach((item) => { const paths = getPathFromInput(item, exclude); filePaths.push(...paths); }); } else { const paths = getPathFromInput(input, exclude); filePaths.push(...paths); } return filePaths; } // TODO: 逻辑需要重写 function saveLocale(localePath) { const keyMap = collector_1.default.getKeyMap(); const localeAbsolutePath = (0, getAbsolutePath_1.getAbsolutePath)(process.cwd(), localePath); if (!fs_extra_1.default.existsSync(localeAbsolutePath)) { fs_extra_1.default.ensureFileSync(localeAbsolutePath); } if (!fs_extra_1.default.statSync(localeAbsolutePath).isFile()) { log_1.default.error(`路径${localePath}不是一个文件,请重新设置localePath参数`); process.exit(1); } (0, saveLocaleFile_1.saveLocaleFile)(keyMap, localeAbsolutePath); log_1.default.verbose(`输出中文语言包到指定位置:`, localeAbsolutePath); } function getPrettierParser(ext) { switch (ext) { case 'vue': return 'vue'; case 'ts': case 'tsx': return 'babel-ts'; default: return 'babel'; } } function getOutputPath(input, output, sourceFilePath) { let outputPath; if (output) { const filePath = sourceFilePath.replace((0, getAbsolutePath_1.getAbsolutePath)(process.cwd(), input) + '/', ''); outputPath = (0, getAbsolutePath_1.getAbsolutePath)(process.cwd(), output, filePath); fs_extra_1.default.ensureFileSync(outputPath); } else { outputPath = (0, getAbsolutePath_1.getAbsolutePath)(process.cwd(), sourceFilePath); } return outputPath; } function formatInquirerResult(answers) { if (answers.translator === constants_1.YOUDAO) { return { translator: answers.translator, youdao: { key: answers.key, secret: answers.secret, }, }; } else if (answers.translator === constants_1.BAIDU) { return { translator: answers.translator, baidu: { key: answers.key, secret: answers.secret, }, }; } else if (answers.translator === constants_1.ALICLOUD) { return { translator: answers.translator, alicloud: { key: answers.key, secret: answers.secret, }, }; } else if (answers.translator === constants_1.OPENAI) { return { translator: answers.translator, openai: { baseUrl: answers.openaiBaseUrl, apiKey: answers.openaiApiKey, model: answers.openaiModel || 'gpt-4o-mini', }, }; } else { return { translator: answers.translator, google: { proxy: answers.proxy, }, }; } } async function getTranslationConfig(currentConfig) { const cachePath = (0, getAbsolutePath_1.getAbsolutePath)(__dirname, '../.cache/configCache.json'); fs_extra_1.default.ensureFileSync(cachePath); const cache = fs_extra_1.default.readFileSync(cachePath, 'utf8') || '{}'; const oldConfigCache = JSON.parse(cache); const openaiConfigured = !!(currentConfig && currentConfig.openai && currentConfig.openai.apiKey); const defaultTranslator = (currentConfig && currentConfig.translator) || constants_1.YOUDAO; const answers = await inquirer_1.default.prompt([ { type: 'list', name: 'translator', message: '请选择翻译接口', default: defaultTranslator, choices: [ { name: '有道翻译', value: constants_1.YOUDAO }, { name: '谷歌翻译', value: constants_1.GOOGLE }, { name: '百度翻译', value: constants_1.BAIDU }, { name: '阿里云机器翻译', value: constants_1.ALICLOUD }, { name: 'OpenAI 翻译', value: constants_1.OPENAI }, ], when(answers) { return !answers.skipTranslate; }, }, { type: 'input', name: 'proxy', message: '使用谷歌服务需要翻墙,请输入代理地址(可选)', default: oldConfigCache.proxy || '', when(answers) { return answers.translator === constants_1.GOOGLE; }, }, { type: 'input', name: 'key', message: '请输入有道翻译appKey', default: oldConfigCache.key || '', when(answers) { return answers.translator === constants_1.YOUDAO; }, validate(input) { return input.length === 0 ? 'appKey不能为空' : true; }, }, { type: 'input', name: 'secret', message: '请输入有道翻译appSecret', default: oldConfigCache.secret || '', when(answers) { return answers.translator === constants_1.YOUDAO; }, validate(input) { return input.length === 0 ? 'appSecret不能为空' : true; }, }, { type: 'input', name: 'key', message: '请输入百度翻译appId', default: oldConfigCache.key || '', when(answers) { return answers.translator === constants_1.BAIDU; }, validate(input) { return input.length === 0 ? 'appKey不能为空' : true; }, }, { type: 'input', name: 'secret', message: '请输入百度翻译appSecret', default: oldConfigCache.secret || '', when(answers) { return answers.translator === constants_1.BAIDU; }, validate(input) { return input.length === 0 ? 'appSecret不能为空' : true; }, }, { type: 'input', name: 'key', message: '请输入阿里云机器翻译accessKeyId', default: oldConfigCache.key || '', when(answers) { return answers.translator === constants_1.ALICLOUD; }, validate(input) { return input.length === 0 ? 'accessKeyId不能为空' : true; }, }, { type: 'input', name: 'secret', message: '请输入阿里云机器翻译accessKeySecret', default: oldConfigCache.secret || '', when(answers) { return answers.translator === constants_1.ALICLOUD; }, validate(input) { return input.length === 0 ? 'accessKeySecret不能为空' : true; }, }, { type: 'input', name: 'openaiBaseUrl', message: 'OpenAI baseUrl (默认 https://api.openai.com/v1,可选)', default: oldConfigCache.openaiBaseUrl || '', when(answers) { return answers.translator === constants_1.OPENAI && !openaiConfigured; }, }, { type: 'password', name: 'openaiApiKey', message: 'OpenAI API Key (必填或设置环境变量 OPENAI_API_KEY)', default: oldConfigCache.openaiApiKey || '', when(answers) { return answers.translator === constants_1.OPENAI && !openaiConfigured; }, }, { type: 'input', name: 'openaiModel', message: 'OpenAI 模型 (默认 gpt-4o-mini,可选)', default: oldConfigCache.openaiModel || 'gpt-4o-mini', when(answers) { return answers.translator === constants_1.OPENAI && !openaiConfigured; }, }, ]); const newConfigCache = Object.assign(oldConfigCache, answers); fs_extra_1.default.writeFileSync(cachePath, JSON.stringify(newConfigCache), 'utf8'); const result = formatInquirerResult(openaiConfigured && answers.translator === constants_1.OPENAI ? { translator: constants_1.OPENAI } : answers); return result; } function formatCode(code, ext, prettierConfig) { let stylizedCode = code; if ((0, assertType_1.isObject)(prettierConfig)) { stylizedCode = prettier_1.default.format(code, { ...prettierConfig, parser: getPrettierParser(ext), }); log_1.default.verbose(`格式化代码完成`); } return stylizedCode; } async function default_1(options) { let i18nConfig = (0, initConfig_1.getI18nConfig)(options); if (!i18nConfig.skipTranslate) { const translationConfig = await getTranslationConfig(i18nConfig); i18nConfig = (0, merge_1.default)(i18nConfig, translationConfig); } // 全局缓存脚手架配置 stateManager_1.default.setToolConfig(i18nConfig); const { input, exclude, output, rules, localePath, locales, skipExtract, skipTranslate, adjustKeyMap, localeFileType, } = i18nConfig; log_1.default.debug(`命令行配置信息:`, i18nConfig); const openaiCfg = i18nConfig.openai || {}; const hasOpenAI = !!(openaiCfg.apiKey || openaiCfg.baseUrl); async function getReservedKeysFromExistedConfig() { const conf = i18nConfig.existedConfig || {}; const local = Array.isArray(conf.existedKeys) ? conf.existedKeys : []; const url = conf.getExistedUrl || ''; const field = conf.mapFieldToKey || 'key'; if (!url) return new Set(local); const client = url.startsWith('https') ? https_1.default : http_1.default; const data = await new Promise((resolve) => { client .get(url, (res) => { let raw = ''; res.on('data', (chunk) => (raw += chunk)); res.on('end', () => { try { resolve(JSON.parse(raw)); } catch (e) { resolve([]); } }); }) .on('error', () => resolve([])); }); let remote = []; if (Array.isArray(data)) { remote = data .map((item) => (item && typeof item === 'object' ? item[field] : undefined)) .filter((k) => typeof k === 'string'); } return new Set([...local, ...remote]); } let oldPrimaryLang = {}; const primaryLangPath = (0, getAbsolutePath_1.getAbsolutePath)(process.cwd(), localePath); if (!fs_extra_1.default.existsSync(primaryLangPath)) { (0, saveLocaleFile_1.saveLocaleFile)({}, primaryLangPath); } oldPrimaryLang = (0, getLang_1.default)(primaryLangPath); if (!skipExtract) { log_1.default.info('正在转换中文,请稍等...'); const sourceFilePaths = getSourceFilePaths(input, exclude); const bar = new cli_progress_1.default.SingleBar({ format: `${chalk_1.default.cyan('提取进度:')} [{bar}] {percentage}% {value}/{total}`, }, cli_progress_1.default.Presets.shades_classic); const startTime = new Date().getTime(); bar.start(sourceFilePaths.length, 0); sourceFilePaths.forEach((sourceFilePath) => { stateManager_1.default.setCurrentSourcePath(sourceFilePath); log_1.default.verbose(`正在提取文件中的中文:`, sourceFilePath); error_logger_1.default.setFilePath(sourceFilePath); const sourceCode = fs_extra_1.default.readFileSync(sourceFilePath, 'utf8'); const ext = path_1.default.extname(sourceFilePath).replace('.', ''); collector_1.default.resetCountOfAdditions(); collector_1.default.setCurrentCollectorPath(sourceFilePath); // 跳过空文件 if (sourceCode.trim() === '') { bar.increment(); return; } const { code } = (0, transform_1.default)(sourceCode, ext, rules, sourceFilePath); log_1.default.verbose(`完成中文提取和语法转换:`, sourceFilePath); // 首次遍历:若启用OpenAI语义key,先不写文件,仅收集中文 if (!hasOpenAI && (collector_1.default.getCountOfAdditions() > 0 || rules[ext].forceImport)) { const stylizedCode = formatCode(code, ext, i18nConfig.prettier); if ((0, isArray_1.default)(input)) { log_1.default.error('input为数组时,暂不支持设置dist参数'); return; } const outputPath = getOutputPath(input, output, sourceFilePath); fs_extra_1.default.writeFileSync(outputPath, stylizedCode, 'utf8'); log_1.default.verbose(`生成文件:`, outputPath); } // 自定义当前文件的keyMap if (adjustKeyMap) { const newkeyMap = adjustKeyMap((0, cloneDeep_1.default)(collector_1.default.getKeyMap()), collector_1.default.getCurrentFileKeyMap(), sourceFilePath); collector_1.default.setKeyMap(newkeyMap); collector_1.default.resetCurrentFileKeyMap(); } bar.increment(); }); // 增量转换时,保留之前的提取的中文结果 if (i18nConfig.incremental) { const newkeyMap = (0, merge_1.default)(oldPrimaryLang, collector_1.default.getKeyMap()); collector_1.default.setKeyMap(newkeyMap); } // If OpenAI config exists, transform keys using semantic snake_case mapping if (hasOpenAI) { const flatMap = {}; Object.keys(collector_1.default.getKeyMap()).forEach((k) => { const v = collector_1.default.getKeyMap()[k]; if (typeof v === 'string') flatMap[k] = v; }); const originals = Object.keys(flatMap).map((k) => flatMap[k]); const reserved = await getReservedKeysFromExistedConfig(); const mapping = await (0, openaiKeyMapper_1.generateSemanticSnakeCaseKeys)(originals, openaiCfg, reserved); stateManager_1.default.setOpenAIKeyMap(mapping); // Re-run transform to rewrite files with processed keys bar.update(0); bar.setTotal(sourceFilePaths.length); // Reset collected map for second pass collector_1.default.setKeyMap({}); collector_1.default.resetCurrentFileKeyMap(); for (const sourceFilePath of sourceFilePaths) { stateManager_1.default.setCurrentSourcePath(sourceFilePath); error_logger_1.default.setFilePath(sourceFilePath); const sourceCode = fs_extra_1.default.readFileSync(sourceFilePath, 'utf8'); const ext = path_1.default.extname(sourceFilePath).replace('.', ''); const { code } = (0, transform_1.default)(sourceCode, ext, rules, sourceFilePath); const stylizedCode = formatCode(code, ext, i18nConfig.prettier); if ((0, isArray_1.default)(input)) { log_1.default.error('input为数组时,暂不支持设置dist参数'); } else { const outputPath = getOutputPath(input, output, sourceFilePath); fs_extra_1.default.writeFileSync(outputPath, stylizedCode, 'utf8'); log_1.default.verbose(`生成文件(二次语义key重写):`, outputPath); } bar.increment(); } } const extName = path_1.default.extname(localePath); const savePath = localePath.replace(extName, `.${localeFileType}`); saveLocale(savePath); bar.stop(); const endTime = new Date().getTime(); log_1.default.info(`耗时${((endTime - startTime) / 1000).toFixed(2)}s`); } error_logger_1.default.printErrors(); console.log(''); // 空一行 if (!skipTranslate) { await (0, translate_1.default)(localePath, locales, oldPrimaryLang, { translator: i18nConfig.translator, google: i18nConfig.google, youdao: i18nConfig.youdao, baidu: i18nConfig.baidu, alicloud: i18nConfig.alicloud, openai: i18nConfig.openai, translationTextMaxLength: i18nConfig.translationTextMaxLength, }); } log_1.default.success('转换完毕!'); if (i18nConfig.exportExcel) { log_1.default.info(`正在导出excel翻译文件`); (0, exportExcel_1.default)(); log_1.default.success(`导出完毕!`); } } exports.default = default_1; //# sourceMappingURL=core.js.map