317 lines
13 KiB
JavaScript
317 lines
13 KiB
JavaScript
"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
|