This commit is contained in:
zhaoyingbo 2022-10-11 15:25:18 +08:00
commit ad42d3fe20
51 changed files with 4800 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

23
CHANGELOG.md Normal file
View File

@ -0,0 +1,23 @@
# 🐤 Change Log
Kiwi Cli
## 1.0.22 (2022-03-04)
- kiwi --extract 修复多文件提取时的并发问题
- kiwi --extract 修复文案key出现undefined的情况
## 1.0.21 (2022-03-01)
- kiwi --extract 添加 --prefix 参数,自定义配置 118N 提取文案路径
## 1.0.202022-02-28
- kiwi 优化在vue环境下中文检测与linter保持同步
## 1.0.192022-01-26
### Breaking changes
- kiwi --extract 添加百度和拼音翻译源,且支持批量文件以,分隔符输入(原本仅支持指定文件夹)
- 配置文件 kiwi-config.json 添加 defaultTranslateKeyApi
## 1.0.182021-12-07
### Breaking changes
- 配置文件 kiwi-config.json 移动至根目录下

122
README.md Normal file
View File

@ -0,0 +1,122 @@
# 🐤 kiwi cli
Kiwi 的 CLI 工具
## 如何使用
> yarn global add kiwi-clis
> 推荐与[🐤 Kiwi-国际化全流程解决方案](https://github.com/alibaba/kiwi)结合使用
## CLI 参数
### kiwi `--init`
初始化项目,生成 kiwi 的配置文件 `kiwi-config.json`
```js
{
// kiwi文件根目录用于放置提取的langs文件
"kiwiDir": "./.kiwi",
// 配置文件目录,若调整配置文件,此处可手动修改
"configFile": "./.kiwi/kiwi-config.json",
// 语言目录名,注意连线和下划线
"srcLang": "zh-CN",
"distLangs": ["en-US", "zh-TW"],
// googleApiKey
"googleApiKey": "",
// baiduApiKey
"baiduApiKey":
"appId": '',
"appKey": ''
},
// 百度翻译的语种代码映射 详情见官方文档 https://fanyi-api.baidu.com/doc/21
"baiduLangMap": {
"en-US": 'en',
"zh-TW": 'cht'
},
// 批量提取文案时生成key值时的默认翻译源, Google/Baidu/Pinyin
"defaultTranslateKeyApi": 'Pinyin',
// import 语句,不同项目请自己配置
"importI18N": "",
// 可跳过的文件夹名或者文加名比如docs、mock等
"ignoreDir": "",
"ignoreFile": ""
}
```
### kiwi `--extract`
1. 一键批量替换指定文件夹下的所有文案
```shell script
# --extract [dirPath] 指定文件夹路径
# --prefix [prefix] 指定文案前缀 I18N.xxxx
kiwi --extract [dirPath] --prefix [prefix]
```
2. commit 提交时自动增量提取,在 precommit 脚本内添加如下指令
```shell script
# 检测提交中是否存在ts或tsx文件
TS_CHANGED=$(git diff --cached --numstat --diff-filter=ACM | grep -F '.ts' | wc -l)
# 对提交的代码中存在未提取的中文文案统一处理
if [ "$TS_CHANGED" -gt 0 ]
then
TS_FILES_LIST=($(git diff --cached --name-only --diff-filter=ACM | grep -F '.ts'))
TS_FILES=''
delim=''
for item in ${TS_FILES_LIST[@]};do
TS_FILES=$TS_FILES$delim$item;
delim=','
done
echo "\033[33m 正在检测未提取的中文文案,请稍后 \033[0m"
kiwi --extract $TS_FILES || exit 1
fi
```
![批量替换](https://raw.githubusercontent.com/alibaba/kiwi/master/kiwi-cli/public/extract.gif)
### kiwi `--import`
导入翻译文案,将翻译人员翻译的文案,导入到项目中
```shell script
# 导入送翻后的文案
kiwi --import [filePath] en-US
```
### kiwi `--export`
导出未翻译的文案
```shell script
# 导出指定语言的文案lang取值为配置中distLangs值如en-US导出还未翻译成英文的中文文案
kiwi --export [filePath] en-US
```
### kiwi `--sync`
同步各种语言的文案,同步未翻译文件
### kiwi `--mock`
使用 Google 翻译,翻译未翻译的文案
如果同时配置 baiduApiKey 与 baiduApiKey 则命令行可手动选择翻译源
### kiwi `--translate`
全量翻译未翻译的中文文案,翻译结果自动导入 en-US zh-TW 等目录
```shell script
kiwi --translate
```

46
dist/const.js vendored Normal file
View File

@ -0,0 +1,46 @@
"use strict";
/**
* @author linhuiw
* @desc 项目配置文件配置信息
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PROJECT_CONFIG = exports.CANARY_CONFIG_FILE = void 0;
exports.CANARY_CONFIG_FILE = 'canary-config.json';
exports.PROJECT_CONFIG = {
dir: './.canary',
defaultConfig: {
canaryDir: './.canary',
srcLang: 'zh-CN',
distLangs: ['en-US', 'zh-CN'],
googleApiKey: '',
baiduApiKey: {
appId: '',
appKey: ''
},
baiduLangMap: {
['en-US']: 'en',
['zh-TW']: 'cht'
},
translateOptions: {
concurrentLimit: 10,
requestOptions: {}
},
defaultTranslateKeyApi: 'Pinyin',
importI18N: `import I18N from 'src/utils/I18N';`,
ignoreDir: '',
ignoreFile: ''
},
langMap: {
['en-US']: 'en',
['en_US']: 'en'
},
zhIndexFile: `import common from './common';
export default Object.assign({}, {
common
});`,
zhTestFile: `export default {
test: '测试'
}`,
};
//# sourceMappingURL=const.js.map

1
dist/const.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"const.js","sourceRoot":"","sources":["../src/const.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEU,QAAA,kBAAkB,GAAG,oBAAoB,CAAC;AAE1C,QAAA,cAAc,GAAG;IAC5B,GAAG,EAAE,WAAW;IAChB,aAAa,EAAE;QACb,SAAS,EAAE,WAAW;QACtB,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;QAC7B,YAAY,EAAE,EAAE;QAChB,WAAW,EAAE;YACX,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,EAAE;SACX;QACD,YAAY,EAAE;YACZ,CAAC,OAAO,CAAC,EAAE,IAAI;YACf,CAAC,OAAO,CAAC,EAAE,KAAK;SACjB;QACD,gBAAgB,EAAE;YAChB,eAAe,EAAE,EAAE;YACnB,cAAc,EAAE,EAAE;SACnB;QACD,sBAAsB,EAAE,QAAQ;QAChC,UAAU,EAAE,oCAAoC;QAChD,SAAS,EAAE,EAAE;QACb,UAAU,EAAE,EAAE;KACf;IACD,OAAO,EAAE;QACP,CAAC,OAAO,CAAC,EAAE,IAAI;QACf,CAAC,OAAO,CAAC,EAAE,IAAI;KAChB;IACD,WAAW,EAAE;;;;IAIX;IACF,UAAU,EAAE;;EAEZ;CACD,CAAC"}

40
dist/export.js vendored Normal file
View File

@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.exportMessages = void 0;
/**
* @author linhuiw
* @desc 导出未翻译文件
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
const fs = require("fs");
const d3_dsv_1 = require("d3-dsv");
const utils_1 = require("./utils");
function exportMessages(file, lang) {
const CONFIG = utils_1.getProjectConfig();
const langs = lang ? [lang] : CONFIG.distLangs;
langs.map(lang => {
const allMessages = utils_1.getAllMessages(CONFIG.srcLang);
const existingTranslations = utils_1.getAllMessages(lang, (message, key) => !/[\u4E00-\u9FA5]/.test(allMessages[key]) || allMessages[key] !== message);
const messagesToTranslate = Object.keys(allMessages)
.filter(key => !existingTranslations.hasOwnProperty(key))
.map(key => {
let message = allMessages[key];
message = JSON.stringify(message).slice(1, -1);
return [key, message];
});
if (messagesToTranslate.length === 0) {
console.log('All the messages have been translated.');
return;
}
const content = d3_dsv_1.tsvFormatRows(messagesToTranslate);
const sourceFile = file || `./export-${lang}`;
fs.writeFileSync(sourceFile, content);
console.log(`Exported ${messagesToTranslate.length} message(s).`);
});
}
exports.exportMessages = exportMessages;
//# sourceMappingURL=export.js.map

1
dist/export.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC;IAC1B,eAAe,EAAE;QACf,MAAM,EAAE,UAAU;KACnB;CACF,CAAC,CAAC;AACH,yBAAyB;AACzB,mCAAuC;AACvC,mCAA2D;AAG3D,SAAS,cAAc,CAAC,IAAa,EAAE,IAAa;IAClD,MAAM,MAAM,GAAG,wBAAgB,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;IAE/C,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACf,MAAM,WAAW,GAAG,sBAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,oBAAoB,GAAG,sBAAc,CACzC,IAAI,EACJ,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,OAAO,CAC5F,CAAC;QACF,MAAM,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;aACjD,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,oBAAoB,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;aACxD,GAAG,CAAC,GAAG,CAAC,EAAE;YACT,IAAI,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC/C,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;QAEL,IAAI,mBAAmB,CAAC,MAAM,KAAK,CAAC,EAAE;YACpC,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;YACtD,OAAO;SACR;QAED,MAAM,OAAO,GAAG,sBAAa,CAAC,mBAAmB,CAAC,CAAC;QACnD,MAAM,UAAU,GAAG,IAAI,IAAI,YAAY,IAAI,EAAE,CAAC;QAC9C,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,YAAY,mBAAmB,CAAC,MAAM,cAAc,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;AACL,CAAC;AAEQ,wCAAc"}

266
dist/extract/extract.js vendored Normal file
View File

@ -0,0 +1,266 @@
"use strict";
/**
* @author doubledream
* @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();
/**
* 剔除 kiwiDir 下的文件
*/
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 first = dir.split(',')[0];
let files = [];
if (file_1.isDirectory(first)) {
const dirPath = path.resolve(process.cwd(), dir);
files = file_1.getSpecifiedFiles(dirPath, CONFIG.ignoreDir, CONFIG.ignoreFile);
}
else {
files = removeLangsFiles(dir.split(','));
}
const filterFiles = files.filter(file => {
return (file_1.isFile(file) && file.endsWith('.ts')) || (file_1.isFile(file) && file.endsWith('.js')) || file.endsWith('.tsx') || file.endsWith('.vue');
});
const allTexts = filterFiles.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 currentFilename 文件路径
* @returns string[]
*/
function getSuggestion(currentFilename) {
let suggestion = [];
const suggestPageRegex = /\/pages\/\w+\/([^\/]+)\/([^\/\.]+)/;
if (currentFilename.includes('/pages/')) {
suggestion = currentFilename.match(suggestPageRegex);
}
if (suggestion) {
suggestion.shift();
}
/** 如果没有匹配到 Key */
if (!(suggestion && suggestion.length)) {
const names = slash(currentFilename).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) {
suggestion = [dir];
}
else {
suggestion = [dir, fileKey];
}
}
return suggestion;
}
/**
* 统一处理key值已提取过的文案直接替换翻译后的key若相同加上出现次数
* @param currentFilename 文件路径
* @param langsPrefix 替换后的前缀
* @param translateTexts 翻译后的key值
* @param targetStrs 当前文件提取后的文案
* @returns any[] 最终可用于替换的key值和文案
*/
function getReplaceableStrs(currentFilename, langsPrefix, translateTexts, targetStrs) {
const finalLangObj = getLangData_1.getSuggestLangObj();
const virtualMemory = {};
const suggestion = getSuggestion(currentFilename);
const replaceableStrs = targetStrs.reduce((prev, curr, i) => {
const _text = curr.text;
let key = utils_1.findMatchKey(finalLangObj, _text);
if (key) {
key = key.replace(/-/g, '_');
}
if (!virtualMemory[_text]) {
if (key) {
virtualMemory[_text] = key;
return prev.concat({
target: curr,
key,
needWrite: false
});
}
const transText = translateTexts[i] && _.camelCase(translateTexts[i]);
let transKey = `${suggestion.length ? suggestion.join('.') + '.' : ''}${transText}`;
transKey = transKey.replace(/-/g, '_');
if (langsPrefix) {
transKey = `${langsPrefix}.${transText}`;
}
let occurTime = 1;
// 防止出现前四位相同但是整体文案不同的情况
while (utils_1.findMatchValue(finalLangObj, transKey) !== _text &&
_.keys(finalLangObj).includes(`${transKey}${occurTime >= 2 ? occurTime : ''}`)) {
occurTime++;
}
if (occurTime >= 2) {
transKey = `${transKey}${occurTime}`;
}
virtualMemory[_text] = transKey;
finalLangObj[transKey] = _text;
return prev.concat({
target: curr,
key: transKey,
needWrite: true
});
}
else {
return prev.concat({
target: curr,
key: virtualMemory[_text],
needWrite: true
});
}
}, []);
return replaceableStrs;
}
/**
* 递归匹配项目中所有的代码的中文
* @param {dirPath} 文件夹路径
*/
function extractAll({ dirPath, prefix }) {
const dir = dirPath || './';
// 去除I18N
const langsPrefix = prefix ? prefix.replace(/^I18N\./, '') : null;
// 翻译源配置错误,则终止
const origin = CONFIG.defaultTranslateKeyApi || 'Pinyin';
if (!['Pinyin', 'Google', 'Baidu'].includes(CONFIG.defaultTranslateKeyApi)) {
console.log(`Kiwi 仅支持 ${utils_1.highlightText('Pinyin、Google、Baidu')},请修改 ${utils_1.highlightText('defaultTranslateKeyApi')} 配置项`);
return;
}
const allTargetStrs = findAllChineseText(dir);
if (allTargetStrs.length === 0) {
console.log(utils_1.highlightText('没有发现可替换的文案!'));
return;
}
// 提示翻译源
if (CONFIG.defaultTranslateKeyApi === 'Pinyin') {
console.log(`当前使用 ${utils_1.highlightText('Pinyin')} 作为key值的翻译源若想得到更好的体验可配置 ${utils_1.highlightText('googleApiKey')}${utils_1.highlightText('baiduApiKey')},并切换 ${utils_1.highlightText('defaultTranslateKeyApi')}`);
}
else {
console.log(`当前使用 ${utils_1.highlightText(CONFIG.defaultTranslateKeyApi)} 作为key值的翻译源`);
}
console.log('即将截取每个中文文案的前5位翻译生成key值并替换中...');
// 对当前文件进行文案key生成和替换
const generateKeyAndReplace = (item) => __awaiter(this, void 0, void 0, function* () {
const currentFilename = item.file;
console.log(`${currentFilename} 替换中...`);
// 过滤掉模板字符串内的中文,避免替换时出现异常
const targetStrs = item.texts.reduce((pre, strObj, i) => {
// 因为文案已经根据位置倒排,所以比较时只需要比较剩下的文案即可
const afterStrs = item.texts.slice(i + 1);
if (afterStrs.some(obj => strObj.range.end <= obj.range.end)) {
return pre;
}
return pre.concat(strObj);
}, []);
const len = item.texts.length - targetStrs.length;
if (len > 0) {
console.log(colors.red(`存在 ${utils_1.highlightText(len)} 处文案无法替换,请避免在模板字符串的变量中嵌套中文`));
}
let translateTexts;
if (origin !== 'Google') {
// 翻译中文文案百度和pinyin将文案进行拼接统一翻译
const delimiter = origin === 'Baidu' ? '\n' : '$';
const translateOriginTexts = targetStrs.reduce((prev, curr, i) => {
const transOriginText = getTransOriginText(curr.text);
if (i === 0) {
return transOriginText;
}
return `${prev}${delimiter}${transOriginText}`;
}, []);
translateTexts = yield utils_1.translateKeyText(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);
}
if (translateTexts.length === 0) {
utils_1.failInfo(`未得到翻译结果,${currentFilename}替换失败!`);
return;
}
const replaceableStrs = getReplaceableStrs(currentFilename, langsPrefix, translateTexts, targetStrs);
yield replaceableStrs
.reduce((prev, obj) => {
return prev.then(() => {
return replace_1.replaceAndUpdate(currentFilename, obj.target, `I18N.${obj.key}`, false, obj.needWrite);
});
}, Promise.resolve())
.then(() => {
// 添加 import I18N
if (!replace_1.hasImportI18N(currentFilename)) {
const code = replace_1.createImportI18N(currentFilename);
file_1.writeFile(currentFilename, code);
}
utils_1.successInfo(`${currentFilename} 替换完成,共替换 ${targetStrs.length} 处文案!`);
})
.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

1
dist/extract/extract.js.map vendored Normal file

File diff suppressed because one or more lines are too long

74
dist/extract/file.js vendored Normal file
View File

@ -0,0 +1,74 @@
"use strict";
/**
* @author doubledream
* @desc 文件处理方法
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isDirectory = exports.isFile = exports.writeFile = exports.readFile = exports.getSpecifiedFiles = void 0;
const path = require("path");
const fs = require("fs");
/**
* 获取文件夹下符合要求的所有文件
* @function getSpecifiedFiles
* @param {string} dir 路径
* @param {ignoreDirectory} 忽略文件夹 {ignoreFile} 忽略的文件
*/
function getSpecifiedFiles(dir, ignoreDirectory = '', ignoreFile = '') {
return fs.readdirSync(dir).reduce((files, file) => {
const name = path.join(dir, file);
const isDirectory = fs.statSync(name).isDirectory();
const isFile = fs.statSync(name).isFile();
if (isDirectory) {
return files.concat(getSpecifiedFiles(name, ignoreDirectory, ignoreFile));
}
const isIgnoreDirectory = !ignoreDirectory ||
(ignoreDirectory &&
!path
.dirname(name)
.split('/')
.includes(ignoreDirectory));
const isIgnoreFile = !ignoreFile || (ignoreFile && path.basename(name) !== ignoreFile);
if (isFile && isIgnoreDirectory && isIgnoreFile) {
return files.concat(name);
}
return files;
}, []);
}
exports.getSpecifiedFiles = getSpecifiedFiles;
/**
* 读取文件
* @param fileName
*/
function readFile(fileName) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf-8');
}
}
exports.readFile = readFile;
/**
* 读取文件
* @param fileName
*/
function writeFile(filePath, file) {
if (fs.existsSync(filePath)) {
fs.writeFileSync(filePath, file);
}
}
exports.writeFile = writeFile;
/**
* 判断是文件
* @param path
*/
function isFile(path) {
return fs.statSync(path).isFile();
}
exports.isFile = isFile;
/**
* 判断是文件夹
* @param path
*/
function isDirectory(path) {
return fs.statSync(path).isDirectory();
}
exports.isDirectory = isDirectory;
//# sourceMappingURL=file.js.map

1
dist/extract/file.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/extract/file.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,6BAA6B;AAE7B,yBAAyB;AAEzB;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,GAAG,EAAE,eAAe,GAAG,EAAE,EAAE,UAAU,GAAG,EAAE;IACnE,OAAO,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,MAAM,WAAW,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QACpD,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;QAE1C,IAAI,WAAW,EAAE;YACf,OAAO,KAAK,CAAC,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC,CAAC;SAC3E;QAED,MAAM,iBAAiB,GACrB,CAAC,eAAe;YAChB,CAAC,eAAe;gBACd,CAAC,IAAI;qBACF,OAAO,CAAC,IAAI,CAAC;qBACb,KAAK,CAAC,GAAG,CAAC;qBACV,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC;QAClC,MAAM,YAAY,GAAG,CAAC,UAAU,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,CAAC;QAEvF,IAAI,MAAM,IAAI,iBAAiB,IAAI,YAAY,EAAE;YAC/C,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;SAC3B;QACD,OAAO,KAAK,CAAC;IACf,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC;AAsCQ,8CAAiB;AApC1B;;;GAGG;AACH,SAAS,QAAQ,CAAC,QAAQ;IACxB,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC3B,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;KAC3C;AACH,CAAC;AA4B2B,4BAAQ;AA1BpC;;;GAGG;AACH,SAAS,SAAS,CAAC,QAAQ,EAAE,IAAI;IAC/B,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC3B,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;KAClC;AACH,CAAC;AAkBqC,8BAAS;AAhB/C;;;GAGG;AACH,SAAS,MAAM,CAAC,IAAI;IAClB,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;AACpC,CAAC;AAUgD,wBAAM;AARvD;;;GAGG;AACH,SAAS,WAAW,CAAC,IAAI;IACvB,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;AACzC,CAAC;AAEwD,kCAAW"}

399
dist/extract/findChineseText.js vendored Normal file
View File

@ -0,0 +1,399 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findTextInVue = exports.findChineseText = void 0;
/**
* @author doubledream
* @desc 利用 Ast 查找对应文件中的中文文案
*/
const ts = require("typescript");
const compiler = require("@angular/compiler");
const compilerVue = require("vue-template-compiler");
const babel = require("@babel/core");
/** unicode cjk 中日韩文 范围 */
const DOUBLE_BYTE_REGEX = /[\u4E00-\u9FFF]/g;
function transerI18n(code, filename, lang) {
if (lang === 'ts') {
return typescriptI18n(code, filename);
}
else {
return javascriptI18n(code, filename);
}
}
function javascriptI18n(code, filename) {
let arr = [];
let visitor = {
StringLiteral(path) {
if (path.node.value.match(DOUBLE_BYTE_REGEX)) {
arr.push(path.node.value);
}
}
};
let arrayPlugin = { visitor };
babel.transformSync(code.toString(), {
filename,
plugins: [arrayPlugin]
});
return arr;
}
function typescriptI18n(code, fileName) {
let arr = [];
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TS);
function visit(node) {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral: {
/** 判断 Ts 中的字符串含有中文 */
const { text } = node;
if (text.match(DOUBLE_BYTE_REGEX)) {
arr.push(text);
}
break;
}
}
ts.forEachChild(node, visit);
}
ts.forEachChild(ast, visit);
return arr;
}
/**
* 去掉文件中的注释
* @param code
* @param fileName
*/
function removeFileComment(code, fileName) {
const printer = ts.createPrinter({ removeComments: true });
const sourceFile = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
return printer.printFile(sourceFile);
}
/**
* 查找 Ts 文件中的中文
* @param code
*/
function findTextInTs(code, fileName) {
const matches = [];
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TSX);
function visit(node) {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral: {
/** 判断 Ts 中的字符串含有中文 */
const { text } = node;
if (text.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
const range = { start, end };
matches.push({
range,
text,
isString: true
});
}
break;
}
case ts.SyntaxKind.JsxElement: {
const { children } = node;
children.forEach(child => {
if (child.kind === ts.SyntaxKind.JsxText) {
const text = child.getText();
/** 修复注释含有中文的情况Angular 文件错误的 Ast 情况 */
const noCommentText = removeFileComment(text, fileName);
if (noCommentText.match(DOUBLE_BYTE_REGEX)) {
const start = child.getStart();
const end = child.getEnd();
const range = { start, end };
matches.push({
range,
text: text.trim(),
isString: false
});
}
}
});
break;
}
case ts.SyntaxKind.TemplateExpression: {
const { pos, end } = node;
const templateContent = code.slice(pos, end);
if (templateContent.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
const range = { start, end };
matches.push({
range,
text: code.slice(start + 1, end - 1),
isString: true
});
}
break;
}
case ts.SyntaxKind.NoSubstitutionTemplateLiteral: {
const { pos, end } = node;
const templateContent = code.slice(pos, end);
if (templateContent.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
const range = { start, end };
matches.push({
range,
text: code.slice(start + 1, end - 1),
isString: true
});
}
}
}
ts.forEachChild(node, visit);
}
ts.forEachChild(ast, visit);
return matches;
}
/**
* 查找 HTML 文件中的中文
* @param code
*/
function findTextInHtml(code) {
const matches = [];
const ast = compiler.parseTemplate(code, 'ast.html', {
preserveWhitespaces: false
});
function visit(node) {
const value = node.value;
if (value && typeof value === 'string' && value.match(DOUBLE_BYTE_REGEX)) {
const valueSpan = node.valueSpan || node.sourceSpan;
let { start: { offset: startOffset }, end: { offset: endOffset } } = valueSpan;
const nodeValue = code.slice(startOffset, endOffset);
let isString = false;
/** 处理带引号的情况 */
if (nodeValue.charAt(0) === '"' || nodeValue.charAt(0) === "'") {
isString = true;
}
const range = { start: startOffset, end: endOffset };
matches.push({
range,
text: value,
isString
});
}
else if (value && typeof value === 'object' && value.source && value.source.match(DOUBLE_BYTE_REGEX)) {
/**
* <span>{{expression}}中文</span>
*/
const chineseMatches = value.source.match(DOUBLE_BYTE_REGEX);
chineseMatches.map(match => {
const valueSpan = node.valueSpan || node.sourceSpan;
let { start: { offset: startOffset }, end: { offset: endOffset } } = valueSpan;
const nodeValue = code.slice(startOffset, endOffset);
const start = nodeValue.indexOf(match);
const end = start + match.length;
const range = { start, end };
matches.push({
range,
text: match[0],
isString: false
});
});
}
if (node.children && node.children.length) {
node.children.forEach(visit);
}
if (node.attributes && node.attributes.length) {
node.attributes.forEach(visit);
}
}
if (ast.nodes && ast.nodes.length) {
ast.nodes.forEach(visit);
}
return matches;
}
/**
* 递归匹配vue代码的中文
* @param code
*/
function findTextInVue(code) {
let rexspace1 = new RegExp(/&ensp;/, 'g');
let rexspace2 = new RegExp(/&emsp;/, 'g');
let rexspace3 = new RegExp(/&nbsp;/, 'g');
code = code
.replace(rexspace1, 'ccsp&;')
.replace(rexspace2, 'ecsp&;')
.replace(rexspace3, 'ncsp&;');
let coverRex1 = new RegExp(/ccsp&;/, 'g');
let coverRex2 = new RegExp(/ecsp&;/, 'g');
let coverRex3 = new RegExp(/ncsp&;/, 'g');
let matches = [];
var result;
const vueObejct = compilerVue.compile(code.toString(), { outputSourceRange: true });
let vueAst = vueObejct.ast;
let expressTemp = findVueText(vueAst);
expressTemp.forEach(item => {
item.arrf = [item.start, item.end];
});
matches = expressTemp;
let outcode = vueObejct.render.toString().replace('with(this)', 'function a()');
let vueTemp = transerI18n(outcode, 'as.vue', null);
/**删除所有的html中的头部空格 */
vueTemp = vueTemp.map(item => {
return item.trim();
});
vueTemp = Array.from(new Set(vueTemp));
let codeStaticArr = [];
vueObejct.staticRenderFns.forEach(item => {
let childcode = item.toString().replace('with(this)', 'function a()');
let vueTempChild = transerI18n(childcode, 'as.vue', null);
codeStaticArr = codeStaticArr.concat(Array.from(new Set(vueTempChild)));
});
vueTemp = Array.from(new Set(codeStaticArr.concat(vueTemp)));
vueTemp.forEach(item => {
let items = item
.replace(/\{/g, '\\{')
.replace(/\}/g, '\\}')
.replace(/\$/g, '\\$')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
.replace(/\+/g, '\\+')
.replace(/\*/g, '\\*')
.replace(/\^/g, '\\^');
let rex = new RegExp(items, 'g');
let codeTemplate = code.substring(vueObejct.ast.start, vueObejct.ast.end);
while ((result = rex.exec(codeTemplate))) {
let res = result;
let last = rex.lastIndex;
last = last - (res[0].length - res[0].trimRight().length);
const range = { start: res.index, end: last };
matches.push({
arrf: [res.index, last],
range,
text: res[0]
.trimRight()
.replace(coverRex1, '&ensp;')
.replace(coverRex2, '&emsp;')
.replace(coverRex3, '&nbsp;'),
isString: (codeTemplate.substr(res.index - 1, 1) === '"' && codeTemplate.substr(last, 1) === '"') ||
(codeTemplate.substr(res.index - 1, 1) === "'" && codeTemplate.substr(last, 1) === "'")
? true
: false
});
}
});
let matchesTemp = matches;
let matchesTempResult = matchesTemp.filter((item, index) => {
let canBe = true;
matchesTemp.forEach(items => {
if ((item.arrf[0] > items.arrf[0] && item.arrf[1] <= items.arrf[1]) ||
(item.arrf[0] >= items.arrf[0] && item.arrf[1] < items.arrf[1]) ||
(item.arrf[0] > items.arrf[0] && item.arrf[1] < items.arrf[1])) {
canBe = false;
}
});
if (canBe)
return item;
});
const sfc = compilerVue.parseComponent(code.toString());
return matchesTempResult.concat(findTextInVueTs(sfc.script.content, 'AS', sfc.script.start));
}
exports.findTextInVue = findTextInVue;
function findTextInVueTs(code, fileName, startNum) {
const matches = [];
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TS);
function visit(node) {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral: {
/** 判断 Ts 中的字符串含有中文 */
const { text } = node;
if (text.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
/** 加一,减一的原因是,去除引号 */
const range = { start: start + startNum, end: end + startNum };
matches.push({
range,
text,
isString: true
});
}
break;
}
case ts.SyntaxKind.TemplateExpression: {
const { pos, end } = node;
let templateContent = code.slice(pos, end);
templateContent = templateContent.toString().replace(/\$\{[^\}]+\}/, '');
if (templateContent.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
/** 加一,减一的原因是,去除`号 */
const range = { start: start + startNum, end: end + startNum };
matches.push({
range,
text: code.slice(start + 1, end - 1),
isString: true
});
}
break;
}
}
ts.forEachChild(node, visit);
}
ts.forEachChild(ast, visit);
return matches;
}
function findVueText(ast) {
let arr = [];
const regex1 = /\`(.+?)\`/g;
function emun(ast) {
if (ast.expression) {
let text = ast.expression.match(regex1);
if (text && text[0].match(DOUBLE_BYTE_REGEX)) {
text.forEach(itemText => {
const varInStr = itemText.match(/(\$\{[^\}]+?\})/g);
if (varInStr)
itemText.match(DOUBLE_BYTE_REGEX) &&
arr.push({ text: ' ' + itemText, range: { start: ast.start + 2, end: ast.end - 2 }, isString: true });
else
itemText.match(DOUBLE_BYTE_REGEX) &&
arr.push({ text: itemText, range: { start: ast.start, end: ast.end }, isString: false });
});
}
else {
ast.tokens &&
ast.tokens.forEach(element => {
if (typeof element === 'string' && element.match(DOUBLE_BYTE_REGEX)) {
arr.push({
text: element,
range: {
start: ast.start + ast.text.indexOf(element),
end: ast.start + ast.text.indexOf(element) + element.length
},
isString: false
});
}
});
}
}
else if (!ast.expression && ast.text) {
ast.text.match(DOUBLE_BYTE_REGEX) &&
arr.push({ text: ast.text, range: { start: ast.start, end: ast.end }, isString: false });
}
else {
ast.children &&
ast.children.forEach(item => {
emun(item);
});
}
}
emun(ast);
return arr;
}
/**
* 递归匹配代码的中文
* @param code
*/
function findChineseText(code, fileName) {
if (fileName.endsWith('.html')) {
return findTextInHtml(code);
}
else if (fileName.endsWith('.vue')) {
return findTextInVue(code);
}
else {
return findTextInTs(code, fileName);
}
}
exports.findChineseText = findChineseText;
//# sourceMappingURL=findChineseText.js.map

1
dist/extract/findChineseText.js.map vendored Normal file

File diff suppressed because one or more lines are too long

72
dist/extract/getLangData.js vendored Normal file
View File

@ -0,0 +1,72 @@
"use strict";
/**
* @author doubledream
* @desc 获取语言文件
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLangData = exports.getSuggestLangObj = void 0;
const globby = require("globby");
const fs = require("fs");
const path = require("path");
const utils_1 = require("../utils");
const CONFIG = utils_1.getProjectConfig();
const LANG_DIR = path.resolve(CONFIG.canaryDir, CONFIG.srcLang);
const I18N_GLOB = `${LANG_DIR}/**/*.ts`;
/**
* 获取对应文件的语言
*/
function getLangData(fileName) {
if (fs.existsSync(fileName)) {
return getLangJson(fileName);
}
else {
return {};
}
}
exports.getLangData = getLangData;
/**
* 获取文件 Json
*/
function getLangJson(fileName) {
const fileContent = fs.readFileSync(fileName, { encoding: 'utf8' });
let obj = fileContent.match(/export\s*default\s*({[\s\S]+);?$/)[1];
obj = obj.replace(/\s*;\s*$/, '');
let jsObj = {};
try {
jsObj = eval('(' + obj + ')');
}
catch (err) {
console.log(obj);
console.error(err);
}
return jsObj;
}
function getI18N() {
const paths = globby.sync(I18N_GLOB);
const langObj = paths.reduce((prev, curr) => {
const filename = curr
.split('/')
.pop()
.replace(/\.tsx?$/, '');
if (filename.replace(/\.tsx?/, '') === 'index') {
return prev;
}
const fileContent = getLangData(curr);
let jsObj = fileContent;
if (Object.keys(jsObj).length === 0) {
console.log(`\`${curr}\` 解析失败,该文件包含的文案无法自动补全`);
}
return Object.assign(Object.assign({}, prev), { [filename]: jsObj });
}, {});
return langObj;
}
/**
* 获取全部语言, 展平
*/
function getSuggestLangObj() {
const langObj = getI18N();
const finalLangObj = utils_1.flatten(langObj);
return finalLangObj;
}
exports.getSuggestLangObj = getSuggestLangObj;
//# sourceMappingURL=getLangData.js.map

