td-web-cli 0.1.6 → 0.1.8

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.
@@ -1,513 +0,0 @@
1
- import { Command } from 'commander';
2
- import { input } from '@inquirer/prompts';
3
- import XLSX from 'xlsx';
4
- import fs from 'fs';
5
- import { fileURLToPath } from 'url';
6
- import path from 'path';
7
- import {
8
- getTimestamp,
9
- logger,
10
- loggerError,
11
- normalizeError,
12
- CheckResult,
13
- languageToolCheck,
14
- getLanguageTool,
15
- } from '../../../utils/index.js';
16
-
17
- // 获取当前文件目录
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = path.dirname(__filename);
20
-
21
- type Row = (string | number | null | undefined)[];
22
-
23
- /**
24
- * 国际化配置类型定义
25
- * defaultKey: 默认语言key
26
- * langs: 语言映射,key为语言标识,value为语言名称数组(支持多名称匹配)
27
- * longCodes: 语言长代码映射,key为语言标识,value为语言长代码
28
- */
29
- interface I18nConfig {
30
- defaultKey: string;
31
- langs: Record<string, string[]>;
32
- longCodes: Record<string, string>;
33
- }
34
-
35
- /**
36
- * 读取并解析配置文件
37
- * @param configPath 配置文件路径
38
- * @returns I18nConfig 配置对象
39
- * @throws 配置文件不存在或格式错误时抛出异常
40
- */
41
- function loadConfig(configPath: string): I18nConfig {
42
- if (!fs.existsSync(configPath)) {
43
- throw new Error(`配置文件不存在:${configPath}`);
44
- }
45
- const content = fs.readFileSync(configPath, { encoding: 'utf-8' });
46
- const json = JSON.parse(content);
47
- if (!json.i18n) {
48
- throw new Error('配置文件格式错误,缺少i18n');
49
- }
50
- if (!json.i18n.defaultKey) {
51
- throw new Error('配置文件格式错误,缺少i18n.defaultKey');
52
- }
53
- if (!json.i18n.langs) {
54
- throw new Error('配置文件格式错误,缺少i18n.langs');
55
- }
56
- if (!json.i18n.longCodes) {
57
- throw new Error('配置文件格式错误,缺少i18n.longCodes');
58
- }
59
- return json.i18n;
60
- }
61
-
62
- /**
63
- * 匹配excel表头列名对应的语言key,支持大小写不敏感匹配
64
- * 先匹配语言key本身,再匹配语言名称数组(包含关系)
65
- * @param colName 表头列名
66
- * @param langs 语言映射
67
- * @returns 匹配到的语言key,未匹配返回null
68
- */
69
- function matchLangKey(
70
- colName: string,
71
- langs: Record<string, string[]>
72
- ): string | null {
73
- if (!colName) return null;
74
- const colNameLower = colName.toLowerCase();
75
-
76
- // 先尝试匹配语言key
77
- for (const langKey of Object.keys(langs)) {
78
- if (langKey.toLowerCase() === colNameLower) {
79
- return langKey;
80
- }
81
- }
82
-
83
- // 再尝试匹配语言名称(包含关系)
84
- for (const [langKey, names] of Object.entries(langs)) {
85
- if (
86
- names.some((name) => name && colNameLower.includes(name.toLowerCase()))
87
- ) {
88
- return langKey;
89
- }
90
- }
91
-
92
- return null;
93
- }
94
-
95
- /**
96
- * 去除字符串首尾的单引号或双引号
97
- * @param str 输入字符串
98
- * @returns 去除引号后的字符串
99
- */
100
- function trimQuotes(str: string): string {
101
- if (
102
- (str.startsWith('"') && str.endsWith('"')) ||
103
- (str.startsWith("'") && str.endsWith("'"))
104
- ) {
105
- return str.slice(1, -1);
106
- }
107
- return str;
108
- }
109
-
110
- /**
111
- * 批量检测词条文本,返回所有检测结果
112
- * @param texts 词条数组
113
- * @param language 语言代码
114
- * @returns 检测结果数组,顺序对应输入texts
115
- *
116
- * 说明:
117
- * 这里将所有词条用换行符拼接成一个字符串,一次性调用语言检测接口,
118
- * 以减少请求次数和提升性能。
119
- * 返回结果数组中只包含一个元素,即合并检测的结果。
120
- */
121
- async function batchCheckTexts(
122
- texts: string[],
123
- language: string
124
- ): Promise<(CheckResult | null)[]> {
125
- const results: (CheckResult | null)[] = [];
126
- try {
127
- // 将词条用换行符拼接,避免词条间干扰,推荐换行分隔
128
- const joinedText = texts.join('\n');
129
- const res = await languageToolCheck(joinedText, language);
130
- results.push(res);
131
- } catch (error) {
132
- loggerError(error, logger);
133
- results.push(null);
134
- }
135
- return results;
136
- }
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
-
186
- /**
187
- * excel转json功能主函数
188
- * 读取用户输入的excel路径,解析内容,根据配置生成多语言json文件
189
- * 并对配置文件中所有语言对应的词条进行语言检测
190
- * @param program Commander命令行实例
191
- */
192
- export async function excel2json(program: Command) {
193
- // 配置文件默认路径
194
- const configPath = path.join(__dirname, '../../../../setting.json');
195
- let i18nConfig: I18nConfig;
196
-
197
- // 加载配置文件
198
- try {
199
- logger.info(`开始加载配置文件:${configPath}`, true);
200
- i18nConfig = loadConfig(configPath);
201
- logger.info('配置文件加载成功', true);
202
- } catch (error: unknown) {
203
- const msg = `读取配置文件失败:${normalizeError(error).stack},程序已退出`;
204
- logger.error(msg);
205
- console.error('程序执行时发生异常,已记录日志,程序已退出');
206
- process.exit(1);
207
- }
208
-
209
- // 尝试调用接口获取支持的语言列表,更新 longCodes
210
- try {
211
- logger.info('尝试获取在线支持的语言列表...', true);
212
- const languageTools = await getLanguageTool();
213
- logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes`, true);
214
-
215
- // 构建新的 longCodes 映射
216
- const newLongCodes: Record<string, string> = {};
217
- // 语言标识对应语言名称列表,方便匹配
218
- const langNameToKey: Record<string, string> = {};
219
- for (const [key, names] of Object.entries(i18nConfig.langs)) {
220
- names.forEach((name) => {
221
- langNameToKey[name.toLowerCase()] = key;
222
- });
223
- }
224
-
225
- for (const lang of languageTools) {
226
- // 尝试根据语言名称匹配配置中的语言key
227
- const lowerName = lang.name.toLowerCase();
228
- const matchedKey =
229
- langNameToKey[lowerName] ||
230
- Object.keys(i18nConfig.langs).find(
231
- (k) => k.toLowerCase() === lowerName
232
- );
233
- if (matchedKey) {
234
- newLongCodes[matchedKey] = lang.longCode;
235
- }
236
- }
237
-
238
- // 替换旧的 longCodes,保留未匹配的旧值
239
- i18nConfig.longCodes = { ...i18nConfig.longCodes, ...newLongCodes };
240
- } catch (error) {
241
- logger.warn(
242
- `获取在线语言列表失败,使用本地配置 longCodes,错误:${normalizeError(error).stack}`
243
- );
244
- console.warn('获取在线语言列表失败,使用本地配置 longCodes');
245
- }
246
-
247
- // 交互式输入excel文件路径并校验
248
- const answer = await input({
249
- message: '请输入excel文件路径:',
250
- validate: (value) => {
251
- const cleaned = value.trim().replace(/^['"]|['"]$/g, '');
252
- if (cleaned.length === 0) return '路径不能为空';
253
- if (!fs.existsSync(cleaned)) return '文件不存在,请输入有效路径';
254
- if (!/\.(xls|xlsx)$/i.test(cleaned))
255
- return '请输入有效的excel文件路径(.xls或.xlsx)';
256
- return true;
257
- },
258
- });
259
-
260
- // 规范化路径,支持相对路径转绝对路径,去除首尾引号
261
- const excelPath = path.resolve(
262
- process.cwd(),
263
- answer.trim().replace(/^['"]|['"]$/g, '')
264
- );
265
-
266
- try {
267
- logger.info(`开始读取excel文件:${excelPath}`, true);
268
-
269
- // 读取excel文件
270
- const workbook = XLSX.readFile(excelPath);
271
- const firstSheetName = workbook.SheetNames[0];
272
- if (!firstSheetName) {
273
- logger.error('excel文件没有任何工作表,程序已退出');
274
- console.error('程序执行时发生异常,已记录日志,程序已退出');
275
- process.exit(1);
276
- }
277
-
278
- // 读取第一个工作表的数据,按行读取,header=1表示返回二维数组
279
- const sheet = workbook.Sheets[firstSheetName];
280
- const rows: Row[] = XLSX.utils.sheet_to_json(sheet, { header: 1 });
281
-
282
- if (rows.length < 2) {
283
- logger.error('工作表数据不足,至少需要两行(表头+数据),程序已退出');
284
- console.error('程序执行时发生异常,已记录日志,程序已退出');
285
- process.exit(1);
286
- }
287
-
288
- logger.info('开始解析表头', true);
289
- // 处理表头行,去除空格,转成字符串
290
- const headerRow = rows[0].map((cell) => (cell ? String(cell).trim() : ''));
291
-
292
- // 根据表头匹配语言列,建立列索引到语言key的映射
293
- const colIndexToLangKey: Record<number, string> = {};
294
- headerRow.forEach((colName, idx) => {
295
- const langKey = matchLangKey(colName, i18nConfig.langs);
296
- if (langKey) {
297
- colIndexToLangKey[idx] = langKey;
298
- }
299
- });
300
-
301
- // 获取默认语言列索引
302
- const defaultLang = i18nConfig.defaultKey;
303
- const defaultColIndex = Object.entries(colIndexToLangKey).find(
304
- ([, langKey]) => langKey === defaultLang
305
- )?.[0];
306
-
307
- if (defaultColIndex === undefined) {
308
- logger.error(`找不到默认语言列:${defaultLang},程序已退出`);
309
- console.error('程序执行时发生异常,已记录日志,程序已退出');
310
- process.exit(1);
311
- }
312
- const defaultColNum = Number(defaultColIndex);
313
-
314
- // 初始化所有语言词条对象(包括默认语言)
315
- const langTranslations: Record<string, Record<string, string>> = {};
316
- Object.values(colIndexToLangKey).forEach((langKey) => {
317
- langTranslations[langKey] = {};
318
- });
319
-
320
- logger.info('开始解析数据行', true);
321
- // 遍历数据行,提取所有语言词条
322
- // key统一用默认语言列的值,其他语言对应的列为翻译内容
323
- const langKeysMap: Record<string, string[]> = {}; // 语言key => 词条数组
324
- Object.keys(langTranslations).forEach((langKey) => {
325
- langKeysMap[langKey] = [];
326
- });
327
-
328
- for (let i = 1; i < rows.length; i++) {
329
- const row = rows[i];
330
- const keyCell = row[defaultColNum];
331
- if (keyCell === undefined || keyCell === null || keyCell === '') continue;
332
-
333
- let key = String(keyCell).trim();
334
- key = trimQuotes(key); // 去除引号
335
-
336
- // 跳过空key,避免写入无效数据
337
- if (key.length === 0) continue;
338
-
339
- // 默认语言的词条即key本身
340
- langTranslations[defaultLang][key] = key;
341
- langKeysMap[defaultLang].push(key);
342
-
343
- // 其他语言词条
344
- for (const [colIdxStr, langKey] of Object.entries(colIndexToLangKey)) {
345
- const colIdx = Number(colIdxStr);
346
- if (langKey === defaultLang) continue;
347
- const valCell = row[colIdx];
348
- if (valCell !== undefined && valCell !== null && valCell !== '') {
349
- const valStr = String(valCell);
350
- langTranslations[langKey][key] = valStr;
351
- langKeysMap[langKey].push(valStr);
352
- } else {
353
- // 如果单元格为空,也要保证检测结果数组长度一致,填空字符串
354
- langKeysMap[langKey].push('');
355
- }
356
- }
357
- }
358
-
359
- // 语言检测结果映射,语言key => 每条词条的错误描述数组
360
- const langCheckErrorsMap: Record<string, string[]> = {};
361
-
362
- // 对所有语言词条批量进行语言检测(包括默认语言)
363
- const langKeysEntries = Object.entries(langKeysMap);
364
- for (let idx = 0; idx < langKeysEntries.length; idx++) {
365
- const [langKey, texts] = langKeysEntries[idx];
366
- const longCode = i18nConfig.longCodes[langKey];
367
- if (!longCode) {
368
- logger.warn(`语言(${langKey})未配置 longCode,跳过检测`, true);
369
- langCheckErrorsMap[langKey] =
370
- texts.length > 0
371
- ? texts.map(() => '未配置 longCode,未进行检测')
372
- : ['无词条'];
373
- continue;
374
- }
375
- if (texts.length === 0) {
376
- logger.info(`语言(${langKey})无词条,跳过检测`, true);
377
- langCheckErrorsMap[langKey] = ['无词条'];
378
- continue;
379
- }
380
-
381
- logger.info(
382
- `开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length} (${idx + 1}/${langKeysEntries.length})`,
383
- true
384
- );
385
-
386
- const checkResults = await batchCheckTexts(texts, longCode);
387
-
388
- if (!checkResults || checkResults.length === 0 || !checkResults[0]) {
389
- logger.error(`语言(${langKey})词条检测失败`, true);
390
- langCheckErrorsMap[langKey] = texts.map(() => '检测失败,未知错误');
391
- continue;
392
- }
393
-
394
- const result = checkResults[0];
395
- if (result.matches.length === 0) {
396
- logger.info(`语言(${langKey})词条检测无错误`, true);
397
- langCheckErrorsMap[langKey] = texts.map(() => '无错误');
398
- } else {
399
- logger.info(
400
- `语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}`,
401
- true
402
- );
403
- // 解析检测结果,拆分到每条词条
404
- langCheckErrorsMap[langKey] = parseCheckResultPerEntry(result, texts);
405
-
406
- // 详细日志输出
407
- for (const match of result.matches) {
408
- logger.info(
409
- `- 错误: ${match.message}\n 出错句子: ${match.sentence}\n 建议替换: ${match.replacements
410
- .map((r) => r.value)
411
- .join(', ')}`
412
- );
413
- }
414
- }
415
- }
416
-
417
- // 输出目录:excel文件所在目录下的“lang_时间戳”文件夹
418
- const excelDir = path.dirname(excelPath);
419
- const timestamp = getTimestamp();
420
- const outputRoot = path.join(excelDir, `lang_${timestamp}`);
421
- if (!fs.existsSync(outputRoot)) {
422
- fs.mkdirSync(outputRoot, { recursive: true });
423
- }
424
-
425
- logger.info(`开始生成语言文件,输出目录:${outputRoot}`, true);
426
-
427
- // 按语言生成对应的json文件,默认语言的key=value不生成文件
428
- for (const [langKey, translations] of Object.entries(langTranslations)) {
429
- if (Object.keys(translations).length === 0) continue;
430
-
431
- if (langKey === defaultLang) {
432
- logger.info(`跳过默认语言(${langKey})的json文件生成`, true);
433
- continue; // 跳过默认语言文件生成
434
- }
435
-
436
- const langDir = path.join(outputRoot, langKey);
437
- if (!fs.existsSync(langDir)) {
438
- fs.mkdirSync(langDir, { recursive: true });
439
- }
440
-
441
- const filePath = path.join(langDir, 'translate.json');
442
- fs.writeFileSync(filePath, JSON.stringify(translations, null, 2), {
443
- encoding: 'utf-8',
444
- });
445
- logger.info(`已生成语言文件:${filePath}`, true);
446
- }
447
-
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);
507
- } catch (error: unknown) {
508
- // 记录错误日志,方便排查
509
- loggerError(error, logger);
510
- console.error('程序执行时发生异常,已记录日志,程序已退出');
511
- process.exit(1);
512
- }
513
- }
@@ -1,3 +0,0 @@
1
- export function run() {
2
- console.log('提取词条');
3
- }
@@ -1,78 +0,0 @@
1
- import { Command } from 'commander';
2
- import { select, Separator } from '@inquirer/prompts';
3
- import { excel2json } from './excel2json/index.js';
4
- import { logger, loggerError } from '../../utils/index.js';
5
-
6
- /**
7
- * 国际化模块主入口
8
- * 提供多个国际化相关功能的交互式选择
9
- * @param program Commander命令行实例,用于传递参数和配置
10
- */
11
- export async function i18n(program: Command) {
12
- try {
13
- logger.info('国际化模块启动,等待用户选择功能');
14
-
15
- // 定义可用功能选项
16
- const moduleChoices = [
17
- {
18
- name: '提取词条',
19
- value: 'extractEntry',
20
- description: '从所给路径中提取词条信息',
21
- },
22
- {
23
- name: 'json转excel',
24
- value: 'json2excel',
25
- description: '将json格式的词条信息转换为excel表格',
26
- },
27
- {
28
- name: 'excel转json',
29
- value: 'excel2json',
30
- description: '将excel表格转换为json格式的词条信息',
31
- },
32
- {
33
- name: 'json合并',
34
- value: 'jsonMerge',
35
- description: '合并多个json格式的词条信息文件',
36
- },
37
- ];
38
-
39
- // 交互式选择需要执行的功能
40
- const answer = await select({
41
- message: '请选择要执行的功能:',
42
- choices: [
43
- ...moduleChoices,
44
- new Separator(), // 分割线,方便未来扩展更多功能
45
- ],
46
- default: 'extractEntry', // 默认选项
47
- pageSize: 10, // 最大显示选项数
48
- loop: true, // 是否循环滚动选项
49
- });
50
-
51
- // 查找选择功能的名称,方便日志输出
52
- const selectedModule = moduleChoices.find((item) => item.value === answer);
53
-
54
- if (!selectedModule) {
55
- logger.warn('未选择有效功能,程序已退出');
56
- process.exit(0);
57
- }
58
-
59
- logger.info(`用户选择功能:${selectedModule.name}`);
60
-
61
- // 根据选择执行对应功能
62
- switch (answer) {
63
- case 'excel2json':
64
- logger.info(`${selectedModule.name}功能开始执行`);
65
- await excel2json(program);
66
- logger.info(`${selectedModule.name}功能执行完成`);
67
- break;
68
- default:
69
- logger.warn(`${selectedModule.name}功能暂未实现,程序已退出`);
70
- process.exit(0);
71
- }
72
- } catch (error: unknown) {
73
- // 记录错误日志,方便排查
74
- loggerError(error, logger);
75
- console.error('程序执行时发生异常,已记录日志,程序已退出');
76
- process.exit(1);
77
- }
78
- }
@@ -1,3 +0,0 @@
1
- export function run() {
2
- console.log('json转excel');
3
- }
@@ -1,3 +0,0 @@
1
- export function run() {
2
- console.log('json合并');
3
- }