td-web-cli 0.1.23 → 0.1.25

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/index.js CHANGED
@@ -40,7 +40,6 @@ async function main() {
40
40
  new Separator(), // 分割线,便于未来扩展更多模块
41
41
  ],
42
42
  default: 'i18n', // 默认选项
43
- pageSize: 10, // 最大显示选项数
44
43
  loop: true, // 选项循环滚动
45
44
  });
46
45
  // 查找选择模块的名称,方便日志输出
@@ -1,2 +1,6 @@
1
- export declare function run(): void;
1
+ import { Command } from 'commander';
2
+ /**
3
+ * 提取前端项目词条主函数
4
+ */
5
+ export declare function extractEntry(program: Command): Promise<void>;
2
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/i18n/extractEntry/index.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAElB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/i18n/extractEntry/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiWpC;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,iBAkNlD"}
@@ -1,3 +1,493 @@
1
- export function run() {
2
- console.log('提取词条');
1
+ import { input, select, confirm, Separator } from '@inquirer/prompts';
2
+ import XLSX from 'xlsx';
3
+ import fs from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import path from 'path';
6
+ import { minimatch } from 'minimatch';
7
+ import { getTimestamp, logger, loggerError, normalizeError, normalizeGitBashPath, } from '../../../utils/index.js';
8
+ // AST 解析相关
9
+ import babelParser from '@babel/parser';
10
+ import traverse from '@babel/traverse';
11
+ import { parse as vueParse } from '@vue/compiler-sfc';
12
+ import { parse as htmlParse } from 'node-html-parser';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ // 支持的文件扩展名
16
+ const SUPPORTED_EXTENSIONS = [
17
+ '.js',
18
+ '.jsx',
19
+ '.ts',
20
+ '.tsx',
21
+ '.vue',
22
+ '.html',
23
+ '.htm',
24
+ ];
25
+ /**
26
+ * 检查字符串是否包含汉字(使用 Unicode 属性转义,匹配所有汉字)
27
+ */
28
+ function containsChinese(text) {
29
+ const hanRegex = /\p{Script=Han}/u;
30
+ return hanRegex.test(text);
31
+ }
32
+ /**
33
+ * 读取并解析配置文件
34
+ */
35
+ function loadConfig(configPath) {
36
+ if (!fs.existsSync(configPath)) {
37
+ throw new Error(`配置文件不存在:${configPath}`);
38
+ }
39
+ const content = fs.readFileSync(configPath, { encoding: 'utf-8' });
40
+ const json = JSON.parse(content);
41
+ if (!json.i18n) {
42
+ throw new Error('配置文件格式错误,缺少i18n');
43
+ }
44
+ if (!json.i18n.defaultKey) {
45
+ throw new Error('配置文件格式错误,缺少i18n.defaultKey');
46
+ }
47
+ if (!json.i18n.langs) {
48
+ throw new Error('配置文件格式错误,缺少i18n.langs');
49
+ }
50
+ return json.i18n;
51
+ }
52
+ /**
53
+ * 从表达式中提取字符串字面量(支持单引号、双引号、模板字符串)
54
+ */
55
+ function extractStringsFromExpression(expr) {
56
+ const strings = new Set();
57
+ const stringRegex = /(["'])(?:(?=(\\?))\2.)*?\1|`([^`\\]*(?:\\.[^`\\]*)*)`/g;
58
+ let match;
59
+ while ((match = stringRegex.exec(expr)) !== null) {
60
+ let str = match[0];
61
+ if (str.startsWith('"') || str.startsWith("'")) {
62
+ str = str.slice(1, -1);
63
+ }
64
+ else if (str.startsWith('`')) {
65
+ str = str.slice(1, -1);
66
+ }
67
+ str = str.replace(/\\(.)/g, '$1');
68
+ if (str && containsChinese(str)) {
69
+ strings.add(str.trim());
70
+ }
71
+ }
72
+ return strings;
73
+ }
74
+ /**
75
+ * 使用 Babel AST 从 JavaScript/TypeScript/JSX 代码中提取包含中文的字符串
76
+ */
77
+ function extractFromJS(code) {
78
+ const strings = new Set();
79
+ const ast = babelParser.parse(code, {
80
+ sourceType: 'module',
81
+ plugins: ['jsx', 'typescript'],
82
+ });
83
+ traverse.default(ast, {
84
+ StringLiteral(path) {
85
+ const text = path.node.value;
86
+ if (text && containsChinese(text)) {
87
+ strings.add(text.trim());
88
+ }
89
+ },
90
+ TemplateLiteral(path) {
91
+ const text = path.node.quasis.map((elem) => elem.value.raw).join('');
92
+ if (text && containsChinese(text)) {
93
+ strings.add(text.trim());
94
+ }
95
+ },
96
+ JSXText(path) {
97
+ const text = path.node.value;
98
+ if (text && containsChinese(text)) {
99
+ strings.add(text.trim());
100
+ }
101
+ },
102
+ JSXAttribute(path) {
103
+ const value = path.node.value;
104
+ if (value &&
105
+ value.type === 'StringLiteral' &&
106
+ containsChinese(value.value)) {
107
+ strings.add(value.value.trim());
108
+ }
109
+ },
110
+ });
111
+ return strings;
112
+ }
113
+ /**
114
+ * 使用 @vue/compiler-sfc 解析 Vue 单文件组件
115
+ */
116
+ function extractFromVue(content) {
117
+ var _a, _b;
118
+ const strings = new Set();
119
+ const { descriptor } = vueParse(content);
120
+ // 处理 script 部分
121
+ if (descriptor.script || descriptor.scriptSetup) {
122
+ const scriptContent = ((_a = descriptor.script) === null || _a === void 0 ? void 0 : _a.content) || ((_b = descriptor.scriptSetup) === null || _b === void 0 ? void 0 : _b.content) || '';
123
+ if (scriptContent) {
124
+ try {
125
+ const scriptStrings = extractFromJS(scriptContent);
126
+ scriptStrings.forEach((s) => strings.add(s));
127
+ }
128
+ catch (err) {
129
+ logger.warn(`解析 Vue script 失败,跳过该脚本内容: ${normalizeError(err).message}`);
130
+ }
131
+ }
132
+ }
133
+ // 处理 template 部分
134
+ if (descriptor.template) {
135
+ const templateContent = descriptor.template.content;
136
+ // 1. 提取文本节点中的中文
137
+ const textRegex = />([^<]+)</g;
138
+ let match;
139
+ while ((match = textRegex.exec(templateContent)) !== null) {
140
+ let text = match[1].trim();
141
+ if (text && containsChinese(text)) {
142
+ if (!text.startsWith('{{') && !text.endsWith('}}')) {
143
+ strings.add(text);
144
+ }
145
+ else {
146
+ const expr = text.slice(2, -2).trim();
147
+ const exprStrings = extractStringsFromExpression(expr);
148
+ exprStrings.forEach((s) => strings.add(s));
149
+ }
150
+ }
151
+ }
152
+ // 2. 提取静态属性值中的中文
153
+ const attrRegex = /\w+\s*=\s*["']([^"']*)["']/g;
154
+ while ((match = attrRegex.exec(templateContent)) !== null) {
155
+ const text = match[1].trim();
156
+ if (text && containsChinese(text)) {
157
+ strings.add(text);
158
+ }
159
+ }
160
+ // 3. 匹配动态绑定中的字符串字面量
161
+ const dynamicBindRegex = /:(?:[a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*["']([^"']*)["']|v-bind:[a-zA-Z_][a-zA-Z0-9_-]*\s*=\s*["']([^"']*)["']/g;
162
+ while ((match = dynamicBindRegex.exec(templateContent)) !== null) {
163
+ const value = match[1] || match[2];
164
+ if (value) {
165
+ const exprStrings = extractStringsFromExpression(value);
166
+ exprStrings.forEach((s) => strings.add(s));
167
+ }
168
+ }
169
+ // 4. 处理插值表达式
170
+ const interpolationRegex = /{{([^}]+)}}/g;
171
+ while ((match = interpolationRegex.exec(templateContent)) !== null) {
172
+ const expr = match[1].trim();
173
+ const exprStrings = extractStringsFromExpression(expr);
174
+ exprStrings.forEach((s) => strings.add(s));
175
+ }
176
+ }
177
+ return strings;
178
+ }
179
+ /**
180
+ * 使用 node-html-parser 解析 HTML 文件
181
+ */
182
+ function extractFromHTML(html) {
183
+ const strings = new Set();
184
+ const root = htmlParse(html);
185
+ function walk(node) {
186
+ if (node.nodeType === 3) {
187
+ // 文本节点
188
+ const text = node.text;
189
+ if (text && typeof text === 'string') {
190
+ const trimmed = text.trim();
191
+ if (trimmed && containsChinese(trimmed)) {
192
+ strings.add(trimmed);
193
+ }
194
+ }
195
+ }
196
+ else if (node.nodeType === 1) {
197
+ // 元素节点
198
+ const tagName = node.tagName;
199
+ if (tagName && typeof tagName === 'string') {
200
+ const lowerTag = tagName.toLowerCase();
201
+ if (lowerTag === 'script') {
202
+ const scriptContent = node.text;
203
+ if (scriptContent && typeof scriptContent === 'string') {
204
+ try {
205
+ const scriptStrings = extractFromJS(scriptContent);
206
+ scriptStrings.forEach((s) => strings.add(s));
207
+ }
208
+ catch (err) {
209
+ logger.warn(`解析内联脚本失败: ${normalizeError(err).message}`);
210
+ }
211
+ }
212
+ return;
213
+ }
214
+ if (lowerTag === 'style') {
215
+ return;
216
+ }
217
+ }
218
+ if (node.attributes) {
219
+ for (const [attrName, attrValue] of Object.entries(node.attributes)) {
220
+ if (attrValue &&
221
+ typeof attrValue === 'string' &&
222
+ containsChinese(attrValue)) {
223
+ strings.add(attrValue.trim());
224
+ }
225
+ }
226
+ }
227
+ if (node.childNodes) {
228
+ node.childNodes.forEach((child) => walk(child));
229
+ }
230
+ }
231
+ }
232
+ walk(root);
233
+ return strings;
234
+ }
235
+ /**
236
+ * 根据文件扩展名选择合适的提取方法
237
+ */
238
+ function extractEntryFromFile(filePath, content) {
239
+ const ext = path.extname(filePath).toLowerCase();
240
+ try {
241
+ if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
242
+ return extractFromJS(content);
243
+ }
244
+ else if (ext === '.vue') {
245
+ return extractFromVue(content);
246
+ }
247
+ else if (['.html', '.htm'].includes(ext)) {
248
+ return extractFromHTML(content);
249
+ }
250
+ }
251
+ catch (err) {
252
+ logger.warn(`解析文件失败(已跳过): ${filePath} - ${normalizeError(err).message}`);
253
+ }
254
+ return new Set();
255
+ }
256
+ /**
257
+ * 递归获取目录下所有匹配的文件路径
258
+ * @param dir 当前扫描目录
259
+ * @param extensions 要匹配的文件扩展名
260
+ * @param ignorePatterns 要忽略的路径模式(支持 glob 通配符,可匹配目录和文件)
261
+ * @param rootDir 项目根目录,用于计算相对路径
262
+ */
263
+ function getFiles(dir, extensions, ignorePatterns, rootDir) {
264
+ let results = [];
265
+ const list = fs.readdirSync(dir);
266
+ for (const item of list) {
267
+ const fullPath = path.join(dir, item);
268
+ const stat = fs.statSync(fullPath);
269
+ // 计算相对于根目录的路径,用于匹配忽略模式
270
+ const relativePath = path.relative(rootDir, fullPath).replace(/\\/g, '/');
271
+ if (stat.isDirectory()) {
272
+ // 检查目录是否匹配任何忽略模式
273
+ const shouldIgnore = ignorePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true, matchBase: true }));
274
+ if (shouldIgnore) {
275
+ continue;
276
+ }
277
+ // 递归扫描子目录
278
+ results = results.concat(getFiles(fullPath, extensions, ignorePatterns, rootDir));
279
+ }
280
+ else {
281
+ const ext = path.extname(item).toLowerCase();
282
+ if (extensions.includes(ext)) {
283
+ // 检查文件是否匹配任何忽略模式
284
+ const shouldIgnore = ignorePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true, matchBase: true }));
285
+ if (!shouldIgnore) {
286
+ results.push(fullPath);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ return results;
292
+ }
293
+ /**
294
+ * 验证 glob 模式是否有效
295
+ */
296
+ function isValidGlobPattern(pattern) {
297
+ try {
298
+ // 使用 minimatch 尝试创建正则表达式,如果模式无效会抛出异常
299
+ minimatch.makeRe(pattern, { dot: true });
300
+ return true;
301
+ }
302
+ catch {
303
+ return false;
304
+ }
305
+ }
306
+ /**
307
+ * 提取前端项目词条主函数
308
+ */
309
+ export async function extractEntry(program) {
310
+ var _a;
311
+ const configPath = path.join(__dirname, '../../../../setting.json');
312
+ let i18nConfig;
313
+ try {
314
+ logger.info(`开始加载配置文件:${configPath}`, true);
315
+ i18nConfig = loadConfig(configPath);
316
+ logger.info('配置文件加载成功', true);
317
+ }
318
+ catch (error) {
319
+ const msg = `读取配置文件失败:${normalizeError(error).stack},程序已退出`;
320
+ logger.error(msg);
321
+ console.error('程序执行时发生异常,已记录日志,程序已退出');
322
+ process.exit(1);
323
+ }
324
+ // 1. 输入项目根目录
325
+ const answer = await input({
326
+ message: '请输入项目根目录:',
327
+ validate: (value) => {
328
+ const cleaned = value.trim().replace(/^['"]|['"]$/g, '');
329
+ if (cleaned.length === 0) {
330
+ return '路径不能为空';
331
+ }
332
+ const normalizedPath = normalizeGitBashPath(cleaned);
333
+ if (!fs.existsSync(normalizedPath)) {
334
+ return '目录不存在,请输入有效路径';
335
+ }
336
+ if (!fs.statSync(normalizedPath).isDirectory()) {
337
+ return '请输入一个目录路径';
338
+ }
339
+ return true;
340
+ },
341
+ });
342
+ const rootDir = normalizeGitBashPath(answer);
343
+ // 2. 交互式配置忽略模式(支持目录和文件,使用 glob 通配符)
344
+ const defaultPatterns = [
345
+ 'node_modules',
346
+ '.git',
347
+ 'dist',
348
+ 'build',
349
+ 'public',
350
+ 'src/components/protocol',
351
+ 'src/phone-repeater',
352
+ 'src/phone/src/plugins',
353
+ 'src/assets/lang',
354
+ 'build-all.js',
355
+ ];
356
+ const useDefault = await confirm({
357
+ message: '是否使用默认忽略模式?默认模式会忽略 node_modules, .git, dist, build, public, src/components/protocol, src/phone-repeater, src/phone/src/plugins, src/assets/lang, build-all.js 及其所有子目录',
358
+ default: true,
359
+ });
360
+ let ignorePatterns = defaultPatterns;
361
+ if (!useDefault) {
362
+ const customInput = await input({
363
+ message: '请输入自定义忽略模式,多个模式用英文逗号分隔。支持 glob 通配符,可忽略目录或文件。\n(直接回车表示不忽略任何内容)\n示例:node_modules,dist/**,build/**,*.log,config.js',
364
+ validate: (value) => {
365
+ // 将输入中的反斜杠统一替换为正斜杠
366
+ const normalizedValue = value.replace(/\\/g, '/');
367
+ const trimmed = normalizedValue.trim();
368
+ // 空字符串表示不忽略,直接通过
369
+ if (!trimmed) {
370
+ return true;
371
+ }
372
+ // 分割并过滤空字符串
373
+ const patterns = trimmed
374
+ .split(',')
375
+ .map((p) => p.trim())
376
+ .filter((p) => p);
377
+ if (patterns.length === 0) {
378
+ return true; // 实际上已经过滤,但保留空处理
379
+ }
380
+ // 校验每个模式的有效性
381
+ for (const pattern of patterns) {
382
+ if (!isValidGlobPattern(pattern)) {
383
+ return `无效的 glob 模式:"${pattern}",请使用正确的通配符格式。`;
384
+ }
385
+ }
386
+ return true;
387
+ },
388
+ });
389
+ // 将用户输入中的反斜杠统一替换为正斜杠,确保模式统一
390
+ const normalizedCustomInput = customInput.replace(/\\/g, '/');
391
+ if (normalizedCustomInput.trim()) {
392
+ const patterns = normalizedCustomInput
393
+ .split(',')
394
+ .map((p) => p.trim())
395
+ .filter((p) => p);
396
+ ignorePatterns = patterns;
397
+ logger.info(`自定义忽略模式:${ignorePatterns.join(', ')}`, true);
398
+ }
399
+ else {
400
+ ignorePatterns = [];
401
+ logger.info('未设置任何忽略模式,将扫描所有文件', true);
402
+ }
403
+ }
404
+ else {
405
+ logger.info(`使用默认忽略模式:${defaultPatterns.join(', ')}`, true);
406
+ }
407
+ // 3. 选择目标语言
408
+ const langChoices = Object.entries(i18nConfig.langs).map(([key, names]) => ({
409
+ name: names[0],
410
+ value: key,
411
+ }));
412
+ const selectedLangKey = await select({
413
+ message: '请选择需要提取的目标语言(将生成该语言对应的翻译列)',
414
+ choices: [
415
+ ...langChoices,
416
+ new Separator(), // 分割线,方便未来扩展更多功能
417
+ ],
418
+ default: i18nConfig.defaultKey,
419
+ loop: true, // 是否循环滚动选项
420
+ });
421
+ const targetLangName = ((_a = i18nConfig.langs[selectedLangKey]) === null || _a === void 0 ? void 0 : _a[0]) || selectedLangKey;
422
+ try {
423
+ logger.info(`开始扫描目录:${rootDir}`, true);
424
+ const files = getFiles(rootDir, SUPPORTED_EXTENSIONS, ignorePatterns, rootDir);
425
+ logger.info(`共找到 ${files.length} 个待扫描文件`, true);
426
+ // 记录每个文件路径下的所有词条
427
+ const fileToTermsMap = new Map();
428
+ let fileCount = 0;
429
+ for (const file of files) {
430
+ try {
431
+ const content = fs.readFileSync(file, 'utf-8');
432
+ const strings = extractEntryFromFile(file, content);
433
+ if (strings.size > 0) {
434
+ const relativePath = path.relative(rootDir, file).replace(/\\/g, '/');
435
+ const terms = Array.from(strings).sort();
436
+ fileToTermsMap.set(relativePath, terms);
437
+ }
438
+ fileCount++;
439
+ if (fileCount % 100 === 0) {
440
+ logger.info(`已处理 ${fileCount} 个文件,当前累计有词条的文件数:${fileToTermsMap.size}`, true);
441
+ }
442
+ }
443
+ catch (err) {
444
+ logger.warn(`读取文件失败(已跳过):${file}`, true);
445
+ }
446
+ }
447
+ if (fileToTermsMap.size === 0) {
448
+ logger.warn('未提取到任何词条,程序退出', true);
449
+ process.exit(0);
450
+ }
451
+ // 构建 Excel 数据
452
+ const sortedFiles = Array.from(fileToTermsMap.keys()).sort();
453
+ const rows = [];
454
+ const merges = [];
455
+ rows.push(['文件路径', targetLangName]);
456
+ let currentRow = 1;
457
+ for (const filePath of sortedFiles) {
458
+ const terms = fileToTermsMap.get(filePath);
459
+ const startRow = currentRow;
460
+ for (let i = 0; i < terms.length; i++) {
461
+ rows.push([filePath, terms[i]]);
462
+ currentRow++;
463
+ }
464
+ const endRow = currentRow - 1;
465
+ if (endRow > startRow) {
466
+ merges.push({
467
+ s: { r: startRow, c: 0 },
468
+ e: { r: endRow, c: 0 },
469
+ });
470
+ }
471
+ }
472
+ const timestamp = getTimestamp();
473
+ const outputFileName = `i18n_terms_${timestamp}.xlsx`;
474
+ const outputPath = path.join(rootDir, outputFileName);
475
+ const wb = XLSX.utils.book_new();
476
+ const ws = XLSX.utils.aoa_to_sheet(rows);
477
+ if (merges.length > 0) {
478
+ ws['!merges'] = merges;
479
+ }
480
+ XLSX.utils.book_append_sheet(wb, ws, 'I18nTerms');
481
+ XLSX.writeFile(wb, outputPath);
482
+ const totalTerms = rows.length - 1;
483
+ logger.info(`Excel 文件已生成:${outputPath}`, true);
484
+ logger.info(`总计导出 ${totalTerms} 条词条,分布在 ${sortedFiles.length} 个文件中`, true);
485
+ logger.info(`目标语言:${targetLangName}`, true);
486
+ logger.info('提取完成', true);
487
+ }
488
+ catch (error) {
489
+ loggerError(error, logger);
490
+ console.error('程序执行时发生异常,已记录日志,程序已退出');
491
+ process.exit(1);
492
+ }
3
493
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/i18n/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,iBAwE1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/i18n/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,iBAiF1C"}
@@ -1,7 +1,9 @@
1
1
  import { select, Separator } from '@inquirer/prompts';
2
2
  import { logger, loggerError } from '../../utils/index.js';
3
3
  import { excel2json } from './excel2json/index.js';
4
+ import { json2excel } from './json2excel/index.js';
4
5
  import { jsonMerge } from './jsonMerge/index.js';
6
+ import { extractEntry } from './extractEntry/index.js';
5
7
  /**
6
8
  * 国际化模块主入口
7
9
  * 提供多个国际化相关功能的交互式选择
@@ -41,7 +43,6 @@ export async function i18n(program) {
41
43
  new Separator(), // 分割线,方便未来扩展更多功能
42
44
  ],
43
45
  default: 'extractEntry', // 默认选项
44
- pageSize: 10, // 最大显示选项数
45
46
  loop: true, // 是否循环滚动选项
46
47
  });
47
48
  // 查找选择功能的名称,方便日志输出
@@ -53,11 +54,21 @@ export async function i18n(program) {
53
54
  logger.info(`用户选择功能:${selectedModule.name}`);
54
55
  // 根据选择执行对应功能
55
56
  switch (answer) {
57
+ case 'extractEntry':
58
+ logger.info(`${selectedModule.name}功能开始执行`);
59
+ await extractEntry(program);
60
+ logger.info(`${selectedModule.name}功能执行完成`);
61
+ break;
56
62
  case 'excel2json':
57
63
  logger.info(`${selectedModule.name}功能开始执行`);
58
64
  await excel2json(program);
59
65
  logger.info(`${selectedModule.name}功能执行完成`);
60
66
  break;
67
+ case 'json2excel':
68
+ logger.info(`${selectedModule.name}功能开始执行`);
69
+ await json2excel(program);
70
+ logger.info(`${selectedModule.name}功能执行完成`);
71
+ break;
61
72
  case 'jsonMerge':
62
73
  logger.info(`${selectedModule.name}功能开始执行`);
63
74
  await jsonMerge(program);
@@ -1,2 +1,7 @@
1
- export declare function run(): void;
1
+ import { Command } from 'commander';
2
+ /**
3
+ * JSON 转 Excel 主函数
4
+ * 将多语言 JSON 文件合并为一个 Excel 文件,表头使用语言名称(从配置的 langs 中取第一个)
5
+ */
6
+ export declare function json2excel(program: Command): Promise<void>;
2
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/i18n/json2excel/index.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAElB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/i18n/json2excel/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+CpC;;;GAGG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,iBAsIhD"}
@@ -1,3 +1,157 @@
1
- export function run() {
2
- console.log('JSON转Excel');
1
+ import { input } from '@inquirer/prompts';
2
+ import XLSX from 'xlsx';
3
+ import fs from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import path from 'path';
6
+ import { getTimestamp, logger, loggerError, normalizeError, normalizeGitBashPath, } from '../../../utils/index.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ /**
10
+ * 读取并解析配置文件(同 excel2json)
11
+ */
12
+ function loadConfig(configPath) {
13
+ if (!fs.existsSync(configPath)) {
14
+ throw new Error(`配置文件不存在:${configPath}`);
15
+ }
16
+ const content = fs.readFileSync(configPath, { encoding: 'utf-8' });
17
+ const json = JSON.parse(content);
18
+ if (!json.i18n) {
19
+ throw new Error('配置文件格式错误,缺少i18n');
20
+ }
21
+ if (!json.i18n.defaultKey) {
22
+ throw new Error('配置文件格式错误,缺少i18n.defaultKey');
23
+ }
24
+ if (!json.i18n.langs) {
25
+ throw new Error('配置文件格式错误,缺少i18n.langs');
26
+ }
27
+ if (!json.i18n.longCodes) {
28
+ throw new Error('配置文件格式错误,缺少i18n.longCodes');
29
+ }
30
+ return json.i18n;
31
+ }
32
+ /**
33
+ * JSON 转 Excel 主函数
34
+ * 将多语言 JSON 文件合并为一个 Excel 文件,表头使用语言名称(从配置的 langs 中取第一个)
35
+ */
36
+ export async function json2excel(program) {
37
+ var _a;
38
+ // 配置文件默认路径
39
+ const configPath = path.join(__dirname, '../../../../setting.json');
40
+ let i18nConfig;
41
+ // 加载配置文件
42
+ try {
43
+ logger.info(`开始加载配置文件:${configPath}`, true);
44
+ i18nConfig = loadConfig(configPath);
45
+ logger.info('配置文件加载成功', true);
46
+ }
47
+ catch (error) {
48
+ const msg = `读取配置文件失败:${normalizeError(error).stack},程序已退出`;
49
+ logger.error(msg);
50
+ console.error('程序执行时发生异常,已记录日志,程序已退出');
51
+ process.exit(1);
52
+ }
53
+ // 交互式输入 JSON 根目录
54
+ const answer = await input({
55
+ message: '请输入存放多语言 JSON 的根目录:',
56
+ validate: (value) => {
57
+ const cleaned = value.trim().replace(/^['"]|['"]$/g, '');
58
+ if (cleaned.length === 0) {
59
+ return '路径不能为空';
60
+ }
61
+ const normalizedPath = normalizeGitBashPath(cleaned);
62
+ if (!fs.existsSync(normalizedPath)) {
63
+ return '目录不存在,请输入有效路径';
64
+ }
65
+ if (!fs.statSync(normalizedPath).isDirectory()) {
66
+ return '请输入一个目录路径';
67
+ }
68
+ return true;
69
+ },
70
+ });
71
+ const rootDir = normalizeGitBashPath(answer);
72
+ try {
73
+ logger.info(`开始扫描目录:${rootDir}`, true);
74
+ // 获取根目录下所有子目录(作为语言 KEY)
75
+ const subDirs = fs.readdirSync(rootDir).filter((name) => {
76
+ const fullPath = path.join(rootDir, name);
77
+ return fs.statSync(fullPath).isDirectory();
78
+ });
79
+ // 加载每个语言的翻译
80
+ const translations = {};
81
+ const availableLangs = [];
82
+ for (const langKey of subDirs) {
83
+ const jsonPath = path.join(rootDir, langKey, 'translate.json');
84
+ if (!fs.existsSync(jsonPath)) {
85
+ logger.warn(`跳过 ${langKey},不存在 translate.json`, true);
86
+ continue;
87
+ }
88
+ try {
89
+ const content = fs.readFileSync(jsonPath, 'utf-8');
90
+ const data = JSON.parse(content);
91
+ translations[langKey] = data;
92
+ availableLangs.push(langKey);
93
+ logger.info(`已加载语言:${langKey},词条数量:${Object.keys(data).length}`, true);
94
+ }
95
+ catch (err) {
96
+ logger.error(`读取 ${langKey} 的 translate.json 失败:${normalizeError(err).message}`, true);
97
+ process.exit(1);
98
+ }
99
+ }
100
+ if (availableLangs.length === 0) {
101
+ logger.error('未找到任何有效的 translate.json 文件,程序退出', true);
102
+ process.exit(1);
103
+ }
104
+ // 默认语言
105
+ const defaultLang = i18nConfig.defaultKey;
106
+ if (!availableLangs.includes(defaultLang)) {
107
+ logger.warn(`默认语言 ${defaultLang} 不存在于已加载的语言中,将使用所有语言的 KEY 并集作为基准`, true);
108
+ }
109
+ // 收集所有 KEY 的并集(作为 Excel 第一列)
110
+ const allKeysSet = new Set();
111
+ for (const lang of availableLangs) {
112
+ Object.keys(translations[lang]).forEach((key) => allKeysSet.add(key));
113
+ }
114
+ const sortedKeys = Array.from(allKeysSet).sort();
115
+ logger.info(`共收集到 ${sortedKeys.length} 个唯一 KEY`, true);
116
+ // 构建表头:第一列使用默认语言的名称(从配置中获取),后面列使用其他语言的名称
117
+ const header = [];
118
+ // 第一列名称:优先使用默认语言的第一个名称,否则用 defaultKey
119
+ const defaultLangNames = i18nConfig.langs[defaultLang];
120
+ const firstColName = (defaultLangNames && defaultLangNames.length > 0)
121
+ ? defaultLangNames[0]
122
+ : defaultLang;
123
+ header.push(firstColName);
124
+ const nonDefaultLangs = availableLangs.filter((lang) => lang !== defaultLang);
125
+ for (const lang of nonDefaultLangs) {
126
+ const names = i18nConfig.langs[lang];
127
+ const colName = (names && names.length > 0) ? names[0] : lang;
128
+ header.push(colName);
129
+ }
130
+ // 构建 Excel 数据行
131
+ const rows = [header];
132
+ for (const key of sortedKeys) {
133
+ const row = [key]; // 第一列是 KEY
134
+ for (const lang of nonDefaultLangs) {
135
+ const val = (_a = translations[lang]) === null || _a === void 0 ? void 0 : _a[key];
136
+ row.push(val !== undefined ? val : null); // 无翻译时留空
137
+ }
138
+ rows.push(row);
139
+ }
140
+ // 生成 Excel 文件
141
+ const timestamp = getTimestamp();
142
+ const outputFileName = `lang_merged_${timestamp}.xlsx`;
143
+ const outputPath = path.join(rootDir, outputFileName);
144
+ const wb = XLSX.utils.book_new();
145
+ const ws = XLSX.utils.aoa_to_sheet(rows);
146
+ XLSX.utils.book_append_sheet(wb, ws, 'Translations');
147
+ XLSX.writeFile(wb, outputPath);
148
+ logger.info(`Excel 文件已生成:${outputPath}`, true);
149
+ logger.info(`共处理语言:${availableLangs.join(', ')}`, true);
150
+ logger.info('转换完成', true);
151
+ }
152
+ catch (error) {
153
+ loggerError(error, logger);
154
+ console.error('程序执行时发生异常,已记录日志,程序已退出');
155
+ process.exit(1);
156
+ }
3
157
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/i18n/jsonMerge/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuGpC;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,iBA4J/C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/i18n/jsonMerge/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsGpC;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,iBA4J/C"}
@@ -54,7 +54,6 @@ async function mergeJsonObjects(baseObj, mergeObj, langKey) {
54
54
  new Separator(),
55
55
  ],
56
56
  default: 'base',
57
- pageSize: 10,
58
57
  loop: true,
59
58
  });
60
59
  if (choice === 'merge') {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,OAAO,iBAoD3C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,OAAO,iBAmD3C"}
@@ -25,7 +25,6 @@ export async function tools(program) {
25
25
  new Separator(), // 分割线,方便未来扩展更多功能
26
26
  ],
27
27
  default: 'getHolidayTime', // 默认选项
28
- pageSize: 10, // 最大显示选项数
29
28
  loop: true, // 是否循环滚动选项
30
29
  });
31
30
  // 查找选择功能的名称,方便日志输出
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-web-cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "A CLI tool for efficiency",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -40,9 +40,14 @@
40
40
  "author": "",
41
41
  "license": "ISC",
42
42
  "dependencies": {
43
+ "@babel/parser": "^7.29.2",
44
+ "@babel/traverse": "^7.29.0",
43
45
  "@inquirer/prompts": "^8.2.0",
46
+ "@vue/compiler-sfc": "^3.5.30",
44
47
  "axios": "^1.13.3",
45
48
  "commander": "^14.0.2",
49
+ "minimatch": "^10.2.4",
50
+ "node-html-parser": "^7.1.0",
46
51
  "xlsx": "^0.18.5"
47
52
  },
48
53
  "devDependencies": {