Commit a62a25d2 authored by 马旭烽's avatar 马旭烽

init: 初始版本0.0.0 命令工具

parents
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.DS_Store
# dependencies
node_modules
/npm-debug.log*
/yarn-error.log
# /yarn.lock
/package-lock.json
/output
/.idea
/config/base.config.json
/config/global.d.ts
**/services/**Api
logs
run
apps/**/locales.json
\ No newline at end of file
# 食用教程
## 安装
- npm : `npm install @linkseeks/i18-cli -D`;
- yarn `yarn add @linkseeks/i18-cli -D`;
- pnpm `pnpm install @linkseeks/i18-cli -D`;
然后项目安装环境依赖
```cmd
yarn add typescript@4.x @types/node@16.x ts-node@10.x -D
```
成功安装!
> 另外它会输出到 output 目录里,如果项目介意git 检测到变动的可以在.gitignore 忽略该文件
## 命令
importLen: 导入语言包
import -zh 中文包路径 -len 其它语言包路径 -lenType 语言类型 (前面都是必选的) -lenRatio(语言系数类型) -d 展示部分隐藏的conso
## 语言长度计算规则
以中文为基准,字符长度的系数为1
|语言类型|中文解释|长度系数|
|-|-|-|
|zhCN|中文|1|
|enUs|英文|2.5|
|koKR|韩文|0.875|
其次 中文最小计算长度为3
假设有个中文字段: '爱惜', 那么计算汉字的长度就为3,如果汉字长度为5,则为5。
然后对应的英文数目就是 汉字计算长度 / 汉字系数 * 英文系数 的结果 向上取整 = 8
## RN / 小程序
添加一条命令: `ts-node node_modules/@linkseeks/i18-cli/src/index.ts importLen -zh src/locales/zh_CN/index.ts -len src/locales/en_US/index.ts -lenType enUs -d`
## 商城端能力中心端导入
1. 命令安装需要加上 `-w`或者 `-W` 来安装依赖,这一块不是很懂
2. 注意路径要正确
# 已完成
## 0.0.0 版本
- 入口导入开发
- [*] 导入为存映射结构的入口文件,ES语法, ts, js, json,输出 翻译的json文件和对应xlsx表格
- [*] 检测不合理的长度值数组
- 额外工作
- [*] 加入丢失Key 检测
# Feature
- [ ] 命令优化,采用读取ts类型的配置文件的形式,进行执行命令 或者交互式的命令行进行工作
- [ ] 支持导出位置选项
- [ ] 支持导入多个语言包
- [ ] 字段长度增加动态字段计算(目前这个还没有加入计算)
- [ ] 配置文件初始化命令
- [ ] 自动调用远程翻译api(这个没玩过,好像要配置token这些,未来再说把)
# 目前
- 目前这个命令工具还是很粗糙。
- 目前来说这个工具开发设计层面比开发层面更重要。
# 结语
- 目前作者精力有限目前暂时也只能完成到这个步骤,如果你有什么好的想法,欢迎找作者交流。如果我还在这个公司的话,这是我的工号: 00405 。
- 这一次工具开发也开阔我了视野,在以往工作方面我已经有几次接触过编译方面的学习,但是相比于哪些可以开发出AST大神们,我还有很多路需要走。我觉得这就是一种缘分,我想要走这条路学习下去。
- 最后就到这里, 也注意爱惜自己身体,┭┮﹏┭┮。
\ No newline at end of file
# 阅读资料提供开发者和学习者快速学习和复习的地方
# 阅读资料提供开发者和学习者快速学习和复习的地方
1. commander 中文文档 https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md
2. nodeje 英文文档 https://nodejs.org/dist/latest-v16.x/docs
3. xlsx 英文文档 https://sheetjs.com/support
\ No newline at end of file
export default {
'modal': '模块'
}
\ No newline at end of file
{
"'model": "JSON"
}
\ No newline at end of file
export default {
'modal': '模块'
}
\ No newline at end of file
export default {
'modal': '模块'
}
\ No newline at end of file
{
"'model": "JSON"
}
\ No newline at end of file
export default {
'modal': 'Modal',
nomarlName: 'I like myLike, and like like me.'
}
\ No newline at end of file
export default {
'modal': '模块'
}
\ No newline at end of file
{
"'model": "JSON"
}
\ No newline at end of file
export default {
'modal': '模块',
'nomarlName': '回头已近三年,物似人非。',
'nor': '中文使者'
}
\ No newline at end of file
{
"name": "@linkseeks/i18-cli",
"version": "0.0.0",
"description": "国际化命令工具",
"main": "index.ts",
"bin": {},
"scripts": {
"test": "ts-node bin/i18-cli.ts",
"dev:comment": "启动开发环境",
"importLen-dev": "ts-node-dev --respawn --transpile-only src/index.ts importLen -zh mock/input/zhCN/V1.ts -len mock/input/en/V1.ts -lenType enUs",
"importLen": "ts-node src/index.ts importLen -zh mock/input/zhCN/V1.ts -len mock/input/en/V1.ts -lenType enUs"
},
"files": [
"bin",
"lib",
"docs"
],
"author": "maXuFeng",
"license": "ISC",
"devDependencies": {
"@types/node": "^16.18.3",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"typescript": "^4.9.3"
},
"dependencies": {
"chalk": "^5.1.2",
"clear-console": "^1.1.0",
"commander": "^9.4.1",
"inquirer": "^9.1.4",
"shelljs": "^0.8.5",
"xlsx": "^0.18.5"
}
}
export enum LanguageNameTypeEnum {
/** 中文 */
zhCN = 'zhCN',
/** 英文 */
enUS = 'enUs',
/** 韩文 */
koKR = 'koKR',
};
export type LanguageMetaTypeEnumType = Record<LanguageNameTypeEnum, {
/** 长度系数 */
LenRatio: number,
}> & {
/** 汉字最小计算长度 */
miniTextLength: number,
};
/** 语言元信息 */
export const LanguageMetaTypeEnum: LanguageMetaTypeEnumType = {
[LanguageNameTypeEnum.zhCN] : {
LenRatio: 1,
},
[LanguageNameTypeEnum.enUS]: {
LenRatio: 2.5,
},
[LanguageNameTypeEnum.koKR]: {
LenRatio: 0.875,
},
miniTextLength: 3,
};
export interface LanguageItemValueTypeV1 {
'唯一标识': string,
'翻译字段': string,
'建议长度': number,
}
export interface LanguageItemTypeV1 {
[key: string] : LanguageItemValueTypeV1
};
export interface CenterLanguageItemTypeV1 {
[key: string]: string,
}
export type SheettRowItemKey_V1 = '唯一标识'|'翻译字段'|'建议长度';
\ No newline at end of file
import { LanguageItemTypeV1, CenterLanguageItemTypeV1, LanguageMetaTypeEnum, LanguageNameTypeEnum, LanguageItemValueTypeV1 } from '../constants';
const zhCNRatio = LanguageMetaTypeEnum.zhCN.LenRatio;
export const getLenObjToMetaLenObj = (originLenObj: CenterLanguageItemTypeV1, lenRatio: number, zhCNMap?: CenterLanguageItemTypeV1): LanguageItemTypeV1 => {
return Object.entries(originLenObj).reduce((p, [key, value]) => {
p[key] = {
'唯一标识': key,
'翻译字段': value,
'建议长度': (zhCNMap && zhCNMap[key])
? Math.ceil(Math.max(zhCNMap[key].length, 3) * zhCNRatio * lenRatio)
: Math.max(value.length, 3) * zhCNRatio,
};
return p;
}, {});
};
export const getLenObjToArrayV1 = (originLenObj: LanguageItemTypeV1) => Object.values(originLenObj);
const LanguageMetaTypeEnumValueList = Object.values(LanguageNameTypeEnum);
export const getLenRatio = (lenType:LanguageNameTypeEnum | string, lenRatio?: number): number => {
if(LanguageMetaTypeEnumValueList.includes(lenType as LanguageNameTypeEnum)){
return LanguageMetaTypeEnum[lenType].LenRatio;
}
else {
if(lenRatio) return lenRatio;
throw new TypeError('未知语言长度系数')
}
};
/** 检测不达标建议长度的字段 */
export const checkLenObjToNoAllowArrayV1 = (originLenObj: LanguageItemTypeV1): LanguageItemValueTypeV1[] => {
return getLenObjToArrayV1(originLenObj).filter(item => item['翻译字段'].length > item['建议长度']);
};
\ No newline at end of file
import { program } from 'commander';
import { ImportModal } from './modal/ImportModal';
import { ExportModal } from './modal/ExportModal';
import { registerUseProgram } from './utils/commanderUtli';
import { normalizeFilePath } from './utils/fileUtil';
import { getOutputFilePath } from './utils/pathUtil';
import { checkLenObjToNoAllowArrayV1, getLenObjToMetaLenObj, getLenRatio } from './helper/i18';
import { LanguageNameTypeEnum, } from './constants';
program
.name('i18命令行工具')
.description('ssy 自定义命令工具')
.usage('<command>[option]')
.version('0.0.0');
const importModal = new ImportModal();
const exportModal = new ExportModal();
const newProgram = registerUseProgram<{}>(program, [
(program) => {
return program
.command('importLen')
.description('导入i18语言包,导出xlsx包,语言包')
.option('-zh,--zhCNPath <zhCNPath>','导入中文包路径')
.option('-len, --otherLenPath <otherLenPath>', '导入其它包语言路径')
.option('-lenType, --otherLenType <otherLenType>', '导入其它语言类型')
.option('-lenRatio, --otherLenRatio [otherLenRatio]', '导入其它语言系数,可选')
.option('-d,--debug', '测试打印部分执行路径', false)
.action(async (args, _program) => {
let { zhCNPath, otherLenPath, otherLenType, otherlenRdio: otherLenRdio, debug } = args;
debug && console.log('命令行打印参数', args)
//check is Allow Or Return
if(!(zhCNPath && otherLenPath&&otherLenType)){
throw new TypeError('导入参数缺失');
};
zhCNPath = await normalizeFilePath(zhCNPath);
otherLenPath = await normalizeFilePath(otherLenPath);
if(debug){
console.log('中文路径', zhCNPath , );
console.log('其它语言路径', otherLenPath,);
console.log('其它语言类型', otherLenPath);
}
const zhCNMap = await importModal.importFileToJSObj_V1(zhCNPath);
const otherLenMap = await importModal.importFileToJSObj_V1(otherLenPath);
const loseKeys = Object.keys(zhCNMap).filter(key => !otherLenMap[key]);
console.log('语言包缺失内容关键地图',loseKeys);
if(debug){
console.log('对应的中文包解析', zhCNMap, typeof zhCNMap);
console.log('对应其它语言解析', otherLenMap);
console.log(otherLenPath, otherLenType, otherLenRdio)
}
const zhCNRatio = getLenRatio(LanguageNameTypeEnum.zhCN);
otherLenRdio = otherLenRdio || getLenRatio(otherLenType, otherLenRdio);
if(debug){
console.log('中文长度系数', zhCNRatio);
console.log('其它语言长度系数', otherLenRdio)
}
const zhCNMetaMap = getLenObjToMetaLenObj(zhCNMap, zhCNRatio);
const otherLenMetaMap = getLenObjToMetaLenObj(otherLenMap, otherLenRdio, zhCNMap);
if(debug){
console.log('中文元数据', zhCNMetaMap);
console.log('其它元数据', otherLenMetaMap);
}
const generateZhCNJsonFilePath = await getOutputFilePath(`${'zhCN'}.json`);
const generateXlsxFilePath = await getOutputFilePath(`${otherLenType}.xlsx`);
const generateJsonFilePath = await getOutputFilePath(`${otherLenType}.json`);
const checkNoAlowArray = checkLenObjToNoAllowArrayV1(otherLenMetaMap);
console.log(['生成文件路径', generateXlsxFilePath, generateJsonFilePath].join('\n'));
console.log('不达标长度', checkNoAlowArray);
await Promise.all([
exportModal.exportJSObjToJSONV1(zhCNMap, generateZhCNJsonFilePath),
// 其它语言包
exportModal.epportJSObjToXlsxV1(otherLenMetaMap, generateXlsxFilePath),
exportModal.exportJSObjToJSONV1(otherLenMap, generateJsonFilePath),
]);
if(loseKeys.length > 1) {
const outputPath = await getOutputFilePath('缺少键值.json');
await exportModal.exportJSObjToJSONV1(
{loseKeys}
,
outputPath
);
}
if(checkNoAlowArray.length > 0) {
const outputPath = await getOutputFilePath(`检验不通过.json`);
await exportModal.exportJSObjToJSONV1(
{checkLenObjToNoAllowArrayV1}
,
outputPath
);
};
console.log('执行结束');
});
},
], {});
newProgram.parse(process.argv);
\ No newline at end of file
import fsp from 'fs/promises';
import xlsx from 'xlsx';
import { LanguageItemTypeV1, SheettRowItemKey_V1 ,} from '../constants';
import { getLenObjToArrayV1, } from '../helper/i18';
import { computedColWidthV1 } from '../utils/xlsxUtil';
export class ExportModal {
async exportJSObjToJSONV1 (obj: object, filePath: string) {
return fsp.writeFile(filePath, JSON.stringify(obj), { encoding: 'utf-8' });
};
async epportJSObjToXlsxV1 (obj: LanguageItemTypeV1, filePath: string) {
const lenItemList = getLenObjToArrayV1(obj);
const sheetItem1: SheettRowItemKey_V1[] = ['唯一标识', '翻译字段', '建议长度'];
const worksheet = xlsx.utils.json_to_sheet(lenItemList);
const workBook = xlsx.utils.book_new();
xlsx.utils.book_append_sheet(workBook, worksheet ,)
worksheet['!cols'] = computedColWidthV1(lenItemList);
xlsx.utils.sheet_add_aoa(worksheet, [sheetItem1], { origin: 'A1' });
return xlsx.writeFileXLSX(workBook, filePath);
};
}
\ No newline at end of file
import XLSX from 'xlsx';
import { getSheetToJSObjs_V1 } from '../utils/xlsxUtil';
import { normalizeFilePath } from '../utils/fileUtil';
import { getNormalizeExt } from '../utils/pathUtil';
export interface importDefaultModalParams {
};
/** 通过ts-node 的import 函数获取解析 ts, js, json的能力 */
export const importDefaultModuleV1 = async (src: string, params?: importDefaultModalParams) => {
const {
} = params || {};
return import(src).then(data => data.default).then(dataTransformV1);
};
/** V1 转换为单个map, 将命名空间都合并在统一map里 */
export const dataTransformV1 = (data: object) => {
const types = Array.from(new Set(Object.values(data).map(val => typeof val)));
if(types.length === 1){
const [ currentType ] = types;
/** 能力中心, 单个map翻译路径 */
if(currentType === 'string') {
return data;
}
/** RN端 */
if(currentType === 'object'){
return Object.values(data).reduce((p, c) => Object.assign(p, c), {});
}
throw new Error('未知导入类型')
}
else{
throw new Error('未知导入类型')
}
};
export type importModalFileEXt_V1Type = '.ts'|'.js'|'.xlsx'|'.json'
/** 导入文件数据模型 */
export class ImportModal {
/** V1 版本数据导入只负责default单文件转js对象 */
async importTSToJSObj_V1(src: string) {
return importDefaultModuleV1(src);
};
async importJSTOJSObj_V1(src: string) {
return importDefaultModuleV1(src);
};
async importXlsxToJSON_V1(src: string) {
const workBook = XLSX.readFile(src);
const objs = getSheetToJSObjs_V1(workBook.Sheets,);
return objs?.[0] || {};
};
async importJSONToJSObj_V1(src: string) {
return importDefaultModuleV1(src)
};
/** 通用文件支持接口V1 */
async importFileToJSObj_V1(src: string) {
src = await normalizeFilePath(src);
const importMap:Record<importModalFileEXt_V1Type, {}> = {
'.ts': this.importTSToJSObj_V1,
'.js': this.importJSTOJSObj_V1,
'.xlsx': this.importXlsxToJSON_V1,
'.json': this.importJSONToJSObj_V1,
};
const extname = getNormalizeExt(src);
if(importMap[extname]) {
return importMap[extname](src);
}
else{
throw new TypeError('未知的类型');
}
}
}
\ No newline at end of file
import { Command } from "commander";
export interface useProgramType<CTX> {
(program: Command, ctx?: CTX) : Command
};
/** 注册程序执行函数 */
export const registerUseProgram =<CTX>(program: Command, usePrograms: useProgramType<CTX>[], ctx: CTX ): Command => {
return usePrograms.reduce((program, useProgram) => useProgram(program, ctx), program);
};
\ No newline at end of file
import fsp from 'fs/promises';
import path from 'path';
/** 标准化文件路径 */
export const normalizeFilePath = async (src: string) => {
if(!path.isAbsolute(src)) {
src = path.join(process.cwd(), src);
};
const isFile = await fsp.stat(src).then(stat => stat.isFile());
if(!isFile){
throw new TypeError('必须是文件类型')
};
src = path.normalize(src);
return src;
};
export const normalizePath = async (src: string) => {
if(!path.isAbsolute(src)) {
src = path.join(process.cwd(), src);
};
src = path.normalize(src);
return src;
};
/** 读取路径,自动补全未生成的路径 */
export const readPathToWrite = async (src: string) => {
src = await normalizePath(src);
const srcArray= src.split(path.sep).map(( _ , index, array) => array.slice(0, index + 1).join(path.sep));
for await (const currentPath of srcArray) {
try {
(await fsp.stat(currentPath)).isDirectory();
continue;
} catch (error) {
try {
const isFileReg = /\./;
if(isFileReg.test(currentPath)){
break;
}
await fsp.mkdir(currentPath, );
continue;
} catch (error) {
throw new Error(JSON.stringify(error));
}
}
}
return src;
}
import path from "path";
import { readPathToWrite } from './fileUtil';
export const getNormalizeExt = (src: string) => path.extname(src).toLowerCase();
export const getOutputFilePath = async (fileName: string) => {
const outputPath = path.join(process.cwd(), 'output', fileName);
await readPathToWrite(outputPath);
return outputPath;
};
\ No newline at end of file
import XLSX from 'xlsx';
import { SheettRowItemKey_V1, LanguageItemValueTypeV1,} from '../constants';
/** 将xlsx 文件转化为js对象 V1 */
export const getSheetToJSObjs_V1 = (sheets: XLSX.WorkSheet, option? : XLSX.Sheet2JSONOpts): XLSX.WorkSheet => {
return Object.values(sheets).map(sheet => XLSX.utils.sheet_to_json(sheet, option)).map(transformXlsxToJS);
};
export const transformXlsxToJS = (items: Record<SheettRowItemKey_V1, string>[]) => {
return items.reduce((mergeObj, item) => {
mergeObj[item['唯一标识']] = item['翻译字段'];
return mergeObj;
}, {});
};
/** 计算列宽 */
export const computedColWidthV1 = (colums: LanguageItemValueTypeV1[]):XLSX.ColInfo[] => {
const columnsMap:Record<SheettRowItemKey_V1, number[]> = colums.reduce((p, cur) => {
p['唯一标识'].push(cur['唯一标识']?.length);
p['建议长度'].push(cur['建议长度']?.toString()?.length);
p['翻译字段'].push(cur['翻译字段']?.length);
return p
}, {
'唯一标识': [4],
'翻译字段': [4],
'建议长度': [4],
})
return [
{
wch: Math.max(...columnsMap['唯一标识'].filter(Boolean).map(item => item * 3)),
},
{
wch: Math.max(...columnsMap['翻译字段'].filter(Boolean).map(item => item * 3)),
},
{
wch: Math.max(...columnsMap['建议长度'].filter(Boolean).map(item => item * 3))
}
];
};
\ No newline at end of file
import path from 'path';
import { readPathToWrite } from '../src/utils/fileUtil';
const filePath = path.join(process.cwd(), 'output', 'test');
readPathToWrite(filePath);
import { dataTransformV1 } from '../src/modal/ImportModal';
dataTransformV1({
name : 'name',
one: 'one'
});
dataTransformV1({
name: {
name: 'name',
one: 'one'
},
one: {
one: 'one'
}
})
{
"compileOnSave": true,
"compilerOptions": {
"esModuleInterop": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"module": "commonjs",
"target": "ES2018",
"allowJs": true,
"noUnusedLocals": true,
"preserveConstEnums": true,
"sourceMap": false,
"declaration": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":true,
"noImplicitThis": true,
"stripInternal": true,
"pretty": true,
"paths": {
"*": ["src/*"]
},
"baseUrl": "./",
"outDir": "lib",
"types": [
"node"
]
},
"exclude": [
"lib",
"node_modules"
]
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment