scancscode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/scanliterals.js +3 -0
- package/bin/slimlangs.js +3 -0
- package/dist/CSCodeScanner.js +223 -0
- package/dist/CSVUtils.js +218 -0
- package/dist/CmdExecutor.js +97 -0
- package/dist/CodeSnippet.js +2 -0
- package/dist/LiteralCollector.js +112 -0
- package/dist/RunSlimLangs.js +4 -0
- package/dist/TableScanner.js +106 -0
- package/dist/TestConvert.test.js +10 -0
- package/dist/TestSlimCsv.test.js +10 -0
- package/dist/index.js +4 -0
- package/package.json +29 -0
- package/src/CSCodeScanner.ts +237 -0
- package/src/CSVUtils.ts +177 -0
- package/src/CmdExecutor.ts +94 -0
- package/src/CodeSnippet.ts +7 -0
- package/src/LiteralCollector.ts +88 -0
- package/src/RunSlimLangs.ts +3 -0
- package/src/TableScanner.ts +67 -0
- package/src/TestConvert.test.ts +6 -0
- package/src/TestSlimCsv.test.ts +6 -0
- package/src/index.ts +3 -0
- package/tsconfig.json +109 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TableScanner = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const glob_1 = require("glob");
|
|
39
|
+
class TableScanner {
|
|
40
|
+
parseTrStrFields(content) {
|
|
41
|
+
let m = content.match(/\/\/ NeedTranslateFields\: \[([a-zA-Z_,\s]*)\]$/m);
|
|
42
|
+
if (m != null) {
|
|
43
|
+
let wordsContent = m[1];
|
|
44
|
+
let words = wordsContent.split(", ");
|
|
45
|
+
return words;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
scanJsonWords(jsonContent, fields, literals) {
|
|
52
|
+
let arr = JSON.parse(jsonContent);
|
|
53
|
+
for (const jsonRow of arr) {
|
|
54
|
+
for (const key of fields) {
|
|
55
|
+
let value = jsonRow[key];
|
|
56
|
+
if (typeof (value) == "string") {
|
|
57
|
+
literals.push(value);
|
|
58
|
+
}
|
|
59
|
+
else if (Array.isArray(value)) {
|
|
60
|
+
literals.push(...value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
scanTableCode(csFullPath, literals, verbose) {
|
|
66
|
+
let jsonFullPath = csFullPath.replace("/Main/", "/Auto/").replace(/.cs$/, ".json");
|
|
67
|
+
if (fs.existsSync(jsonFullPath) && fs.existsSync(csFullPath)) {
|
|
68
|
+
let csContent = fs.readFileSync(csFullPath, "utf-8");
|
|
69
|
+
let trstrFields = this.parseTrStrFields(csContent);
|
|
70
|
+
if (trstrFields.length > 0) {
|
|
71
|
+
if (verbose) {
|
|
72
|
+
console.log(`扫描表格: ${csFullPath} , 需要翻译字段: ${trstrFields}`);
|
|
73
|
+
}
|
|
74
|
+
let jsonContent = fs.readFileSync(jsonFullPath, "utf-8");
|
|
75
|
+
this.scanJsonWords(jsonContent, trstrFields, literals);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.log(`跳过表格: ${csFullPath}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (verbose) {
|
|
85
|
+
if (fs.existsSync(csFullPath) == false) {
|
|
86
|
+
console.warn(`表格 CS 文件不存在: ${jsonFullPath}`);
|
|
87
|
+
}
|
|
88
|
+
if (fs.existsSync(jsonFullPath) == false) {
|
|
89
|
+
console.warn(`表格 JSON 文件不存在: ${csFullPath}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 扫描
|
|
96
|
+
*/
|
|
97
|
+
scanTableCodes(folder, literals, verbose) {
|
|
98
|
+
let dir = folder;
|
|
99
|
+
let files = glob_1.glob.sync("*.cs", { cwd: dir });
|
|
100
|
+
for (const filePath of files.reverse()) {
|
|
101
|
+
let csFullPath = folder + filePath;
|
|
102
|
+
this.scanTableCode(csFullPath, literals, verbose);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.TableScanner = TableScanner;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const CmdExecutor_1 = require("./CmdExecutor");
|
|
8
|
+
(0, node_test_1.default)("test convert", () => {
|
|
9
|
+
CmdExecutor_1.CmdExecutor.testConvert();
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const CmdExecutor_1 = require("./CmdExecutor");
|
|
8
|
+
(0, node_test_1.default)("test slim csv", () => {
|
|
9
|
+
CmdExecutor_1.CmdExecutor.testSlimCsv();
|
|
10
|
+
});
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scancscode",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"scanliterals": "bin/scanliterals.js",
|
|
8
|
+
"slimlangs": "bin/slimlangs.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"scanliterals":"node bin/scanliterals.js",
|
|
12
|
+
"slimlangs":"node bin/slimlangs.js",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"command-line-args": "^6.0.1",
|
|
20
|
+
"fast-csv": "^5.0.5",
|
|
21
|
+
"glob": "^13.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/command-line-args": "^5.2.3",
|
|
25
|
+
"@types/node": "^25.0.3",
|
|
26
|
+
"ts-node": "^10.9.2",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { CodeSnippet } from "./CodeSnippet";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 扫描C#代码, 扫描出其中所有形如 uictrl.text = 其他内容; 的代码片段, 其他内容可以是: $"<font color={colorStr}>{itemConfig.Name}</ font>"; 等等, 其他内容中形如 {变量} 的部分需要提取出来, 转换成 string.Format("<font color={0}>{1}</ font>", colorStr, itemConfig.Name); 的格式
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class CSCodeScanner {
|
|
8
|
+
// 正则表达式匹配 uictrl.text = $"..."; 的模式
|
|
9
|
+
private static readonly assignmentPattern = /([\w\.]+\.text\s*=(?![=>])\s*)([^;]+)\s*;/mg;
|
|
10
|
+
|
|
11
|
+
// 正则表达式匹配 匹配每个字符串
|
|
12
|
+
private static readonly stringPattern = /\$?"([^"]*)"/mg;
|
|
13
|
+
// 正则表达式匹配插值表达式 {...}
|
|
14
|
+
private static readonly interpolationPattern = /\{([^}]+?)(\:[a-zA-Z\d-\.\#]+)?\}/mg;
|
|
15
|
+
|
|
16
|
+
private static readonly stringFormatPattern = /(?<![a-zA-Z_])([sS]tring\.Format)(?=\(.*?\))/mg;
|
|
17
|
+
|
|
18
|
+
private static readonly interpolationHeadPattern = /^[\$\@]*(.+)$/m;
|
|
19
|
+
|
|
20
|
+
public static isNativeString2(s: string) {
|
|
21
|
+
return s == '""';
|
|
22
|
+
}
|
|
23
|
+
public static isNativeString(s: string) {
|
|
24
|
+
let m = s.match(this.interpolationHeadPattern);
|
|
25
|
+
if (m != null) {
|
|
26
|
+
let stringContent = m[1];
|
|
27
|
+
return this.isNativeString2(stringContent);
|
|
28
|
+
}
|
|
29
|
+
return this.isNativeString2(s);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static getLineIndexFromIndex(content: string, index: number): [number, number] {
|
|
33
|
+
let lineIndex = 0
|
|
34
|
+
// 从第0个字符开始遍历到index位置, 计算换行符数量
|
|
35
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
36
|
+
if (content[i] === '\n') {
|
|
37
|
+
lineIndex++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let lineColumn = content.lastIndexOf('\n', index);
|
|
41
|
+
return [lineIndex + 1, index - lineColumn];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public static scanFile(filePath: string, content: string): CodeSnippet[] {
|
|
45
|
+
const snippets: CodeSnippet[] = [];
|
|
46
|
+
|
|
47
|
+
const assignMatches = [...content.matchAll(CSCodeScanner.assignmentPattern)];
|
|
48
|
+
for (let j = 0; j < assignMatches.length; j++) {
|
|
49
|
+
const assignMatch = assignMatches[j];
|
|
50
|
+
let assignLine = assignMatch[0];
|
|
51
|
+
let assignHead = assignMatch[1];
|
|
52
|
+
let bodyLine = assignMatch[2];
|
|
53
|
+
let convertedAssignLine: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 需要翻译的文本
|
|
57
|
+
*/
|
|
58
|
+
let literals: string[] = [];
|
|
59
|
+
let unexpects: string[] = [];
|
|
60
|
+
const stringMatchs = [...bodyLine.matchAll(CSCodeScanner.stringPattern)];
|
|
61
|
+
if (stringMatchs.length > 0) {
|
|
62
|
+
// 通过是否单行字符串并以 ".TR(); 结尾和判断是否内插字符串来判断是否需要在结尾附加 .TR()
|
|
63
|
+
let isSingleString = false;
|
|
64
|
+
if (stringMatchs.length == 1 && stringMatchs[0][0] == bodyLine) {
|
|
65
|
+
let stringMatchFirst = stringMatchs[0];
|
|
66
|
+
let stringLine = stringMatchFirst[0];
|
|
67
|
+
if (stringLine.startsWith("$") || stringLine.startsWith("@$")) {
|
|
68
|
+
let stringContent = stringMatchFirst[1];
|
|
69
|
+
const variableMatches0 = [...stringContent.matchAll(CSCodeScanner.interpolationPattern)];
|
|
70
|
+
isSingleString = variableMatches0.length == 0;
|
|
71
|
+
} else {
|
|
72
|
+
isSingleString = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isSingleString) {
|
|
77
|
+
let stringMatchFirst = stringMatchs[0];
|
|
78
|
+
let stringLineIndex = stringMatchFirst.index;
|
|
79
|
+
let prefix = bodyLine.substring(0, stringLineIndex);
|
|
80
|
+
let stringLineFirst = stringMatchFirst[0];
|
|
81
|
+
if (this.isNativeString(stringLineFirst)) {
|
|
82
|
+
convertedAssignLine = assignMatch[1] + prefix + stringMatchFirst[0] + ";";
|
|
83
|
+
} else {
|
|
84
|
+
convertedAssignLine = assignMatch[1] + prefix + stringMatchFirst[0] + ".TR();";
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
let stringLines: string[] = [];
|
|
88
|
+
stringLines.push(assignMatch[1]);
|
|
89
|
+
let lastIndex = 0;
|
|
90
|
+
for (let i = 0; i < stringMatchs.length; i++) {
|
|
91
|
+
let stringMatch = stringMatchs[i];
|
|
92
|
+
let stringLine = stringMatch[0];
|
|
93
|
+
let stringContent = stringMatch[1];
|
|
94
|
+
let stringLineIndex = stringMatch.index;
|
|
95
|
+
let stringStartIndexInContent = assignMatch.index + assignHead.length + stringMatch.index
|
|
96
|
+
/**
|
|
97
|
+
* 转换后的字符串
|
|
98
|
+
*/
|
|
99
|
+
let convertedString: string;
|
|
100
|
+
if (stringLine.startsWith("$") || stringLine.startsWith("@$")) {
|
|
101
|
+
// 开始转换插值
|
|
102
|
+
// 提取插值表达式中的变量
|
|
103
|
+
const variableMatches0 = [...stringContent.matchAll(CSCodeScanner.interpolationPattern)];
|
|
104
|
+
if (variableMatches0.length > 0) {
|
|
105
|
+
let variableMatches: RegExpExecArray[] = [];
|
|
106
|
+
let map = new Map<string, RegExpExecArray>();
|
|
107
|
+
for (const variableMatche of variableMatches0) {
|
|
108
|
+
if (!map.has(variableMatche[1])) {
|
|
109
|
+
map.set(variableMatche[1], variableMatche);
|
|
110
|
+
variableMatches.push(variableMatche);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 构建格式化字符串
|
|
115
|
+
let formatString = stringContent;
|
|
116
|
+
variableMatches.forEach((variableMatche, index) => {
|
|
117
|
+
let varName = variableMatche[1];
|
|
118
|
+
let suffix = variableMatche[2] ?? "";
|
|
119
|
+
let replaced = formatString.replaceAll(`{${varName}${suffix}}`, `{${index}${suffix}}`);
|
|
120
|
+
if (replaced == formatString) {
|
|
121
|
+
let [lineNum, colNum] = CSCodeScanner.getLineIndexFromIndex(content, stringStartIndexInContent);
|
|
122
|
+
let tip = `可能无法处理的复杂情形1(${filePath}:${lineNum}:${colNum}): ${stringContent}`;
|
|
123
|
+
console.error(tip);
|
|
124
|
+
unexpects.push(tip);
|
|
125
|
+
} else {
|
|
126
|
+
formatString = replaced;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// 构建string.Format调用
|
|
132
|
+
let formatCall = `string.Format("${formatString}"`;
|
|
133
|
+
for (const variableMatche of variableMatches) {
|
|
134
|
+
let varName = variableMatche[1];
|
|
135
|
+
formatCall += `, ${varName}`;
|
|
136
|
+
}
|
|
137
|
+
formatCall += ")";
|
|
138
|
+
|
|
139
|
+
convertedString = formatCall;
|
|
140
|
+
|
|
141
|
+
literals.push(formatString);
|
|
142
|
+
} else {
|
|
143
|
+
// throw new Error("暂不支持插值字符串转换为 string.Format, 请手动处理");
|
|
144
|
+
let isValidFormat = true;
|
|
145
|
+
if (stringContent.indexOf("{") != -1 || stringContent.indexOf("}") != -1) {
|
|
146
|
+
if (bodyLine.indexOf("{") != -1 && bodyLine.indexOf("}") != -1) {
|
|
147
|
+
let [lineNum, colNum] = CSCodeScanner.getLineIndexFromIndex(content, stringStartIndexInContent);
|
|
148
|
+
let tip = `可能无法处理的复杂情形2(${filePath}:${lineNum}:${colNum}): ${stringContent}`;
|
|
149
|
+
console.error(tip);
|
|
150
|
+
unexpects.push(tip);
|
|
151
|
+
isValidFormat = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let formattedStringLine = stringLine;
|
|
155
|
+
if (isValidFormat) {
|
|
156
|
+
// let stringStartIndex = stringMatch.index
|
|
157
|
+
// let stringEndIndex = stringStartIndex + stringLine.length
|
|
158
|
+
// let prefix = bodyLine.substring(stringStartIndex - 2, stringStartIndex)
|
|
159
|
+
// let suffix = bodyLine.substring(stringEndIndex, stringEndIndex + ".TR()".length)
|
|
160
|
+
// if ((stringStartIndex == 0 || prefix == "+ ") && suffix != ".TR()") {
|
|
161
|
+
// formattedStringLine = formattedStringLine + ".TR()"
|
|
162
|
+
// }
|
|
163
|
+
formattedStringLine = this.replaceStringsTR(stringMatch, stringLine, bodyLine);
|
|
164
|
+
literals.push(stringContent);
|
|
165
|
+
}
|
|
166
|
+
convertedString = formattedStringLine;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
literals.push(stringContent);
|
|
170
|
+
convertedString = this.replaceStringsTR(stringMatch, stringLine, bodyLine);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 串联字符串列表
|
|
174
|
+
let prefix = bodyLine.substring(lastIndex, stringLineIndex);
|
|
175
|
+
lastIndex = stringLineIndex + stringLine.length;
|
|
176
|
+
stringLines.push(prefix);
|
|
177
|
+
stringLines.push(convertedString);
|
|
178
|
+
}
|
|
179
|
+
let matchEnd = stringMatchs[stringMatchs.length - 1];
|
|
180
|
+
let endIndex = matchEnd.index + matchEnd[0].length;
|
|
181
|
+
stringLines.push(bodyLine.substring(endIndex));
|
|
182
|
+
stringLines.push(";");
|
|
183
|
+
convertedAssignLine = stringLines.join("");
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
convertedAssignLine = assignLine;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let convertedAssignLine2 = convertedAssignLine.replaceAll(this.stringFormatPattern, "Tr.Format");
|
|
190
|
+
snippets.push({
|
|
191
|
+
originalIndex: assignMatch.index,
|
|
192
|
+
originalCode: assignLine,
|
|
193
|
+
convertedCode: convertedAssignLine2,
|
|
194
|
+
literals,
|
|
195
|
+
unexpects,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
return snippets;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public static replaceStringsTR(stringMatch: RegExpExecArray, stringLine: string, bodyLine: string) {
|
|
204
|
+
if (this.isNativeString(stringLine)) {
|
|
205
|
+
return stringLine;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let formattedStringLine = stringLine;
|
|
209
|
+
let stringStartIndex = stringMatch.index;
|
|
210
|
+
let stringEndIndex = stringStartIndex + stringLine.length;
|
|
211
|
+
let prefix = bodyLine.substring(stringStartIndex - 2, stringStartIndex);
|
|
212
|
+
let suffix = bodyLine.substring(stringEndIndex, stringEndIndex + ".TR()".length);
|
|
213
|
+
if ((stringStartIndex == 0 || prefix == "+ ") && suffix != ".TR()") {
|
|
214
|
+
formattedStringLine = formattedStringLine + ".TR()";
|
|
215
|
+
}
|
|
216
|
+
return formattedStringLine;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public static replaceInFile(content: string, snippets: CodeSnippet[]) {
|
|
220
|
+
let parts: string[] = [];
|
|
221
|
+
if (snippets.length > 0) {
|
|
222
|
+
let lastIndex = 0;
|
|
223
|
+
for (const snippet of snippets) {
|
|
224
|
+
let part = content.substring(lastIndex, snippet.originalIndex);
|
|
225
|
+
lastIndex = snippet.originalIndex + snippet.originalCode.length;
|
|
226
|
+
parts.push(part);
|
|
227
|
+
parts.push(snippet.convertedCode);
|
|
228
|
+
}
|
|
229
|
+
let endSnippet = snippets[snippets.length - 1];
|
|
230
|
+
let partEnd = content.substring(endSnippet.originalIndex + endSnippet.originalCode.length);
|
|
231
|
+
parts.push(partEnd);
|
|
232
|
+
}
|
|
233
|
+
let convertedContent = parts.join("");
|
|
234
|
+
return convertedContent;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
}
|
package/src/CSVUtils.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as csv from "fast-csv";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class CSVUtils {
|
|
6
|
+
filePath: string;
|
|
7
|
+
constructor(filePath: string) {
|
|
8
|
+
this.filePath = filePath;
|
|
9
|
+
}
|
|
10
|
+
async parseCsv() {
|
|
11
|
+
if (fs.existsSync(this.filePath)) {
|
|
12
|
+
let stream = csv.parseFile(this.filePath, {
|
|
13
|
+
delimiter: ",",
|
|
14
|
+
});
|
|
15
|
+
let rows: string[][] = await stream.toArray();
|
|
16
|
+
stream.destroy();
|
|
17
|
+
return rows;
|
|
18
|
+
} else {
|
|
19
|
+
console.error(`文件不存在: ${this.filePath}`);
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
getShortKey(content: string) {
|
|
24
|
+
// let max = 16
|
|
25
|
+
// if (content.length < max) {
|
|
26
|
+
// return content
|
|
27
|
+
// } else {
|
|
28
|
+
// return content.substring(0, max)
|
|
29
|
+
// }
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
tryGetEntry(content: string, map01: Map<string, string[]>, map02: Map<string, string[]>): [boolean, string, string[] | undefined] {
|
|
33
|
+
if (map02.has(content)) {
|
|
34
|
+
let row2 = map02.get(content)!;
|
|
35
|
+
return [row2 != null, content, row2];
|
|
36
|
+
}
|
|
37
|
+
let shortKey = this.getShortKey(content);
|
|
38
|
+
if (map01.has(shortKey)) {
|
|
39
|
+
let row1 = map01.get(shortKey)!;
|
|
40
|
+
if (row1 != null && (row1[0] == shortKey || row1[1] == content)) {
|
|
41
|
+
return [true, shortKey, row1];
|
|
42
|
+
} else {
|
|
43
|
+
return [false, shortKey, undefined];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [false, shortKey, undefined];
|
|
47
|
+
}
|
|
48
|
+
merge(rows0: string[][], rows1: string[], langs: string[]) {
|
|
49
|
+
// 短key
|
|
50
|
+
let map01: Map<string, string[]> = new Map();
|
|
51
|
+
// 全文key
|
|
52
|
+
let map02: Map<string, string[]> = new Map();
|
|
53
|
+
if (rows0 != null) {
|
|
54
|
+
for (let index = 1; index < rows0.length; index++) {
|
|
55
|
+
const row = rows0[index];
|
|
56
|
+
let key = row[0];
|
|
57
|
+
map01.set(key, row);
|
|
58
|
+
}
|
|
59
|
+
for (let index = 1; index < rows0.length; index++) {
|
|
60
|
+
const row = rows0[index];
|
|
61
|
+
let key = row[1];
|
|
62
|
+
if (key != null && key != "") {
|
|
63
|
+
map02.set(key, row);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let rows2: string[][] = [];
|
|
69
|
+
if (rows0 != null && rows0.length > 0) {
|
|
70
|
+
// rows2.push(rows0[0]);
|
|
71
|
+
// 合并 langs 中没有的语言列
|
|
72
|
+
let headers = [...rows0[0]];
|
|
73
|
+
for (const lang of langs) {
|
|
74
|
+
if (!headers.includes(lang)) {
|
|
75
|
+
headers.push(lang);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
rows2.push(headers);
|
|
79
|
+
} else {
|
|
80
|
+
rows2.push(["key", ...langs]);
|
|
81
|
+
}
|
|
82
|
+
for (let index = 0; index < rows1.length; index++) {
|
|
83
|
+
const content = rows1[index];
|
|
84
|
+
let [exist, key, row] = this.tryGetEntry(content, map01, map02);
|
|
85
|
+
if (exist) {
|
|
86
|
+
if (row != null) {
|
|
87
|
+
rows2.push(row);
|
|
88
|
+
} else {
|
|
89
|
+
if (key != content) {
|
|
90
|
+
rows2.push([key, content]);
|
|
91
|
+
} else {
|
|
92
|
+
rows2.push([content, '']);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (key != content) {
|
|
97
|
+
rows2.push([key, content]);
|
|
98
|
+
} else {
|
|
99
|
+
rows2.push([content, '']);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return rows2;
|
|
104
|
+
}
|
|
105
|
+
static async writeCsv(filePath: string, rows: string[][]) {
|
|
106
|
+
let str = await csv.writeToString(rows);
|
|
107
|
+
fs.writeFileSync(filePath, str, "utf-8");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static async updateToFile(filePath: string, literals: string[], langs: string[]) {
|
|
111
|
+
let csvUtils = new CSVUtils(filePath);
|
|
112
|
+
let rows0 = await csvUtils.parseCsv();
|
|
113
|
+
let rows2 = csvUtils.merge(rows0, literals, langs);
|
|
114
|
+
await CSVUtils.writeCsv(filePath, rows2);
|
|
115
|
+
console.log(`已经更新多语言表: ${filePath} , 共有 ${rows2.length - 1} 条目`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 表格第一列为 key + 语言列表, 按照 langs 精简表格列, 只保留指定key列和指定语言列
|
|
120
|
+
* @param filePath csv文件路径
|
|
121
|
+
* @param outFilePath 输出csv文件路径
|
|
122
|
+
* @param langs 指定要保留的语言列表
|
|
123
|
+
*/
|
|
124
|
+
static async slimCsvWithLangs(filePaths: string[], outFilePath: string, langs: string[]) {
|
|
125
|
+
let rows2: string[][] = [];
|
|
126
|
+
let keySpace: Set<string> = new Set();
|
|
127
|
+
let conflictKeySpace: Set<string> = new Set();
|
|
128
|
+
let existAnyInput = false;
|
|
129
|
+
for (const filePath of filePaths) {
|
|
130
|
+
if (fs.existsSync(filePath) == false) {
|
|
131
|
+
console.warn(`文件不存在: ${filePath}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
existAnyInput = true;
|
|
135
|
+
let csvUtils = new CSVUtils(filePath);
|
|
136
|
+
let rows0 = await csvUtils.parseCsv();
|
|
137
|
+
let header = rows0[0];
|
|
138
|
+
if (rows2.length == 0) {
|
|
139
|
+
rows2.push([header[0], ...langs]);
|
|
140
|
+
}
|
|
141
|
+
let langIndexes: number[] = [];
|
|
142
|
+
for (const lang of langs) {
|
|
143
|
+
let langIndex = header.indexOf(lang);
|
|
144
|
+
if (langIndex != -1) {
|
|
145
|
+
langIndexes.push(langIndex);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (let index = 1; index < rows0.length; index++) {
|
|
149
|
+
const row = rows0[index];
|
|
150
|
+
let newRow: string[] = [];
|
|
151
|
+
let key = row[0];
|
|
152
|
+
if (keySpace.has(key)) {
|
|
153
|
+
conflictKeySpace.add(key);
|
|
154
|
+
} else {
|
|
155
|
+
keySpace.add(key);
|
|
156
|
+
}
|
|
157
|
+
newRow.push(key);
|
|
158
|
+
for (const langIndex of langIndexes) {
|
|
159
|
+
newRow.push(row[langIndex]);
|
|
160
|
+
}
|
|
161
|
+
rows2.push(newRow);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (existAnyInput == false) {
|
|
165
|
+
console.warn(`不存在有效文件, 未输出文件:`, filePaths);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (rows2.length > 0) {
|
|
169
|
+
await CSVUtils.writeCsv(outFilePath, rows2);
|
|
170
|
+
if (conflictKeySpace.size > 0) {
|
|
171
|
+
console.error(`冲突的key有 ${conflictKeySpace.size} 个:`, conflictKeySpace);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
console.warn(`没有生成任何数据,未输出文件。`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { CSVUtils } from "./CSVUtils";
|
|
2
|
+
import { LiteralCollector } from "./LiteralCollector";
|
|
3
|
+
import commandLineArgs from 'command-line-args'
|
|
4
|
+
|
|
5
|
+
function isNullOrEmpty<T>(arr: T[]): boolean {
|
|
6
|
+
return arr == null || arr.length == 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class CmdExecutor {
|
|
10
|
+
static testConvert() {
|
|
11
|
+
let cwd = "../../../GameClient/"
|
|
12
|
+
let cscodeFolders = [cwd + "Assets/Bundles/FGUI/"]
|
|
13
|
+
let gameConfigFolders = [cwd + "Assets/Bundles/GameConfigs/"]
|
|
14
|
+
let outCsvFile = "E:/DATA/Projects/e-gbl-client/client/Assets/Bundles/GameConfigs/Translation/hello.csv"
|
|
15
|
+
let langs = ["zh_cn"];
|
|
16
|
+
let literalCollector = new LiteralCollector();
|
|
17
|
+
literalCollector.convert(cscodeFolders, gameConfigFolders, outCsvFile, langs)
|
|
18
|
+
// node . --cscodedir ../../../GameClient/Assets/Bundles/FGUI/ --configdir ../../../GameClient/Assets/Bundles/GameConfigs/ --outcsv E:/DATA/Projects/e-gbl-client/client/Assets/Bundles/GameConfigs/Translation/hello.csv --langs zh_cn
|
|
19
|
+
}
|
|
20
|
+
static runConvertWithCmdOptions() {
|
|
21
|
+
const optionDefinitions = [
|
|
22
|
+
{ name: 'cscodedir', type: String, multiple: true },
|
|
23
|
+
{ name: 'configdir', type: String, multiple: true },
|
|
24
|
+
{ name: 'outcsv', type: String },
|
|
25
|
+
{ name: "langs", type: String, multiple: true, defaultOption: true },
|
|
26
|
+
{ name: "verbose", alias: 'v', type: Boolean },
|
|
27
|
+
]
|
|
28
|
+
const options = commandLineArgs(optionDefinitions)
|
|
29
|
+
let cscodedir: string[] = options.cscodedir ?? []
|
|
30
|
+
let configdir: string[] = options.configdir ?? []
|
|
31
|
+
let outcsv: string = options.outcsv
|
|
32
|
+
let langs: string[] = options.langs ?? ["zh_cn"]
|
|
33
|
+
let verbose: boolean = options.verbose ?? false
|
|
34
|
+
|
|
35
|
+
let argv = process.argv
|
|
36
|
+
if (isNullOrEmpty(cscodedir) && isNullOrEmpty(configdir)) {
|
|
37
|
+
if (isNullOrEmpty(cscodedir)) {
|
|
38
|
+
console.error(`cscodedir missing:`, argv)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (isNullOrEmpty(configdir)) {
|
|
42
|
+
console.error(`configdir missing:`, argv)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (outcsv == null) {
|
|
47
|
+
console.error(`outcsv missing:`, argv)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (isNullOrEmpty(langs)) {
|
|
51
|
+
console.error(`langs missing:`, argv)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
console.log(`convert cmd options: `, cscodedir, configdir, outcsv, langs, verbose)
|
|
55
|
+
let literalCollector = new LiteralCollector();
|
|
56
|
+
literalCollector.convert(cscodedir, configdir, outcsv, langs, verbose)
|
|
57
|
+
console.log("convert done.")
|
|
58
|
+
}
|
|
59
|
+
static testSlimCsv() {
|
|
60
|
+
let inCsvFile = ["E:/DATA/Projects/e-gbl-client/client/Assets/Bundles/GameConfigs/Translation/hello.csv"]
|
|
61
|
+
let outCsvFile = "E:/DATA/Projects/e-gbl-client/client/Assets/Bundles/GameConfigs/Translation/hello-out.csv"
|
|
62
|
+
let langs = ["zh_cn"];
|
|
63
|
+
CSVUtils.slimCsvWithLangs(inCsvFile, outCsvFile, langs);
|
|
64
|
+
// node bin/slimlangs.js --incsv E:/DATA/Projects/e-gbl-client/client/Assets/Bundles/GameConfigs/Translation/hello.csv --outcsv E:/DATA/Projects/e-gbl-client/client/Assets/Bundles/GameConfigs/Translation/hello-out.csv --langs zh_cn
|
|
65
|
+
}
|
|
66
|
+
static runSlimCsvWithLangs() {
|
|
67
|
+
const optionDefinitions = [
|
|
68
|
+
{ name: 'incsv', type: String, multiple: true },
|
|
69
|
+
{ name: 'outcsv', type: String },
|
|
70
|
+
{ name: 'langs', type: String, multiple: true, defaultOption: true },
|
|
71
|
+
];
|
|
72
|
+
const options = commandLineArgs(optionDefinitions);
|
|
73
|
+
let incsv: string[] = options.incsv ?? [];
|
|
74
|
+
let outcsv: string = options.outcsv;
|
|
75
|
+
let langs: string[] = options.langs ?? ["zh_cn"]
|
|
76
|
+
|
|
77
|
+
console.log(`slim csv cmd options: `, incsv, outcsv, langs);
|
|
78
|
+
let argv = process.argv;
|
|
79
|
+
if (isNullOrEmpty(incsv)) {
|
|
80
|
+
console.error(`incsv missing:`, argv);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (outcsv == null) {
|
|
84
|
+
console.error(`outcsv missing:`, argv);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (isNullOrEmpty(langs)) {
|
|
88
|
+
console.error(`langs missing:`, argv);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
CSVUtils.slimCsvWithLangs(incsv, outcsv, langs);
|
|
92
|
+
console.log("slim csv with langs done.");
|
|
93
|
+
}
|
|
94
|
+
}
|