@lmcc-dev/mult-fetch-mcp-server 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/README.zh.md +19 -10
- package/dist/i18n-test-report.json +8 -0
- package/dist/i18n-unused-keys-report.json +8 -0
- package/dist/src/client.js +75 -62
- package/dist/src/index.js +0 -0
- package/dist/src/lib/BrowserFetcher.js +12 -12
- package/dist/src/lib/NodeFetcher.js +165 -106
- package/dist/src/lib/fetch.js +40 -0
- package/dist/src/lib/fetchers/FetcherFactory.js +43 -0
- package/dist/src/lib/fetchers/browser/BrowserFetcher.js +381 -0
- package/dist/src/lib/fetchers/browser/BrowserInstance.js +180 -0
- package/dist/src/lib/fetchers/browser/PageOperations.js +168 -0
- package/dist/src/lib/fetchers/common/types.js +41 -0
- package/dist/src/lib/fetchers/common/utils.js +141 -0
- package/dist/src/lib/fetchers/index.js +62 -0
- package/dist/src/lib/fetchers/node/HttpClient.js +239 -0
- package/dist/src/lib/fetchers/node/NodeFetcher.js +180 -0
- package/dist/src/lib/i18n/index.js +105 -17
- package/dist/src/lib/i18n/keys/base.js +41 -0
- package/dist/src/lib/i18n/keys/browser.js +91 -0
- package/dist/src/lib/i18n/keys/client.js +68 -0
- package/dist/src/lib/i18n/keys/errors.js +49 -0
- package/dist/src/lib/i18n/keys/factory.js +46 -0
- package/dist/src/lib/i18n/keys/fetcher.js +87 -0
- package/dist/src/lib/i18n/keys/index.js +16 -0
- package/dist/src/lib/i18n/keys/node.js +69 -0
- package/dist/src/lib/i18n/keys/prompts.js +79 -0
- package/dist/src/lib/i18n/keys/resources.js +53 -0
- package/dist/src/lib/i18n/keys/server.js +67 -0
- package/dist/src/lib/i18n/keys/tools.js +39 -0
- package/dist/src/lib/i18n/keys.js +504 -0
- package/dist/src/lib/i18n/locales/en/browser.js +79 -55
- package/dist/src/lib/i18n/locales/en/client.js +57 -33
- package/dist/src/lib/i18n/locales/en/errors.js +36 -12
- package/dist/src/lib/i18n/locales/en/factory.js +38 -0
- package/dist/src/lib/i18n/locales/en/fetcher.js +67 -60
- package/dist/src/lib/i18n/locales/en/index.js +9 -7
- package/dist/src/lib/i18n/locales/en/node.js +57 -35
- package/dist/src/lib/i18n/locales/en/prompts.js +42 -65
- package/dist/src/lib/i18n/locales/en/resources.js +52 -40
- package/dist/src/lib/i18n/locales/en/server.js +49 -24
- package/dist/src/lib/i18n/locales/en/tools.js +27 -19
- package/dist/src/lib/i18n/locales/zh/browser.js +79 -56
- package/dist/src/lib/i18n/locales/zh/client.js +57 -32
- package/dist/src/lib/i18n/locales/zh/errors.js +36 -12
- package/dist/src/lib/i18n/locales/zh/factory.js +38 -0
- package/dist/src/lib/i18n/locales/zh/fetcher.js +67 -60
- package/dist/src/lib/i18n/locales/zh/index.js +9 -7
- package/dist/src/lib/i18n/locales/zh/node.js +61 -36
- package/dist/src/lib/i18n/locales/zh/prompts.js +35 -57
- package/dist/src/lib/i18n/locales/zh/resources.js +51 -39
- package/dist/src/lib/i18n/locales/zh/server.js +51 -25
- package/dist/src/lib/i18n/locales/zh/tools.js +27 -19
- package/dist/src/lib/i18n/logger.js +90 -19
- package/dist/src/lib/logger.js +135 -14
- package/dist/src/lib/server/browser.js +12 -18
- package/dist/src/lib/server/fetcher.js +13 -34
- package/dist/src/lib/server/index.js +2 -2
- package/dist/src/lib/server/prompts.js +1 -1
- package/dist/src/lib/server/resources.js +1 -1
- package/dist/src/lib/server/tools.js +404 -308
- package/dist/src/lib/utils/ErrorHandler.js +125 -0
- package/dist/src/lib/utils/errorDetection.js +41 -0
- package/dist/src/lib/utils/errors.js +279 -0
- package/dist/src/mcp-server.js +0 -0
- package/dist/src/test-i18n.js +139 -0
- package/dist/tests/BrowserFetcher.test.js +951 -0
- package/dist/tests/NodeFetcher.test.js +263 -0
- package/dist/tests/client.test.js +211 -0
- package/dist/tests/fetch.test.js +70 -0
- package/dist/tests/fetchers/Fetcher.test.js +108 -0
- package/dist/tests/fetchers/browser/BrowserFetcher.test.js +195 -0
- package/dist/tests/fetchers/browser/BrowserInstance.test.js +163 -0
- package/dist/tests/fetchers/node/NodeFetcher.test.js +211 -0
- package/dist/tests/i18n-missing-keys.js +364 -0
- package/dist/tests/i18n-remove-unused-keys.js +236 -0
- package/dist/tests/i18n-test-report.json +2004 -0
- package/dist/tests/i18n-unused-keys.js +425 -0
- package/dist/tests/i18n.test.js +178 -0
- package/dist/tests/logger.test.js +100 -0
- package/dist/tests/mcp-server.test.js +113 -0
- package/dist/tests/server/browser.test.js +201 -0
- package/dist/tests/server/fetcher.test.js +173 -0
- package/dist/tests/server/index.test.js +122 -0
- package/dist/tests/server/prompts.test.js +246 -0
- package/dist/tests/server/resources.test.js +395 -0
- package/dist/tests/server/tools.test.js +223 -0
- package/dist/tests/setup.js +36 -0
- package/dist/tests/src/lib/i18n/index.js +108 -0
- package/dist/tests/src/lib/i18n/keys/base.js +47 -0
- package/dist/tests/src/lib/i18n/keys/browser.js +93 -0
- package/dist/tests/src/lib/i18n/keys/client.js +70 -0
- package/dist/tests/src/lib/i18n/keys/errors.js +34 -0
- package/dist/tests/src/lib/i18n/keys/fetcher.js +84 -0
- package/dist/tests/src/lib/i18n/keys/index.js +31 -0
- package/dist/tests/src/lib/i18n/keys/node.js +56 -0
- package/dist/tests/src/lib/i18n/keys/prompts.js +82 -0
- package/dist/tests/src/lib/i18n/keys/resources.js +50 -0
- package/dist/tests/src/lib/i18n/keys/server.js +64 -0
- package/dist/tests/src/lib/i18n/keys/tools.js +34 -0
- package/dist/tests/src/lib/i18n/locales/en/browser.js +88 -0
- package/dist/tests/src/lib/i18n/locales/en/client.js +66 -0
- package/dist/tests/src/lib/i18n/locales/en/errors.js +28 -0
- package/dist/tests/src/lib/i18n/locales/en/fetcher.js +71 -0
- package/dist/tests/src/lib/i18n/locales/en/index.js +29 -0
- package/dist/tests/src/lib/i18n/locales/en/node.js +51 -0
- package/dist/tests/src/lib/i18n/locales/en/prompts.js +52 -0
- package/dist/tests/src/lib/i18n/locales/en/resources.js +50 -0
- package/dist/tests/src/lib/i18n/locales/en/server.js +56 -0
- package/dist/tests/src/lib/i18n/locales/en/tools.js +28 -0
- package/dist/tests/src/lib/i18n/locales/zh/browser.js +87 -0
- package/dist/tests/src/lib/i18n/locales/zh/client.js +66 -0
- package/dist/tests/src/lib/i18n/locales/zh/errors.js +28 -0
- package/dist/tests/src/lib/i18n/locales/zh/fetcher.js +71 -0
- package/dist/tests/src/lib/i18n/locales/zh/index.js +29 -0
- package/dist/tests/src/lib/i18n/locales/zh/node.js +51 -0
- package/dist/tests/src/lib/i18n/locales/zh/prompts.js +53 -0
- package/dist/tests/src/lib/i18n/locales/zh/resources.js +50 -0
- package/dist/tests/src/lib/i18n/locales/zh/server.js +57 -0
- package/dist/tests/src/lib/i18n/locales/zh/tools.js +28 -0
- package/dist/tests/src/lib/i18n/logger.js +114 -0
- package/dist/tests/src/lib/logger.js +181 -0
- package/dist/tests/test-direct-client.js +0 -0
- package/dist/tests/test-i18n.js +530 -0
- package/dist/tests/test-mcp-methods.js +1 -1
- package/dist/tests/test-mcp.js +0 -0
- package/dist/tests/test-mini4k.js +0 -0
- package/dist/tests/tests/test-i18n.js +588 -0
- package/dist/tests/types.test.js +57 -0
- package/dist/tests/utils/ErrorHandler.test.js +144 -0
- package/dist/vitest.config.js +29 -0
- package/package.json +32 -19
- package/dist/src/cli-language.js +0 -78
- package/dist/src/lib/example.js +0 -93
- package/dist/src/lib/server/logger.js +0 -23
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author: Martin <lmccc.dev@gmail.com>
|
|
3
|
+
* Co-Author: AI Assistant (Claude)
|
|
4
|
+
* Description: This code was collaboratively developed by Martin and AI Assistant.
|
|
5
|
+
*/
|
|
6
|
+
// 检测未使用的国际化键 (Detect unused internationalization keys)
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { enTranslation } from '../src/lib/i18n/locales/en/index.js';
|
|
11
|
+
import { zhTranslation } from '../src/lib/i18n/locales/zh/index.js';
|
|
12
|
+
import * as allKeys from '../src/lib/i18n/keys/index.js';
|
|
13
|
+
// 获取当前文件的目录路径 (Get the directory path of the current file)
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
// 默认测试配置 (Default test configuration)
|
|
17
|
+
const defaultConfig = {
|
|
18
|
+
generateReport: true,
|
|
19
|
+
reportPath: path.join(__dirname, '../i18n-unused-keys-report.json'),
|
|
20
|
+
verbose: false,
|
|
21
|
+
srcDir: path.join(__dirname, '../src'),
|
|
22
|
+
excludeDirs: ['node_modules', 'dist', 'build', 'coverage'],
|
|
23
|
+
fileExtensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
24
|
+
preserveErrorKeys: true, // 默认保留错误相关的键 (Preserve error-related keys by default)
|
|
25
|
+
reportOnly: false, // 默认不只生成报告 (Don't only generate report by default)
|
|
26
|
+
strictMode: true // 默认使用严格模式 (Use strict mode by default)
|
|
27
|
+
};
|
|
28
|
+
// 解析命令行参数 (Parse command line arguments)
|
|
29
|
+
function parseCommandLineArgs() {
|
|
30
|
+
const config = { ...defaultConfig };
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const arg = args[i];
|
|
34
|
+
switch (arg) {
|
|
35
|
+
case '--no-report':
|
|
36
|
+
config.generateReport = false;
|
|
37
|
+
break;
|
|
38
|
+
case '--report-path':
|
|
39
|
+
if (i + 1 < args.length) {
|
|
40
|
+
config.reportPath = args[++i];
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
case '--verbose':
|
|
44
|
+
config.verbose = true;
|
|
45
|
+
break;
|
|
46
|
+
case '--src-dir':
|
|
47
|
+
if (i + 1 < args.length) {
|
|
48
|
+
config.srcDir = args[++i];
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
case '--exclude-dirs':
|
|
52
|
+
if (i + 1 < args.length) {
|
|
53
|
+
config.excludeDirs = args[++i].split(',');
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case '--file-extensions':
|
|
57
|
+
if (i + 1 < args.length) {
|
|
58
|
+
config.fileExtensions = args[++i].split(',');
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case '--no-preserve-error-keys':
|
|
62
|
+
config.preserveErrorKeys = false;
|
|
63
|
+
break;
|
|
64
|
+
case '--report-only':
|
|
65
|
+
config.reportOnly = true;
|
|
66
|
+
break;
|
|
67
|
+
case '--no-strict-mode':
|
|
68
|
+
config.strictMode = false;
|
|
69
|
+
break;
|
|
70
|
+
case '--help':
|
|
71
|
+
printHelp();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return config;
|
|
77
|
+
}
|
|
78
|
+
// 打印帮助信息 (Print help information)
|
|
79
|
+
function printHelp() {
|
|
80
|
+
console.log(`
|
|
81
|
+
未使用国际化键检测工具 (Unused Internationalization Keys Detection Tool)
|
|
82
|
+
|
|
83
|
+
用法: node --loader ts-node/esm tests/i18n-unused-keys.ts [选项]
|
|
84
|
+
|
|
85
|
+
选项:
|
|
86
|
+
--no-report 不生成报告文件 (Don't generate report file)
|
|
87
|
+
--report-path <path> 指定报告文件路径 (Specify report file path)
|
|
88
|
+
--verbose 显示详细信息 (Show verbose information)
|
|
89
|
+
--src-dir <path> 指定源代码目录 (Specify source code directory)
|
|
90
|
+
--exclude-dirs <dirs> 指定排除的目录,用逗号分隔 (Specify excluded directories, separated by commas)
|
|
91
|
+
--file-extensions <exts> 指定文件扩展名,用逗号分隔 (Specify file extensions, separated by commas)
|
|
92
|
+
--no-preserve-error-keys 不保留错误相关的键 (Don't preserve error-related keys)
|
|
93
|
+
--report-only 只生成报告,不执行删除 (Only generate report, don't delete)
|
|
94
|
+
--no-strict-mode 不使用严格模式,可能会误报 (Don't use strict mode, may have false positives)
|
|
95
|
+
--help 显示帮助信息 (Show help information)
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 递归获取对象中的所有键 (Recursively get all keys in an object)
|
|
100
|
+
* @param obj 要获取键的对象 (Object to get keys from)
|
|
101
|
+
* @param prefix 键前缀 (Key prefix)
|
|
102
|
+
* @returns 键数组 (Array of keys)
|
|
103
|
+
*/
|
|
104
|
+
function getAllKeysFromObject(obj, prefix = '') {
|
|
105
|
+
let keys = [];
|
|
106
|
+
for (const key in obj) {
|
|
107
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
108
|
+
// 递归获取嵌套对象的键 (Recursively get keys of nested objects)
|
|
109
|
+
keys = [...keys, ...getAllKeysFromObject(obj[key], prefix ? `${prefix}.${key}` : key)];
|
|
110
|
+
}
|
|
111
|
+
else if (typeof obj[key] === 'string') {
|
|
112
|
+
// 直接使用键值作为完整的键路径 (Use key value as the complete key path)
|
|
113
|
+
keys.push(obj[key]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return keys;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 从键模块中获取所有键 (Get all keys from key modules)
|
|
120
|
+
* @returns 键数组 (Array of keys)
|
|
121
|
+
*/
|
|
122
|
+
function getAllKeysFromModules() {
|
|
123
|
+
const allKeysSet = new Set();
|
|
124
|
+
// 遍历所有键模块 (Iterate through all key modules)
|
|
125
|
+
for (const moduleKey in allKeys) {
|
|
126
|
+
const keyModule = allKeys[moduleKey];
|
|
127
|
+
if (typeof keyModule === 'object' && keyModule !== null) {
|
|
128
|
+
// 获取模块中的所有键 (Get all keys in the module)
|
|
129
|
+
const keysInModule = getAllKeysFromObject(keyModule);
|
|
130
|
+
keysInModule.forEach(key => allKeysSet.add(key));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return Array.from(allKeysSet);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 获取翻译值 (Get translation value)
|
|
137
|
+
* @param key 键 (Key)
|
|
138
|
+
* @param translations 翻译资源 (Translation resources)
|
|
139
|
+
* @returns 翻译值 (Translation value)
|
|
140
|
+
*/
|
|
141
|
+
function getTranslationValue(key, translations) {
|
|
142
|
+
// 检查是否是命名空间格式的键 (Check if the key is in namespace format)
|
|
143
|
+
const parts = key.split('.');
|
|
144
|
+
if (parts.length > 1) {
|
|
145
|
+
const namespace = parts[0];
|
|
146
|
+
const subKey = parts.slice(1).join('.');
|
|
147
|
+
// 检查命名空间是否存在 (Check if the namespace exists)
|
|
148
|
+
if (translations[namespace] && translations[namespace][subKey] !== undefined) {
|
|
149
|
+
return translations[namespace][subKey];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 直接在翻译资源中查找键 (Look up the key directly in translation resources)
|
|
153
|
+
for (const moduleName in translations) {
|
|
154
|
+
const module = translations[moduleName];
|
|
155
|
+
if (module && module[key] !== undefined) {
|
|
156
|
+
return module[key];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 获取项目中的所有源代码文件 (Get all source code files in the project)
|
|
163
|
+
* @param config 测试配置 (Test configuration)
|
|
164
|
+
* @returns 文件路径数组 (Array of file paths)
|
|
165
|
+
*/
|
|
166
|
+
async function getAllSourceFiles(config) {
|
|
167
|
+
const { srcDir, excludeDirs, fileExtensions } = config;
|
|
168
|
+
console.log(`查找源代码文件,目录: ${srcDir} (Finding source code files, directory: ${srcDir})`);
|
|
169
|
+
try {
|
|
170
|
+
// 使用更简单的方法查找文件 (Use a simpler method to find files)
|
|
171
|
+
const allFiles = [];
|
|
172
|
+
// 递归查找文件 (Recursively find files)
|
|
173
|
+
function findFilesRecursively(dir) {
|
|
174
|
+
const files = fs.readdirSync(dir);
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
const filePath = path.join(dir, file);
|
|
177
|
+
const stat = fs.statSync(filePath);
|
|
178
|
+
// 检查是否是目录 (Check if it's a directory)
|
|
179
|
+
if (stat.isDirectory()) {
|
|
180
|
+
// 检查是否应该排除该目录 (Check if the directory should be excluded)
|
|
181
|
+
if (!excludeDirs.some(excludeDir => file === excludeDir)) {
|
|
182
|
+
findFilesRecursively(filePath);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (stat.isFile()) {
|
|
186
|
+
// 检查文件扩展名 (Check file extension)
|
|
187
|
+
const ext = path.extname(file);
|
|
188
|
+
if (fileExtensions.includes(ext)) {
|
|
189
|
+
allFiles.push(filePath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 开始递归查找 (Start recursive search)
|
|
195
|
+
findFilesRecursively(srcDir);
|
|
196
|
+
// 也包括测试目录 (Also include test directory)
|
|
197
|
+
const testDir = path.join(__dirname, '..');
|
|
198
|
+
if (testDir !== srcDir) {
|
|
199
|
+
findFilesRecursively(testDir);
|
|
200
|
+
}
|
|
201
|
+
console.log(`找到 ${allFiles.length} 个源代码文件 (Found ${allFiles.length} source code files)`);
|
|
202
|
+
return allFiles;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error(`获取源代码文件时出错 (Error getting source code files):`, error);
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 检查文件中是否使用了指定的键 (Check if a key is used in a file)
|
|
211
|
+
* @param filePath 文件路径 (File path)
|
|
212
|
+
* @param key 键 (Key)
|
|
213
|
+
* @returns 是否使用了键 (Whether the key is used)
|
|
214
|
+
*/
|
|
215
|
+
function isKeyUsedInFile(filePath, key) {
|
|
216
|
+
try {
|
|
217
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
218
|
+
// 检查直接使用键的情况 (Check direct key usage)
|
|
219
|
+
if (content.includes(`'${key}'`) || content.includes(`"${key}"`)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
// 检查使用 t 函数的情况 (Check usage with t function)
|
|
223
|
+
if (content.includes(`t('${key}'`) || content.includes(`t("${key}"`)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
// 检查使用 log 函数的情况 (Check usage with log function)
|
|
227
|
+
if (content.includes(`log('${key}'`) || content.includes(`log("${key}"`)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
// 检查动态生成的键名 (Check dynamically generated key names)
|
|
231
|
+
const parts = key.split('.');
|
|
232
|
+
if (parts.length > 1) {
|
|
233
|
+
const namespace = parts[0];
|
|
234
|
+
const subKey = parts.slice(1).join('.');
|
|
235
|
+
// 检查使用模板字符串的情况 (Check usage with template strings)
|
|
236
|
+
if (content.includes(`\`${namespace}.`) ||
|
|
237
|
+
(content.includes(`\`\${`) && content.includes(`}.${subKey}\``))) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
// 检查使用字符串拼接的情况 (Check usage with string concatenation)
|
|
241
|
+
if (content.includes(`'${namespace}.' +`) || content.includes(`"${namespace}." +`) ||
|
|
242
|
+
content.includes(`+ '.${subKey}'`) || content.includes(`+ ".${subKey}"`)) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
// 检查使用变量拼接的情况 (Check usage with variable concatenation)
|
|
246
|
+
if (content.includes(`${namespace}.` + subKey) ||
|
|
247
|
+
content.includes(`${namespace}["${subKey}"]`) ||
|
|
248
|
+
content.includes(`${namespace}['${subKey}']`)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
console.error(`读取文件 ${filePath} 时出错 (Error reading file ${filePath}):`, error);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 检查键是否在任何文件中使用 (Check if a key is used in any file)
|
|
261
|
+
* @param key 键 (Key)
|
|
262
|
+
* @param files 文件路径数组 (Array of file paths)
|
|
263
|
+
* @param config 测试配置 (Test configuration)
|
|
264
|
+
* @returns 是否使用了键 (Whether the key is used)
|
|
265
|
+
*/
|
|
266
|
+
function isKeyUsed(key, files, config) {
|
|
267
|
+
// 如果配置为保留错误相关的键,并且键以 errors. 开头,则认为它是使用的
|
|
268
|
+
if (config.preserveErrorKeys && key.startsWith('errors.')) {
|
|
269
|
+
if (config.verbose) {
|
|
270
|
+
console.log(`保留错误相关的键: ${key} (Preserving error-related key: ${key})`);
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
// 在严格模式下,保留所有可能动态生成的键
|
|
275
|
+
if (config.strictMode) {
|
|
276
|
+
// 保留所有可能通过动态方式使用的键
|
|
277
|
+
const parts = key.split('.');
|
|
278
|
+
if (parts.length > 1) {
|
|
279
|
+
const namespace = parts[0];
|
|
280
|
+
// 检查是否有文件包含这个命名空间的动态键生成
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
283
|
+
// 检查各种动态键生成模式
|
|
284
|
+
if (content.includes(`${namespace}.`) &&
|
|
285
|
+
(content.includes('variable') ||
|
|
286
|
+
content.includes('dynamic') ||
|
|
287
|
+
content.includes('concat') ||
|
|
288
|
+
content.includes('join') ||
|
|
289
|
+
content.includes('template'))) {
|
|
290
|
+
if (config.verbose) {
|
|
291
|
+
console.log(`可能动态使用的键: ${key} 在文件 ${file} 中 (Potentially dynamically used key: ${key} in file ${file})`);
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// 检查每个文件中是否使用了键
|
|
299
|
+
for (const file of files) {
|
|
300
|
+
if (isKeyUsedInFile(file, key)) {
|
|
301
|
+
if (config.verbose) {
|
|
302
|
+
console.log(`键 ${key} 在文件 ${file} 中使用 (Key ${key} is used in file ${file})`);
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* 生成报告 (Generate report)
|
|
311
|
+
* @param report 报告数据 (Report data)
|
|
312
|
+
* @param reportPath 报告文件路径 (Report file path)
|
|
313
|
+
*/
|
|
314
|
+
function generateReport(report, reportPath) {
|
|
315
|
+
try {
|
|
316
|
+
// 创建目录 (Create directory)
|
|
317
|
+
const dir = path.dirname(reportPath);
|
|
318
|
+
if (!fs.existsSync(dir)) {
|
|
319
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
320
|
+
}
|
|
321
|
+
// 写入报告文件 (Write report file)
|
|
322
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
323
|
+
console.log(`报告已生成: ${reportPath} (Report generated: ${reportPath})`);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error(`生成报告时出错 (Error generating report):`, error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 检查未使用的键 (Check unused keys)
|
|
331
|
+
* @param config 测试配置 (Test configuration)
|
|
332
|
+
*/
|
|
333
|
+
async function checkUnusedKeys(config) {
|
|
334
|
+
console.log('\n=== 检查未使用的国际化键 (Check unused internationalization keys) ===');
|
|
335
|
+
try {
|
|
336
|
+
// 获取所有键 (Get all keys)
|
|
337
|
+
const allKeys = getAllKeysFromModules();
|
|
338
|
+
console.log(`总计找到 ${allKeys.length} 个键 (Total keys found: ${allKeys.length})`);
|
|
339
|
+
// 获取所有源代码文件 (Get all source code files)
|
|
340
|
+
const files = await getAllSourceFiles(config);
|
|
341
|
+
console.log(`总计找到 ${files.length} 个源代码文件 (Total source code files found: ${files.length})`);
|
|
342
|
+
// 检查每个键是否被使用 (Check if each key is used)
|
|
343
|
+
const unusedKeys = [];
|
|
344
|
+
const usedKeys = [];
|
|
345
|
+
for (const key of allKeys) {
|
|
346
|
+
if (isKeyUsed(key, files, config)) {
|
|
347
|
+
usedKeys.push(key);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
unusedKeys.push(key);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// 输出结果 (Output results)
|
|
354
|
+
if (unusedKeys.length > 0) {
|
|
355
|
+
console.log(`\n❌ 发现 ${unusedKeys.length} 个未使用的键 (Found ${unusedKeys.length} unused keys)`);
|
|
356
|
+
console.log(`✅ 发现 ${usedKeys.length} 个使用的键 (Found ${usedKeys.length} used keys)`);
|
|
357
|
+
if (config.verbose) {
|
|
358
|
+
for (const key of unusedKeys) {
|
|
359
|
+
const enValue = getTranslationValue(key, enTranslation);
|
|
360
|
+
const zhValue = getTranslationValue(key, zhTranslation);
|
|
361
|
+
console.log(`键 (Key): ${key}`);
|
|
362
|
+
console.log(`英文值 (English value): ${enValue}`);
|
|
363
|
+
console.log(`中文值 (Chinese value): ${zhValue}`);
|
|
364
|
+
console.log('---');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.log('\n✅ 所有键都被使用 (All keys are used)');
|
|
370
|
+
}
|
|
371
|
+
// 生成报告 (Generate report)
|
|
372
|
+
if (config.generateReport) {
|
|
373
|
+
const report = {
|
|
374
|
+
timestamp: new Date().toISOString(),
|
|
375
|
+
totalKeys: allKeys.length,
|
|
376
|
+
totalUsedKeys: usedKeys.length,
|
|
377
|
+
totalFiles: files.length,
|
|
378
|
+
unusedKeys: unusedKeys.map(key => ({
|
|
379
|
+
key,
|
|
380
|
+
enValue: getTranslationValue(key, enTranslation),
|
|
381
|
+
zhValue: getTranslationValue(key, zhTranslation)
|
|
382
|
+
})),
|
|
383
|
+
hasUnusedKeys: unusedKeys.length > 0
|
|
384
|
+
};
|
|
385
|
+
generateReport(report, config.reportPath);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
unusedKeys,
|
|
389
|
+
hasUnusedKeys: unusedKeys.length > 0
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.error('\n❌ 检查未使用的键时出错 (Error checking unused keys):');
|
|
394
|
+
console.error(error);
|
|
395
|
+
return {
|
|
396
|
+
unusedKeys: [],
|
|
397
|
+
hasUnusedKeys: true
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 主函数 (Main function)
|
|
403
|
+
*/
|
|
404
|
+
async function main() {
|
|
405
|
+
console.log('=== 检测未使用的国际化键 (Detecting unused internationalization keys) ===');
|
|
406
|
+
try {
|
|
407
|
+
// 解析命令行参数 (Parse command line arguments)
|
|
408
|
+
const config = parseCommandLineArgs();
|
|
409
|
+
// 检查是否只生成报告 (Check if only generating report)
|
|
410
|
+
if (config.reportOnly) {
|
|
411
|
+
console.log('只生成报告,不执行删除 (Only generating report, not deleting)');
|
|
412
|
+
}
|
|
413
|
+
// 检查未使用的键 (Check unused keys)
|
|
414
|
+
const result = await checkUnusedKeys(config);
|
|
415
|
+
// 设置退出码 (Set exit code)
|
|
416
|
+
process.exit(result.hasUnusedKeys ? 1 : 0);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
console.error('❌ 程序执行出错 (Error executing program):');
|
|
420
|
+
console.error(error);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// 执行主函数 (Execute main function)
|
|
425
|
+
main();
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author: Martin <lmccc.dev@gmail.com>
|
|
3
|
+
* Co-Author: AI Assistant (Claude)
|
|
4
|
+
* Description: This code was collaboratively developed by Martin and AI Assistant.
|
|
5
|
+
*/
|
|
6
|
+
import i18next, { t, changeLanguage, getCurrentLanguage } from '../src/lib/i18n/index.js';
|
|
7
|
+
import { createLogger } from '../src/lib/i18n/logger.js';
|
|
8
|
+
import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
describe('i18n模块测试 (i18n Module Tests)', () => {
|
|
10
|
+
// 保存原始环境变量
|
|
11
|
+
const originalEnv = process.env.MCP_LANG;
|
|
12
|
+
// 在每个测试后恢复环境变量
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
process.env.MCP_LANG = originalEnv;
|
|
15
|
+
});
|
|
16
|
+
test('默认语言为英语 (Default language is English)', () => {
|
|
17
|
+
// 确保没有设置MCP_LANG环境变量
|
|
18
|
+
delete process.env.MCP_LANG;
|
|
19
|
+
// 重新初始化i18next
|
|
20
|
+
i18next.init({
|
|
21
|
+
resources: i18next.options.resources,
|
|
22
|
+
lng: process.env.MCP_LANG || 'en',
|
|
23
|
+
fallbackLng: 'en',
|
|
24
|
+
interpolation: {
|
|
25
|
+
escapeValue: false
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// 验证当前语言为英语
|
|
29
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
30
|
+
});
|
|
31
|
+
test('可以切换语言 (Can switch language)', () => {
|
|
32
|
+
// 切换到中文
|
|
33
|
+
changeLanguage('zh');
|
|
34
|
+
// 验证当前语言为中文
|
|
35
|
+
expect(getCurrentLanguage()).toBe('zh');
|
|
36
|
+
// 切换回英语
|
|
37
|
+
changeLanguage('en');
|
|
38
|
+
// 验证当前语言为英语
|
|
39
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
40
|
+
});
|
|
41
|
+
test('翻译函数能正确工作 (Translation function works correctly)', () => {
|
|
42
|
+
// 切换到英语
|
|
43
|
+
changeLanguage('en');
|
|
44
|
+
// 测试一个存在的翻译键
|
|
45
|
+
const serverStartingKey = 'server.starting';
|
|
46
|
+
const serverStartingTranslation = t(serverStartingKey);
|
|
47
|
+
// 验证翻译结果不为空且不等于键名
|
|
48
|
+
expect(serverStartingTranslation).not.toBe('');
|
|
49
|
+
expect(serverStartingTranslation).not.toBe(serverStartingKey);
|
|
50
|
+
// 切换到中文
|
|
51
|
+
changeLanguage('zh');
|
|
52
|
+
// 测试同一个翻译键
|
|
53
|
+
const zhTranslation = t(serverStartingKey);
|
|
54
|
+
// 验证中文翻译结果不为空且不等于键名
|
|
55
|
+
expect(zhTranslation).not.toBe('');
|
|
56
|
+
expect(zhTranslation).not.toBe(serverStartingKey);
|
|
57
|
+
// 验证中文翻译与英文翻译不同
|
|
58
|
+
expect(zhTranslation).not.toBe(serverStartingTranslation);
|
|
59
|
+
});
|
|
60
|
+
test('带参数的翻译 (Translation with parameters)', () => {
|
|
61
|
+
// 切换到英语
|
|
62
|
+
changeLanguage('en');
|
|
63
|
+
// 使用一个简单的翻译键进行测试
|
|
64
|
+
const testKey = 'server.starting';
|
|
65
|
+
const params = { name: 'Test Server' };
|
|
66
|
+
const translationWithParams = t(testKey, params);
|
|
67
|
+
// 验证翻译结果不为空且不等于键名
|
|
68
|
+
expect(translationWithParams).not.toBe('');
|
|
69
|
+
expect(translationWithParams).not.toBe(testKey);
|
|
70
|
+
});
|
|
71
|
+
test('不存在的翻译键返回键名 (Non-existent translation key returns key name)', () => {
|
|
72
|
+
// 测试不存在的翻译键
|
|
73
|
+
const nonExistentKey = 'non.existent.key';
|
|
74
|
+
const translation = t(nonExistentKey);
|
|
75
|
+
// 验证翻译结果等于键名
|
|
76
|
+
expect(translation).toBe(nonExistentKey);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('i18n Logger模块测试 (i18n Logger Module Tests)', () => {
|
|
80
|
+
// 保存原始的console方法
|
|
81
|
+
const originalConsoleLog = console.log;
|
|
82
|
+
const originalConsoleError = console.error;
|
|
83
|
+
// 在每个测试前设置mock
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
console.log = vi.fn();
|
|
86
|
+
console.error = vi.fn();
|
|
87
|
+
process.env.DEBUG = 'false';
|
|
88
|
+
});
|
|
89
|
+
// 在每个测试后恢复原始方法
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
console.log = originalConsoleLog;
|
|
92
|
+
console.error = originalConsoleError;
|
|
93
|
+
delete process.env.DEBUG;
|
|
94
|
+
});
|
|
95
|
+
test('创建日志记录器 (Create logger)', () => {
|
|
96
|
+
// 创建日志记录器
|
|
97
|
+
const logger = createLogger('TEST');
|
|
98
|
+
// 验证logger对象包含预期的方法
|
|
99
|
+
expect(logger).toHaveProperty('info');
|
|
100
|
+
expect(logger).toHaveProperty('error');
|
|
101
|
+
expect(logger).toHaveProperty('warn');
|
|
102
|
+
expect(logger).toHaveProperty('debug');
|
|
103
|
+
});
|
|
104
|
+
test('info方法在forceLog=true时输出日志 (info method outputs logs when forceLog=true)', () => {
|
|
105
|
+
// 创建日志记录器
|
|
106
|
+
const logger = createLogger('TEST');
|
|
107
|
+
// 调用info方法,设置forceLog为true
|
|
108
|
+
logger.info('test.info', {}, true);
|
|
109
|
+
// 验证console.error被调用(i18n logger使用console.error输出所有日志)
|
|
110
|
+
expect(console.error).toHaveBeenCalled();
|
|
111
|
+
// 验证调用参数包含前缀和消息
|
|
112
|
+
const callArg = console.error.mock.calls[0][0];
|
|
113
|
+
expect(callArg).toContain('[TEST]');
|
|
114
|
+
});
|
|
115
|
+
test('info方法在DEBUG=false且forceDebug=false时不输出日志 (info method does not output logs when DEBUG=false and forceDebug=false)', () => {
|
|
116
|
+
// 创建日志记录器
|
|
117
|
+
const logger = createLogger('TEST');
|
|
118
|
+
// 调用info方法,不强制输出
|
|
119
|
+
logger.info('test.info');
|
|
120
|
+
// 验证console.error没有被调用
|
|
121
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
test('info方法在forceDebug=true时输出日志 (info method outputs logs when forceDebug=true)', () => {
|
|
124
|
+
// 创建日志记录器
|
|
125
|
+
const logger = createLogger('TEST');
|
|
126
|
+
// 调用info方法,强制输出
|
|
127
|
+
logger.info('test.info', {}, true);
|
|
128
|
+
// 验证console.error被调用
|
|
129
|
+
expect(console.error).toHaveBeenCalled();
|
|
130
|
+
// 验证调用参数包含前缀和消息
|
|
131
|
+
const callArg = console.error.mock.calls[0][0];
|
|
132
|
+
expect(callArg).toContain('[TEST]');
|
|
133
|
+
});
|
|
134
|
+
test('error方法总是输出日志 (error method always outputs logs)', () => {
|
|
135
|
+
// 创建日志记录器
|
|
136
|
+
const logger = createLogger('TEST');
|
|
137
|
+
// 调用error方法
|
|
138
|
+
logger.error('test.error');
|
|
139
|
+
// 验证console.error被调用
|
|
140
|
+
expect(console.error).toHaveBeenCalled();
|
|
141
|
+
// 验证调用参数包含前缀和消息
|
|
142
|
+
const callArg = console.error.mock.calls[0][0];
|
|
143
|
+
expect(callArg).toContain('[TEST]');
|
|
144
|
+
expect(callArg).toContain('test.error');
|
|
145
|
+
});
|
|
146
|
+
test('warn方法总是输出日志 (warn method always outputs logs)', () => {
|
|
147
|
+
// 创建日志记录器
|
|
148
|
+
const logger = createLogger('TEST');
|
|
149
|
+
// 调用warn方法
|
|
150
|
+
logger.warn('test.warn');
|
|
151
|
+
// 验证console.error被调用
|
|
152
|
+
expect(console.error).toHaveBeenCalled();
|
|
153
|
+
// 验证调用参数包含前缀和消息
|
|
154
|
+
const callArg = console.error.mock.calls[0][0];
|
|
155
|
+
expect(callArg).toContain('[TEST]');
|
|
156
|
+
expect(callArg).toContain('test.warn');
|
|
157
|
+
});
|
|
158
|
+
test('debug方法在forceDebug=true时输出日志 (debug method outputs logs when forceDebug=true)', () => {
|
|
159
|
+
// 创建日志记录器
|
|
160
|
+
const logger = createLogger('TEST');
|
|
161
|
+
// 调用debug方法,强制输出
|
|
162
|
+
logger.debug('test.debug', {}, true);
|
|
163
|
+
// 验证console.error被调用
|
|
164
|
+
expect(console.error).toHaveBeenCalled();
|
|
165
|
+
// 验证调用参数包含前缀和消息
|
|
166
|
+
const callArg = console.error.mock.calls[0][0];
|
|
167
|
+
expect(callArg).toContain('[TEST]');
|
|
168
|
+
expect(callArg).toContain('test.debug');
|
|
169
|
+
});
|
|
170
|
+
test('debug方法在forceDebug=false时不输出日志 (debug method does not output logs when forceDebug=false)', () => {
|
|
171
|
+
// 创建日志记录器
|
|
172
|
+
const logger = createLogger('TEST');
|
|
173
|
+
// 调用debug方法,不强制输出
|
|
174
|
+
logger.debug('test.debug');
|
|
175
|
+
// 验证console.error没有被调用
|
|
176
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
});
|