1
dist/extract/getLangData.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"getLangData.js","sourceRoot":"","sources":["../../src/extract/getLangData.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,iCAAiC;AACjC,yBAAyB;AACzB,6BAA6B;AAC7B,oCAAqD;AAErD,MAAM,MAAM,GAAG,wBAAgB,EAAE,CAAC;AAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;AAChE,MAAM,SAAS,GAAG,GAAG,QAAQ,UAAU,CAAC;AAExC;;GAEG;AACH,SAAS,WAAW,CAAC,QAAQ;IAC3B,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC3B,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC;KAC9B;SAAM;QACL,OAAO,EAAE,CAAC;KACX;AACH,CAAC;AAsD2B,kCAAW;AApDvC;;GAEG;AACH,SAAS,WAAW,CAAC,QAAQ;IAC3B,MAAM,WAAW,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACpE,IAAI,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAClC,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI;QACF,KAAK,GAAG,IAAI,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;KAC/B;IAAC,OAAO,GAAG,EAAE;QACZ,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;KACpB;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,OAAO;IACd,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;QAC1C,MAAM,QAAQ,GAAG,IAAI;aAClB,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,EAAE;aACL,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAC1B,IAAI,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,OAAO,EAAE;YAC9C,OAAO,IAAI,CAAC;SACb;QAED,MAAM,WAAW,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,KAAK,GAAG,WAAW,CAAC;QAExB,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE;YACnC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,wBAAwB,CAAC,CAAC;SAChD;QAED,uCACK,IAAI,KACP,CAAC,QAAQ,CAAC,EAAE,KAAK,IACjB;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB;IACxB,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;IAC1B,MAAM,YAAY,GAAG,eAAO,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,YAAY,CAAC;AACtB,CAAC;AAEQ,8CAAiB"}

233
dist/extract/replace.js vendored Normal file
View File

