td-web-cli 0.1.24 → 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 +0 -1
- package/dist/modules/i18n/extractEntry/index.d.ts +5 -1
- package/dist/modules/i18n/extractEntry/index.d.ts.map +1 -1
- package/dist/modules/i18n/extractEntry/index.js +492 -2
- package/dist/modules/i18n/index.d.ts.map +1 -1
- package/dist/modules/i18n/index.js +6 -1
- package/dist/modules/i18n/jsonMerge/index.d.ts.map +1 -1
- package/dist/modules/i18n/jsonMerge/index.js +0 -1
- package/dist/modules/tools/index.d.ts.map +1 -1
- package/dist/modules/tools/index.js +0 -1
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/i18n/extractEntry/index.ts"],"names":[],"mappings":"AAAA,
|
|
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
|
-
|
|
2
|
-
|
|
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;
|
|
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"}
|
|
@@ -3,6 +3,7 @@ import { logger, loggerError } from '../../utils/index.js';
|
|
|
3
3
|
import { excel2json } from './excel2json/index.js';
|
|
4
4
|
import { json2excel } from './json2excel/index.js';
|
|
5
5
|
import { jsonMerge } from './jsonMerge/index.js';
|
|
6
|
+
import { extractEntry } from './extractEntry/index.js';
|
|
6
7
|
/**
|
|
7
8
|
* 国际化模块主入口
|
|
8
9
|
* 提供多个国际化相关功能的交互式选择
|
|
@@ -42,7 +43,6 @@ export async function i18n(program) {
|
|
|
42
43
|
new Separator(), // 分割线,方便未来扩展更多功能
|
|
43
44
|
],
|
|
44
45
|
default: 'extractEntry', // 默认选项
|
|
45
|
-
pageSize: 10, // 最大显示选项数
|
|
46
46
|
loop: true, // 是否循环滚动选项
|
|
47
47
|
});
|
|
48
48
|
// 查找选择功能的名称,方便日志输出
|
|
@@ -54,6 +54,11 @@ export async function i18n(program) {
|
|
|
54
54
|
logger.info(`用户选择功能:${selectedModule.name}`);
|
|
55
55
|
// 根据选择执行对应功能
|
|
56
56
|
switch (answer) {
|
|
57
|
+
case 'extractEntry':
|
|
58
|
+
logger.info(`${selectedModule.name}功能开始执行`);
|
|
59
|
+
await extractEntry(program);
|
|
60
|
+
logger.info(`${selectedModule.name}功能执行完成`);
|
|
61
|
+
break;
|
|
57
62
|
case 'excel2json':
|
|
58
63
|
logger.info(`${selectedModule.name}功能开始执行`);
|
|
59
64
|
await excel2json(program);
|
|
@@ -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;
|
|
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"}
|
|
@@ -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,
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "td-web-cli",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|