td-web-cli 0.1.4 → 0.1.6
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/dist/modules/i18n/excel2json/index.js +121 -19
- package/dist/utils/index.js +39 -24
- package/logs/20260127.txt +150 -0
- package/package.json +1 -1
- package/src/modules/i18n/excel2json/index.ts +147 -19
- package/src/utils/index.ts +45 -30
|
@@ -95,6 +95,45 @@ async function batchCheckTexts(texts, language) {
|
|
|
95
95
|
}
|
|
96
96
|
return results;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* 将语言检测结果转换成每条词条对应的错误描述数组
|
|
100
|
+
* @param checkResult 语言检测结果
|
|
101
|
+
* @param texts 词条数组(对应检测文本)
|
|
102
|
+
* @returns 按词条分割的检测错误描述数组,顺序对应输入texts
|
|
103
|
+
*/
|
|
104
|
+
function parseCheckResultPerEntry(checkResult, texts) {
|
|
105
|
+
// 初始化每条词条对应的错误信息数组
|
|
106
|
+
const entryErrors = new Array(texts.length).fill('').map(() => '');
|
|
107
|
+
// 语言检测返回的matches是针对整个拼接文本的,需要拆分到对应词条
|
|
108
|
+
// 计算每条词条在拼接文本中的起始位置和结束位置
|
|
109
|
+
const positions = [];
|
|
110
|
+
let pos = 0;
|
|
111
|
+
for (const text of texts) {
|
|
112
|
+
const len = text.length;
|
|
113
|
+
positions.push({ start: pos, end: pos + len });
|
|
114
|
+
pos += len + 1; // +1是换行符长度
|
|
115
|
+
}
|
|
116
|
+
// 遍历所有错误匹配项,将错误信息分配到对应词条
|
|
117
|
+
for (const match of checkResult.matches) {
|
|
118
|
+
const errorOffset = match.offset;
|
|
119
|
+
// 找出错误所在的词条索引
|
|
120
|
+
const idx = positions.findIndex((range) => errorOffset >= range.start && errorOffset < range.end);
|
|
121
|
+
if (idx === -1)
|
|
122
|
+
continue;
|
|
123
|
+
// 生成错误信息字符串
|
|
124
|
+
const errMsg = `错误: ${match.message}\n出错句子: ${match.sentence}\n建议替换: ${match.replacements
|
|
125
|
+
.map((r) => r.value)
|
|
126
|
+
.join(', ')}`;
|
|
127
|
+
// 多条错误用换行分隔
|
|
128
|
+
if (entryErrors[idx]) {
|
|
129
|
+
entryErrors[idx] += '\n' + errMsg;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
entryErrors[idx] = errMsg;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return entryErrors;
|
|
136
|
+
}
|
|
98
137
|
/**
|
|
99
138
|
* excel转json功能主函数
|
|
100
139
|
* 读取用户输入的excel路径,解析内容,根据配置生成多语言json文件
|
|
@@ -104,13 +143,13 @@ async function batchCheckTexts(texts, language) {
|
|
|
104
143
|
export async function excel2json(program) {
|
|
105
144
|
var _a;
|
|
106
145
|
// 配置文件默认路径
|
|
107
|
-
const configPath = path.join(__dirname, '
|
|
146
|
+
const configPath = path.join(__dirname, '../../../../setting.json');
|
|
108
147
|
let i18nConfig;
|
|
109
148
|
// 加载配置文件
|
|
110
149
|
try {
|
|
111
|
-
logger.info(`开始加载配置文件:${configPath}
|
|
150
|
+
logger.info(`开始加载配置文件:${configPath}`, true);
|
|
112
151
|
i18nConfig = loadConfig(configPath);
|
|
113
|
-
logger.info('配置文件加载成功');
|
|
152
|
+
logger.info('配置文件加载成功', true);
|
|
114
153
|
}
|
|
115
154
|
catch (error) {
|
|
116
155
|
const msg = `读取配置文件失败:${normalizeError(error).stack},程序已退出`;
|
|
@@ -120,9 +159,9 @@ export async function excel2json(program) {
|
|
|
120
159
|
}
|
|
121
160
|
// 尝试调用接口获取支持的语言列表,更新 longCodes
|
|
122
161
|
try {
|
|
123
|
-
logger.info('尝试获取在线支持的语言列表...');
|
|
162
|
+
logger.info('尝试获取在线支持的语言列表...', true);
|
|
124
163
|
const languageTools = await getLanguageTool();
|
|
125
|
-
logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes
|
|
164
|
+
logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes`, true);
|
|
126
165
|
// 构建新的 longCodes 映射
|
|
127
166
|
const newLongCodes = {};
|
|
128
167
|
// 语言标识对应语言名称列表,方便匹配
|
|
@@ -146,6 +185,7 @@ export async function excel2json(program) {
|
|
|
146
185
|
}
|
|
147
186
|
catch (error) {
|
|
148
187
|
logger.warn(`获取在线语言列表失败,使用本地配置 longCodes,错误:${normalizeError(error).stack}`);
|
|
188
|
+
console.warn('获取在线语言列表失败,使用本地配置 longCodes');
|
|
149
189
|
}
|
|
150
190
|
// 交互式输入excel文件路径并校验
|
|
151
191
|
const answer = await input({
|
|
@@ -164,7 +204,7 @@ export async function excel2json(program) {
|
|
|
164
204
|
// 规范化路径,支持相对路径转绝对路径,去除首尾引号
|
|
165
205
|
const excelPath = path.resolve(process.cwd(), answer.trim().replace(/^['"]|['"]$/g, ''));
|
|
166
206
|
try {
|
|
167
|
-
logger.info(`开始读取excel文件:${excelPath}
|
|
207
|
+
logger.info(`开始读取excel文件:${excelPath}`, true);
|
|
168
208
|
// 读取excel文件
|
|
169
209
|
const workbook = XLSX.readFile(excelPath);
|
|
170
210
|
const firstSheetName = workbook.SheetNames[0];
|
|
@@ -181,7 +221,7 @@ export async function excel2json(program) {
|
|
|
181
221
|
console.error('程序执行时发生异常,已记录日志,程序已退出');
|
|
182
222
|
process.exit(1);
|
|
183
223
|
}
|
|
184
|
-
logger.info('开始解析表头');
|
|
224
|
+
logger.info('开始解析表头', true);
|
|
185
225
|
// 处理表头行,去除空格,转成字符串
|
|
186
226
|
const headerRow = rows[0].map((cell) => (cell ? String(cell).trim() : ''));
|
|
187
227
|
// 根据表头匹配语言列,建立列索引到语言key的映射
|
|
@@ -206,7 +246,7 @@ export async function excel2json(program) {
|
|
|
206
246
|
Object.values(colIndexToLangKey).forEach((langKey) => {
|
|
207
247
|
langTranslations[langKey] = {};
|
|
208
248
|
});
|
|
209
|
-
logger.info('开始解析数据行');
|
|
249
|
+
logger.info('开始解析数据行', true);
|
|
210
250
|
// 遍历数据行,提取所有语言词条
|
|
211
251
|
// key统一用默认语言列的值,其他语言对应的列为翻译内容
|
|
212
252
|
const langKeysMap = {}; // 语言key => 词条数组
|
|
@@ -237,31 +277,49 @@ export async function excel2json(program) {
|
|
|
237
277
|
langTranslations[langKey][key] = valStr;
|
|
238
278
|
langKeysMap[langKey].push(valStr);
|
|
239
279
|
}
|
|
280
|
+
else {
|
|
281
|
+
// 如果单元格为空,也要保证检测结果数组长度一致,填空字符串
|
|
282
|
+
langKeysMap[langKey].push('');
|
|
283
|
+
}
|
|
240
284
|
}
|
|
241
285
|
}
|
|
286
|
+
// 语言检测结果映射,语言key => 每条词条的错误描述数组
|
|
287
|
+
const langCheckErrorsMap = {};
|
|
242
288
|
// 对所有语言词条批量进行语言检测(包括默认语言)
|
|
243
|
-
|
|
289
|
+
const langKeysEntries = Object.entries(langKeysMap);
|
|
290
|
+
for (let idx = 0; idx < langKeysEntries.length; idx++) {
|
|
291
|
+
const [langKey, texts] = langKeysEntries[idx];
|
|
244
292
|
const longCode = i18nConfig.longCodes[langKey];
|
|
245
293
|
if (!longCode) {
|
|
246
|
-
logger.warn(`语言(${langKey})未配置 longCode
|
|
294
|
+
logger.warn(`语言(${langKey})未配置 longCode,跳过检测`, true);
|
|
295
|
+
langCheckErrorsMap[langKey] =
|
|
296
|
+
texts.length > 0
|
|
297
|
+
? texts.map(() => '未配置 longCode,未进行检测')
|
|
298
|
+
: ['无词条'];
|
|
247
299
|
continue;
|
|
248
300
|
}
|
|
249
301
|
if (texts.length === 0) {
|
|
250
|
-
logger.info(`语言(${langKey})
|
|
302
|
+
logger.info(`语言(${langKey})无词条,跳过检测`, true);
|
|
303
|
+
langCheckErrorsMap[langKey] = ['无词条'];
|
|
251
304
|
continue;
|
|
252
305
|
}
|
|
253
|
-
logger.info(`开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length}
|
|
306
|
+
logger.info(`开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length} (${idx + 1}/${langKeysEntries.length})`, true);
|
|
254
307
|
const checkResults = await batchCheckTexts(texts, longCode);
|
|
255
308
|
if (!checkResults || checkResults.length === 0 || !checkResults[0]) {
|
|
256
|
-
logger.error(`语言(${langKey})
|
|
309
|
+
logger.error(`语言(${langKey})词条检测失败`, true);
|
|
310
|
+
langCheckErrorsMap[langKey] = texts.map(() => '检测失败,未知错误');
|
|
257
311
|
continue;
|
|
258
312
|
}
|
|
259
313
|
const result = checkResults[0];
|
|
260
314
|
if (result.matches.length === 0) {
|
|
261
|
-
logger.info(`语言(${langKey})
|
|
315
|
+
logger.info(`语言(${langKey})词条检测无错误`, true);
|
|
316
|
+
langCheckErrorsMap[langKey] = texts.map(() => '无错误');
|
|
262
317
|
}
|
|
263
318
|
else {
|
|
264
|
-
logger.info(`语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}
|
|
319
|
+
logger.info(`语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}`, true);
|
|
320
|
+
// 解析检测结果,拆分到每条词条
|
|
321
|
+
langCheckErrorsMap[langKey] = parseCheckResultPerEntry(result, texts);
|
|
322
|
+
// 详细日志输出
|
|
265
323
|
for (const match of result.matches) {
|
|
266
324
|
logger.info(`- 错误: ${match.message}\n 出错句子: ${match.sentence}\n 建议替换: ${match.replacements
|
|
267
325
|
.map((r) => r.value)
|
|
@@ -276,13 +334,13 @@ export async function excel2json(program) {
|
|
|
276
334
|
if (!fs.existsSync(outputRoot)) {
|
|
277
335
|
fs.mkdirSync(outputRoot, { recursive: true });
|
|
278
336
|
}
|
|
279
|
-
logger.info(`开始生成语言文件,输出目录:${outputRoot}
|
|
337
|
+
logger.info(`开始生成语言文件,输出目录:${outputRoot}`, true);
|
|
280
338
|
// 按语言生成对应的json文件,默认语言的key=value不生成文件
|
|
281
339
|
for (const [langKey, translations] of Object.entries(langTranslations)) {
|
|
282
340
|
if (Object.keys(translations).length === 0)
|
|
283
341
|
continue;
|
|
284
342
|
if (langKey === defaultLang) {
|
|
285
|
-
logger.info(`跳过默认语言(${langKey})的json
|
|
343
|
+
logger.info(`跳过默认语言(${langKey})的json文件生成`, true);
|
|
286
344
|
continue; // 跳过默认语言文件生成
|
|
287
345
|
}
|
|
288
346
|
const langDir = path.join(outputRoot, langKey);
|
|
@@ -293,9 +351,53 @@ export async function excel2json(program) {
|
|
|
293
351
|
fs.writeFileSync(filePath, JSON.stringify(translations, null, 2), {
|
|
294
352
|
encoding: 'utf-8',
|
|
295
353
|
});
|
|
296
|
-
logger.info(`已生成语言文件:${filePath}
|
|
354
|
+
logger.info(`已生成语言文件:${filePath}`, true);
|
|
355
|
+
}
|
|
356
|
+
// 生成语言检测结果excel文件
|
|
357
|
+
logger.info('开始生成语言检测结果excel文件', true);
|
|
358
|
+
// 构造检测结果excel的表头:默认语言列 + 其他语言列(对应原文列名)
|
|
359
|
+
// 这里表头用原excel的表头中对应语言列的值
|
|
360
|
+
const errorSheetHeader = [];
|
|
361
|
+
// 按列索引顺序遍历,匹配语言key,构造表头
|
|
362
|
+
Object.entries(colIndexToLangKey)
|
|
363
|
+
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
|
364
|
+
.forEach(([colIdxStr, langKey]) => {
|
|
365
|
+
const colIdx = Number(colIdxStr);
|
|
366
|
+
// 表头为原excel表头中对应列的文字
|
|
367
|
+
errorSheetHeader.push(headerRow[colIdx]);
|
|
368
|
+
});
|
|
369
|
+
// 构造检测结果excel的内容,每一列对应语言检测错误描述
|
|
370
|
+
// 每行对应原excel中一条数据行
|
|
371
|
+
const errorSheetData = [errorSheetHeader];
|
|
372
|
+
// 数据行数(不包括表头)
|
|
373
|
+
const dataRowCount = rows.length - 1;
|
|
374
|
+
for (let i = 0; i < dataRowCount; i++) {
|
|
375
|
+
const rowErrors = [];
|
|
376
|
+
// 按列索引顺序遍历,填充对应语言的检测错误
|
|
377
|
+
Object.entries(colIndexToLangKey)
|
|
378
|
+
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
|
379
|
+
.forEach(([colIdxStr, langKey]) => {
|
|
380
|
+
const errorsArr = langCheckErrorsMap[langKey];
|
|
381
|
+
if (errorsArr && errorsArr.length > i) {
|
|
382
|
+
rowErrors.push(errorsArr[i] || '');
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// 可能某些语言词条数量不足时,填空
|
|
386
|
+
rowErrors.push('');
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
errorSheetData.push(rowErrors);
|
|
297
390
|
}
|
|
298
|
-
|
|
391
|
+
// 生成excel工作簿和工作表
|
|
392
|
+
const errorWorkbook = XLSX.utils.book_new();
|
|
393
|
+
const errorSheet = XLSX.utils.aoa_to_sheet(errorSheetData);
|
|
394
|
+
XLSX.utils.book_append_sheet(errorWorkbook, errorSheet, 'LanguageCheckResults');
|
|
395
|
+
// 写入检测结果excel文件,固定文件名 lang_check_results.xlsx
|
|
396
|
+
const errorExcelPath = path.join(outputRoot, `lang_check_results.xlsx`);
|
|
397
|
+
XLSX.writeFile(errorWorkbook, errorExcelPath);
|
|
398
|
+
logger.info(`语言检测结果excel文件已生成:${errorExcelPath}`, true);
|
|
399
|
+
// 最终完成提示,包含输出目录
|
|
400
|
+
logger.info(`全部转换完成,语言文件输出目录:${outputRoot}`, true);
|
|
299
401
|
}
|
|
300
402
|
catch (error) {
|
|
301
403
|
// 记录错误日志,方便排查
|
package/dist/utils/index.js
CHANGED
|
@@ -27,32 +27,32 @@ export function getTimestamp() {
|
|
|
27
27
|
return `${year}${month}${day}${hour}${minute}${second}`;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @returns
|
|
30
|
+
* 获取项目根目录路径,兼容 ESM
|
|
31
|
+
* @returns 根目录绝对路径
|
|
32
32
|
*/
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (process.argv.length > 1) {
|
|
41
|
-
return path.resolve(process.cwd(), process.argv[1]);
|
|
42
|
-
}
|
|
43
|
-
// 最终兜底
|
|
44
|
-
return '';
|
|
45
|
-
}
|
|
33
|
+
function getRootDir() {
|
|
34
|
+
// 当前模块文件路径
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
// 当前模块目录
|
|
37
|
+
const __dirname = path.dirname(__filename);
|
|
38
|
+
// 根目录为当前模块目录的上两级,视项目结构调整
|
|
39
|
+
return path.resolve(__dirname, '../../');
|
|
46
40
|
}
|
|
47
41
|
/**
|
|
48
42
|
* 默认日志配置
|
|
49
|
-
* logsDir
|
|
43
|
+
* logsDir 默认设置为项目根目录的 logs 文件夹
|
|
50
44
|
*/
|
|
51
45
|
const defaultOptions = {
|
|
52
|
-
logsDir: '',
|
|
53
|
-
filenameFormatter: (date) =>
|
|
46
|
+
logsDir: path.resolve(getRootDir(), 'logs'),
|
|
47
|
+
filenameFormatter: (date) => {
|
|
48
|
+
// 使用本地时间格式化,格式 YYYYMMDD.txt
|
|
49
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
50
|
+
const year = date.getFullYear();
|
|
51
|
+
const month = pad(date.getMonth() + 1);
|
|
52
|
+
const day = pad(date.getDate());
|
|
53
|
+
return `${year}${month}${day}.txt`;
|
|
54
|
+
},
|
|
54
55
|
env: process.env.NODE_ENV || 'production',
|
|
55
|
-
entryFilePath: getEntryFilePath(),
|
|
56
56
|
};
|
|
57
57
|
/**
|
|
58
58
|
* 格式化日志内容,支持字符串或对象
|
|
@@ -62,7 +62,24 @@ const defaultOptions = {
|
|
|
62
62
|
* @returns 格式化后的日志字符串
|
|
63
63
|
*/
|
|
64
64
|
function formatLogLine(level, message, date) {
|
|
65
|
-
|
|
65
|
+
// 使用本地时间格式化为 ISO-like 字符串(带时区偏移)
|
|
66
|
+
// 格式示例:2026-01-23T13:55:00+08:00
|
|
67
|
+
function formatLocalISO(date) {
|
|
68
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
69
|
+
const year = date.getFullYear();
|
|
70
|
+
const month = pad(date.getMonth() + 1);
|
|
71
|
+
const day = pad(date.getDate());
|
|
72
|
+
const hour = pad(date.getHours());
|
|
73
|
+
const minute = pad(date.getMinutes());
|
|
74
|
+
const second = pad(date.getSeconds());
|
|
75
|
+
// 计算时区偏移,单位分钟
|
|
76
|
+
const tzOffset = -date.getTimezoneOffset();
|
|
77
|
+
const sign = tzOffset >= 0 ? '+' : '-';
|
|
78
|
+
const tzHour = pad(Math.floor(Math.abs(tzOffset) / 60));
|
|
79
|
+
const tzMinute = pad(Math.abs(tzOffset) % 60);
|
|
80
|
+
return `${year}-${month}-${day}T${hour}:${minute}:${second}${sign}${tzHour}:${tzMinute}`;
|
|
81
|
+
}
|
|
82
|
+
const timeStr = formatLocalISO(date);
|
|
66
83
|
let msgStr;
|
|
67
84
|
if (typeof message === 'string') {
|
|
68
85
|
msgStr = message;
|
|
@@ -89,10 +106,8 @@ export class Logger {
|
|
|
89
106
|
constructor(options) {
|
|
90
107
|
var _a, _b;
|
|
91
108
|
const opts = { ...defaultOptions, ...options };
|
|
92
|
-
// 如果未传 logsDir
|
|
93
|
-
this.logsDir =
|
|
94
|
-
opts.logsDir ||
|
|
95
|
-
path.resolve(path.dirname(opts.entryFilePath), '..', 'logs');
|
|
109
|
+
// 如果未传 logsDir,则默认设置为根目录的 logs 文件夹
|
|
110
|
+
this.logsDir = opts.logsDir;
|
|
96
111
|
this.filenameFormatter =
|
|
97
112
|
(_a = opts.filenameFormatter) !== null && _a !== void 0 ? _a : defaultOptions.filenameFormatter;
|
|
98
113
|
this.env = (_b = opts.env) !== null && _b !== void 0 ? _b : defaultOptions.env;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
[2026-01-27T15:35:46+08:00] [INFO] td-web-cli程序启动
|
|
2
|
+
[2026-01-27T15:35:46+08:00] [INFO] 命令行参数解析完成:
|
|
3
|
+
[2026-01-27T15:35:59+08:00] [INFO] 用户选择模块:国际化
|
|
4
|
+
[2026-01-27T15:35:59+08:00] [INFO] 国际化模块开始执行
|
|
5
|
+
[2026-01-27T15:35:59+08:00] [INFO] 国际化模块启动,等待用户选择功能
|
|
6
|
+
[2026-01-27T15:36:01+08:00] [INFO] 用户选择功能:excel转json
|
|
7
|
+
[2026-01-27T15:36:01+08:00] [INFO] excel转json功能开始执行
|
|
8
|
+
[2026-01-27T15:36:01+08:00] [INFO] 开始加载配置文件:D:\person\code\td-web-cli\setting.json
|
|
9
|
+
[2026-01-27T15:36:01+08:00] [INFO] 配置文件加载成功
|
|
10
|
+
[2026-01-27T15:36:01+08:00] [INFO] 尝试获取在线支持的语言列表...
|
|
11
|
+
[2026-01-27T15:36:03+08:00] [INFO] 成功获取语言列表,覆盖配置文件中的 longCodes
|
|
12
|
+
[2026-01-27T15:36:11+08:00] [INFO] 开始读取excel文件:d:\test\词条\A9-V4-CTC01翻译词条-20251215 - 副本.xlsx
|
|
13
|
+
[2026-01-27T15:36:11+08:00] [INFO] 开始解析表头
|
|
14
|
+
[2026-01-27T15:36:11+08:00] [INFO] 开始解析数据行
|
|
15
|
+
[2026-01-27T15:36:11+08:00] [INFO] 开始对语言(cn)词条进行语言检测,词条数量:12
|
|
16
|
+
[2026-01-27T15:36:12+08:00] [INFO] 语言(cn)词条检测无错误
|
|
17
|
+
[2026-01-27T15:36:12+08:00] [INFO] 开始对语言(en)词条进行语言检测,词条数量:12
|
|
18
|
+
[2026-01-27T15:36:13+08:00] [INFO] 语言(en)词条检测发现问题,词条数量: 4
|
|
19
|
+
[2026-01-27T15:36:13+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
|
|
20
|
+
出错句子: Enjoy your new WiFi network!
|
|
21
|
+
建议替换: Wi-Fi
|
|
22
|
+
[2026-01-27T15:36:13+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
|
|
23
|
+
出错句子: The old WiFi network has been disconnected.
|
|
24
|
+
建议替换: Wi-Fi
|
|
25
|
+
[2026-01-27T15:36:13+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
|
|
26
|
+
出错句子: To access the setup page again, please connect to your new WiFi network.
|
|
27
|
+
建议替换: Wi-Fi
|
|
28
|
+
[2026-01-27T15:36:13+08:00] [INFO] - 错误: Possible spelling mistake found.
|
|
29
|
+
出错句子: Return to Login
|
|
30
|
+
BrosTrend N300 Wi-Fi to Ethernet Adapter
|
|
31
|
+
Applying settings...
|
|
32
|
+
建议替换: Bros Trend
|
|
33
|
+
[2026-01-27T15:36:13+08:00] [INFO] 开始对语言(zh)词条进行语言检测,词条数量:12
|
|
34
|
+
[2026-01-27T15:36:14+08:00] [ERROR] 程序执行时发生错误:Error: https://api.languagetool.org/v2/check接口报错:AxiosError: Request failed with status code 400
|
|
35
|
+
at settle (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/settle.js:19:12)
|
|
36
|
+
at IncomingMessage.handleStreamEnd (file:///D:/person/code/td-web-cli/node_modules/axios/lib/adapters/http.js:798:11)
|
|
37
|
+
at IncomingMessage.emit (node:events:530:35)
|
|
38
|
+
at endReadableNT (node:internal/streams/readable:1698:12)
|
|
39
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
|
|
40
|
+
at Axios.request (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/Axios.js:45:41)
|
|
41
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
|
42
|
+
at async postData (file:///D:/person/code/td-web-cli/dist/api/index.js:10:17)
|
|
43
|
+
at async languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:246:21)
|
|
44
|
+
at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
|
|
45
|
+
at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:304:34)
|
|
46
|
+
at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
|
|
47
|
+
at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
|
|
48
|
+
at languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:253:15)
|
|
49
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
|
50
|
+
at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
|
|
51
|
+
at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:304:34)
|
|
52
|
+
at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
|
|
53
|
+
at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
|
|
54
|
+
[2026-01-27T15:36:14+08:00] [ERROR] 语言(zh)词条检测失败
|
|
55
|
+
[2026-01-27T15:36:14+08:00] [INFO] 开始对语言(uk)词条进行语言检测,词条数量:12
|
|
56
|
+
[2026-01-27T15:36:16+08:00] [INFO] 语言(uk)词条检测发现问题,词条数量: 3
|
|
57
|
+
[2026-01-27T15:36:16+08:00] [INFO] - 错误: «Підключіть» — нерекомендоване слово, кращий варіант: увімкнути, під'єднати, приєднати.
|
|
58
|
+
出错句子: Підключіть точку доступу до вашого роутера або активного порту Ethernet за допомогою кабелю Ethernet.
|
|
59
|
+
建议替换: Увімкнути, Під'єднати, Приєднати
|
|
60
|
+
[2026-01-27T15:36:16+08:00] [INFO] - 错误: «підключіться» — нерекомендоване слово, кращий варіант: увімкнутися, під'єднатися, приєднатися.
|
|
61
|
+
出错句子: Щоб знову отримати доступ до сторінки налаштувань, підключіться до своєї нової мережі WiFi.
|
|
62
|
+
建议替换: увімкнутися, під'єднатися, приєднатися
|
|
63
|
+
[2026-01-27T15:36:16+08:00] [INFO] - 错误: «веб-інтерфейс» — написання не відповідає чинній версії правопису, виправлення: вебінтерфейс.
|
|
64
|
+
出错句子: Після скидання пристрою відвідайте http://brostrendwifi.com для входу в веб-інтерфейс.
|
|
65
|
+
建议替换: вебінтерфейс
|
|
66
|
+
[2026-01-27T15:36:16+08:00] [INFO] 开始生成语言文件,输出目录:d:\test\词条\lang_20260127153616
|
|
67
|
+
[2026-01-27T15:36:16+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127153616\cn\translate.json
|
|
68
|
+
[2026-01-27T15:36:16+08:00] [INFO] 跳过默认语言(en)的json文件生成
|
|
69
|
+
[2026-01-27T15:36:16+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127153616\zh\translate.json
|
|
70
|
+
[2026-01-27T15:36:16+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127153616\uk\translate.json
|
|
71
|
+
[2026-01-27T15:36:16+08:00] [INFO] 开始生成语言检测结果excel文件
|
|
72
|
+
[2026-01-27T15:36:16+08:00] [INFO] 语言检测结果excel文件已生成:d:\test\词条\lang_20260127153616\lang_check_results.xlsx
|
|
73
|
+
[2026-01-27T15:36:16+08:00] [INFO] 全部转换完成,语言文件输出目录:d:\test\词条\lang_20260127153616
|
|
74
|
+
[2026-01-27T15:36:16+08:00] [INFO] excel转json功能执行完成
|
|
75
|
+
[2026-01-27T15:36:16+08:00] [INFO] 国际化模块执行完成
|
|
76
|
+
[2026-01-27T16:26:38+08:00] [INFO] td-web-cli程序启动
|
|
77
|
+
[2026-01-27T16:26:38+08:00] [INFO] 命令行参数解析完成:
|
|
78
|
+
[2026-01-27T16:26:41+08:00] [INFO] 用户选择模块:国际化
|
|
79
|
+
[2026-01-27T16:26:41+08:00] [INFO] 国际化模块开始执行
|
|
80
|
+
[2026-01-27T16:26:41+08:00] [INFO] 国际化模块启动,等待用户选择功能
|
|
81
|
+
[2026-01-27T16:26:42+08:00] [INFO] 用户选择功能:excel转json
|
|
82
|
+
[2026-01-27T16:26:42+08:00] [INFO] excel转json功能开始执行
|
|
83
|
+
[2026-01-27T16:26:42+08:00] [INFO] 开始加载配置文件:D:\person\code\td-web-cli\setting.json
|
|
84
|
+
[2026-01-27T16:26:42+08:00] [INFO] 配置文件加载成功
|
|
85
|
+
[2026-01-27T16:26:42+08:00] [INFO] 尝试获取在线支持的语言列表...
|
|
86
|
+
[2026-01-27T16:26:44+08:00] [INFO] 成功获取语言列表,覆盖配置文件中的 longCodes
|
|
87
|
+
[2026-01-27T16:26:49+08:00] [INFO] 开始读取excel文件:d:\test\词条\A9-V4-CTC01翻译词条-20251215 - 副本.xlsx
|
|
88
|
+
[2026-01-27T16:26:49+08:00] [INFO] 开始解析表头
|
|
89
|
+
[2026-01-27T16:26:49+08:00] [INFO] 开始解析数据行
|
|
90
|
+
[2026-01-27T16:26:49+08:00] [INFO] 开始对语言(cn)词条进行语言检测,词条数量:12 (1/4)
|
|
91
|
+
[2026-01-27T16:26:50+08:00] [INFO] 语言(cn)词条检测无错误
|
|
92
|
+
[2026-01-27T16:26:50+08:00] [INFO] 开始对语言(en)词条进行语言检测,词条数量:12 (2/4)
|
|
93
|
+
[2026-01-27T16:26:51+08:00] [INFO] 语言(en)词条检测发现问题,词条数量: 4
|
|
94
|
+
[2026-01-27T16:26:51+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
|
|
95
|
+
出错句子: Enjoy your new WiFi network!
|
|
96
|
+
建议替换: Wi-Fi
|
|
97
|
+
[2026-01-27T16:26:51+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
|
|
98
|
+
出错句子: The old WiFi network has been disconnected.
|
|
99
|
+
建议替换: Wi-Fi
|
|
100
|
+
[2026-01-27T16:26:51+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
|
|
101
|
+
出错句子: To access the setup page again, please connect to your new WiFi network.
|
|
102
|
+
建议替换: Wi-Fi
|
|
103
|
+
[2026-01-27T16:26:51+08:00] [INFO] - 错误: Possible spelling mistake found.
|
|
104
|
+
出错句子: Return to Login
|
|
105
|
+
BrosTrend N300 Wi-Fi to Ethernet Adapter
|
|
106
|
+
Applying settings...
|
|
107
|
+
建议替换: Bros Trend
|
|
108
|
+
[2026-01-27T16:26:51+08:00] [INFO] 开始对语言(zh)词条进行语言检测,词条数量:12 (3/4)
|
|
109
|
+
[2026-01-27T16:26:51+08:00] [ERROR] 程序执行时发生错误:Error: https://api.languagetool.org/v2/check接口报错:AxiosError: Request failed with status code 400
|
|
110
|
+
at settle (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/settle.js:19:12)
|
|
111
|
+
at IncomingMessage.handleStreamEnd (file:///D:/person/code/td-web-cli/node_modules/axios/lib/adapters/http.js:798:11)
|
|
112
|
+
at IncomingMessage.emit (node:events:530:35)
|
|
113
|
+
at endReadableNT (node:internal/streams/readable:1698:12)
|
|
114
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
|
|
115
|
+
at Axios.request (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/Axios.js:45:41)
|
|
116
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
|
117
|
+
at async postData (file:///D:/person/code/td-web-cli/dist/api/index.js:10:17)
|
|
118
|
+
at async languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:246:21)
|
|
119
|
+
at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
|
|
120
|
+
at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:307:34)
|
|
121
|
+
at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
|
|
122
|
+
at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
|
|
123
|
+
at languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:253:15)
|
|
124
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
|
125
|
+
at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
|
|
126
|
+
at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:307:34)
|
|
127
|
+
at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
|
|
128
|
+
at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
|
|
129
|
+
[2026-01-27T16:26:51+08:00] [ERROR] 语言(zh)词条检测失败
|
|
130
|
+
[2026-01-27T16:26:51+08:00] [INFO] 开始对语言(uk)词条进行语言检测,词条数量:12 (4/4)
|
|
131
|
+
[2026-01-27T16:26:53+08:00] [INFO] 语言(uk)词条检测发现问题,词条数量: 3
|
|
132
|
+
[2026-01-27T16:26:53+08:00] [INFO] - 错误: «Підключіть» — нерекомендоване слово, кращий варіант: увімкнути, під'єднати, приєднати.
|
|
133
|
+
出错句子: Підключіть точку доступу до вашого роутера або активного порту Ethernet за допомогою кабелю Ethernet.
|
|
134
|
+
建议替换: Увімкнути, Під'єднати, Приєднати
|
|
135
|
+
[2026-01-27T16:26:53+08:00] [INFO] - 错误: «підключіться» — нерекомендоване слово, кращий варіант: увімкнутися, під'єднатися, приєднатися.
|
|
136
|
+
出错句子: Щоб знову отримати доступ до сторінки налаштувань, підключіться до своєї нової мережі WiFi.
|
|
137
|
+
建议替换: увімкнутися, під'єднатися, приєднатися
|
|
138
|
+
[2026-01-27T16:26:53+08:00] [INFO] - 错误: «веб-інтерфейс» — написання не відповідає чинній версії правопису, виправлення: вебінтерфейс.
|
|
139
|
+
出错句子: Після скидання пристрою відвідайте http://brostrendwifi.com для входу в веб-інтерфейс.
|
|
140
|
+
建议替换: вебінтерфейс
|
|
141
|
+
[2026-01-27T16:26:53+08:00] [INFO] 开始生成语言文件,输出目录:d:\test\词条\lang_20260127162653
|
|
142
|
+
[2026-01-27T16:26:53+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127162653\cn\translate.json
|
|
143
|
+
[2026-01-27T16:26:53+08:00] [INFO] 跳过默认语言(en)的json文件生成
|
|
144
|
+
[2026-01-27T16:26:53+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127162653\zh\translate.json
|
|
145
|
+
[2026-01-27T16:26:53+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127162653\uk\translate.json
|
|
146
|
+
[2026-01-27T16:26:53+08:00] [INFO] 开始生成语言检测结果excel文件
|
|
147
|
+
[2026-01-27T16:26:53+08:00] [INFO] 语言检测结果excel文件已生成:d:\test\词条\lang_20260127162653\lang_check_results.xlsx
|
|
148
|
+
[2026-01-27T16:26:53+08:00] [INFO] 全部转换完成,语言文件输出目录:d:\test\词条\lang_20260127162653
|
|
149
|
+
[2026-01-27T16:26:53+08:00] [INFO] excel转json功能执行完成
|
|
150
|
+
[2026-01-27T16:26:53+08:00] [INFO] 国际化模块执行完成
|
package/package.json
CHANGED
|
@@ -135,6 +135,54 @@ async function batchCheckTexts(
|
|
|
135
135
|
return results;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* 将语言检测结果转换成每条词条对应的错误描述数组
|
|
140
|
+
* @param checkResult 语言检测结果
|
|
141
|
+
* @param texts 词条数组(对应检测文本)
|
|
142
|
+
* @returns 按词条分割的检测错误描述数组,顺序对应输入texts
|
|
143
|
+
*/
|
|
144
|
+
function parseCheckResultPerEntry(
|
|
145
|
+
checkResult: CheckResult,
|
|
146
|
+
texts: string[]
|
|
147
|
+
): string[] {
|
|
148
|
+
// 初始化每条词条对应的错误信息数组
|
|
149
|
+
const entryErrors = new Array(texts.length).fill('').map(() => '');
|
|
150
|
+
|
|
151
|
+
// 语言检测返回的matches是针对整个拼接文本的,需要拆分到对应词条
|
|
152
|
+
// 计算每条词条在拼接文本中的起始位置和结束位置
|
|
153
|
+
const positions: { start: number; end: number }[] = [];
|
|
154
|
+
let pos = 0;
|
|
155
|
+
for (const text of texts) {
|
|
156
|
+
const len = text.length;
|
|
157
|
+
positions.push({ start: pos, end: pos + len });
|
|
158
|
+
pos += len + 1; // +1是换行符长度
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 遍历所有错误匹配项,将错误信息分配到对应词条
|
|
162
|
+
for (const match of checkResult.matches) {
|
|
163
|
+
const errorOffset = match.offset;
|
|
164
|
+
// 找出错误所在的词条索引
|
|
165
|
+
const idx = positions.findIndex(
|
|
166
|
+
(range) => errorOffset >= range.start && errorOffset < range.end
|
|
167
|
+
);
|
|
168
|
+
if (idx === -1) continue;
|
|
169
|
+
|
|
170
|
+
// 生成错误信息字符串
|
|
171
|
+
const errMsg = `错误: ${match.message}\n出错句子: ${match.sentence}\n建议替换: ${match.replacements
|
|
172
|
+
.map((r) => r.value)
|
|
173
|
+
.join(', ')}`;
|
|
174
|
+
|
|
175
|
+
// 多条错误用换行分隔
|
|
176
|
+
if (entryErrors[idx]) {
|
|
177
|
+
entryErrors[idx] += '\n' + errMsg;
|
|
178
|
+
} else {
|
|
179
|
+
entryErrors[idx] = errMsg;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return entryErrors;
|
|
184
|
+
}
|
|
185
|
+
|
|
138
186
|
/**
|
|
139
187
|
* excel转json功能主函数
|
|
140
188
|
* 读取用户输入的excel路径,解析内容,根据配置生成多语言json文件
|
|
@@ -143,14 +191,14 @@ async function batchCheckTexts(
|
|
|
143
191
|
*/
|
|
144
192
|
export async function excel2json(program: Command) {
|
|
145
193
|
// 配置文件默认路径
|
|
146
|
-
const configPath = path.join(__dirname, '
|
|
194
|
+
const configPath = path.join(__dirname, '../../../../setting.json');
|
|
147
195
|
let i18nConfig: I18nConfig;
|
|
148
196
|
|
|
149
197
|
// 加载配置文件
|
|
150
198
|
try {
|
|
151
|
-
logger.info(`开始加载配置文件:${configPath}
|
|
199
|
+
logger.info(`开始加载配置文件:${configPath}`, true);
|
|
152
200
|
i18nConfig = loadConfig(configPath);
|
|
153
|
-
logger.info('配置文件加载成功');
|
|
201
|
+
logger.info('配置文件加载成功', true);
|
|
154
202
|
} catch (error: unknown) {
|
|
155
203
|
const msg = `读取配置文件失败:${normalizeError(error).stack},程序已退出`;
|
|
156
204
|
logger.error(msg);
|
|
@@ -160,9 +208,9 @@ export async function excel2json(program: Command) {
|
|
|
160
208
|
|
|
161
209
|
// 尝试调用接口获取支持的语言列表,更新 longCodes
|
|
162
210
|
try {
|
|
163
|
-
logger.info('尝试获取在线支持的语言列表...');
|
|
211
|
+
logger.info('尝试获取在线支持的语言列表...', true);
|
|
164
212
|
const languageTools = await getLanguageTool();
|
|
165
|
-
logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes
|
|
213
|
+
logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes`, true);
|
|
166
214
|
|
|
167
215
|
// 构建新的 longCodes 映射
|
|
168
216
|
const newLongCodes: Record<string, string> = {};
|
|
@@ -193,6 +241,7 @@ export async function excel2json(program: Command) {
|
|
|
193
241
|
logger.warn(
|
|
194
242
|
`获取在线语言列表失败,使用本地配置 longCodes,错误:${normalizeError(error).stack}`
|
|
195
243
|
);
|
|
244
|
+
console.warn('获取在线语言列表失败,使用本地配置 longCodes');
|
|
196
245
|
}
|
|
197
246
|
|
|
198
247
|
// 交互式输入excel文件路径并校验
|
|
@@ -215,7 +264,7 @@ export async function excel2json(program: Command) {
|
|
|
215
264
|
);
|
|
216
265
|
|
|
217
266
|
try {
|
|
218
|
-
logger.info(`开始读取excel文件:${excelPath}
|
|
267
|
+
logger.info(`开始读取excel文件:${excelPath}`, true);
|
|
219
268
|
|
|
220
269
|
// 读取excel文件
|
|
221
270
|
const workbook = XLSX.readFile(excelPath);
|
|
@@ -236,7 +285,7 @@ export async function excel2json(program: Command) {
|
|
|
236
285
|
process.exit(1);
|
|
237
286
|
}
|
|
238
287
|
|
|
239
|
-
logger.info('开始解析表头');
|
|
288
|
+
logger.info('开始解析表头', true);
|
|
240
289
|
// 处理表头行,去除空格,转成字符串
|
|
241
290
|
const headerRow = rows[0].map((cell) => (cell ? String(cell).trim() : ''));
|
|
242
291
|
|
|
@@ -268,7 +317,7 @@ export async function excel2json(program: Command) {
|
|
|
268
317
|
langTranslations[langKey] = {};
|
|
269
318
|
});
|
|
270
319
|
|
|
271
|
-
logger.info('开始解析数据行');
|
|
320
|
+
logger.info('开始解析数据行', true);
|
|
272
321
|
// 遍历数据行,提取所有语言词条
|
|
273
322
|
// key统一用默认语言列的值,其他语言对应的列为翻译内容
|
|
274
323
|
const langKeysMap: Record<string, string[]> = {}; // 语言key => 词条数组
|
|
@@ -300,40 +349,61 @@ export async function excel2json(program: Command) {
|
|
|
300
349
|
const valStr = String(valCell);
|
|
301
350
|
langTranslations[langKey][key] = valStr;
|
|
302
351
|
langKeysMap[langKey].push(valStr);
|
|
352
|
+
} else {
|
|
353
|
+
// 如果单元格为空,也要保证检测结果数组长度一致,填空字符串
|
|
354
|
+
langKeysMap[langKey].push('');
|
|
303
355
|
}
|
|
304
356
|
}
|
|
305
357
|
}
|
|
306
358
|
|
|
359
|
+
// 语言检测结果映射,语言key => 每条词条的错误描述数组
|
|
360
|
+
const langCheckErrorsMap: Record<string, string[]> = {};
|
|
361
|
+
|
|
307
362
|
// 对所有语言词条批量进行语言检测(包括默认语言)
|
|
308
|
-
|
|
363
|
+
const langKeysEntries = Object.entries(langKeysMap);
|
|
364
|
+
for (let idx = 0; idx < langKeysEntries.length; idx++) {
|
|
365
|
+
const [langKey, texts] = langKeysEntries[idx];
|
|
309
366
|
const longCode = i18nConfig.longCodes[langKey];
|
|
310
367
|
if (!longCode) {
|
|
311
|
-
logger.warn(`语言(${langKey})未配置 longCode
|
|
368
|
+
logger.warn(`语言(${langKey})未配置 longCode,跳过检测`, true);
|
|
369
|
+
langCheckErrorsMap[langKey] =
|
|
370
|
+
texts.length > 0
|
|
371
|
+
? texts.map(() => '未配置 longCode,未进行检测')
|
|
372
|
+
: ['无词条'];
|
|
312
373
|
continue;
|
|
313
374
|
}
|
|
314
375
|
if (texts.length === 0) {
|
|
315
|
-
logger.info(`语言(${langKey})
|
|
376
|
+
logger.info(`语言(${langKey})无词条,跳过检测`, true);
|
|
377
|
+
langCheckErrorsMap[langKey] = ['无词条'];
|
|
316
378
|
continue;
|
|
317
379
|
}
|
|
318
380
|
|
|
319
381
|
logger.info(
|
|
320
|
-
`开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length}
|
|
382
|
+
`开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length} (${idx + 1}/${langKeysEntries.length})`,
|
|
383
|
+
true
|
|
321
384
|
);
|
|
322
385
|
|
|
323
386
|
const checkResults = await batchCheckTexts(texts, longCode);
|
|
324
387
|
|
|
325
388
|
if (!checkResults || checkResults.length === 0 || !checkResults[0]) {
|
|
326
|
-
logger.error(`语言(${langKey})
|
|
389
|
+
logger.error(`语言(${langKey})词条检测失败`, true);
|
|
390
|
+
langCheckErrorsMap[langKey] = texts.map(() => '检测失败,未知错误');
|
|
327
391
|
continue;
|
|
328
392
|
}
|
|
329
393
|
|
|
330
394
|
const result = checkResults[0];
|
|
331
395
|
if (result.matches.length === 0) {
|
|
332
|
-
logger.info(`语言(${langKey})
|
|
396
|
+
logger.info(`语言(${langKey})词条检测无错误`, true);
|
|
397
|
+
langCheckErrorsMap[langKey] = texts.map(() => '无错误');
|
|
333
398
|
} else {
|
|
334
399
|
logger.info(
|
|
335
|
-
`语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}
|
|
400
|
+
`语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}`,
|
|
401
|
+
true
|
|
336
402
|
);
|
|
403
|
+
// 解析检测结果,拆分到每条词条
|
|
404
|
+
langCheckErrorsMap[langKey] = parseCheckResultPerEntry(result, texts);
|
|
405
|
+
|
|
406
|
+
// 详细日志输出
|
|
337
407
|
for (const match of result.matches) {
|
|
338
408
|
logger.info(
|
|
339
409
|
`- 错误: ${match.message}\n 出错句子: ${match.sentence}\n 建议替换: ${match.replacements
|
|
@@ -352,14 +422,14 @@ export async function excel2json(program: Command) {
|
|
|
352
422
|
fs.mkdirSync(outputRoot, { recursive: true });
|
|
353
423
|
}
|
|
354
424
|
|
|
355
|
-
logger.info(`开始生成语言文件,输出目录:${outputRoot}
|
|
425
|
+
logger.info(`开始生成语言文件,输出目录:${outputRoot}`, true);
|
|
356
426
|
|
|
357
427
|
// 按语言生成对应的json文件,默认语言的key=value不生成文件
|
|
358
428
|
for (const [langKey, translations] of Object.entries(langTranslations)) {
|
|
359
429
|
if (Object.keys(translations).length === 0) continue;
|
|
360
430
|
|
|
361
431
|
if (langKey === defaultLang) {
|
|
362
|
-
logger.info(`跳过默认语言(${langKey})的json
|
|
432
|
+
logger.info(`跳过默认语言(${langKey})的json文件生成`, true);
|
|
363
433
|
continue; // 跳过默认语言文件生成
|
|
364
434
|
}
|
|
365
435
|
|
|
@@ -372,10 +442,68 @@ export async function excel2json(program: Command) {
|
|
|
372
442
|
fs.writeFileSync(filePath, JSON.stringify(translations, null, 2), {
|
|
373
443
|
encoding: 'utf-8',
|
|
374
444
|
});
|
|
375
|
-
logger.info(`已生成语言文件:${filePath}
|
|
445
|
+
logger.info(`已生成语言文件:${filePath}`, true);
|
|
376
446
|
}
|
|
377
447
|
|
|
378
|
-
|
|
448
|
+
// 生成语言检测结果excel文件
|
|
449
|
+
logger.info('开始生成语言检测结果excel文件', true);
|
|
450
|
+
|
|
451
|
+
// 构造检测结果excel的表头:默认语言列 + 其他语言列(对应原文列名)
|
|
452
|
+
// 这里表头用原excel的表头中对应语言列的值
|
|
453
|
+
const errorSheetHeader: string[] = [];
|
|
454
|
+
|
|
455
|
+
// 按列索引顺序遍历,匹配语言key,构造表头
|
|
456
|
+
Object.entries(colIndexToLangKey)
|
|
457
|
+
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
|
458
|
+
.forEach(([colIdxStr, langKey]) => {
|
|
459
|
+
const colIdx = Number(colIdxStr);
|
|
460
|
+
// 表头为原excel表头中对应列的文字
|
|
461
|
+
errorSheetHeader.push(headerRow[colIdx]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// 构造检测结果excel的内容,每一列对应语言检测错误描述
|
|
465
|
+
// 每行对应原excel中一条数据行
|
|
466
|
+
const errorSheetData: (string | null)[][] = [errorSheetHeader];
|
|
467
|
+
|
|
468
|
+
// 数据行数(不包括表头)
|
|
469
|
+
const dataRowCount = rows.length - 1;
|
|
470
|
+
|
|
471
|
+
for (let i = 0; i < dataRowCount; i++) {
|
|
472
|
+
const rowErrors: (string | null)[] = [];
|
|
473
|
+
|
|
474
|
+
// 按列索引顺序遍历,填充对应语言的检测错误
|
|
475
|
+
Object.entries(colIndexToLangKey)
|
|
476
|
+
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
|
477
|
+
.forEach(([colIdxStr, langKey]) => {
|
|
478
|
+
const errorsArr = langCheckErrorsMap[langKey];
|
|
479
|
+
if (errorsArr && errorsArr.length > i) {
|
|
480
|
+
rowErrors.push(errorsArr[i] || '');
|
|
481
|
+
} else {
|
|
482
|
+
// 可能某些语言词条数量不足时,填空
|
|
483
|
+
rowErrors.push('');
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
errorSheetData.push(rowErrors);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 生成excel工作簿和工作表
|
|
491
|
+
const errorWorkbook = XLSX.utils.book_new();
|
|
492
|
+
const errorSheet = XLSX.utils.aoa_to_sheet(errorSheetData);
|
|
493
|
+
XLSX.utils.book_append_sheet(
|
|
494
|
+
errorWorkbook,
|
|
495
|
+
errorSheet,
|
|
496
|
+
'LanguageCheckResults'
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// 写入检测结果excel文件,固定文件名 lang_check_results.xlsx
|
|
500
|
+
const errorExcelPath = path.join(outputRoot, `lang_check_results.xlsx`);
|
|
501
|
+
XLSX.writeFile(errorWorkbook, errorExcelPath);
|
|
502
|
+
|
|
503
|
+
logger.info(`语言检测结果excel文件已生成:${errorExcelPath}`, true);
|
|
504
|
+
|
|
505
|
+
// 最终完成提示,包含输出目录
|
|
506
|
+
logger.info(`全部转换完成,语言文件输出目录:${outputRoot}`, true);
|
|
379
507
|
} catch (error: unknown) {
|
|
380
508
|
// 记录错误日志,方便排查
|
|
381
509
|
loggerError(error, logger);
|
package/src/utils/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ type LogLevel = 'INFO' | 'WARN' | 'ERROR';
|
|
|
41
41
|
*/
|
|
42
42
|
interface LoggerOptions {
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* 日志目录,默认根目录下的 logs 文件夹
|
|
45
45
|
*/
|
|
46
46
|
logsDir?: string;
|
|
47
47
|
|
|
@@ -55,41 +55,36 @@ interface LoggerOptions {
|
|
|
55
55
|
* 当前环境,默认 process.env.NODE_ENV
|
|
56
56
|
*/
|
|
57
57
|
env?: string;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 程序入口文件绝对路径,用于确定日志目录位置
|
|
61
|
-
*/
|
|
62
|
-
entryFilePath?: string;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
/**
|
|
66
|
-
*
|
|
67
|
-
* @returns
|
|
61
|
+
* 获取项目根目录路径,兼容 ESM
|
|
62
|
+
* @returns 根目录绝对路径
|
|
68
63
|
*/
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return path.resolve(process.cwd(), process.argv[1]);
|
|
77
|
-
}
|
|
78
|
-
// 最终兜底
|
|
79
|
-
return '';
|
|
80
|
-
}
|
|
64
|
+
function getRootDir(): string {
|
|
65
|
+
// 当前模块文件路径
|
|
66
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
67
|
+
// 当前模块目录
|
|
68
|
+
const __dirname = path.dirname(__filename);
|
|
69
|
+
// 根目录为当前模块目录的上两级,视项目结构调整
|
|
70
|
+
return path.resolve(__dirname, '../../');
|
|
81
71
|
}
|
|
82
72
|
|
|
83
73
|
/**
|
|
84
74
|
* 默认日志配置
|
|
85
|
-
* logsDir
|
|
75
|
+
* logsDir 默认设置为项目根目录的 logs 文件夹
|
|
86
76
|
*/
|
|
87
77
|
const defaultOptions: LoggerOptions = {
|
|
88
|
-
logsDir: '',
|
|
89
|
-
filenameFormatter: (date: Date) =>
|
|
90
|
-
|
|
78
|
+
logsDir: path.resolve(getRootDir(), 'logs'),
|
|
79
|
+
filenameFormatter: (date: Date) => {
|
|
80
|
+
// 使用本地时间格式化,格式 YYYYMMDD.txt
|
|
81
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
82
|
+
const year = date.getFullYear();
|
|
83
|
+
const month = pad(date.getMonth() + 1);
|
|
84
|
+
const day = pad(date.getDate());
|
|
85
|
+
return `${year}${month}${day}.txt`;
|
|
86
|
+
},
|
|
91
87
|
env: process.env.NODE_ENV || 'production',
|
|
92
|
-
entryFilePath: getEntryFilePath(),
|
|
93
88
|
};
|
|
94
89
|
|
|
95
90
|
/**
|
|
@@ -100,7 +95,29 @@ const defaultOptions: LoggerOptions = {
|
|
|
100
95
|
* @returns 格式化后的日志字符串
|
|
101
96
|
*/
|
|
102
97
|
function formatLogLine(level: LogLevel, message: unknown, date: Date): string {
|
|
103
|
-
|
|
98
|
+
// 使用本地时间格式化为 ISO-like 字符串(带时区偏移)
|
|
99
|
+
// 格式示例:2026-01-23T13:55:00+08:00
|
|
100
|
+
function formatLocalISO(date: Date): string {
|
|
101
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
102
|
+
|
|
103
|
+
const year = date.getFullYear();
|
|
104
|
+
const month = pad(date.getMonth() + 1);
|
|
105
|
+
const day = pad(date.getDate());
|
|
106
|
+
const hour = pad(date.getHours());
|
|
107
|
+
const minute = pad(date.getMinutes());
|
|
108
|
+
const second = pad(date.getSeconds());
|
|
109
|
+
|
|
110
|
+
// 计算时区偏移,单位分钟
|
|
111
|
+
const tzOffset = -date.getTimezoneOffset();
|
|
112
|
+
const sign = tzOffset >= 0 ? '+' : '-';
|
|
113
|
+
const tzHour = pad(Math.floor(Math.abs(tzOffset) / 60));
|
|
114
|
+
const tzMinute = pad(Math.abs(tzOffset) % 60);
|
|
115
|
+
|
|
116
|
+
return `${year}-${month}-${day}T${hour}:${minute}:${second}${sign}${tzHour}:${tzMinute}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const timeStr = formatLocalISO(date);
|
|
120
|
+
|
|
104
121
|
let msgStr: string;
|
|
105
122
|
|
|
106
123
|
if (typeof message === 'string') {
|
|
@@ -132,10 +149,8 @@ export class Logger {
|
|
|
132
149
|
constructor(options?: LoggerOptions) {
|
|
133
150
|
const opts = { ...defaultOptions, ...options };
|
|
134
151
|
|
|
135
|
-
// 如果未传 logsDir
|
|
136
|
-
this.logsDir =
|
|
137
|
-
opts.logsDir ||
|
|
138
|
-
path.resolve(path.dirname(opts.entryFilePath!), '..', 'logs');
|
|
152
|
+
// 如果未传 logsDir,则默认设置为根目录的 logs 文件夹
|
|
153
|
+
this.logsDir = opts.logsDir!;
|
|
139
154
|
this.filenameFormatter =
|
|
140
155
|
opts.filenameFormatter ?? defaultOptions.filenameFormatter!;
|
|
141
156
|
this.env = opts.env ?? defaultOptions.env!;
|