@ -0,0 +1,233 @@
"use strict";
/**
* @author doubledream
* @desc 更新文件
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createImportI18N = exports.hasImportI18N = exports.replaceAndUpdate = void 0;
const fs = require("fs-extra");
const _ = require("lodash");
const prettier = require("prettier");
const ts = require("typescript");
const file_1 = require("./file");
const getLangData_1 = require("./getLangData");
const utils_1 = require("../utils");
const CONFIG = utils_1.getProjectConfig();
const srcLangDir = utils_1.getLangDir(CONFIG.srcLang);
function updateLangFiles(keyValue, text, validateDuplicate) {
if (!_.startsWith(keyValue, 'I18N.')) {
return;
}
const [, filename, ...restPath] = keyValue.split('.');
const fullKey = restPath.join('.');
const targetFilename = `${srcLangDir}/${filename}.ts`;
if (!fs.existsSync(targetFilename)) {
fs.writeFileSync(targetFilename, generateNewLangFile(fullKey, text));
addImportToMainLangFile(filename);
utils_1.successInfo(`成功新建语言文件 ${targetFilename}`);
}
else {
// 清除 require 缓存,解决手动更新语言文件后再自动抽取,导致之前更新失效的问题
const mainContent = getLangData_1.getLangData(targetFilename);
const obj = mainContent;
if (Object.keys(obj).length === 0) {
utils_1.failInfo(`${filename} 解析失败,该文件包含的文案无法自动补全`);
}
if (validateDuplicate && _.get(obj, fullKey) !== undefined) {
utils_1.failInfo(`${targetFilename} 中已存在 key 为 \`${fullKey}\` 的翻译,请重新命名变量`);
throw new Error('duplicate');
}
// \n 会被自动转义成 \\n这里转回来
text = text.replace(/\\n/gm, '\n');
_.set(obj, fullKey, text);
fs.writeFileSync(targetFilename, prettierFile(`export default ${JSON.stringify(obj, null, 2)}`));
}
}
/**
* 使用 Prettier 格式化文件
* @param fileContent
*/
function prettierFile(fileContent) {
try {
return prettier.format(fileContent, {
parser: 'typescript',
trailingComma: 'all',
singleQuote: true
});
}
catch (e) {
utils_1.failInfo(`代码格式化报错!${e.toString()}\n代码为:${fileContent}`);
return fileContent;
}
}
function generateNewLangFile(key, value) {
const obj = _.set({}, key, value);
return prettierFile(`export default ${JSON.stringify(obj, null, 2)}`);
}
function addImportToMainLangFile(newFilename) {
let mainContent = '';
if (fs.existsSync(`${srcLangDir}/index.ts`)) {
mainContent = fs.readFileSync(`${srcLangDir}/index.ts`, 'utf8');
mainContent = mainContent.replace(/^(\s*import.*?;)$/m, `$1\nimport ${newFilename} from './${newFilename}';`);
if (/(}\);)/.test(mainContent)) {
if (/\,\n(}\);)/.test(mainContent)) {
/** 最后一行包含,号 */
mainContent = mainContent.replace(/(}\);)/, ` ${newFilename},\n$1`);
}
else {
/** 最后一行不包含,号 */
mainContent = mainContent.replace(/\n(}\);)/, `,\n ${newFilename},\n$1`);
}
}
// 兼容 export default { common };的写法
if (/(};)/.test(mainContent)) {
if (/\,\n(};)/.test(mainContent)) {
/** 最后一行包含,号 */
mainContent = mainContent.replace(/(};)/, ` ${newFilename},\n$1`);
}
else {
/** 最后一行不包含,号 */
mainContent = mainContent.replace(/\n(};)/, `,\n ${newFilename},\n$1`);
}
}
}
else {
mainContent = `import ${newFilename} from './${newFilename}';\n\nexport default Object.assign({}, {\n ${newFilename},\n});`;
}
fs.writeFileSync(`${srcLangDir}/index.ts`, mainContent);
}
/**
* 检查是否添加 import I18N 命令
* @param filePath 文件路径
*/
function hasImportI18N(filePath) {
const code = file_1.readFile(filePath);
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TSX);
let hasImportI18N = false;
function visit(node) {
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
const importClause = node.importClause;
// import I18N from 'src/utils/I18N';
if (_.get(importClause, 'kind') === ts.SyntaxKind.ImportClause) {
if (importClause.name) {
if (importClause.name.escapedText === 'I18N') {
hasImportI18N = true;
}
}
else {
const namedBindings = importClause.namedBindings;
// import { I18N } from 'src/utils/I18N';
if (namedBindings.kind === ts.SyntaxKind.NamedImports) {
namedBindings.elements.forEach(element => {
if (element.kind === ts.SyntaxKind.ImportSpecifier && _.get(element, 'name.escapedText') === 'I18N') {
hasImportI18N = true;
}
});
}
// import * as I18N from 'src/utils/I18N';
if (namedBindings.kind === ts.SyntaxKind.NamespaceImport) {
if (_.get(namedBindings, 'name.escapedText') === 'I18N') {
hasImportI18N = true;
}
}
}
}
}
}
ts.forEachChild(ast, visit);
return hasImportI18N;
}
exports.hasImportI18N = hasImportI18N;
/**
* 在合适的位置添加 import I18N 语句
* @param filePath 文件路径
*/
function createImportI18N(filePath) {
const code = file_1.readFile(filePath);
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TSX);
const isTsFile = _.endsWith(filePath, '.ts');
const isJsFile = _.endsWith(filePath, '.js');
const isTsxFile = _.endsWith(filePath, '.tsx');
const isVueFile = _.endsWith(filePath, '.vue');
if (isTsFile || isTsxFile || isJsFile) {
const importStatement = `${CONFIG.importI18N}\n`;
const pos = ast.getStart(ast, false);
const updateCode = code.slice(0, pos) + importStatement + code.slice(pos);
return updateCode;
}
else if (isVueFile) {
const importStatement = `${CONFIG.importI18N}\n`;
const updateCode = code.replace(/<script>/g, `<script>\n${importStatement}`);
return updateCode;
}
}
exports.createImportI18N = createImportI18N;
/**
* 更新文件
* @param filePath 当前文件路径
* @param arg 目标字符串对象
* @param val 目标 key
* @param validateDuplicate 是否校验文件中已经存在要写入的 key
* @param needWrite 是否只需要替换不需要更新 langs 文件
*/
function replaceAndUpdate(filePath, arg, val, validateDuplicate, needWrite = true) {
const code = file_1.readFile(filePath);
const isHtmlFile = _.endsWith(filePath, '.html');
const isVueFile = _.endsWith(filePath, '.vue');
let newCode = code;
let finalReplaceText = arg.text;
const { start, end } = arg.range;
// 若是字符串,删掉两侧的引号
if (arg.isString) {
// 如果引号左侧是 等号,则可能是 jsx 的 props此时要替换成 {
const preTextStart = start - 1;
const [last2Char, last1Char] = code.slice(preTextStart, start + 1).split('');
let finalReplaceVal = val;
if (last2Char === '=') {
if (isHtmlFile) {
finalReplaceVal = '{{' + val + '}}';
}
else if (isVueFile) {
finalReplaceVal = '{{' + val + '}}';
}
else {
finalReplaceVal = '{' + val + '}';
}
}
// 若是模板字符串,看看其中是否包含变量
if (last1Char === '`') {
const varInStr = arg.text.match(/(\$\{[^\}]+?\})/g);
if (varInStr) {
const kvPair = varInStr.map((str, index) => {
return `val${index + 1}: ${str.replace(/^\${([^\}]+)\}$/, '$1')}`;
});
finalReplaceVal = `I18N.template(${val}, { ${kvPair.join(',\n')} })`;
varInStr.forEach((str, index) => {
finalReplaceText = finalReplaceText.replace(str, `{val${index + 1}}`);
});
}
}
newCode = `${code.slice(0, start)}${finalReplaceVal}${code.slice(end)}`;
}
else {
if (isHtmlFile || isVueFile) {
newCode = `${code.slice(0, start)}{{${val}}}${code.slice(end)}`;
}
else {
newCode = `${code.slice(0, start)}{${val}}${code.slice(end)}`;
}
}
try {
if (needWrite) {
// 更新语言文件
updateLangFiles(val, finalReplaceText, validateDuplicate);
}
// 若更新成功再替换代码
return file_1.writeFile(filePath, newCode);
}
catch (e) {
return Promise.reject(e.message);
}
}
exports.replaceAndUpdate = replaceAndUpdate;
//# sourceMappingURL=replace.js.map

1
dist/extract/replace.js.map vendored Normal file

File diff suppressed because one or more lines are too long

75
dist/import.js vendored Normal file
View File

@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.importMessages = void 0;
/**
* @author linhuiw
* @desc 导入翻译文件
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
const fs = require("fs");
const path = require("path");
const _ = require("lodash");
const d3_dsv_1 = require("d3-dsv");
const utils_1 = require("./utils");
const CONFIG = utils_1.getProjectConfig();
function getMessagesToImport(file) {
const content = fs.readFileSync(file).toString();
const messages = d3_dsv_1.tsvParseRows(content, ([key, value]) => {
try {
// value 的形式和 JSON 中的字符串值一致,其中的特殊字符是以转义形式存在的,
// 如换行符 \n在 value 中占两个字符,需要转成真正的换行符。
value = JSON.parse(`"${value}"`);
}
catch (e) {
throw new Error(`Illegal message: ${value}`);
}
return [key, value];
});
const rst = {};
const duplicateKeys = new Set();
messages.forEach(([key, value]) => {
if (rst.hasOwnProperty(key)) {
duplicateKeys.add(key);
}
rst[key] = value;
});
if (duplicateKeys.size > 0) {
const errorMessage = 'Duplicate messages detected: \n' + [...duplicateKeys].join('\n');
console.error(errorMessage);
process.exit(1);
}
return rst;
}
function writeMessagesToFile(messages, file, lang) {
const kiwiDir = CONFIG.canaryDir;
const srcMessages = require(path.resolve(kiwiDir, CONFIG.srcLang, file)).default;
const dstFile = path.resolve(kiwiDir, lang, file);
const oldDstMessages = require(dstFile).default;
const rst = {};
utils_1.traverse(srcMessages, (message, key) => {
_.setWith(rst, key, _.get(messages, key) || _.get(oldDstMessages, key), Object);
});
fs.writeFileSync(dstFile + '.ts', 'export default ' + JSON.stringify(rst, null, 2));
}
function importMessages(file, lang) {
let messagesToImport = getMessagesToImport(file);
const allMessages = utils_1.getAllMessages(CONFIG.srcLang);
messagesToImport = _.pickBy(messagesToImport, (message, key) => allMessages.hasOwnProperty(key));
const keysByFiles = _.groupBy(Object.keys(messagesToImport), key => key.split('.')[0]);
const messagesByFiles = _.mapValues(keysByFiles, (keys, file) => {
const rst = {};
_.forEach(keys, key => {
_.setWith(rst, key.substr(file.length + 1), messagesToImport[key], Object);
});
return rst;
});
_.forEach(messagesByFiles, (messages, file) => {
writeMessagesToFile(messages, file, lang);
});
}
exports.importMessages = importMessages;
//# sourceMappingURL=import.js.map

1
dist/import.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"import.js","sourceRoot":"","sources":["../src/import.ts"],"names":[],"mappings":"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"}

146
dist/index.js vendored Normal file
View File

@ -0,0 +1,146 @@
#!/usr/bin/env node
"use strict";
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 });
const commander = require("commander");
const inquirer = require("inquirer");
const lodash_1 = require("lodash");
const init_1 = require("./init");
const sync_1 = require("./sync");
const export_1 = require("./export");
const import_1 = require("./import");
const unused_1 = require("./unused");
const mock_1 = require("./mock");
const extract_1 = require("./extract/extract");
const translate_1 = require("./translate");
const utils_1 = require("./utils");
const ora = require("ora");
/**
* 进度条加载
* @param text
* @param callback
*/
function spining(text, callback) {
const spinner = ora(`${text}中...`).start();
if (callback) {
if (callback() !== false) {
spinner.succeed(`${text}成功`);
}
else {
spinner.fail(`${text}失败`);
}
}
}
commander
.version('0.2.0')
.option('--init', '初始化项目', { isDefault: true })
.option('--import [file] [lang]', '导入翻译文案')
.option('--export [file] [lang]', '导出未翻译的文案')
.option('--sync', '同步各种语言的文案')
.option('--mock', '使用 Google 或者 Baidu 翻译 输出mock文件')
.option('--translate', '使用 Google 或者 Baidu 翻译 翻译结果自动替换目标语种文案')
.option('--unused', '导出未使用的文案')
.option('--extract [dirPath]', '一键替换指定文件夹下的所有中文文案')
.option('--prefix [prefix]', '指定替换中文文案前缀')
.parse(process.argv);
if (commander.init) {
(() => __awaiter(void 0, void 0, void 0, function* () {
const result = yield inquirer.prompt({
type: 'confirm',
name: 'confirm',
default: true,
message: '项目中是否已存在kiwi相关目录'
});
if (!result.confirm) {
spining('初始化项目', () => __awaiter(void 0, void 0, void 0, function* () {
init_1.initProject();
}));
}
else {
const value = yield inquirer.prompt({
type: 'input',
name: 'dir',
message: '请输入相关目录:'
});
spining('初始化项目', () => __awaiter(void 0, void 0, void 0, function* () {
init_1.initProject(value.dir);
}));
}
}))();
}
if (commander.import) {
spining('导入翻译文案', () => {
if (commander.import === true || commander.args.length === 0) {
console.log('请按格式输入:--import [file] [lang]');
return false;
}
else if (commander.args) {
import_1.importMessages(commander.import, commander.args[0]);
}
});
}
if (commander.export) {
spining('导出未翻译的文案', () => {
if (commander.export === true && commander.args.length === 0) {
export_1.exportMessages();
}
else if (commander.args) {
export_1.exportMessages(commander.export, commander.args[0]);
}
});
}
if (commander.sync) {
spining('文案同步', () => {
sync_1.sync();
});
}
if (commander.unused) {
spining('导出未使用的文案', () => {
unused_1.findUnUsed();
});
}
if (commander.mock) {
sync_1.sync(() => __awaiter(void 0, void 0, void 0, function* () {
const { pass, origin } = yield utils_1.getTranslateOriginType();
if (pass) {
const spinner = ora(`使用 ${origin} 翻译中...`).start();
yield mock_1.mockLangs(origin);
spinner.succeed(`使用 ${origin} 翻译成功`);
}
}));
}
if (commander.translate) {
sync_1.sync(() => __awaiter(void 0, void 0, void 0, function* () {
const { pass, origin } = yield utils_1.getTranslateOriginType();
if (pass) {
const spinner = ora(`使用 ${origin} 翻译中...`).start();
yield translate_1.translate(origin);
spinner.succeed(`使用 ${origin} 翻译成功`);
}
}));
}
if (commander.extract) {
console.log(lodash_1.isString(commander.prefix));
if (commander.prefix === true) {
console.log('请指定翻译后文案 key 值的前缀 --prefix xxxx');
}
else if (lodash_1.isString(commander.prefix) && !new RegExp(/^I18N(\.[-_a-zA-Z1-9$]+)+$/).test(commander.prefix)) {
console.log('前缀必须以I18N开头,后续跟上字母、下滑线、破折号、$ 字符组成的变量名');
}
else {
const extractAllParams = {
prefix: lodash_1.isString(commander.prefix) && commander.prefix,
dirPath: lodash_1.isString(commander.extract) && commander.extract
};
extract_1.extractAll(extractAllParams);
}
}
//# sourceMappingURL=index.js.map

1
dist/index.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

65
dist/init.js vendored Normal file
View File

@ -0,0 +1,65 @@
"use strict";
/**
* @author linhuiw
* @desc 初始化 kiwi 项目的文件以及配置
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.initProject = void 0;
const path = require("path");
const fs = require("fs");
const const_1 = require("./const");
/**
* 创建配置文件
* @param existDir 配置文件夹地址
* @returns
*/
function createConfigFile(existDir) {
// 在根目录创建配置文件
const configDir = path.resolve(process.cwd(), `./${const_1.CANARY_CONFIG_FILE}`);
// 如果已经有配置文件就不要动了
if (fs.existsSync(configDir))
return;
const config = Object.assign({}, const_1.PROJECT_CONFIG.defaultConfig);
// 有配置文件夹
if (existDir && fs.existsSync(existDir)) {
config.canaryDir = existDir;
}
// 创建新的配置文件
fs.writeFile(configDir, JSON.stringify(config, null, 2), err => err && console.log(err));
}
/**
* 创建中文配置示例文件
*/
function createCnFile() {
// 中文配置文件夹地址
const cnDir = `${const_1.PROJECT_CONFIG.dir}/zh-CN`;
// 没有则创建
if (!fs.existsSync(cnDir)) {
fs.mkdirSync(cnDir);
fs.writeFile(`${cnDir}/index.ts`, const_1.PROJECT_CONFIG.zhIndexFile, err => err && console.log(err));
fs.writeFile(`${cnDir}/common.ts`, const_1.PROJECT_CONFIG.zhTestFile, err => err && console.log(err));
}
}
/**
* 初始化国际化项目
* @param existDir 配置文件夹地址
*/
function initProject(existDir) {
// 有用户输入的文件夹,不存在默认位置创建
if (existDir && !fs.existsSync(existDir)) {
console.log('\n输入的目录不存在已为你生成默认文件夹');
fs.mkdirSync(const_1.PROJECT_CONFIG.dir);
}
// 没有输入,默认位置创建
else if (!existDir && !fs.existsSync(const_1.PROJECT_CONFIG.dir)) {
fs.mkdirSync(const_1.PROJECT_CONFIG.dir);
}
// 创建配置文件
createConfigFile(existDir);
// 没有已经存在的配置文件夹,创建默认的配置文件示例
if (!(existDir && fs.existsSync(existDir))) {
createCnFile();
}
}
exports.initProject = initProject;
//# sourceMappingURL=init.js.map

1
dist/init.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAGH,6BAA6B;AAC7B,yBAAyB;AACzB,mCAA6D;AAE7D;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,QAAiB;IACzC,aAAa;IACb,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,0BAAkB,EAAE,CAAC,CAAC;IACzE,iBAAiB;IACjB,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO;IACrC,MAAM,MAAM,qBAAQ,sBAAc,CAAC,aAAa,CAAE,CAAC;IACnD,SAAS;IACT,IAAI,QAAQ,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QACvC,MAAM,CAAC,SAAS,GAAG,QAAQ,CAAC;KAC7B;IACD,WAAW;IACX,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED;;GAEG;AACH,SAAS,YAAY;IACnB,YAAY;IACZ,MAAM,KAAK,GAAG,GAAG,sBAAc,CAAC,GAAG,QAAQ,CAAC;IAC5C,QAAQ;IACR,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE;QACzB,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACpB,EAAE,CAAC,SAAS,CAAC,GAAG,KAAK,WAAW,EAAE,sBAAc,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9F,EAAE,CAAC,SAAS,CAAC,GAAG,KAAK,YAAY,EAAE,sBAAc,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;KAC/F;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,QAAiB;IACpC,sBAAsB;IACtB,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QACxC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACrC,EAAE,CAAC,SAAS,CAAC,sBAAc,CAAC,GAAG,CAAC,CAAC;KAClC;IACD,cAAc;SACT,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,sBAAc,CAAC,GAAG,CAAC,EAAE;QACxD,EAAE,CAAC,SAAS,CAAC,sBAAc,CAAC,GAAG,CAAC,CAAC;KAClC;IACD,SAAS;IACT,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,IAAI,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,EAAE;QAC1C,YAAY,EAAE,CAAC;KAChB;AACH,CAAC;AAEQ,kCAAW"}

128
dist/mock.js vendored Normal file
View File

@ -0,0 +1,128 @@
"use strict";
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.getAllUntranslatedTexts = exports.mockLangs = void 0;
/**
* @author linhuiw
* @desc 翻译方法
* @TODO: index 文件需要添加 mock
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
const path = require("path");
const fs = require("fs");
const _ = require("lodash");
const utils_1 = require("./utils");
const translate_1 = require("./translate");
const CONFIG = utils_1.getProjectConfig();
/**
* 获取中文文案
*/
function getSourceText() {
const srcLangDir = utils_1.getLangDir(CONFIG.srcLang);
const srcFile = path.resolve(srcLangDir, 'index.ts');
const { default: texts } = require(srcFile);
return texts;
}
/**
* 获取对应语言文案
* @param dstLang
*/
function getDistText(dstLang) {
const distLangDir = utils_1.getLangDir(dstLang);
const distFile = path.resolve(distLangDir, 'index.ts');
let distTexts = {};
if (fs.existsSync(distFile)) {
distTexts = require(distFile).default;
}
return distTexts;
}
/**
* 获取所有未翻译的文案
* @param 目标语种
*/
function getAllUntranslatedTexts(toLang) {
const texts = getSourceText();
const distTexts = getDistText(toLang);
const untranslatedTexts = {};
/** 遍历文案 */
utils_1.traverse(texts, (text, path) => {
const distText = _.get(distTexts, path);
if (text === distText || !distText) {
untranslatedTexts[path] = text;
}
});
return untranslatedTexts;
}
exports.getAllUntranslatedTexts = getAllUntranslatedTexts;
/**
* Mock 对应语言
* @param dstLang
*/
function mockCurrentLang(dstLang, origin) {
return __awaiter(this, void 0, void 0, function* () {
const untranslatedTexts = getAllUntranslatedTexts(dstLang);
let mocks = {};
if (origin === 'Google') {
mocks = yield translate_1.googleTranslateTexts(untranslatedTexts, dstLang);
}
else {
mocks = yield translate_1.baiduTranslateTexts(untranslatedTexts, dstLang);
}
/** 所有任务执行完毕后写入mock文件 */
return writeMockFile(dstLang, mocks);
});
}
/**
* 写入 Mock 文件
* @param dstLang
* @param mocks
*/
function writeMockFile(dstLang, mocks) {
const fileContent = 'export default ' + JSON.stringify(mocks, null, 2);
const filePath = path.resolve(utils_1.getLangDir(dstLang), 'mock.ts');
return new Promise((resolve, reject) => {
fs.writeFile(filePath, fileContent, err => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
/**
* Mock 语言的未翻译的文案
* @param lang
*/
function mockLangs(origin) {
return __awaiter(this, void 0, void 0, function* () {
const langs = CONFIG.distLangs;
if (origin === 'Google') {
const mockPromise = langs.map(lang => {
return mockCurrentLang(lang, origin);
});
return Promise.all(mockPromise);
}
else {
for (var i = 0; i < langs.length; i++) {
yield mockCurrentLang(langs[i], origin);
}
return Promise.resolve();
}
});
}
exports.mockLangs = mockLangs;
//# sourceMappingURL=mock.js.map

1
dist/mock.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"mock.js","sourceRoot":"","sources":["../src/mock.ts"],"names":[],"mappings":"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"}

113
dist/sync.js vendored Normal file
View File

@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sync = void 0;
/**
* @author linhuiw
* @desc 翻译文件
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
const fs = require("fs");
const path = require("path");
const _ = require("lodash");
const utils_1 = require("./utils");
const CONFIG = utils_1.getProjectConfig();
/**
* 获取中文文案文件的翻译优先使用已有翻译若找不到则使用 google 翻译
* */
function getTranslations(file, toLang) {
const translations = {};
const fileNameWithoutExt = path.basename(file).split('.')[0];
const srcLangDir = utils_1.getLangDir(CONFIG.srcLang);
const distLangDir = utils_1.getLangDir(toLang);
const srcFile = path.resolve(srcLangDir, file);
const distFile = path.resolve(distLangDir, file);
const { default: texts } = require(srcFile);
let distTexts;
if (fs.existsSync(distFile)) {
distTexts = require(distFile).default;
}
utils_1.traverse(texts, (text, path) => {
const key = fileNameWithoutExt + '.' + path;
const distText = _.get(distTexts, path);
translations[key] = distText || text;
});
return translations;
}
/**
* 将翻译写入文件
* */
function writeTranslations(file, toLang, translations) {
const fileNameWithoutExt = path.basename(file).split('.')[0];
const srcLangDir = utils_1.getLangDir(CONFIG.srcLang);
const srcFile = path.resolve(srcLangDir, file);
const { default: texts } = require(srcFile);
const rst = {};
utils_1.traverse(texts, (text, path) => {
const key = fileNameWithoutExt + '.' + path;
// 使用 setWith 而不是 set保证 numeric key 创建的不是数组,而是对象
// https://github.com/lodash/lodash/issues/1316#issuecomment-120753100
_.setWith(rst, path, translations[key], Object);
});
const fileContent = 'export default ' + JSON.stringify(rst, null, 2);
const filePath = path.resolve(utils_1.getLangDir(toLang), path.basename(file));
return new Promise((resolve, reject) => {
fs.writeFile(filePath, fileContent, err => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
/**
* 翻译对应的文件
* @param file
* @param toLang
*/
function translateFile(file, toLang) {
const translations = getTranslations(file, toLang);
const toLangDir = path.resolve(__dirname, `../${toLang}`);
if (!fs.existsSync(toLangDir)) {
fs.mkdirSync(toLangDir);
}
writeTranslations(file, toLang, translations);
}
/**
* 翻译所有文件
*/
function sync(callback) {
const srcLangDir = utils_1.getLangDir(CONFIG.srcLang);
fs.readdir(srcLangDir, (err, files) => {
if (err) {
console.error(err);
}
else {
files = files.filter(file => file.endsWith('.ts') && file !== 'index.ts' && file !== 'mock.ts').map(file => file);
const translateFiles = toLang => Promise.all(files.map(file => {
translateFile(file, toLang);
}));
Promise.all(CONFIG.distLangs.map(translateFiles)).then(() => {
const langDirs = CONFIG.distLangs.map(utils_1.getLangDir);
langDirs.map(dir => {
const filePath = path.resolve(dir, 'index.ts');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
fs.copyFileSync(path.resolve(srcLangDir, 'index.ts'), filePath);
});
callback && callback();
}, e => {
console.error(e);
process.exit(1);
});
}
});
}
exports.sync = sync;
//# sourceMappingURL=sync.js.map

1
dist/sync.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"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"}

195
dist/translate.js vendored Normal file
View File

@ -0,0 +1,195 @@
"use strict";
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.googleTranslateTexts = exports.baiduTranslateTexts = exports.translate = void 0;
/**
* @author zongwenjian
* @desc 全量翻译 translate命令
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
const path = require("path");
const fs = require("fs");
const baiduTranslate = require("baidu-translate");
const d3_dsv_1 = require("d3-dsv");
const utils_1 = require("./utils");
const import_1 = require("./import");
const mock_1 = require("./mock");
const CONFIG = utils_1.getProjectConfig();
/**
* 百度单次翻译任务
* @param text 待翻译文案
* @param toLang 目标语种
*/
function translateTextByBaidu(text, toLang) {
const { baiduApiKey: { appId, appKey }, baiduLangMap } = CONFIG;
return utils_1.withTimeout(new Promise((resolve, reject) => {
baiduTranslate(appId, appKey, baiduLangMap[toLang], 'zh')(text)
.then(data => {
if (data && data.trans_result) {
resolve(data.trans_result);
}
else {
reject(`\n百度翻译api调用异常 error_code: ${data.error_code}, error_msg: ${data.error_msg}`);
}
})
.catch(err => {
reject(err);
});
}), 3000);
}
/** 文案首字母大小 变量小写 */
function textToUpperCaseByFirstWord(text) {
// 翻译文案首字母大写,变量小写
return text
? `${text.charAt(0).toUpperCase()}${text.slice(1)}`.replace(/(\{.*?\})/g, text => text.toLowerCase())
: '';
}
/**
* 使用google翻译所有待翻译的文案
* @param untranslatedTexts 待翻译文案
* @param toLang 目标语种
*/
function googleTranslateTexts(untranslatedTexts, toLang) {
return __awaiter(this, void 0, void 0, function* () {
const translateAllTexts = Object.keys(untranslatedTexts).map(key => {
return utils_1.translateText(untranslatedTexts[key], toLang).then(translatedText => [key, translatedText]);
});
return new Promise(resolve => {
const result = {};
Promise.all(translateAllTexts).then(res => {
res.forEach(([key, translatedText]) => {
result[key] = translatedText;
});
resolve(result);
});
});
});
}
exports.googleTranslateTexts = googleTranslateTexts;
/**
* 使用百度翻译所有待翻译的文案
* @param untranslatedTexts 待翻译文案
* @param toLang 目标语种
*/
function baiduTranslateTexts(untranslatedTexts, toLang) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
const result = {};
const untranslatedKeys = Object.keys(untranslatedTexts);
const taskLists = {};
let lastIndex = 0;
// 由于百度api单词翻译字符长度限制需要将待翻译的文案拆分成单个子任务
untranslatedKeys.reduce((pre, next, index) => {
const byteLen = Buffer.byteLength(pre, 'utf8');
if (byteLen > 5500) {
// 获取翻译字节数大于5500放到单独任务里面处理
taskLists[lastIndex] = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(translateTextByBaidu(pre, toLang));
}, 1500);
});
};
lastIndex = index;
return untranslatedTexts[next];
}
else if (index === untranslatedKeys.length - 1) {
taskLists[lastIndex] = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(translateTextByBaidu(`${pre}\n${untranslatedTexts[next]}`, toLang));
}, 1500);
});
};
}
return `${pre}\n${untranslatedTexts[next]}`;
}, '');
// 由于百度api调用QPS只有1, 考虑网络延迟 每1.5s请求一个子任务
const taskKeys = Object.keys(taskLists);
if (taskKeys.length > 0) {
for (var i = 0; i < taskKeys.length; i++) {
const langIndexKey = taskKeys[i];
const taskItemFun = taskLists[langIndexKey];
const data = yield taskItemFun();
(data || []).forEach(({ dst }, index) => {
const currTextKey = untranslatedKeys[Number(langIndexKey) + index];
result[currTextKey] = textToUpperCaseByFirstWord(dst);
});
}
}
resolve(result);
}));
});
}
exports.baiduTranslateTexts = baiduTranslateTexts;
/**
* 执行翻译任务自动导入翻译结果
* @param dstLang
*/
function runTranslateApi(dstLang, origin) {
return __awaiter(this, void 0, void 0, function* () {
const untranslatedTexts = mock_1.getAllUntranslatedTexts(dstLang);
let mocks = {};
if (origin === 'Google') {
mocks = yield googleTranslateTexts(untranslatedTexts, dstLang);
}
else {
mocks = yield baiduTranslateTexts(untranslatedTexts, dstLang);
}
const messagesToTranslate = Object.keys(mocks).map(key => [key, mocks[key]]);
if (messagesToTranslate.length === 0) {
return Promise.resolve();
}
const content = d3_dsv_1.tsvFormatRows(messagesToTranslate);
// 输出tsv文件
return new Promise((resolve, reject) => {
const filePath = path.resolve(utils_1.getLangDir(dstLang), `${dstLang}_translate.tsv`);
fs.writeFile(filePath, content, err => {
if (err) {
reject(err);
}
else {
console.log(`${dstLang} 自动翻译完成`);
// 自动导入翻译结果
import_1.importMessages(filePath, dstLang);
resolve();
}
});
});
});
}
/**
* 全量翻译
* @param origin 翻译源
*/
function translate(origin) {
return __awaiter(this, void 0, void 0, function* () {
const langs = CONFIG.distLangs;
if (origin === 'Google') {
const mockPromise = langs.map(lang => {
return runTranslateApi(lang, origin);
});
return Promise.all(mockPromise);
}
else {
for (var i = 0; i < langs.length; i++) {
yield runTranslateApi(langs[i], origin);
}
return Promise.resolve();
}
});
}
exports.translate = translate;
//# sourceMappingURL=translate.js.map

1
dist/translate.js.map vendored Normal file

File diff suppressed because one or more lines are too long

101
dist/unused.js vendored Normal file
View File

@ -0,0 +1,101 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findUnUsed = void 0;
/**
* @author linhuiw
* @desc 查找未使用的 key
*/
const fs = require("fs");
const path = require("path");
const utils_1 = require("./utils");
const lookingForString = '';
function findUnUsed() {
const srcLangDir = path.resolve(utils_1.getKiwiDir(), 'zh-CN');
let files = fs.readdirSync(srcLangDir);
files = files.filter(file => file.endsWith('.ts') && file !== 'index.ts');
const unUnsedKeys = [];
files.map(file => {
const srcFile = path.resolve(srcLangDir, file);
const { default: messages } = require(srcFile);
const filename = path.basename(file, '.ts');
utils_1.traverse(messages, (text, path) => {
const key = `I18N.${filename}.${path}`;
const hasKey = recursiveReadFile('./src', key);
if (!hasKey) {
unUnsedKeys.push(key);
}
});
});
console.log(unUnsedKeys, 'unUnsedKeys');
}
exports.findUnUsed = findUnUsed;
/**
* 递归查找文件
* @param fileName
*/
function recursiveReadFile(fileName, text) {
let hasText = false;
if (!fs.existsSync(fileName))
return;
if (isFile(fileName) && !hasText) {
check(fileName, text, () => {
hasText = true;
});
}
if (isDirectory(fileName)) {
var files = fs.readdirSync(fileName).filter(file => {
return !file.startsWith('.') && !['node_modules', 'build', 'dist'].includes(file);
});
files.forEach(function (val, key) {
var temp = path.join(fileName, val);
if (isDirectory(temp) && !hasText) {
hasText = recursiveReadFile(temp, text);
}
if (isFile(temp) && !hasText) {
check(temp, text, () => {
hasText = true;
});
}
});
}
return hasText;
}
/**
* 检查文件
* @param fileName
*/
function check(fileName, text, callback) {
var data = readFile(fileName);
var exc = new RegExp(text);
if (exc.test(data)) {
callback();
}
}
/**
* 判断是文件夹
* @param fileName
*/
function isDirectory(fileName) {
if (fs.existsSync(fileName)) {
return fs.statSync(fileName).isDirectory();
}
}
/**
* 判断是否是文件
* @param fileName
*/
function isFile(fileName) {
if (fs.existsSync(fileName)) {
return fs.statSync(fileName).isFile();
}
}
/**
* 读取文件
* @param fileName
*/
function readFile(fileName) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf-8');
}
}
//# sourceMappingURL=unused.js.map

1
dist/unused.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"unused.js","sourceRoot":"","sources":["../src/unused.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,yBAAyB;AACzB,6BAA6B;AAC7B,mCAA2D;AAE3D,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,SAAS,UAAU;IACjB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAU,EAAE,EAAE,OAAO,CAAC,CAAC;IACvD,IAAI,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IACvC,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,UAAU,CAAC,CAAC;IAC1E,MAAM,WAAW,GAAG,EAAE,CAAC;IACvB,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACf,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAC/C,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAE5C,gBAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YAChC,MAAM,GAAG,GAAG,QAAQ,QAAQ,IAAI,IAAI,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YAC/C,IAAI,CAAC,MAAM,EAAE;gBACX,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;aACvB;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;AAC1C,CAAC;AA0EQ,gCAAU;AAzEnB;;;GAGG;AACH,SAAS,iBAAiB,CAAC,QAAQ,EAAE,IAAI;IACvC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO;IACrC,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE;QAChC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE;YACzB,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC,CAAC,CAAC;KACJ;IACD,IAAI,WAAW,CAAC,QAAQ,CAAC,EAAE;QACzB,IAAI,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;YACjD,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpF,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,OAAO,CAAC,UAAS,GAAG,EAAE,GAAG;YAC7B,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YACpC,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gBACjC,OAAO,GAAG,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;aACzC;YACD,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gBAC5B,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;oBACrB,OAAO,GAAG,IAAI,CAAC;gBACjB,CAAC,CAAC,CAAC;aACJ;QACH,CAAC,CAAC,CAAC;KACJ;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ;IACrC,IAAI,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAClB,QAAQ,EAAE,CAAC;KACZ;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,QAAQ;IAC3B,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC3B,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;KAC5C;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,MAAM,CAAC,QAAQ;IACtB,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC3B,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;KACvC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CAAC,QAAQ;IACxB,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC3B,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;KAC3C;AACH,CAAC"}

295
dist/utils.js vendored Normal file
View File

@ -0,0 +1,295 @@
"use strict";
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.highlightText = exports.failInfo = exports.successInfo = exports.translateKeyText = exports.getTranslateOriginType = exports.lookForFiles = exports.flatten = exports.findMatchValue = exports.findMatchKey = exports.translateText = exports.getProjectConfig = exports.getAllMessages = exports.withTimeout = exports.retry = exports.traverse = exports.getLangDir = exports.getKiwiDir = void 0;
/**
* @author linhuiw
* @desc 工具方法
*/
const path = require("path");
const _ = require("lodash");
const inquirer = require("inquirer");
const fs = require("fs");
const pinyin_pro_1 = require("pinyin-pro");
const const_1 = require("./const");
const colors = require('colors');
function lookForFiles(dir, fileName) {
const files = fs.readdirSync(dir);
for (let file of files) {
const currName = path.join(dir, file);
const info = fs.statSync(currName);
if (info.isDirectory()) {
if (file === '.git' || file === 'node_modules') {
continue;
}
const result = lookForFiles(currName, fileName);
if (result) {
return result;
}
}
else if (info.isFile() && file === fileName) {
return currName;
}
}
}
exports.lookForFiles = lookForFiles;
/**
* 获得项目配置信息
*/
function getProjectConfig() {
const configFile = path.resolve(process.cwd(), `./${const_1.CANARY_CONFIG_FILE}`);
let obj = const_1.PROJECT_CONFIG.defaultConfig;
if (configFile && fs.existsSync(configFile)) {
obj = Object.assign(Object.assign({}, obj), JSON.parse(fs.readFileSync(configFile, 'utf8')));
}
return obj;
}
exports.getProjectConfig = getProjectConfig;
/**
* 获取语言资源的根目录
*/
function getKiwiDir() {
const config = getProjectConfig();
if (config) {
return config.canaryDir;
}
}
exports.getKiwiDir = getKiwiDir;
/**
* 获取对应语言的目录位置
* @param lang
*/
function getLangDir(lang) {
const langsDir = getKiwiDir();
return path.resolve(langsDir, lang);
}
exports.getLangDir = getLangDir;
/**
* 深度优先遍历对象中的所有 string 属性即文案
*/
function traverse(obj, cb) {
function traverseInner(obj, cb, path) {
_.forEach(obj, (val, key) => {
if (typeof val === 'string') {
cb(val, [...path, key].join('.'));
}
else if (typeof val === 'object' && val !== null) {
traverseInner(val, cb, [...path, key]);
}
});
}
traverseInner(obj, cb, []);
}
exports.traverse = traverse;
/**
* 获取所有文案
*/
function getAllMessages(lang, filter = (message, key) => true) {
const srcLangDir = getLangDir(lang);
let files = fs.readdirSync(srcLangDir);
files = files.filter(file => file.endsWith('.ts') && file !== 'index.ts').map(file => path.resolve(srcLangDir, file));
const allMessages = files.map(file => {
const { default: messages } = require(file);
const fileNameWithoutExt = path.basename(file).split('.')[0];
const flattenedMessages = {};
console.log(fileNameWithoutExt, messages);
traverse(messages, (message, path) => {
const key = fileNameWithoutExt + '.' + path;
if (filter(message, key)) {
flattenedMessages[key] = message;
}
});
return flattenedMessages;
});
return Object.assign({}, ...allMessages);
}
exports.getAllMessages = getAllMessages;
/**
* 重试方法
* @param asyncOperation
* @param times
*/
function retry(asyncOperation, times = 1) {
let runTimes = 1;
const handleReject = e => {
if (runTimes++ < times) {
return asyncOperation().catch(handleReject);
}
else {
throw e;
}
};
return asyncOperation().catch(handleReject);
}
exports.retry = retry;
/**
* 设置超时
* @param promise
* @param ms
*/
function withTimeout(promise, ms) {
const timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(`Promise timed out after ${ms} ms.`);
}, ms);
});
return Promise.race([promise, timeoutPromise]);
}
exports.withTimeout = withTimeout;
/**
* 使用google翻译
*/
function translateText(text, toLang) {
const CONFIG = getProjectConfig();
const options = CONFIG.translateOptions;
const { translate: googleTranslate } = require('google-translate')(CONFIG.googleApiKey, options);
return withTimeout(new Promise((resolve, reject) => {
googleTranslate(text, 'zh', const_1.PROJECT_CONFIG.langMap[toLang], (err, translation) => {
if (err) {
reject(err);
}
else {
resolve(translation.translatedText);
}
});
}), 5000);
}
exports.translateText = translateText;
/**
* 翻译中文
*/
function translateKeyText(text, origin) {
const CONFIG = getProjectConfig();
const { appId, appKey } = CONFIG.baiduApiKey;
const baiduTranslate = require('baidu-translate');
function _translateText() {
return withTimeout(new Promise((resolve, reject) => {
// Baidu
if (origin === 'Baidu') {
baiduTranslate(appId, appKey, 'en', 'zh')(text)
.then(data => {
if (data && data.trans_result) {
const result = data.trans_result.map(item => item.dst) || [];
resolve(result);
}
})
.catch(err => {
reject(err);
});
}
// Pinyin
if (origin === 'Pinyin') {
const result = pinyin_pro_1.pinyin(text, { toneType: 'none' });
resolve(result.split('$'));
}
}), 3000);
}
return retry(_translateText, 3);
}
exports.translateKeyText = translateKeyText;
function findMatchKey(langObj, text) {
for (const key in langObj) {
if (langObj[key] === text) {
return key;
}
}
return null;
}
exports.findMatchKey = findMatchKey;
function findMatchValue(langObj, key) {
return langObj[key];
}
exports.findMatchValue = findMatchValue;
/**
* 将对象拍平
* @param obj 原始对象
* @param prefix
*/
function flatten(obj, prefix = '') {
var propName = prefix ? prefix + '.' : '', ret = {};
for (var attribute in obj) {
var attr = attribute.replace(/-/g, '_');
if (_.isArray(obj[attr])) {
var len = obj[attr].length;
ret[attr] = obj[attr].join(',');
}
else if (typeof obj[attr] === 'object') {
_.extend(ret, flatten(obj[attr], propName + attr));
}
else {
ret[propName + attr] = obj[attr];
}
}
return ret;
}
exports.flatten = flatten;
/**
* 获取翻译源类型
*/
function getTranslateOriginType() {
return __awaiter(this, void 0, void 0, function* () {
const { googleApiKey, baiduApiKey } = getProjectConfig();
let translateType = ['Google', 'Baidu'];
if (!googleApiKey) {
translateType = translateType.filter(item => item !== 'Google');
}
if (!baiduApiKey || !baiduApiKey.appId || !baiduApiKey.appKey) {
translateType = translateType.filter(item => item !== 'Baidu');
}
if (translateType.length === 0) {
console.log('请配置 googleApiKey 或 baiduApiKey ');
return {
pass: false,
origin: ''
};
}
if (translateType.length == 1) {
return {
pass: true,
origin: translateType[0]
};
}
const { origin } = yield inquirer.prompt({
type: 'list',
name: 'origin',
message: '请选择使用的翻译源',
default: 'Google',
choices: ['Google', 'Baidu']
});
return {
pass: true,
origin: origin
};
});
}
exports.getTranslateOriginType = getTranslateOriginType;
/**
* 成功的提示
*/
function successInfo(message) {
console.log('successInfo: ', colors.green(message));
}
exports.successInfo = successInfo;
/**
* 失败的提示
*/
function failInfo(message) {
console.log('failInfo: ', colors.red(message));
}
exports.failInfo = failInfo;
/**
* 普通提示
*/
function highlightText(message) {
return colors.yellow(`${message}`);
}
exports.highlightText = highlightText;
//# sourceMappingURL=utils.js.map

1
dist/utils.js.map vendored Normal file

File diff suppressed because one or more lines are too long

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "kiwi-clis",
"version": "1.0.23",
"description": "a cli for kiwi",
"main": "dist/index.js",
"scripts": {
"dev": "tsc --watch",
"prepublish": "tsc"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/alibaba/kiwi.git"
},
"keywords": [
"cli",
"kiwi",
"i18n"
],
"bin": {
"kiwi": "dist/index.js"
},
"author": "linhuiw",
"license": "ISC",
"bugs": {
"url": "https://github.com/alibaba/kiwi/issues"
},
"homepage": "https://github.com/alibaba/kiwi#readme",
"dependencies": {
"@angular/compiler": "^7.2.0",
"@babel/core": "^7.5.5",
"@types/commander": "^2.12.2",
"@types/lodash": "^4.14.119",
"@types/node": "^10.12.14",
"baidu-translate": "^1.1.0",
"colors": "^1.4.0",
"commander": "^2.19.0",
"d3-dsv": "^1.0.10",
"fs-extra": "^7.0.1",
"globby": "^7.1.1",
"google-translate": "^3.0.0",
"inquirer": "^5.2.0",
"lodash": "^4.17.11",
"ora": "^3.0.0",
"pinyin-pro": "^3.3.1",
"prettier": "^1.16.4",
"randomstring": "^1.1.5",
"slash2": "^2.0.0",
"ts-node": "^7.0.1",
"typescript": "^3.2.2",
"vue-template-compiler": "^2.6.11"
}
}

BIN
public/extract.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

44
src/const.ts Normal file
View File

@ -0,0 +1,44 @@
/**
* @author zhaoyingbo
* @desc
*/
export const CANARY_CONFIG_FILE = 'canary-config.json';
export const PROJECT_CONFIG = {
dir: './.canary',
defaultConfig: {
canaryDir: './.canary',
srcLang: 'zh-CN',
distLangs: ['en-US', 'zh-CN'],
googleApiKey: '',
baiduApiKey: {
appId: '',
appKey: ''
},
baiduLangMap: {
['en-US']: 'en',
['zh-TW']: 'cht'
},
translateOptions: {
concurrentLimit: 10,
requestOptions: {}
},
defaultTranslateKeyApi: 'Pinyin', // 批量提取文案时生成key值时的默认翻译源
importI18N: `import I18N from 'src/utils/I18N';`,
ignoreDir: '',
ignoreFile: ''
},
langMap: {
['en-US']: 'en',
['en_US']: 'en'
},
zhIndexFile: `import common from './common';
export default Object.assign({}, {
common
});`,
zhTestFile: `export default {
test: '测试'
}`,
};

45
src/export.ts Normal file
View File

@ -0,0 +1,45 @@
/**
* @author linhuiw
* @desc
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
import * as fs from 'fs';
import { tsvFormatRows } from 'd3-dsv';
import { getAllMessages, getProjectConfig } from './utils';
import * as _ from 'lodash';
function exportMessages(file?: string, lang?: string) {
const CONFIG = getProjectConfig();
const langs = lang ? [lang] : CONFIG.distLangs;
langs.map(lang => {
const allMessages = getAllMessages(CONFIG.srcLang);
const existingTranslations = getAllMessages(
lang,
(message, key) => !/[\u4E00-\u9FA5]/.test(allMessages[key]) || allMessages[key] !== message
);
const messagesToTranslate = Object.keys(allMessages)
.filter(key => !existingTranslations.hasOwnProperty(key))
.map(key => {
let message = allMessages[key];
message = JSON.stringify(message).slice(1, -1);
return [key, message];
});
if (messagesToTranslate.length === 0) {
console.log('All the messages have been translated.');
return;
}
const content = tsvFormatRows(messagesToTranslate);
const sourceFile = file || `./export-${lang}`;
fs.writeFileSync(sourceFile, content);
console.log(`Exported ${messagesToTranslate.length} message(s).`);
});
}
export { exportMessages };

293
src/extract/extract.ts Normal file
View File

@ -0,0 +1,293 @@
/**
* @author doubledream
* @desc
*/
import * as _ from 'lodash';
import * as slash from 'slash2';
import * as path from 'path';
import * as colors from 'colors';
import { getSpecifiedFiles, readFile, writeFile, isFile, isDirectory } from './file';
import { findChineseText } from './findChineseText';
import { getSuggestLangObj } from './getLangData';
import {
translateText,
findMatchKey,
findMatchValue,
translateKeyText,
successInfo,
failInfo,
highlightText
} from '../utils';
import { replaceAndUpdate, hasImportI18N, createImportI18N } from './replace';
import { getProjectConfig } from '../utils';
const CONFIG = getProjectConfig();
/**
* kiwiDir
*/
function removeLangsFiles(files: string[]) {
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: string) {
const first = dir.split(',')[0];
let files = [];
if (isDirectory(first)) {
const dirPath = path.resolve(process.cwd(), dir);
files = getSpecifiedFiles(dirPath, CONFIG.ignoreDir, CONFIG.ignoreFile);
} else {
files = removeLangsFiles(dir.split(','));
}
const filterFiles = files.filter(file => {
return (isFile(file) && file.endsWith('.ts')) || (isFile(file) && file.endsWith('.js')) || file.endsWith('.tsx') || file.endsWith('.vue');
});
const allTexts = filterFiles.reduce((pre, file) => {
const code = readFile(file);
const texts = findChineseText(code, file);
// 调整文案顺序,保证从后面的文案往前替换,避免位置更新导致替换出错
const sortTexts = _.sortBy(texts, obj => -obj.range.start);
if (texts.length > 0) {
console.log(`${highlightText(file)} 发现 ${highlightText(texts.length)} 处中文文案`);
}
return texts.length > 0 ? pre.concat({ file, texts: sortTexts }) : pre;
}, []);
return allTexts;
}
/**
* key值的翻译原文
*/
function getTransOriginText(text: string) {
// 避免翻译的字符里包含数字或者特殊字符等情况,只过滤出汉字和字母
const reg = /[a-zA-Z\u4e00-\u9fa5]+/g;
const findText = text.match(reg) || [];
const transOriginText = findText ? findText.join('').slice(0, 5) : '中文符号';
return transOriginText;
}
/**
* @param currentFilename
* @returns string[]
*/
function getSuggestion(currentFilename: string) {
let suggestion = [];
const suggestPageRegex = /\/pages\/\w+\/([^\/]+)\/([^\/\.]+)/;
if (currentFilename.includes('/pages/')) {
suggestion = currentFilename.match(suggestPageRegex);
}
if (suggestion) {
suggestion.shift();
}
/** 如果没有匹配到 Key */
if (!(suggestion && suggestion.length)) {
const names = slash(currentFilename).split('/');
const fileName = _.last(names) as any;
const fileKey = fileName.split('.')[0].replace(new RegExp('-', 'g'), '_');
const dir = names[names.length - 2].replace(new RegExp('-', 'g'), '_');
if (dir === fileKey) {
suggestion = [dir];
} else {
suggestion = [dir, fileKey];
}
}
return suggestion;
}
/**
* key值key若相同
* @param currentFilename
* @param langsPrefix
* @param translateTexts key值
* @param targetStrs
* @returns any[] key值和文案
*/
function getReplaceableStrs(currentFilename: string, langsPrefix: string, translateTexts: string[], targetStrs: any[]) {
const finalLangObj = getSuggestLangObj();
const virtualMemory = {};
const suggestion = getSuggestion(currentFilename);
const replaceableStrs = targetStrs.reduce((prev, curr, i) => {
const _text = curr.text;
let key = findMatchKey(finalLangObj, _text);
if (key) {
key = key.replace(/-/g, '_');
}
if (!virtualMemory[_text]) {
if (key) {
virtualMemory[_text] = key;
return prev.concat({
target: curr,
key,
needWrite: false
});
}
const transText = translateTexts[i] && _.camelCase(translateTexts[i] as string);
let transKey = `${suggestion.length ? suggestion.join('.') + '.' : ''}${transText}`;
transKey = transKey.replace(/-/g, '_');
if (langsPrefix) {
transKey = `${langsPrefix}.${transText}`;
}
let occurTime = 1;
// 防止出现前四位相同但是整体文案不同的情况
while (
findMatchValue(finalLangObj, transKey) !== _text &&
_.keys(finalLangObj).includes(`${transKey}${occurTime >= 2 ? occurTime : ''}`)
) {
occurTime++;
}
if (occurTime >= 2) {
transKey = `${transKey}${occurTime}`;
}
virtualMemory[_text] = transKey;
finalLangObj[transKey] = _text;
return prev.concat({
target: curr,
key: transKey,
needWrite: true
});
} else {
return prev.concat({
target: curr,
key: virtualMemory[_text],
needWrite: true
});
}
}, []);
return replaceableStrs;
}
/**
*
* @param {dirPath}
*/
function extractAll({ dirPath, prefix }: { dirPath?: string; prefix?: string }) {
const dir = dirPath || './';
// 去除I18N
const langsPrefix = prefix ? prefix.replace(/^I18N\./, '') : null;
// 翻译源配置错误,则终止
const origin = CONFIG.defaultTranslateKeyApi || 'Pinyin';
if (!['Pinyin', 'Google', 'Baidu'].includes(CONFIG.defaultTranslateKeyApi)) {
console.log(
`Kiwi 仅支持 ${highlightText('Pinyin、Google、Baidu')},请修改 ${highlightText('defaultTranslateKeyApi')} 配置项`
);
return;
}
const allTargetStrs = findAllChineseText(dir);
if (allTargetStrs.length === 0) {
console.log(highlightText('没有发现可替换的文案!'));
return;
}
// 提示翻译源
if (CONFIG.defaultTranslateKeyApi === 'Pinyin') {
console.log(
`当前使用 ${highlightText('Pinyin')} 作为key值的翻译源若想得到更好的体验可配置 ${highlightText(
'googleApiKey'
)} ${highlightText('baiduApiKey')} ${highlightText('defaultTranslateKeyApi')}`
);
} else {
console.log(`当前使用 ${highlightText(CONFIG.defaultTranslateKeyApi)} 作为key值的翻译源`);
}
console.log('即将截取每个中文文案的前5位翻译生成key值并替换中...');
// 对当前文件进行文案key生成和替换
const generateKeyAndReplace = async item => {
const currentFilename = item.file;
console.log(`${currentFilename} 替换中...`);
// 过滤掉模板字符串内的中文,避免替换时出现异常
const targetStrs = item.texts.reduce((pre, strObj, i) => {
// 因为文案已经根据位置倒排,所以比较时只需要比较剩下的文案即可
const afterStrs = item.texts.slice(i + 1);
if (afterStrs.some(obj => strObj.range.end <= obj.range.end)) {
return pre;
}
return pre.concat(strObj);
}, []);
const len = item.texts.length - targetStrs.length;
if (len > 0) {
console.log(colors.red(`存在 ${highlightText(len)} 处文案无法替换,请避免在模板字符串的变量中嵌套中文`));
}
let translateTexts;
if (origin !== 'Google') {
// 翻译中文文案百度和pinyin将文案进行拼接统一翻译
const delimiter = origin === 'Baidu' ? '\n' : '$';
const translateOriginTexts = targetStrs.reduce((prev, curr, i) => {
const transOriginText = getTransOriginText(curr.text);
if (i === 0) {
return transOriginText;
}
return `${prev}${delimiter}${transOriginText}`;
}, []);
translateTexts = await translateKeyText(translateOriginTexts, origin);
} else {
// google并发性较好且未找到有效的分隔符故仍然逐个文案进行翻译
const translatePromises = targetStrs.reduce((prev, curr) => {
const transOriginText = getTransOriginText(curr.text);
return prev.concat(translateText(transOriginText, 'en_US'));
}, []);
[...translateTexts] = await Promise.all(translatePromises);
}
if (translateTexts.length === 0) {
failInfo(`未得到翻译结果,${currentFilename}替换失败!`);
return;
}
const replaceableStrs = getReplaceableStrs(currentFilename, langsPrefix, translateTexts, targetStrs);
await replaceableStrs
.reduce((prev, obj) => {
return prev.then(() => {
return replaceAndUpdate(currentFilename, obj.target, `I18N.${obj.key}`, false, obj.needWrite);
});
}, Promise.resolve())
.then(() => {
// 添加 import I18N
if (!hasImportI18N(currentFilename)) {
const code = createImportI18N(currentFilename);
writeFile(currentFilename, code);
}
successInfo(`${currentFilename} 替换完成,共替换 ${targetStrs.length} 处文案!`);
})
.catch(e => {
failInfo(e.message);
});
};
allTargetStrs
.reduce((prev, current) => {
return prev.then(() => {
return generateKeyAndReplace(current);
});
}, Promise.resolve())
.then(() => {
successInfo('全部替换完成!');
})
.catch((e: any) => {
failInfo(e.message);
});
}
export { extractAll };

