bmc-i18n-extract-cli
Version:
这是一款能够自动将代码里的中文转成i18n国际化标记的命令行工具。当然,你也可以用它实现将中文语言包自动翻译成其他语言。适用于vue2、vue3和react
465 lines • 20.2 kB
JavaScript
"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