canary_clis/dist/extract/extract.js
2022-12-20 00:02:24 +00:00

317 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
/**
* @author zhaoyingbo
* @desc 提取指定文件夹下的中文
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractAll = void 0;
const _ = require("lodash");
const slash = require("slash2");
const path = require("path");
const colors = require("colors");
const file_1 = require("./file");
const findChineseText_1 = require("./findChineseText");
const getLangData_1 = require("./getLangData");
const utils_1 = require("../utils");
const replace_1 = require("./replace");
const utils_2 = require("../utils");
const CONFIG = utils_2.getProjectConfig();
/**
* 剔除配置文件夹下的文件
*/
function removeLangsFiles(files) {
const langsDir = path.resolve(process.cwd(), CONFIG.canaryDir);
return files.filter(file => {
const completeFile = path.resolve(process.cwd(), file);
return !completeFile.includes(langsDir);
});
}
/**
* 递归匹配项目中所有的代码的中文
*/
function findAllChineseText(dir) {
// 使用逗号分割用户输入,可以混合输入文件和文件夹
const dirList = dir.split(' ');
let files = [];
// 获取用户输入的全部文件
dirList.forEach(dir => {
const dirPath = path.resolve(process.cwd(), dir);
// 输入文件夹
if (file_1.isDirectory(dir)) {
files.push(...file_1.getSpecifiedFiles(dirPath, CONFIG.ignoreDir, CONFIG.ignoreFile));
}
// 输入文件
else {
files.push(dirPath);
}
});
// 过滤输入中包含的配置文件夹下的文件
files = removeLangsFiles(files);
// 过滤非代码文件
files = files.filter(file => {
return (file_1.isFile(file) && ['ts', 'js', 'tsx', 'jsx', 'vue'].includes(file_1.getSuffix(file)));
});
// 匹配代码文件中的中文
const allTexts = files.reduce((pre, file) => {
const code = file_1.readFile(file);
const texts = findChineseText_1.findChineseText(code, file);
// 调整文案顺序,保证从后面的文案往前替换,避免位置更新导致替换出错
const sortTexts = _.sortBy(texts, obj => -obj.range.start);
if (texts.length > 0) {
console.log(`${utils_1.highlightText(file)} 发现 ${utils_1.highlightText(texts.length)} 处中文文案`);
}
return texts.length > 0 ? pre.concat({ file, texts: sortTexts }) : pre;
}, []);
return allTexts;
}
/**
* 处理作为key值的翻译原文
*/
function getTransOriginText(text) {
// 避免翻译的字符里包含数字或者特殊字符等情况,只过滤出汉字和字母
const reg = /[a-zA-Z\u4e00-\u9fa5]+/g;
const findText = text.match(reg) || [];
const transOriginText = findText ? findText.join('').slice(0, 5) : '中文符号';
return transOriginText;
}
/**
* 根据文件名和上级目录生成对应的前缀
* @param fileName 文件路径
* @returns 例如home.index. | ''
*/
function getFilePrefix(fileName) {
// 反斜杠路由替换成正斜杠
const forwardFileName = slash(fileName);
let filePrefix = [];
// 如果是在page下的目录直接匹配前缀
// oldRegex : \/pages\/\w+\/([^\/]+)\/([^\/\.]+)
const suggestPageRegex = /\/pages\/([^\/]+)\/([^\/\.]+)/;
if (forwardFileName.includes('/pages/')) {
filePrefix = forwardFileName.match(suggestPageRegex);
filePrefix && filePrefix.shift();
}
// 如果没有匹配到前缀
if (!(filePrefix && filePrefix.length)) {
const names = forwardFileName.split('/');
const fileName = _.last(names);
// 去除拓展名
const fileKey = fileName.split('.')[0].replace(new RegExp('-', 'g'), '_');
// 上级目录名
const dir = names[names.length - 2].replace(new RegExp('-', 'g'), '_');
if (dir === fileKey) {
filePrefix = [dir];
}
else {
filePrefix = [dir, fileKey];
}
}
return filePrefix.length ? `${filePrefix.join('.')}.` : '';
}
/**
* 统一处理key值已提取过的文案直接替换翻译后的key若相同加上出现次数
* @param filePath 文件路径
* @param langsPrefix 替换后的前缀
* @param translateTexts 翻译后的key值
* @param targetStrs 当前文件提取后的文案
* @returns any[] 最终可用于替换的key值和文案
*/
function getLangKeys(filePath, langsPrefix, translateTexts, targetStrs) {
// 获取项目原语言的配置并拍平
const srcLangObj = getLangData_1.getSrcLangObj();
// 是区分是否是本函数生成而非原始数据从而确定是否需要needWrite
const genKeys = {};
// 为文案生成对应的键值
return targetStrs.reduce((prev, curr, i) => {
const { text } = curr;
// 如果已生成过对应的键则直接返回
if (genKeys[text]) {
return prev.concat({
target: curr,
key: genKeys[text],
needWrite: true
});
}
// 在原配置中找对应的键
let key = utils_1.findMatchKey(srcLangObj, text);
// 如果找到了就替换中划线为下划线并暂存
if (key) {
key = key.replace(/-/g, '_');
return prev.concat({
target: curr,
key,
// 这里由于原语言配置就有键,所以不需要重写进配置文件
needWrite: false
});
}
// 没有生成过,原配置也没有,重新生成一个
// 获取翻译后的键并驼峰化
const transKey = translateTexts[i] && _.camelCase(translateTexts[i]);
// 如果用户定义了前缀就直接加上
if (langsPrefix) {
key = `${langsPrefix}.${transKey}`;
}
// 没前缀就加上文件名和上级目录作为前缀
else {
key = `${getFilePrefix(filePath)}${transKey}`;
}
key = key.replace(/-/g, '_');
// 防止出现前五位相同但是整体文案不同的情况
if (srcLangObj[key] && srcLangObj[key] !== text) {
// 已经存在的Key
const existKeys = _.keys(srcLangObj);
let occurTime = 2;
// 获取已经重复的次数
while (existKeys.includes(`${key}${occurTime}`)) {
occurTime += 1;
}
// 拼接重复次数例如common.index.key1
key = key + occurTime;
}
// 写进新生成的键列表
genKeys[text] = key;
// 写入原配置,方便后续比较重复
srcLangObj[key] = text;
return prev.concat({
target: curr,
key,
needWrite: true
});
}, []);
}
/**
* 递归匹配项目中所有的代码的中文
* @returns
*/
function extractAll({ dirPath, prefix }) {
// 翻译源配置校验
let origin = CONFIG.defaultTranslateKeyApi;
// 用户设置了空字符串
if (!origin) {
origin = 'Pinyin';
console.log(`配置文件未配置 ${utils_1.highlightText('defaultTranslateKeyApi')} , 使用默认值 ${utils_1.highlightText('Pinyin')}\n`);
}
// 对用户填入值进行校验
else if (!['Pinyin', 'Google', 'Baidu'].includes(origin)) {
console.log(`Canary 仅支持 ${utils_1.highlightText('Pinyin、Google、Baidu')},请修改 ${utils_1.highlightText('defaultTranslateKeyApi')} 配置项`);
return;
}
// 地址为用户输入默认为src文件夹
const dir = dirPath || './src';
// 去除I18N前缀后续全局加
const langsPrefix = prefix ? prefix.replace(/^I18N\./, '') : null;
// 获取目标文件下全部中文文案,并按文件夹归类
const allTargetStrs = findAllChineseText(dir);
if (allTargetStrs.length === 0) {
console.log(utils_1.highlightText('没有发现可替换的文案!'));
return;
}
// 提示翻译源
if (origin === 'Pinyin') {
console.log(`\n当前使用 ${utils_1.highlightText('Pinyin')} 作为key值的翻译源若想得到更好的体验可配置 ${utils_1.highlightText('googleApiKey')}${utils_1.highlightText('baiduApiKey')},并切换 ${utils_1.highlightText('defaultTranslateKeyApi')}`);
}
else {
console.log(`\n当前使用 ${utils_1.highlightText(origin)} 作为key值的翻译源`);
}
console.log('即将截取每个中文文案的前5位翻译生成key值并替换中...\n');
/**
* 对当前文件进行文案key生成和替换
*/
const generateKeyAndReplace = (item) => __awaiter(this, void 0, void 0, function* () {
const { texts, file: filePath } = item;
console.log(`${utils_1.highlightText(filePath)} 替换中...`);
// 过滤掉模板字符串内的中文避免替换时出现异常示例https://imoaix.cn/canary-clis/1.png
const targetStrs = texts.reduce((pre, strObj, i) => {
// 因为文案已经根据位置倒排,所以比较时只需要比较剩下的文案即可
const afterStrs = texts.slice(i + 1);
// 如果是在模板字符串中的中文,其结束位置必然小于等于模板字符串本身的结束位置
if (afterStrs.some(obj => strObj.range.end <= obj.range.end)) {
return pre;
}
return pre.concat(strObj);
}, []);
// 比对前后数量提示用户不存在过滤后长度为0的情况
const len = texts.length - targetStrs.length;
if (len > 0) {
console.log(colors.red(`存在 ${utils_1.highlightText(len)} 处文案无法替换,请避免在模板字符串的变量中嵌套中文`));
}
// 翻译后的文案
let translateTexts;
// 翻译中文文案百度和pinyin将文案拼接成一条统一翻译
if (origin !== 'Google') {
// 使用不同的分割符
const delimiter = origin === 'Baidu' ? '\n' : '$';
// 拼接字符串
const translateOriginTexts = targetStrs.reduce((prev, curr, i) => {
// 截取文案前5位仅包含中文和字母
const transOriginText = getTransOriginText(curr.text);
if (i === 0) {
return transOriginText;
}
return `${prev}${delimiter}${transOriginText}`;
}, []);
// 翻译后的文案
translateTexts = yield utils_1.translateWithBaiduPinyin(translateOriginTexts, origin);
}
else {
// google并发性较好且未找到有效的分隔符故仍然逐个文案进行翻译
const translatePromises = targetStrs.reduce((prev, curr) => {
const transOriginText = getTransOriginText(curr.text);
return prev.concat(utils_1.translateText(transOriginText, 'en_US'));
}, []);
[...translateTexts] = yield Promise.all(translatePromises);
}
// 翻译结果示例https://imoaix.cn/canary-clis/2.png
if (translateTexts.length === 0) {
utils_1.failInfo(`未得到翻译结果,${filePath}替换失败!`);
return;
}
// 统一处理Key值如翻译后结果相同加上出现次数结果示例https://imoaix.cn/canary-clis/3.png
const langKeys = getLangKeys(filePath, langsPrefix, translateTexts, targetStrs);
yield langKeys
.reduce((prev, { target, key, needWrite }) => {
return prev.then(() => {
// 根据生成的键值对更新源码文件以及语言文件
return replace_1.replaceAndUpdate(filePath, target, `I18N.${key}`, false, needWrite);
});
}, Promise.resolve())
.then(() => {
// 添加 import I18N
if (!replace_1.hasImportI18N(filePath)) {
const code = replace_1.createImportI18N(filePath);
file_1.writeFile(filePath, code);
}
utils_1.successInfo(`${filePath} 替换完成,共替换 ${targetStrs.length} 处文案!\n`);
})
.catch(e => {
utils_1.failInfo(e.message);
});
});
/**
* 遍历每个文件夹中的中文文案
* 不并发处理的原因是,不同文件的相同文案需要尽可能指向同一个文件,以减少整体国际化文件体积
* 每个文件处理的时候需要判定之前处理过的文件的文案列表
*/
allTargetStrs
.reduce((prev, current) => {
return prev.then(() => {
return generateKeyAndReplace(current);
});
}, Promise.resolve())
.then(() => {
utils_1.successInfo('全部替换完成!');
})
.catch((e) => {
utils_1.failInfo(e.message);
});
}
exports.extractAll = extractAll;
//# sourceMappingURL=extract.js.map