78
src/extract/file.ts Normal file
View File

@ -0,0 +1,78 @@
/**
* @author doubledream
* @desc
*/
import * as path from 'path';
import * as _ from 'lodash';
import * as fs from 'fs';
/**
*
* @function getSpecifiedFiles
* @param {string} dir
* @param {ignoreDirectory} {ignoreFile}
*/
function getSpecifiedFiles(dir, ignoreDirectory = '', ignoreFile = '') {
return fs.readdirSync(dir).reduce((files, file) => {
const name = path.join(dir, file);
const isDirectory = fs.statSync(name).isDirectory();
const isFile = fs.statSync(name).isFile();
if (isDirectory) {
return files.concat(getSpecifiedFiles(name, ignoreDirectory, ignoreFile));
}
const isIgnoreDirectory =
!ignoreDirectory ||
(ignoreDirectory &&
!path
.dirname(name)
.split('/')
.includes(ignoreDirectory));
const isIgnoreFile = !ignoreFile || (ignoreFile && path.basename(name) !== ignoreFile);
if (isFile && isIgnoreDirectory && isIgnoreFile) {
return files.concat(name);
}
return files;
}, []);
}
/**
*
* @param fileName
*/
function readFile(fileName) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf-8');
}
}
/**
*
* @param fileName
*/
function writeFile(filePath, file) {
if (fs.existsSync(filePath)) {
fs.writeFileSync(filePath, file);
}
}
/**
*
* @param path
*/
function isFile(path) {
return fs.statSync(path).isFile();
}
/**
*
* @param path
*/
function isDirectory(path) {
return fs.statSync(path).isDirectory();
}
export { getSpecifiedFiles, readFile, writeFile, isFile, isDirectory };

View File

@ -0,0 +1,421 @@
/**
* @author doubledream
* @desc Ast
*/
import * as ts from 'typescript';
import * as compiler from '@angular/compiler';
import * as compilerVue from 'vue-template-compiler';
import * as babel from '@babel/core';
/** unicode cjk 中日韩文 范围 */
const DOUBLE_BYTE_REGEX = /[\u4E00-\u9FFF]/g;
function transerI18n(code, filename, lang) {
if (lang === 'ts') {
return typescriptI18n(code, filename);
} else {
return javascriptI18n(code, filename);
}
}
function javascriptI18n(code, filename) {
let arr = [];
let visitor = {
StringLiteral(path) {
if (path.node.value.match(DOUBLE_BYTE_REGEX)) {
arr.push(path.node.value);
}
}
};
let arrayPlugin = { visitor };
babel.transformSync(code.toString(), {
filename,
plugins: [arrayPlugin]
});
return arr;
}
function typescriptI18n(code, fileName) {
let arr = [];
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TS);
function visit(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral: {
/** 判断 Ts 中的字符串含有中文 */
const { text } = node as ts.StringLiteral;
if (text.match(DOUBLE_BYTE_REGEX)) {
arr.push(text);
}
break;
}
}
ts.forEachChild(node, visit);
}
ts.forEachChild(ast, visit);
return arr;
}
/**
*
* @param code
* @param fileName
*/
function removeFileComment(code, fileName) {
const printer = ts.createPrinter({ removeComments: true });
const sourceFile = ts.createSourceFile(
'',
code,
ts.ScriptTarget.ES2015,
true,
fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
);
return printer.printFile(sourceFile);
}
/**
* Ts
* @param code
*/
function findTextInTs(code: string, fileName: string) {
const matches = [];
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TSX);
function visit(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral: {
/** 判断 Ts 中的字符串含有中文 */
const { text } = node as ts.StringLiteral;
if (text.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
const range = { start, end };
matches.push({
range,
text,
isString: true
});
}
break;
}
case ts.SyntaxKind.JsxElement: {
const { children } = node as ts.JsxElement;
children.forEach(child => {
if (child.kind === ts.SyntaxKind.JsxText) {
const text = child.getText();
/** 修复注释含有中文的情况Angular 文件错误的 Ast 情况 */
const noCommentText = removeFileComment(text, fileName);
if (noCommentText.match(DOUBLE_BYTE_REGEX)) {
const start = child.getStart();
const end = child.getEnd();
const range = { start, end };
matches.push({
range,
text: text.trim(),
isString: false
});
}
}
});
break;
}
case ts.SyntaxKind.TemplateExpression: {
const { pos, end } = node;
const templateContent = code.slice(pos, end);
if (templateContent.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
const range = { start, end };
matches.push({
range,
text: code.slice(start + 1, end - 1),
isString: true
});
}
break;
}
case ts.SyntaxKind.NoSubstitutionTemplateLiteral: {
const { pos, end } = node;
const templateContent = code.slice(pos, end);
if (templateContent.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
const range = { start, end };
matches.push({
range,
text: code.slice(start + 1, end - 1),
isString: true
});
}
}
}
ts.forEachChild(node, visit);
}
ts.forEachChild(ast, visit);
return matches;
}
/**
* HTML
* @param code
*/
function findTextInHtml(code) {
const matches = [];
const ast = compiler.parseTemplate(code, 'ast.html', {
preserveWhitespaces: false
});
function visit(node) {
const value = node.value;
if (value && typeof value === 'string' && value.match(DOUBLE_BYTE_REGEX)) {
const valueSpan = node.valueSpan || node.sourceSpan;
let {
start: { offset: startOffset },
end: { offset: endOffset }
} = valueSpan;
const nodeValue = code.slice(startOffset, endOffset);
let isString = false;
/** 处理带引号的情况 */
if (nodeValue.charAt(0) === '"' || nodeValue.charAt(0) === "'") {
isString = true;
}
const range = { start: startOffset, end: endOffset };
matches.push({
range,
text: value,
isString
});
} else if (value && typeof value === 'object' && value.source && value.source.match(DOUBLE_BYTE_REGEX)) {
/**
* <span>{{expression}}</span>
*/
const chineseMatches = value.source.match(DOUBLE_BYTE_REGEX);
chineseMatches.map(match => {
const valueSpan = node.valueSpan || node.sourceSpan;
let {
start: { offset: startOffset },
end: { offset: endOffset }
} = valueSpan;
const nodeValue = code.slice(startOffset, endOffset);
const start = nodeValue.indexOf(match);
const end = start + match.length;
const range = { start, end };
matches.push({
range,
text: match[0],
isString: false
});
});
}
if (node.children && node.children.length) {
node.children.forEach(visit);
}
if (node.attributes && node.attributes.length) {
node.attributes.forEach(visit);
}
}
if (ast.nodes && ast.nodes.length) {
ast.nodes.forEach(visit);
}
return matches;
}
/**
* vue代码的中文
* @param code
*/
function findTextInVue(code: string) {
let rexspace1 = new RegExp(/&ensp;/, 'g');
let rexspace2 = new RegExp(/&emsp;/, 'g');
let rexspace3 = new RegExp(/&nbsp;/, 'g');
code = code
.replace(rexspace1, 'ccsp&;')
.replace(rexspace2, 'ecsp&;')
.replace(rexspace3, 'ncsp&;');
let coverRex1 = new RegExp(/ccsp&;/, 'g');
let coverRex2 = new RegExp(/ecsp&;/, 'g');
let coverRex3 = new RegExp(/ncsp&;/, 'g');
let matches = [];
var result;
const vueObejct = compilerVue.compile(code.toString(), { outputSourceRange: true });
let vueAst = vueObejct.ast;
let expressTemp = findVueText(vueAst);
expressTemp.forEach(item => {
item.arrf = [item.start, item.end];
});
matches = expressTemp;
let outcode = vueObejct.render.toString().replace('with(this)', 'function a()');
let vueTemp = transerI18n(outcode, 'as.vue', null);
/**删除所有的html中的头部空格 */
vueTemp = vueTemp.map(item => {
return item.trim();
});
vueTemp = Array.from(new Set(vueTemp));
let codeStaticArr = [];
vueObejct.staticRenderFns.forEach(item => {
let childcode = item.toString().replace('with(this)', 'function a()');
let vueTempChild = transerI18n(childcode, 'as.vue', null);
codeStaticArr = codeStaticArr.concat(Array.from(new Set(vueTempChild)));
});
vueTemp = Array.from(new Set(codeStaticArr.concat(vueTemp)));
vueTemp.forEach(item => {
let items = item
.replace(/\{/g, '\\{')
.replace(/\}/g, '\\}')
.replace(/\$/g, '\\$')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
.replace(/\+/g, '\\+')
.replace(/\*/g, '\\*')
.replace(/\^/g, '\\^');
let rex = new RegExp(items, 'g');
let codeTemplate = code.substring((vueObejct.ast as any).start, (vueObejct.ast as any).end);
while ((result = rex.exec(codeTemplate))) {
let res = result;
let last = rex.lastIndex;
last = last - (res[0].length - res[0].trimRight().length);
const range = { start: res.index, end: last };
matches.push({
arrf: [res.index, last],
range,
text: res[0]
.trimRight()
.replace(coverRex1, '&ensp;')
.replace(coverRex2, '&emsp;')
.replace(coverRex3, '&nbsp;'),
isString:
(codeTemplate.substr(res.index - 1, 1) === '"' && codeTemplate.substr(last, 1) === '"') ||
(codeTemplate.substr(res.index - 1, 1) === "'" && codeTemplate.substr(last, 1) === "'")
? true
: false
});
}
});
let matchesTemp = matches;
let matchesTempResult = matchesTemp.filter((item, index) => {
let canBe = true;
matchesTemp.forEach(items => {
if (
(item.arrf[0] > items.arrf[0] && item.arrf[1] <= items.arrf[1]) ||
(item.arrf[0] >= items.arrf[0] && item.arrf[1] < items.arrf[1]) ||
(item.arrf[0] > items.arrf[0] && item.arrf[1] < items.arrf[1])
) {
canBe = false;
}
});
if (canBe) return item;
});
const sfc = compilerVue.parseComponent(code.toString());
return matchesTempResult.concat(findTextInVueTs(sfc.script.content, 'AS', sfc.script.start));
}
function findTextInVueTs(code: string, fileName: string, startNum: number) {
const matches = [];
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TS);
function visit(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral: {
/** 判断 Ts 中的字符串含有中文 */
const { text } = node as ts.StringLiteral;
if (text.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
/** 加一,减一的原因是,去除引号 */
const range = { start: start + startNum, end: end + startNum };
matches.push({
range,
text,
isString: true
});
}
break;
}
case ts.SyntaxKind.TemplateExpression: {
const { pos, end } = node;
let templateContent = code.slice(pos, end);
templateContent = templateContent.toString().replace(/\$\{[^\}]+\}/, '');
if (templateContent.match(DOUBLE_BYTE_REGEX)) {
const start = node.getStart();
const end = node.getEnd();
/** 加一,减一的原因是,去除`号 */
const range = { start: start + startNum, end: end + startNum };
matches.push({
range,
text: code.slice(start + 1, end - 1),
isString: true
});
}
break;
}
}
ts.forEachChild(node, visit);
}
ts.forEachChild(ast, visit);
return matches;
}
function findVueText(ast) {
let arr = [];
const regex1 = /\`(.+?)\`/g;
function emun(ast) {
if (ast.expression) {
let text = ast.expression.match(regex1);
if (text && text[0].match(DOUBLE_BYTE_REGEX)) {
text.forEach(itemText => {
const varInStr = itemText.match(/(\$\{[^\}]+?\})/g);
if (varInStr)
itemText.match(DOUBLE_BYTE_REGEX) &&
arr.push({ text: ' ' + itemText, range: { start: ast.start + 2, end: ast.end - 2 }, isString: true });
else
itemText.match(DOUBLE_BYTE_REGEX) &&
arr.push({ text: itemText, range: { start: ast.start, end: ast.end }, isString: false });
});
} else {
ast.tokens &&
ast.tokens.forEach(element => {
if (typeof element === 'string' && element.match(DOUBLE_BYTE_REGEX)) {
arr.push({
text: element,
range: {
start: ast.start + ast.text.indexOf(element),
end: ast.start + ast.text.indexOf(element) + element.length
},
isString: false
});
}
});
}
} else if (!ast.expression && ast.text) {
ast.text.match(DOUBLE_BYTE_REGEX) &&
arr.push({ text: ast.text, range: { start: ast.start, end: ast.end }, isString: false });
} else {
ast.children &&
ast.children.forEach(item => {
emun(item);
});
}
}
emun(ast);
return arr;
}
/**
*
* @param code
*/
function findChineseText(code: string, fileName: string) {
if (fileName.endsWith('.html')) {
return findTextInHtml(code);
} else if (fileName.endsWith('.vue')) {
return findTextInVue(code);
} else {
return findTextInTs(code, fileName);
}
}
export { findChineseText, findTextInVue };

View File

@ -0,0 +1,78 @@
/**
* @author doubledream
* @desc
*/
import * as globby from 'globby';
import * as fs from 'fs';
import * as path from 'path';
import { getProjectConfig, flatten } from '../utils';
const CONFIG = getProjectConfig();
const LANG_DIR = path.resolve(CONFIG.canaryDir, CONFIG.srcLang);
const I18N_GLOB = `${LANG_DIR}/**/*.ts`;
/**
*
*/
function getLangData(fileName) {
if (fs.existsSync(fileName)) {
return getLangJson(fileName);
} else {
return {};
}
}
/**
* Json
*/
function getLangJson(fileName) {
const fileContent = fs.readFileSync(fileName, { encoding: 'utf8' });
let obj = fileContent.match(/export\s*default\s*({[\s\S]+);?$/)[1];
obj = obj.replace(/\s*;\s*$/, '');
let jsObj = {};
try {
jsObj = eval('(' + obj + ')');
} catch (err) {
console.log(obj);
console.error(err);
}
return jsObj;
}
function getI18N() {
const paths = globby.sync(I18N_GLOB);
const langObj = paths.reduce((prev, curr) => {
const filename = curr
.split('/')
.pop()
.replace(/\.tsx?$/, '');
if (filename.replace(/\.tsx?/, '') === 'index') {
return prev;
}
const fileContent = getLangData(curr);
let jsObj = fileContent;
if (Object.keys(jsObj).length === 0) {
console.log(`\`${curr}\` 解析失败,该文件包含的文案无法自动补全`);
}
return {
...prev,
[filename]: jsObj
};
}, {});
return langObj;
}
/**
* ,
*/
function getSuggestLangObj() {
const langObj = getI18N();
const finalLangObj = flatten(langObj);
return finalLangObj;
}
export { getSuggestLangObj, getLangData };

239
src/extract/replace.ts Normal file
View File

@ -0,0 +1,239 @@
/**
* @author doubledream
* @desc
*/
import * as fs from 'fs-extra';
import * as _ from 'lodash';
import * as prettier from 'prettier';
import * as ts from 'typescript';
import { readFile, writeFile } from './file';
import { getLangData } from './getLangData';
import { getProjectConfig, getLangDir, successInfo, failInfo, highlightText } from '../utils';
const CONFIG = getProjectConfig();
const srcLangDir = getLangDir(CONFIG.srcLang);
function updateLangFiles(keyValue, text, validateDuplicate) {
if (!_.startsWith(keyValue, 'I18N.')) {
return;
}
const [, filename, ...restPath] = keyValue.split('.');
const fullKey = restPath.join('.');
const targetFilename = `${srcLangDir}/${filename}.ts`;
if (!fs.existsSync(targetFilename)) {
fs.writeFileSync(targetFilename, generateNewLangFile(fullKey, text));
addImportToMainLangFile(filename);
successInfo(`成功新建语言文件 ${targetFilename}`);
} else {
// 清除 require 缓存,解决手动更新语言文件后再自动抽取,导致之前更新失效的问题
const mainContent = getLangData(targetFilename);
const obj = mainContent;
if (Object.keys(obj).length === 0) {
failInfo(`${filename} 解析失败,该文件包含的文案无法自动补全`);
}
if (validateDuplicate && _.get(obj, fullKey) !== undefined) {
failInfo(`${targetFilename} 中已存在 key 为 \`${fullKey}\` 的翻译,请重新命名变量`);
throw new Error('duplicate');
}
// \n 会被自动转义成 \\n这里转回来
text = text.replace(/\\n/gm, '\n');
_.set(obj, fullKey, text);
fs.writeFileSync(targetFilename, prettierFile(`export default ${JSON.stringify(obj, null, 2)}`));
}
}
/**
* 使 Prettier
* @param fileContent
*/
function prettierFile(fileContent) {
try {
return prettier.format(fileContent, {
parser: 'typescript',
trailingComma: 'all',
singleQuote: true
});
} catch (e) {
failInfo(`代码格式化报错!${e.toString()}\n代码为${fileContent}`);
return fileContent;
}
}
function generateNewLangFile(key, value) {
const obj = _.set({}, key, value);
return prettierFile(`export default ${JSON.stringify(obj, null, 2)}`);
}
function addImportToMainLangFile(newFilename) {
let mainContent = '';
if (fs.existsSync(`${srcLangDir}/index.ts`)) {
mainContent = fs.readFileSync(`${srcLangDir}/index.ts`, 'utf8');
mainContent = mainContent.replace(/^(\s*import.*?;)$/m, `$1\nimport ${newFilename} from './${newFilename}';`);
if (/(}\);)/.test(mainContent)) {
if (/\,\n(}\);)/.test(mainContent)) {
/** 最后一行包含,号 */
mainContent = mainContent.replace(/(}\);)/, ` ${newFilename},\n$1`);
} else {
/** 最后一行不包含,号 */
mainContent = mainContent.replace(/\n(}\);)/, `,\n ${newFilename},\n$1`);
}
}
// 兼容 export default { common };的写法
if (/(};)/.test(mainContent)) {
if (/\,\n(};)/.test(mainContent)) {
/** 最后一行包含,号 */
mainContent = mainContent.replace(/(};)/, ` ${newFilename},\n$1`);
} else {
/** 最后一行不包含,号 */
mainContent = mainContent.replace(/\n(};)/, `,\n ${newFilename},\n$1`);
}
}
} else {
mainContent = `import ${newFilename} from './${newFilename}';\n\nexport default Object.assign({}, {\n ${newFilename},\n});`;
}
fs.writeFileSync(`${srcLangDir}/index.ts`, mainContent);
}
/**
* import I18N
* @param filePath
*/
function hasImportI18N(filePath) {
const code = readFile(filePath);
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TSX);
let hasImportI18N = false;
function visit(node) {
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
const importClause = node.importClause;
// import I18N from 'src/utils/I18N';
if (_.get(importClause, 'kind') === ts.SyntaxKind.ImportClause) {
if (importClause.name) {
if (importClause.name.escapedText === 'I18N') {
hasImportI18N = true;
}
} else {
const namedBindings = importClause.namedBindings;
// import { I18N } from 'src/utils/I18N';
if (namedBindings.kind === ts.SyntaxKind.NamedImports) {
namedBindings.elements.forEach(element => {
if (element.kind === ts.SyntaxKind.ImportSpecifier && _.get(element, 'name.escapedText') === 'I18N') {
hasImportI18N = true;
}
});
}
// import * as I18N from 'src/utils/I18N';
if (namedBindings.kind === ts.SyntaxKind.NamespaceImport) {
if (_.get(namedBindings, 'name.escapedText') === 'I18N') {
hasImportI18N = true;
}
}
}
}
}
}
ts.forEachChild(ast, visit);
return hasImportI18N;
}
/**
* import I18N
* @param filePath
*/
function createImportI18N(filePath) {
const code = readFile(filePath);
const ast = ts.createSourceFile('', code, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TSX);
const isTsFile = _.endsWith(filePath, '.ts');
const isJsFile = _.endsWith(filePath, '.js');
const isTsxFile = _.endsWith(filePath, '.tsx');
const isVueFile = _.endsWith(filePath, '.vue');
if (isTsFile || isTsxFile || isJsFile) {
const importStatement = `${CONFIG.importI18N}\n`;
const pos = ast.getStart(ast, false);
const updateCode = code.slice(0, pos) + importStatement + code.slice(pos);
return updateCode;
} else if (isVueFile) {
const importStatement = `${CONFIG.importI18N}\n`;
const updateCode = code.replace(/<script>/g, `<script>\n${importStatement}`);
return updateCode;
}
}
/**
*
* @param filePath
* @param arg
* @param val key
* @param validateDuplicate key
* @param needWrite langs
*/
function replaceAndUpdate(filePath, arg, val, validateDuplicate, needWrite = true) {
const code = readFile(filePath);
const isHtmlFile = _.endsWith(filePath, '.html');
const isVueFile = _.endsWith(filePath, '.vue');
let newCode = code;
let finalReplaceText = arg.text;
const { start, end } = arg.range;
// 若是字符串,删掉两侧的引号
if (arg.isString) {
// 如果引号左侧是 等号,则可能是 jsx 的 props此时要替换成 {
const preTextStart = start - 1;
const [last2Char, last1Char] = code.slice(preTextStart, start + 1).split('');
let finalReplaceVal = val;
if (last2Char === '=') {
if (isHtmlFile) {
finalReplaceVal = '{{' + val + '}}';
} else if (isVueFile) {
finalReplaceVal = '{{' + val + '}}';
} else {
finalReplaceVal = '{' + val + '}';
}
}
// 若是模板字符串,看看其中是否包含变量
if (last1Char === '`') {
const varInStr = arg.text.match(/(\$\{[^\}]+?\})/g);
if (varInStr) {
const kvPair = varInStr.map((str, index) => {
return `val${index + 1}: ${str.replace(/^\${([^\}]+)\}$/, '$1')}`;
});
finalReplaceVal = `I18N.template(${val}, { ${kvPair.join(',\n')} })`;
varInStr.forEach((str, index) => {
finalReplaceText = finalReplaceText.replace(str, `{val${index + 1}}`);
});
}
}
newCode = `${code.slice(0, start)}${finalReplaceVal}${code.slice(end)}`;
} else {
if (isHtmlFile || isVueFile) {
newCode = `${code.slice(0, start)}{{${val}}}${code.slice(end)}`;
} else {
newCode = `${code.slice(0, start)}{${val}}${code.slice(end)}`;
}
}
try {
if (needWrite) {
// 更新语言文件
updateLangFiles(val, finalReplaceText, validateDuplicate);
}
// 若更新成功再替换代码
return writeFile(filePath, newCode);
} catch (e) {
return Promise.reject(e.message);
}
}
export { replaceAndUpdate, hasImportI18N, createImportI18N };

75
src/import.ts Normal file
View File

@ -0,0 +1,75 @@
/**
* @author linhuiw
* @desc
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
import * as fs from 'fs';
import * as path from 'path';
import * as _ from 'lodash';
import { tsvParseRows } from 'd3-dsv';
import { getAllMessages, getProjectConfig, traverse } from './utils';
const CONFIG = getProjectConfig();
function getMessagesToImport(file: string) {
const content = fs.readFileSync(file).toString();
const messages = tsvParseRows(content, ([key, value]) => {
try {
// value 的形式和 JSON 中的字符串值一致,其中的特殊字符是以转义形式存在的,
// 如换行符 \n在 value 中占两个字符,需要转成真正的换行符。
value = JSON.parse(`"${value}"`);
} catch (e) {
throw new Error(`Illegal message: ${value}`);
}
return [key, value];
});
const rst = {};
const duplicateKeys = new Set();
messages.forEach(([key, value]) => {
if (rst.hasOwnProperty(key)) {
duplicateKeys.add(key);
}
rst[key] = value;
});
if (duplicateKeys.size > 0) {
const errorMessage = 'Duplicate messages detected: \n' + [...duplicateKeys].join('\n');
console.error(errorMessage);
process.exit(1);
}
return rst;
}
function writeMessagesToFile(messages: any, file: string, lang: string) {
const kiwiDir = CONFIG.canaryDir;
const srcMessages = require(path.resolve(kiwiDir, CONFIG.srcLang, file)).default;
const dstFile = path.resolve(kiwiDir, lang, file);
const oldDstMessages = require(dstFile).default;
const rst = {};
traverse(srcMessages, (message, key) => {
_.setWith(rst, key, _.get(messages, key) || _.get(oldDstMessages, key), Object);
});
fs.writeFileSync(dstFile + '.ts', 'export default ' + JSON.stringify(rst, null, 2));
}
function importMessages(file: string, lang: string) {
let messagesToImport = getMessagesToImport(file);
const allMessages = getAllMessages(CONFIG.srcLang);
messagesToImport = _.pickBy(messagesToImport, (message, key) => allMessages.hasOwnProperty(key));
const keysByFiles = _.groupBy(Object.keys(messagesToImport), key => key.split('.')[0]);
const messagesByFiles = _.mapValues(keysByFiles, (keys, file) => {
const rst = {};
_.forEach(keys, key => {
_.setWith(rst, key.substr(file.length + 1), messagesToImport[key], Object);
});
return rst;
});
_.forEach(messagesByFiles, (messages, file) => {
writeMessagesToFile(messages, file, lang);
});
}
export { importMessages };

141
src/index.ts Normal file
View File

@ -0,0 +1,141 @@
#!/usr/bin/env node
import * as commander from 'commander';
import * as inquirer from 'inquirer';
import { isString } from 'lodash';
import { initProject } from './init';
import { sync } from './sync';
import { exportMessages } from './export';
import { importMessages } from './import';
import { findUnUsed } from './unused';
import { mockLangs } from './mock';
import { extractAll } from './extract/extract';
import { translate } from './translate';
import { getTranslateOriginType } from './utils';
import * as ora from 'ora';
/**
*
* @param text
* @param callback
*/
function spining(text, callback) {
const spinner = ora(`${text}中...`).start();
if (callback) {
if (callback() !== false) {
spinner.succeed(`${text}成功`);
} else {
spinner.fail(`${text}失败`);
}
}
}
commander
.version('0.2.0')
.option('--init', '初始化项目', { isDefault: true })
.option('--import [file] [lang]', '导入翻译文案')
.option('--export [file] [lang]', '导出未翻译的文案')
.option('--sync', '同步各种语言的文案')
.option('--mock', '使用 Google 或者 Baidu 翻译 输出mock文件')
.option('--translate', '使用 Google 或者 Baidu 翻译 翻译结果自动替换目标语种文案')
.option('--unused', '导出未使用的文案')
.option('--extract [dirPath]', '一键替换指定文件夹下的所有中文文案')
.option('--prefix [prefix]', '指定替换中文文案前缀')
.parse(process.argv);
if (commander.init) {
(async () => {
const result = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
default: true,
message: '项目中是否已存在kiwi相关目录'
});
if (!result.confirm) {
spining('初始化项目', async () => {
initProject();
});
} else {
const value = await inquirer.prompt({
type: 'input',
name: 'dir',
message: '请输入相关目录:'
});
spining('初始化项目', async () => {
initProject(value.dir);
});
}
})();
}
if (commander.import) {
spining('导入翻译文案', () => {
if (commander.import === true || commander.args.length === 0) {
console.log('请按格式输入:--import [file] [lang]');
return false;
} else if (commander.args) {
importMessages(commander.import, commander.args[0]);
}
});
}
if (commander.export) {
spining('导出未翻译的文案', () => {
if (commander.export === true && commander.args.length === 0) {
exportMessages();
} else if (commander.args) {
exportMessages(commander.export, commander.args[0]);
}
});
}
if (commander.sync) {
spining('文案同步', () => {
sync();
});
}
if (commander.unused) {
spining('导出未使用的文案', () => {
findUnUsed();
});
}
if (commander.mock) {
sync(async () => {
const { pass, origin } = await getTranslateOriginType();
if (pass) {
const spinner = ora(`使用 ${origin} 翻译中...`).start();
await mockLangs(origin);
spinner.succeed(`使用 ${origin} 翻译成功`);
}
});
}
if (commander.translate) {
sync(async () => {
const { pass, origin } = await getTranslateOriginType();
if (pass) {
const spinner = ora(`使用 ${origin} 翻译中...`).start();
await translate(origin);
spinner.succeed(`使用 ${origin} 翻译成功`);
}
});
}
if (commander.extract) {
console.log(isString(commander.prefix));
if (commander.prefix === true) {
console.log('请指定翻译后文案 key 值的前缀 --prefix xxxx');
} else if (isString(commander.prefix) && !new RegExp(/^I18N(\.[-_a-zA-Z1-9$]+)+$/).test(commander.prefix)) {
console.log('前缀必须以I18N开头,后续跟上字母、下滑线、破折号、$ 字符组成的变量名');
} else {
const extractAllParams = {
prefix: isString(commander.prefix) && commander.prefix,
dirPath: isString(commander.extract) && commander.extract
};
extractAll(extractAllParams);
}
}

66
src/init.ts Normal file
View File

@ -0,0 +1,66 @@
/**
* @author zhaoyingbo
* @desc kiwi
*/
import * as _ from 'lodash';
import * as path from 'path';
import * as fs from 'fs';
import { PROJECT_CONFIG, CANARY_CONFIG_FILE } from './const';
/**
*
* @param existDir
* @returns
*/
function createConfigFile(existDir?: string) {
// 在根目录创建配置文件
const configDir = path.resolve(process.cwd(), `./${CANARY_CONFIG_FILE}`);
// 如果已经有配置文件就不要动了
if (fs.existsSync(configDir)) return;
const config = { ...PROJECT_CONFIG.defaultConfig };
// 有配置文件夹
if (existDir && fs.existsSync(existDir)) {
config.canaryDir = existDir;
}
// 创建新的配置文件
fs.writeFile(configDir, JSON.stringify(config, null, 2), err => err && console.log(err));
}
/**
*
*/
function createCnFile() {
// 中文配置文件夹地址
const cnDir = `${PROJECT_CONFIG.dir}/zh-CN`;
// 没有则创建
if (!fs.existsSync(cnDir)) {
fs.mkdirSync(cnDir);
fs.writeFile(`${cnDir}/index.ts`, PROJECT_CONFIG.zhIndexFile, err => err && console.log(err));
fs.writeFile(`${cnDir}/common.ts`, PROJECT_CONFIG.zhTestFile, err => err && console.log(err));
}
}
/**
*
* @param existDir
*/
function initProject(existDir?: string) {
// 有用户输入的文件夹,不存在默认位置创建
if (existDir && !fs.existsSync(existDir)) {
console.log('\n输入的目录不存在已为你生成默认文件夹');
fs.mkdirSync(PROJECT_CONFIG.dir);
}
// 没有输入,默认位置创建
else if (!existDir && !fs.existsSync(PROJECT_CONFIG.dir)) {
fs.mkdirSync(PROJECT_CONFIG.dir);
}
// 创建配置文件
createConfigFile(existDir);
// 没有已经存在的配置文件夹,创建默认的配置文件示例
if (!(existDir && fs.existsSync(existDir))) {
createCnFile();
}
}
export { initProject };

115
src/mock.ts Normal file
View File

@ -0,0 +1,115 @@
/**
* @author linhuiw
* @desc
* @TODO: index mock
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
import * as path from 'path';
import * as fs from 'fs';
import * as _ from 'lodash';
import { traverse, getProjectConfig, getLangDir, translateText } from './utils';
import { baiduTranslateTexts, googleTranslateTexts } from './translate';
const CONFIG = getProjectConfig();
/**
*
*/
function getSourceText() {
const srcLangDir = getLangDir(CONFIG.srcLang);
const srcFile = path.resolve(srcLangDir, 'index.ts');
const { default: texts } = require(srcFile);
return texts;
}
/**
*
* @param dstLang
*/
function getDistText(dstLang) {
const distLangDir = getLangDir(dstLang);
const distFile = path.resolve(distLangDir, 'index.ts');
let distTexts = {};
if (fs.existsSync(distFile)) {
distTexts = require(distFile).default;
}
return distTexts;
}
/**
*
* @param
*/
function getAllUntranslatedTexts(toLang) {
const texts = getSourceText();
const distTexts = getDistText(toLang);
const untranslatedTexts = {};
/** 遍历文案 */
traverse(texts, (text, path) => {
const distText = _.get(distTexts, path);
if (text === distText || !distText) {
untranslatedTexts[path] = text;
}
});
return untranslatedTexts;
}
/**
* Mock
* @param dstLang
*/
async function mockCurrentLang(dstLang: string, origin: string) {
const untranslatedTexts = getAllUntranslatedTexts(dstLang);
let mocks = {};
if (origin === 'Google') {
mocks = await googleTranslateTexts(untranslatedTexts, dstLang);
} else {
mocks = await baiduTranslateTexts(untranslatedTexts, dstLang);
}
/** 所有任务执行完毕后写入mock文件 */
return writeMockFile(dstLang, mocks);
}
/**
* Mock
* @param dstLang
* @param mocks
*/
function writeMockFile(dstLang, mocks) {
const fileContent = 'export default ' + JSON.stringify(mocks, null, 2);
const filePath = path.resolve(getLangDir(dstLang), 'mock.ts');
return new Promise((resolve, reject) => {
fs.writeFile(filePath, fileContent, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
/**
* Mock
* @param lang
*/
async function mockLangs(origin: string) {
const langs = CONFIG.distLangs;
if (origin === 'Google') {
const mockPromise = langs.map(lang => {
return mockCurrentLang(lang, origin);
});
return Promise.all(mockPromise);
} else {
for (var i = 0; i < langs.length; i++) {
await mockCurrentLang(langs[i], origin);
}
return Promise.resolve();
}
}
export { mockLangs, getAllUntranslatedTexts };

123
src/sync.ts Normal file
View File

@ -0,0 +1,123 @@
/**
* @author linhuiw
* @desc
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
import * as fs from 'fs';
import * as path from 'path';
import * as _ from 'lodash';
import { traverse, getProjectConfig, getLangDir } from './utils';
const CONFIG = getProjectConfig();
/**
* 使使 google
* */
function getTranslations(file, toLang) {
const translations = {};
const fileNameWithoutExt = path.basename(file).split('.')[0];
const srcLangDir = getLangDir(CONFIG.srcLang);
const distLangDir = getLangDir(toLang);
const srcFile = path.resolve(srcLangDir, file);
const distFile = path.resolve(distLangDir, file);
const { default: texts } = require(srcFile);
let distTexts;
if (fs.existsSync(distFile)) {
distTexts = require(distFile).default;
}
traverse(texts, (text, path) => {
const key = fileNameWithoutExt + '.' + path;
const distText = _.get(distTexts, path);
translations[key] = distText || text;
});
return translations;
}
/**
*
* */
function writeTranslations(file, toLang, translations) {
const fileNameWithoutExt = path.basename(file).split('.')[0];
const srcLangDir = getLangDir(CONFIG.srcLang);
const srcFile = path.resolve(srcLangDir, file);
const { default: texts } = require(srcFile);
const rst = {};
traverse(texts, (text, path) => {
const key = fileNameWithoutExt + '.' + path;
// 使用 setWith 而不是 set保证 numeric key 创建的不是数组,而是对象
// https://github.com/lodash/lodash/issues/1316#issuecomment-120753100
_.setWith(rst, path, translations[key], Object);
});
const fileContent = 'export default ' + JSON.stringify(rst, null, 2);
const filePath = path.resolve(getLangDir(toLang), path.basename(file));
return new Promise((resolve, reject) => {
fs.writeFile(filePath, fileContent, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
/**
*
* @param file
* @param toLang
*/
function translateFile(file, toLang) {
const translations = getTranslations(file, toLang);
const toLangDir = path.resolve(__dirname, `../${toLang}`);
if (!fs.existsSync(toLangDir)) {
fs.mkdirSync(toLangDir);
}
writeTranslations(file, toLang, translations);
}
/**
*
*/
function sync(callback?) {
const srcLangDir = getLangDir(CONFIG.srcLang);
fs.readdir(srcLangDir, (err, files) => {
if (err) {
console.error(err);
} else {
files = files.filter(file => file.endsWith('.ts') && file !== 'index.ts' && file !== 'mock.ts').map(file => file);
const translateFiles = toLang =>
Promise.all(
files.map(file => {
translateFile(file, toLang);
})
);
Promise.all(CONFIG.distLangs.map(translateFiles)).then(
() => {
const langDirs = CONFIG.distLangs.map(getLangDir);
langDirs.map(dir => {
const filePath = path.resolve(dir, 'index.ts');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
fs.copyFileSync(path.resolve(srcLangDir, 'index.ts'), filePath);
});
callback && callback();
},
e => {
console.error(e);
process.exit(1);
}
);
}
});
}
export { sync };

184
src/translate.ts Normal file
View File

@ -0,0 +1,184 @@
/**
* @author zongwenjian
* @desc translate命令
*/
require('ts-node').register({
compilerOptions: {
module: 'commonjs'
}
});
import * as path from 'path';
import * as fs from 'fs';
import * as baiduTranslate from 'baidu-translate';
import { tsvFormatRows } from 'd3-dsv';
import { getProjectConfig, getLangDir, withTimeout, translateText } from './utils';
import { importMessages } from './import';
import { getAllUntranslatedTexts } from './mock';
const CONFIG = getProjectConfig();
/**
*
* @param text
* @param toLang
*/
function translateTextByBaidu(text, toLang) {
const {
baiduApiKey: { appId, appKey },
baiduLangMap
} = CONFIG;
return withTimeout(
new Promise((resolve, reject) => {
baiduTranslate(appId, appKey, baiduLangMap[toLang], 'zh')(text)
.then(data => {
if (data && data.trans_result) {
resolve(data.trans_result);
} else {
reject(`\n百度翻译api调用异常 error_code: ${data.error_code}, error_msg: ${data.error_msg}`);
}
})
.catch(err => {
reject(err);
});
}),
3000
);
}
/** 文案首字母大小 变量小写 */
function textToUpperCaseByFirstWord(text) {
// 翻译文案首字母大写,变量小写
return text
? `${text.charAt(0).toUpperCase()}${text.slice(1)}`.replace(/(\{.*?\})/g, text => text.toLowerCase())
: '';
}
/**
* 使google翻译所有待翻译的文案
* @param untranslatedTexts
* @param toLang
*/
async function googleTranslateTexts(untranslatedTexts, toLang) {
const translateAllTexts = Object.keys(untranslatedTexts).map(key => {
return translateText(untranslatedTexts[key], toLang).then(translatedText => [key, translatedText]);
});
return new Promise(resolve => {
const result = {};
Promise.all(translateAllTexts).then(res => {
res.forEach(([key, translatedText]) => {
result[key] = translatedText;
});
resolve(result);
});
});
}
/**
* 使
* @param untranslatedTexts
* @param toLang
*/
async function baiduTranslateTexts(untranslatedTexts, toLang) {
return new Promise(async resolve => {
const result = {};
const untranslatedKeys = Object.keys(untranslatedTexts);
const taskLists = {};
let lastIndex = 0;
// 由于百度api单词翻译字符长度限制需要将待翻译的文案拆分成单个子任务
untranslatedKeys.reduce((pre, next, index) => {
const byteLen = Buffer.byteLength(pre, 'utf8');
if (byteLen > 5500) {
// 获取翻译字节数大于5500放到单独任务里面处理
taskLists[lastIndex] = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(translateTextByBaidu(pre, toLang));
}, 1500);
});
};
lastIndex = index;
return untranslatedTexts[next];
} else if (index === untranslatedKeys.length - 1) {
taskLists[lastIndex] = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(translateTextByBaidu(`${pre}\n${untranslatedTexts[next]}`, toLang));
}, 1500);
});
};
}
return `${pre}\n${untranslatedTexts[next]}`;
}, '');
// 由于百度api调用QPS只有1, 考虑网络延迟 每1.5s请求一个子任务
const taskKeys = Object.keys(taskLists);
if (taskKeys.length > 0) {
for (var i = 0; i < taskKeys.length; i++) {
const langIndexKey = taskKeys[i];
const taskItemFun = taskLists[langIndexKey];
const data = await taskItemFun();
(data || []).forEach(({ dst }, index) => {
const currTextKey = untranslatedKeys[Number(langIndexKey) + index];
result[currTextKey] = textToUpperCaseByFirstWord(dst);
});
}
}
resolve(result);
});
}
/**
*
* @param dstLang
*/
async function runTranslateApi(dstLang: string, origin: string) {
const untranslatedTexts = getAllUntranslatedTexts(dstLang);
let mocks = {};
if (origin === 'Google') {
mocks = await googleTranslateTexts(untranslatedTexts, dstLang);
} else {
mocks = await baiduTranslateTexts(untranslatedTexts, dstLang);
}
const messagesToTranslate = Object.keys(mocks).map(key => [key, mocks[key]]);
if (messagesToTranslate.length === 0) {
return Promise.resolve();
}
const content = tsvFormatRows(messagesToTranslate);
// 输出tsv文件
return new Promise((resolve, reject) => {
const filePath = path.resolve(getLangDir(dstLang), `${dstLang}_translate.tsv`);
fs.writeFile(filePath, content, err => {
if (err) {
reject(err);
} else {
console.log(`${dstLang} 自动翻译完成`);
// 自动导入翻译结果
importMessages(filePath, dstLang);
resolve();
}
});
});
}
/**
*
* @param origin
*/
async function translate(origin: string) {
const langs = CONFIG.distLangs;
if (origin === 'Google') {
const mockPromise = langs.map(lang => {
return runTranslateApi(lang, origin);
});
return Promise.all(mockPromise);
} else {
for (var i = 0; i < langs.length; i++) {
await runTranslateApi(langs[i], origin);
}
return Promise.resolve();
}
}
export { translate, baiduTranslateTexts, googleTranslateTexts };

104
src/unused.ts Normal file
View File

@ -0,0 +1,104 @@
/**
* @author linhuiw
* @desc 使 key
*/
import * as fs from 'fs';
import * as path from 'path';
import { getKiwiDir, getLangDir, traverse } from './utils';
const lookingForString = '';
function findUnUsed() {
const srcLangDir = path.resolve(getKiwiDir(), 'zh-CN');
let files = fs.readdirSync(srcLangDir);
files = files.filter(file => file.endsWith('.ts') && file !== 'index.ts');
const unUnsedKeys = [];
files.map(file => {
const srcFile = path.resolve(srcLangDir, file);
const { default: messages } = require(srcFile);
const filename = path.basename(file, '.ts');
traverse(messages, (text, path) => {
const key = `I18N.${filename}.${path}`;
const hasKey = recursiveReadFile('./src', key);
if (!hasKey) {
unUnsedKeys.push(key);
}
});
});
console.log(unUnsedKeys, 'unUnsedKeys');
}
/**
*
* @param fileName
*/
function recursiveReadFile(fileName, text) {
let hasText = false;
if (!fs.existsSync(fileName)) return;
if (isFile(fileName) && !hasText) {
check(fileName, text, () => {
hasText = true;
});
}
if (isDirectory(fileName)) {
var files = fs.readdirSync(fileName).filter(file => {
return !file.startsWith('.') && !['node_modules', 'build', 'dist'].includes(file);
});
files.forEach(function(val, key) {
var temp = path.join(fileName, val);
if (isDirectory(temp) && !hasText) {
hasText = recursiveReadFile(temp, text);
}
if (isFile(temp) && !hasText) {
check(temp, text, () => {
hasText = true;
});
}
});
}
return hasText;
}
/**
*
* @param fileName
*/
function check(fileName, text, callback) {
var data = readFile(fileName);
var exc = new RegExp(text);
if (exc.test(data)) {
callback();
}
}
/**
*
* @param fileName
*/
function isDirectory(fileName) {
if (fs.existsSync(fileName)) {
return fs.statSync(fileName).isDirectory();
}
}
/**
*
* @param fileName
*/
function isFile(fileName) {
if (fs.existsSync(fileName)) {
return fs.statSync(fileName).isFile();
}
}
/**
*
* @param fileName
*/
function readFile(fileName) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf-8');
}
}
export { findUnUsed };

315
src/utils.ts Normal file
View File

@ -0,0 +1,315 @@
/**
* @author linhuiw
* @desc
*/
import * as path from 'path';
import * as _ from 'lodash';
import * as inquirer from 'inquirer';
import * as fs from 'fs';
import { pinyin } from 'pinyin-pro';
import { PROJECT_CONFIG, CANARY_CONFIG_FILE } from './const';
const colors = require('colors');
function lookForFiles(dir: string, fileName: string): string {
const files = fs.readdirSync(dir);
for (let file of files) {
const currName = path.join(dir, file);
const info = fs.statSync(currName);
if (info.isDirectory()) {
if (file === '.git' || file === 'node_modules') {
continue;
}
const result = lookForFiles(currName, fileName);
if (result) {
return result;
}
} else if (info.isFile() && file === fileName) {
return currName;
}
}
}
/**
*
*/
function getProjectConfig() {
const configFile = path.resolve(process.cwd(), `./${CANARY_CONFIG_FILE}`);
let obj = PROJECT_CONFIG.defaultConfig;
if (configFile && fs.existsSync(configFile)) {
obj = {
...obj,
...JSON.parse(fs.readFileSync(configFile, 'utf8'))
};
}
return obj;
}
/**
*
*/
function getKiwiDir() {
const config = getProjectConfig();
if (config) {
return config.canaryDir;
}
}
/**
*
* @param lang
*/
function getLangDir(lang) {
const langsDir = getKiwiDir();
return path.resolve(langsDir, lang);
}
/**
* string
*/
function traverse(obj, cb) {
function traverseInner(obj, cb, path) {
_.forEach(obj, (val, key) => {
if (typeof val === 'string') {
cb(val, [...path, key].join('.'));
} else if (typeof val === 'object' && val !== null) {
traverseInner(val, cb, [...path, key]);
}
});
}
traverseInner(obj, cb, []);
}
/**
*
*/
function getAllMessages(lang: string, filter = (message: string, key: string) => true) {
const srcLangDir = getLangDir(lang);
let files = fs.readdirSync(srcLangDir);
files = files.filter(file => file.endsWith('.ts') && file !== 'index.ts').map(file => path.resolve(srcLangDir, file));
const allMessages = files.map(file => {
const { default: messages } = require(file);
const fileNameWithoutExt = path.basename(file).split('.')[0];
const flattenedMessages = {};
console.log(fileNameWithoutExt, messages)
traverse(messages, (message, path) => {
const key = fileNameWithoutExt + '.' + path;
if (filter(message, key)) {
flattenedMessages[key] = message;
}
});
return flattenedMessages;
});
return Object.assign({}, ...allMessages);
}
/**
*
* @param asyncOperation
* @param times
*/
function retry(asyncOperation, times = 1) {
let runTimes = 1;
const handleReject = e => {
if (runTimes++ < times) {
return asyncOperation().catch(handleReject);
} else {
throw e;
}
};
return asyncOperation().catch(handleReject);
}
/**
*
* @param promise
* @param ms
*/
function withTimeout(promise, ms) {
const timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(`Promise timed out after ${ms} ms.`);
}, ms);
});
return Promise.race([promise, timeoutPromise]);
}
/**
* 使google翻译
*/
function translateText(text, toLang) {
const CONFIG = getProjectConfig();
const options = CONFIG.translateOptions;
const { translate: googleTranslate } = require('google-translate')(CONFIG.googleApiKey, options);
return withTimeout(
new Promise((resolve, reject) => {
googleTranslate(text, 'zh', PROJECT_CONFIG.langMap[toLang], (err, translation) => {
if (err) {
reject(err);
} else {
resolve(translation.translatedText);
}
});
}),
5000
);
}
/**
*
*/
function translateKeyText(text: string, origin: string) {
const CONFIG = getProjectConfig();
const { appId, appKey } = CONFIG.baiduApiKey;
const baiduTranslate = require('baidu-translate');
function _translateText() {
return withTimeout(
new Promise((resolve, reject) => {
// Baidu
if (origin === 'Baidu') {
baiduTranslate(appId, appKey, 'en', 'zh')(text)
.then(data => {
if (data && data.trans_result) {
const result = data.trans_result.map(item => item.dst) || [];
resolve(result);
}
})
.catch(err => {
reject(err);
});
}
// Pinyin
if (origin === 'Pinyin') {
const result = pinyin(text, { toneType: 'none' });
resolve(result.split('$'));
}
}),
3000
);
}
return retry(_translateText, 3);
}
function findMatchKey(langObj, text) {
for (const key in langObj) {
if (langObj[key] === text) {
return key;
}
}
return null;
}
function findMatchValue(langObj, key) {
return langObj[key];
}
/**
*
* @param obj
* @param prefix
*/
function flatten(obj, prefix = '') {
var propName = prefix ? prefix + '.' : '',
ret = {};
for (var attribute in obj) {
var attr = attribute.replace(/-/g, '_');
if (_.isArray(obj[attr])) {
var len = obj[attr].length;
ret[attr] = obj[attr].join(',');
} else if (typeof obj[attr] === 'object') {
_.extend(ret, flatten(obj[attr], propName + attr));
} else {
ret[propName + attr] = obj[attr];
}
}
return ret;
}
/**
*
*/
async function getTranslateOriginType() {
const { googleApiKey, baiduApiKey } = getProjectConfig();
let translateType = ['Google', 'Baidu'];
if (!googleApiKey) {
translateType = translateType.filter(item => item !== 'Google');
}
if (!baiduApiKey || !baiduApiKey.appId || !baiduApiKey.appKey) {
translateType = translateType.filter(item => item !== 'Baidu');
}
if (translateType.length === 0) {
console.log('请配置 googleApiKey 或 baiduApiKey ');
return {
pass: false,
origin: ''
};
}
if (translateType.length == 1) {
return {
pass: true,
origin: translateType[0]
};
}
const { origin } = await inquirer.prompt({
type: 'list',
name: 'origin',
message: '请选择使用的翻译源',
default: 'Google',
choices: ['Google', 'Baidu']
});
return {
pass: true,
origin: origin
};
}
/**
*
*/
function successInfo(message: string) {
console.log('successInfo: ', colors.green(message));
}
/**
*
*/
function failInfo(message: string) {
console.log('failInfo: ', colors.red(message));
}
/**
*
*/
function highlightText(message: string | number) {
return colors.yellow(`${message}`);
}
export {
getKiwiDir,
getLangDir,
traverse,
retry,
withTimeout,
getAllMessages,
getProjectConfig,
translateText,
findMatchKey,
findMatchValue,
flatten,
lookForFiles,
getTranslateOriginType,
translateKeyText,
successInfo,
failInfo,
highlightText
};

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "dist",
"lib": [
"es6",
"es2016.array.include"
],
"skipLibCheck": true,
"sourceMap": true,
"rootDir": "src"
},
"exclude": [
"node_modules",
".vscode-test"
]
}