@lmcc-dev/mult-fetch-mcp-server 1.1.0 → 1.2.1

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.
Files changed (216) hide show
  1. package/README.md +8 -8
  2. package/README.zh.md +16 -8
  3. package/dist/i18n-test-report.json +8 -0
  4. package/dist/i18n-unused-keys-report.json +8 -0
  5. package/dist/src/client.js +70 -37
  6. package/dist/src/index.js +0 -0
  7. package/dist/src/lib/BrowserFetcher.js +12 -12
  8. package/dist/src/lib/NodeFetcher.js +22 -22
  9. package/dist/src/lib/fetch.js +40 -0
  10. package/dist/src/lib/fetchers/FetcherFactory.js +43 -0
  11. package/dist/src/lib/fetchers/browser/BrowserFetcher.js +381 -0
  12. package/dist/src/lib/fetchers/browser/BrowserInstance.js +180 -0
  13. package/dist/src/lib/fetchers/browser/PageOperations.js +168 -0
  14. package/dist/src/lib/fetchers/common/types.js +41 -0
  15. package/dist/src/lib/fetchers/common/utils.js +141 -0
  16. package/dist/src/lib/fetchers/index.js +62 -0
  17. package/dist/src/lib/fetchers/node/HttpClient.js +239 -0
  18. package/dist/src/lib/fetchers/node/NodeFetcher.js +180 -0
  19. package/dist/src/lib/i18n/index.js +105 -17
  20. package/dist/src/lib/i18n/keys/base.js +41 -0
  21. package/dist/src/lib/i18n/keys/browser.js +91 -0
  22. package/dist/src/lib/i18n/keys/client.js +68 -0
  23. package/dist/src/lib/i18n/keys/errors.js +49 -0
  24. package/dist/src/lib/i18n/keys/factory.js +46 -0
  25. package/dist/src/lib/i18n/keys/fetcher.js +87 -0
  26. package/dist/src/lib/i18n/keys/index.js +16 -0
  27. package/dist/src/lib/i18n/keys/node.js +69 -0
  28. package/dist/src/lib/i18n/keys/prompts.js +79 -0
  29. package/dist/src/lib/i18n/keys/resources.js +53 -0
  30. package/dist/src/lib/i18n/keys/server.js +67 -0
  31. package/dist/src/lib/i18n/keys/tools.js +39 -0
  32. package/dist/src/lib/i18n/keys.js +488 -119
  33. package/dist/src/lib/i18n/locales/en/browser.js +67 -57
  34. package/dist/src/lib/i18n/locales/en/client.js +57 -33
  35. package/dist/src/lib/i18n/locales/en/errors.js +36 -12
  36. package/dist/src/lib/i18n/locales/en/factory.js +38 -0
  37. package/dist/src/lib/i18n/locales/en/fetcher.js +8 -2
  38. package/dist/src/lib/i18n/locales/en/index.js +10 -8
  39. package/dist/src/lib/i18n/locales/en/node.js +57 -35
  40. package/dist/src/lib/i18n/locales/en/prompts.js +42 -65
  41. package/dist/src/lib/i18n/locales/en/resources.js +52 -55
  42. package/dist/src/lib/i18n/locales/en/server.js +49 -24
  43. package/dist/src/lib/i18n/locales/en/tools.js +27 -19
  44. package/dist/src/lib/i18n/locales/zh/browser.js +71 -62
  45. package/dist/src/lib/i18n/locales/zh/client.js +57 -32
  46. package/dist/src/lib/i18n/locales/zh/errors.js +36 -12
  47. package/dist/src/lib/i18n/locales/zh/factory.js +38 -0
  48. package/dist/src/lib/i18n/locales/zh/fetcher.js +36 -30
  49. package/dist/src/lib/i18n/locales/zh/index.js +10 -8
  50. package/dist/src/lib/i18n/locales/zh/node.js +61 -36
  51. package/dist/src/lib/i18n/locales/zh/prompts.js +35 -57
  52. package/dist/src/lib/i18n/locales/zh/resources.js +52 -55
  53. package/dist/src/lib/i18n/locales/zh/server.js +51 -25
  54. package/dist/src/lib/i18n/locales/zh/tools.js +27 -19
  55. package/dist/src/lib/i18n/logger.js +90 -19
  56. package/dist/src/lib/logger.js +135 -14
  57. package/dist/src/lib/server/browser.js +42 -10
  58. package/dist/src/lib/server/fetcher.js +87 -37
  59. package/dist/src/lib/server/index.js +2 -2
  60. package/dist/src/lib/server/prompts.js +1 -1
  61. package/dist/src/lib/server/resources.js +1 -1
  62. package/dist/src/lib/server/tools.js +46 -10
  63. package/dist/src/lib/utils/ErrorHandler.js +125 -0
  64. package/dist/src/mcp-server.js +0 -0
  65. package/dist/src/test-i18n.js +139 -0
  66. package/dist/tests/BrowserFetcher.test.js +0 -0
  67. package/dist/tests/NodeFetcher.test.js +0 -0
  68. package/dist/tests/client.test.js +42 -81
  69. package/dist/tests/fetch.test.js +70 -0
  70. package/dist/tests/fetchers/Fetcher.test.js +108 -0
  71. package/dist/tests/fetchers/browser/BrowserFetcher.test.js +195 -0
  72. package/dist/tests/fetchers/browser/BrowserInstance.test.js +163 -0
  73. package/dist/tests/fetchers/node/NodeFetcher.test.js +211 -0
  74. package/dist/tests/i18n-missing-keys.js +364 -0
  75. package/dist/tests/i18n-remove-unused-keys.js +236 -0
  76. package/dist/tests/i18n-test-report.json +2004 -0
  77. package/dist/tests/i18n-unused-keys.js +425 -0
  78. package/dist/tests/i18n.test.js +6 -7
  79. package/dist/tests/logger.test.js +14 -13
  80. package/dist/tests/mcp-server.test.js +7 -7
  81. package/dist/tests/server/browser.test.js +25 -21
  82. package/dist/tests/server/fetcher.test.js +58 -101
  83. package/dist/tests/server/index.test.js +30 -30
  84. package/dist/tests/server/prompts.test.js +36 -33
  85. package/dist/tests/server/resources.test.js +120 -84
  86. package/dist/tests/server/tools.test.js +7 -6
  87. package/dist/tests/setup.js +36 -0
  88. package/dist/tests/src/lib/i18n/index.js +108 -0
  89. package/dist/tests/src/lib/i18n/keys/base.js +47 -0
  90. package/dist/tests/src/lib/i18n/keys/browser.js +93 -0
  91. package/dist/tests/src/lib/i18n/keys/client.js +70 -0
  92. package/dist/tests/src/lib/i18n/keys/errors.js +34 -0
  93. package/dist/tests/src/lib/i18n/keys/fetcher.js +84 -0
  94. package/dist/tests/src/lib/i18n/keys/index.js +31 -0
  95. package/dist/tests/src/lib/i18n/keys/node.js +56 -0
  96. package/dist/tests/src/lib/i18n/keys/prompts.js +82 -0
  97. package/dist/tests/src/lib/i18n/keys/resources.js +50 -0
  98. package/dist/tests/src/lib/i18n/keys/server.js +64 -0
  99. package/dist/tests/src/lib/i18n/keys/tools.js +34 -0
  100. package/dist/tests/src/lib/i18n/locales/en/browser.js +88 -0
  101. package/dist/tests/src/lib/i18n/locales/en/client.js +66 -0
  102. package/dist/tests/src/lib/i18n/locales/en/errors.js +28 -0
  103. package/dist/tests/src/lib/i18n/locales/en/fetcher.js +71 -0
  104. package/dist/tests/src/lib/i18n/locales/en/index.js +29 -0
  105. package/dist/tests/src/lib/i18n/locales/en/node.js +51 -0
  106. package/dist/tests/src/lib/i18n/locales/en/prompts.js +52 -0
  107. package/dist/tests/src/lib/i18n/locales/en/resources.js +50 -0
  108. package/dist/tests/src/lib/i18n/locales/en/server.js +56 -0
  109. package/dist/tests/src/lib/i18n/locales/en/tools.js +28 -0
  110. package/dist/tests/src/lib/i18n/locales/zh/browser.js +87 -0
  111. package/dist/tests/src/lib/i18n/locales/zh/client.js +66 -0
  112. package/dist/tests/src/lib/i18n/locales/zh/errors.js +28 -0
  113. package/dist/tests/src/lib/i18n/locales/zh/fetcher.js +71 -0
  114. package/dist/tests/src/lib/i18n/locales/zh/index.js +29 -0
  115. package/dist/tests/src/lib/i18n/locales/zh/node.js +51 -0
  116. package/dist/tests/src/lib/i18n/locales/zh/prompts.js +53 -0
  117. package/dist/tests/src/lib/i18n/locales/zh/resources.js +50 -0
  118. package/dist/tests/src/lib/i18n/locales/zh/server.js +57 -0
  119. package/dist/tests/src/lib/i18n/locales/zh/tools.js +28 -0
  120. package/dist/tests/src/lib/i18n/logger.js +114 -0
  121. package/dist/tests/src/lib/logger.js +181 -0
  122. package/dist/tests/test-direct-client.js +0 -0
  123. package/dist/tests/test-i18n.js +530 -0
  124. package/dist/tests/test-mcp-methods.js +1 -1
  125. package/dist/tests/test-mcp.js +0 -0
  126. package/dist/tests/test-mini4k.js +0 -0
  127. package/dist/tests/tests/test-i18n.js +588 -0
  128. package/dist/tests/types.test.js +1 -1
  129. package/dist/tests/utils/ErrorHandler.test.js +144 -0
  130. package/dist/vitest.config.js +29 -0
  131. package/package.json +31 -27
  132. package/dist/src/cli-language.js +0 -78
  133. package/dist/src/client.d.ts +0 -6
  134. package/dist/src/client.js.map +0 -1
  135. package/dist/src/index.d.ts +0 -15
  136. package/dist/src/index.js.map +0 -1
  137. package/dist/src/lib/BrowserFetcher.d.ts +0 -175
  138. package/dist/src/lib/BrowserFetcher.js.map +0 -1
  139. package/dist/src/lib/NodeFetcher.d.ts +0 -92
  140. package/dist/src/lib/NodeFetcher.js.map +0 -1
  141. package/dist/src/lib/example.js +0 -93
  142. package/dist/src/lib/i18n/index.js.map +0 -1
  143. package/dist/src/lib/i18n/locales/en/browser.d.ts +0 -61
  144. package/dist/src/lib/i18n/locales/en/browser.js.map +0 -1
  145. package/dist/src/lib/i18n/locales/en/client.d.ts +0 -39
  146. package/dist/src/lib/i18n/locales/en/client.js.map +0 -1
  147. package/dist/src/lib/i18n/locales/en/errors.d.ts +0 -17
  148. package/dist/src/lib/i18n/locales/en/errors.js.map +0 -1
  149. package/dist/src/lib/i18n/locales/en/fetcher.d.ts +0 -66
  150. package/dist/src/lib/i18n/locales/en/fetcher.js.map +0 -1
  151. package/dist/src/lib/i18n/locales/en/index.d.ts +0 -344
  152. package/dist/src/lib/i18n/locales/en/index.js.map +0 -1
  153. package/dist/src/lib/i18n/locales/en/node.d.ts +0 -41
  154. package/dist/src/lib/i18n/locales/en/node.js.map +0 -1
  155. package/dist/src/lib/i18n/locales/en/prompts.d.ts +0 -65
  156. package/dist/src/lib/i18n/locales/en/prompts.js.map +0 -1
  157. package/dist/src/lib/i18n/locales/en/resources.d.ts +0 -41
  158. package/dist/src/lib/i18n/locales/en/resources.js.map +0 -1
  159. package/dist/src/lib/i18n/locales/en/server.d.ts +0 -30
  160. package/dist/src/lib/i18n/locales/en/server.js.map +0 -1
  161. package/dist/src/lib/i18n/locales/en/tools.d.ts +0 -24
  162. package/dist/src/lib/i18n/locales/en/tools.js.map +0 -1
  163. package/dist/src/lib/i18n/locales/zh/browser.d.ts +0 -61
  164. package/dist/src/lib/i18n/locales/zh/browser.js.map +0 -1
  165. package/dist/src/lib/i18n/locales/zh/client.d.ts +0 -38
  166. package/dist/src/lib/i18n/locales/zh/client.js.map +0 -1
  167. package/dist/src/lib/i18n/locales/zh/errors.d.ts +0 -17
  168. package/dist/src/lib/i18n/locales/zh/errors.js.map +0 -1
  169. package/dist/src/lib/i18n/locales/zh/fetcher.d.ts +0 -66
  170. package/dist/src/lib/i18n/locales/zh/fetcher.js.map +0 -1
  171. package/dist/src/lib/i18n/locales/zh/index.d.ts +0 -343
  172. package/dist/src/lib/i18n/locales/zh/index.js.map +0 -1
  173. package/dist/src/lib/i18n/locales/zh/node.d.ts +0 -41
  174. package/dist/src/lib/i18n/locales/zh/node.js.map +0 -1
  175. package/dist/src/lib/i18n/locales/zh/prompts.d.ts +0 -65
  176. package/dist/src/lib/i18n/locales/zh/prompts.js.map +0 -1
  177. package/dist/src/lib/i18n/locales/zh/resources.d.ts +0 -41
  178. package/dist/src/lib/i18n/locales/zh/resources.js.map +0 -1
  179. package/dist/src/lib/i18n/locales/zh/server.d.ts +0 -30
  180. package/dist/src/lib/i18n/locales/zh/server.js.map +0 -1
  181. package/dist/src/lib/i18n/locales/zh/tools.d.ts +0 -24
  182. package/dist/src/lib/i18n/locales/zh/tools.js.map +0 -1
  183. package/dist/src/lib/i18n/logger.d.ts +0 -38
  184. package/dist/src/lib/i18n/logger.js.map +0 -1
  185. package/dist/src/lib/logger.d.ts +0 -32
  186. package/dist/src/lib/logger.js.map +0 -1
  187. package/dist/src/lib/server/browser.d.ts +0 -34
  188. package/dist/src/lib/server/browser.js.map +0 -1
  189. package/dist/src/lib/server/fetcher.d.ts +0 -20
  190. package/dist/src/lib/server/fetcher.js.map +0 -1
  191. package/dist/src/lib/server/index.d.ts +0 -10
  192. package/dist/src/lib/server/index.js.map +0 -1
  193. package/dist/src/lib/server/logger.js +0 -23
  194. package/dist/src/lib/server/prompts.d.ts +0 -11
  195. package/dist/src/lib/server/prompts.js.map +0 -1
  196. package/dist/src/lib/server/resources.d.ts +0 -11
  197. package/dist/src/lib/server/resources.js.map +0 -1
  198. package/dist/src/lib/server/tools.d.ts +0 -12
  199. package/dist/src/lib/server/tools.js.map +0 -1
  200. package/dist/src/lib/server/types.d.ts +0 -71
  201. package/dist/src/lib/server/types.js.map +0 -1
  202. package/dist/src/lib/types.d.ts +0 -119
  203. package/dist/src/lib/types.js.map +0 -1
  204. package/dist/src/mcp-server.d.ts +0 -6
  205. package/dist/src/mcp-server.js.map +0 -1
  206. package/dist/tests/jest.setup.d.ts +0 -6
  207. package/dist/tests/jest.setup.js +0 -30
  208. package/dist/tests/jest.setup.js.map +0 -1
  209. package/dist/tests/test-direct-client.d.ts +0 -6
  210. package/dist/tests/test-direct-client.js.map +0 -1
  211. package/dist/tests/test-mcp-methods.d.ts +0 -6
  212. package/dist/tests/test-mcp-methods.js.map +0 -1
  213. package/dist/tests/test-mcp.d.ts +0 -6
  214. package/dist/tests/test-mcp.js.map +0 -1
  215. package/dist/tests/test-mini4k.d.ts +0 -6
  216. package/dist/tests/test-mini4k.js.map +0 -1
@@ -0,0 +1,364 @@
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
+ // 检测源码中使用但未在 i18n 中定义的国际化键 (Detect internationalization keys used in source code but not defined in i18n)
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import * as allKeys from '../src/lib/i18n/keys/index.js';
11
+ // 获取当前文件的目录路径 (Get the directory path of the current file)
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ // 默认测试配置 (Default test configuration)
15
+ const defaultConfig = {
16
+ generateReport: true,
17
+ reportPath: path.join(__dirname, '../i18n-missing-keys-report.json'),
18
+ verbose: false,
19
+ srcDir: path.join(__dirname, '../src'),
20
+ excludeDirs: ['node_modules', 'dist', 'build', 'coverage'],
21
+ fileExtensions: ['.ts', '.tsx', '.js', '.jsx'],
22
+ keyFunctionNames: ['t', 'log'] // 使用国际化键的函数名 (Function names that use internationalization keys)
23
+ };
24
+ // 解析命令行参数 (Parse command line arguments)
25
+ function parseCommandLineArgs() {
26
+ const config = { ...defaultConfig };
27
+ const args = process.argv.slice(2);
28
+ for (let i = 0; i < args.length; i++) {
29
+ const arg = args[i];
30
+ switch (arg) {
31
+ case '--no-report':
32
+ config.generateReport = false;
33
+ break;
34
+ case '--report-path':
35
+ if (i + 1 < args.length) {
36
+ config.reportPath = args[++i];
37
+ }
38
+ break;
39
+ case '--verbose':
40
+ config.verbose = true;
41
+ break;
42
+ case '--src-dir':
43
+ if (i + 1 < args.length) {
44
+ config.srcDir = args[++i];
45
+ }
46
+ break;
47
+ case '--exclude-dirs':
48
+ if (i + 1 < args.length) {
49
+ config.excludeDirs = args[++i].split(',');
50
+ }
51
+ break;
52
+ case '--file-extensions':
53
+ if (i + 1 < args.length) {
54
+ config.fileExtensions = args[++i].split(',');
55
+ }
56
+ break;
57
+ case '--key-functions':
58
+ if (i + 1 < args.length) {
59
+ config.keyFunctionNames = args[++i].split(',');
60
+ }
61
+ break;
62
+ case '--help':
63
+ printHelp();
64
+ process.exit(0);
65
+ break;
66
+ }
67
+ }
68
+ return config;
69
+ }
70
+ // 打印帮助信息 (Print help information)
71
+ function printHelp() {
72
+ console.log(`
73
+ 未定义国际化键检测工具 (Missing Internationalization Keys Detection Tool)
74
+
75
+ 用法: node --loader ts-node/esm tests/i18n-missing-keys.ts [选项]
76
+
77
+ 选项:
78
+ --no-report 不生成报告文件 (Don't generate report file)
79
+ --report-path <path> 指定报告文件路径 (Specify report file path)
80
+ --verbose 显示详细信息 (Show verbose information)
81
+ --src-dir <path> 指定源代码目录 (Specify source code directory)
82
+ --exclude-dirs <dirs> 指定排除的目录,用逗号分隔 (Specify excluded directories, separated by commas)
83
+ --file-extensions <exts> 指定文件扩展名,用逗号分隔 (Specify file extensions, separated by commas)
84
+ --key-functions <funcs> 指定使用国际化键的函数名,用逗号分隔 (Specify function names that use internationalization keys, separated by commas)
85
+ --help 显示帮助信息 (Show help information)
86
+ `);
87
+ }
88
+ /**
89
+ * 递归获取对象中的所有键 (Recursively get all keys in an object)
90
+ * @param obj 要获取键的对象 (Object to get keys from)
91
+ * @param prefix 键前缀 (Key prefix)
92
+ * @returns 键数组 (Array of keys)
93
+ */
94
+ function getAllKeysFromObject(obj, prefix = '') {
95
+ let keys = [];
96
+ for (const key in obj) {
97
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
98
+ // 递归获取嵌套对象的键 (Recursively get keys of nested objects)
99
+ keys = [...keys, ...getAllKeysFromObject(obj[key], prefix ? `${prefix}.${key}` : key)];
100
+ }
101
+ else if (typeof obj[key] === 'string') {
102
+ // 直接使用键值作为完整的键路径 (Use key value as the complete key path)
103
+ keys.push(obj[key]);
104
+ }
105
+ }
106
+ return keys;
107
+ }
108
+ /**
109
+ * 从键模块中获取所有键 (Get all keys from key modules)
110
+ * @returns 键数组 (Array of keys)
111
+ */
112
+ function getAllKeysFromModules() {
113
+ const allKeysSet = new Set();
114
+ // 遍历所有键模块 (Iterate through all key modules)
115
+ for (const moduleKey in allKeys) {
116
+ const keyModule = allKeys[moduleKey];
117
+ if (typeof keyModule === 'object' && keyModule !== null) {
118
+ // 获取模块中的所有键 (Get all keys in the module)
119
+ const keysInModule = getAllKeysFromObject(keyModule);
120
+ keysInModule.forEach(key => allKeysSet.add(key));
121
+ }
122
+ }
123
+ return Array.from(allKeysSet);
124
+ }
125
+ /**
126
+ * 获取翻译值 (Get translation value)
127
+ * @param key 键 (Key)
128
+ * @param translations 翻译资源 (Translation resources)
129
+ * @returns 翻译值 (Translation value)
130
+ */
131
+ function getTranslationValue(key, translations) {
132
+ // 检查是否是命名空间格式的键 (Check if the key is in namespace format)
133
+ const parts = key.split('.');
134
+ if (parts.length > 1) {
135
+ const namespace = parts[0];
136
+ const subKey = parts.slice(1).join('.');
137
+ // 检查命名空间是否存在 (Check if the namespace exists)
138
+ if (translations[namespace] && translations[namespace][subKey] !== undefined) {
139
+ return translations[namespace][subKey];
140
+ }
141
+ }
142
+ // 直接在翻译资源中查找键 (Look up the key directly in translation resources)
143
+ for (const moduleName in translations) {
144
+ const module = translations[moduleName];
145
+ if (module && module[key] !== undefined) {
146
+ return module[key];
147
+ }
148
+ }
149
+ return undefined;
150
+ }
151
+ /**
152
+ * 获取项目中的所有源代码文件 (Get all source code files in the project)
153
+ * @param config 测试配置 (Test configuration)
154
+ * @returns 文件路径数组 (Array of file paths)
155
+ */
156
+ async function getAllSourceFiles(config) {
157
+ const { srcDir, excludeDirs, fileExtensions } = config;
158
+ console.log(`查找源代码文件,目录: ${srcDir} (Finding source code files, directory: ${srcDir})`);
159
+ try {
160
+ // 使用更简单的方法查找文件 (Use a simpler method to find files)
161
+ const allFiles = [];
162
+ // 递归查找文件 (Recursively find files)
163
+ function findFilesRecursively(dir) {
164
+ const files = fs.readdirSync(dir);
165
+ for (const file of files) {
166
+ const filePath = path.join(dir, file);
167
+ const stat = fs.statSync(filePath);
168
+ // 检查是否是目录 (Check if it's a directory)
169
+ if (stat.isDirectory()) {
170
+ // 检查是否应该排除该目录 (Check if the directory should be excluded)
171
+ if (!excludeDirs.some(excludeDir => file === excludeDir)) {
172
+ findFilesRecursively(filePath);
173
+ }
174
+ }
175
+ else if (stat.isFile()) {
176
+ // 检查文件扩展名 (Check file extension)
177
+ const ext = path.extname(file);
178
+ if (fileExtensions.includes(ext)) {
179
+ allFiles.push(filePath);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ // 开始递归查找 (Start recursive search)
185
+ findFilesRecursively(srcDir);
186
+ console.log(`找到 ${allFiles.length} 个源代码文件 (Found ${allFiles.length} source code files)`);
187
+ return allFiles;
188
+ }
189
+ catch (error) {
190
+ console.error(`获取源代码文件时出错 (Error getting source code files):`, error);
191
+ return [];
192
+ }
193
+ }
194
+ /**
195
+ * 从文件中提取国际化键 (Extract internationalization keys from a file)
196
+ * @param filePath 文件路径 (File path)
197
+ * @param keyFunctionNames 使用国际化键的函数名 (Function names that use internationalization keys)
198
+ * @returns 提取的键数组 (Array of extracted keys)
199
+ */
200
+ function extractKeysFromFile(filePath, keyFunctionNames) {
201
+ try {
202
+ const content = fs.readFileSync(filePath, 'utf-8');
203
+ const extractedKeys = [];
204
+ // 为每个函数名创建正则表达式 (Create regular expressions for each function name)
205
+ for (const funcName of keyFunctionNames) {
206
+ // 匹配 funcName('key') 或 funcName("key") 格式 (Match funcName('key') or funcName("key") format)
207
+ const singleQuoteRegex = new RegExp(`${funcName}\\s*\\(\\s*['](.*?)[']`, 'g');
208
+ const doubleQuoteRegex = new RegExp(`${funcName}\\s*\\(\\s*["](.*?)["]`, 'g');
209
+ // 查找所有匹配项 (Find all matches)
210
+ let match;
211
+ // 处理单引号匹配 (Process single quote matches)
212
+ while ((match = singleQuoteRegex.exec(content)) !== null) {
213
+ if (match[1] && !match[1].includes('$')) { // 排除模板字符串 (Exclude template strings)
214
+ // 排除特殊字符 (Exclude special characters)
215
+ if (match[1].length > 1 && !['\\n', '.', 'location'].includes(match[1])) {
216
+ extractedKeys.push(match[1]);
217
+ }
218
+ }
219
+ }
220
+ // 处理双引号匹配 (Process double quote matches)
221
+ while ((match = doubleQuoteRegex.exec(content)) !== null) {
222
+ if (match[1] && !match[1].includes('$')) { // 排除模板字符串 (Exclude template strings)
223
+ // 排除特殊字符 (Exclude special characters)
224
+ if (match[1].length > 1 && !['\\n', '.', 'location'].includes(match[1])) {
225
+ extractedKeys.push(match[1]);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ return extractedKeys;
231
+ }
232
+ catch (error) {
233
+ console.error(`读取文件 ${filePath} 时出错 (Error reading file ${filePath}):`, error);
234
+ return [];
235
+ }
236
+ }
237
+ /**
238
+ * 检查源码中使用但未在 i18n 中定义的键 (Check keys used in source code but not defined in i18n)
239
+ * @param config 测试配置 (Test configuration)
240
+ */
241
+ async function checkMissingKeys(config) {
242
+ console.log('\n=== 检查未定义的国际化键 (Check missing internationalization keys) ===');
243
+ try {
244
+ // 获取所有已定义的键 (Get all defined keys)
245
+ const definedKeys = getAllKeysFromModules();
246
+ console.log(`总计找到 ${definedKeys.length} 个已定义的键 (Total defined keys found: ${definedKeys.length})`);
247
+ // 获取所有源代码文件 (Get all source code files)
248
+ const files = await getAllSourceFiles(config);
249
+ console.log(`总计找到 ${files.length} 个源代码文件 (Total source code files found: ${files.length})`);
250
+ // 从所有文件中提取键 (Extract keys from all files)
251
+ const usedKeysSet = new Set();
252
+ const missingKeys = [];
253
+ for (const file of files) {
254
+ const extractedKeys = extractKeysFromFile(file, config.keyFunctionNames);
255
+ for (const key of extractedKeys) {
256
+ usedKeysSet.add(key);
257
+ // 检查键是否已定义 (Check if the key is defined)
258
+ if (!definedKeys.includes(key)) {
259
+ // 查找已有的相同键记录 (Find existing record for the same key)
260
+ const existingRecord = missingKeys.find(record => record.key === key);
261
+ if (existingRecord) {
262
+ // 添加文件到现有记录 (Add file to existing record)
263
+ if (!existingRecord.files.includes(file)) {
264
+ existingRecord.files.push(file);
265
+ }
266
+ }
267
+ else {
268
+ // 创建新记录 (Create new record)
269
+ missingKeys.push({
270
+ key,
271
+ files: [file]
272
+ });
273
+ }
274
+ }
275
+ }
276
+ }
277
+ // 输出结果 (Output results)
278
+ if (missingKeys.length > 0) {
279
+ console.log(`\n❌ 发现 ${missingKeys.length} 个未定义的键 (Found ${missingKeys.length} missing keys)`);
280
+ if (config.verbose) {
281
+ for (const { key, files } of missingKeys) {
282
+ console.log(`键 (Key): ${key}`);
283
+ console.log(`文件 (Files):`);
284
+ for (const file of files) {
285
+ console.log(` - ${file}`);
286
+ }
287
+ console.log('---');
288
+ }
289
+ }
290
+ }
291
+ else {
292
+ console.log('\n✅ 所有使用的键都已定义 (All used keys are defined)');
293
+ }
294
+ // 生成报告 (Generate report)
295
+ if (config.generateReport) {
296
+ const report = {
297
+ timestamp: new Date().toISOString(),
298
+ totalDefinedKeys: definedKeys.length,
299
+ totalUsedKeys: usedKeysSet.size,
300
+ totalFiles: files.length,
301
+ missingKeys: missingKeys.map(({ key, files }) => ({
302
+ key,
303
+ files,
304
+ fileCount: files.length
305
+ })),
306
+ hasMissingKeys: missingKeys.length > 0
307
+ };
308
+ generateReport(report, config.reportPath);
309
+ }
310
+ return {
311
+ missingKeys,
312
+ hasMissingKeys: missingKeys.length > 0
313
+ };
314
+ }
315
+ catch (error) {
316
+ console.error('\n❌ 检查未定义的键时出错 (Error checking missing keys):');
317
+ console.error(error);
318
+ return {
319
+ missingKeys: [],
320
+ hasMissingKeys: true
321
+ };
322
+ }
323
+ }
324
+ /**
325
+ * 生成报告 (Generate report)
326
+ * @param report 报告数据 (Report data)
327
+ * @param reportPath 报告文件路径 (Report file path)
328
+ */
329
+ function generateReport(report, reportPath) {
330
+ try {
331
+ // 创建目录 (Create directory)
332
+ const dir = path.dirname(reportPath);
333
+ if (!fs.existsSync(dir)) {
334
+ fs.mkdirSync(dir, { recursive: true });
335
+ }
336
+ // 写入报告文件 (Write report file)
337
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
338
+ console.log(`报告已生成: ${reportPath} (Report generated: ${reportPath})`);
339
+ }
340
+ catch (error) {
341
+ console.error(`生成报告时出错 (Error generating report):`, error);
342
+ }
343
+ }
344
+ /**
345
+ * 主函数 (Main function)
346
+ */
347
+ async function main() {
348
+ console.log('=== 检测未定义的国际化键 (Detecting missing internationalization keys) ===');
349
+ try {
350
+ // 解析命令行参数 (Parse command line arguments)
351
+ const config = parseCommandLineArgs();
352
+ // 检查未定义的键 (Check missing keys)
353
+ const result = await checkMissingKeys(config);
354
+ // 设置退出码 (Set exit code)
355
+ process.exit(result.hasMissingKeys ? 1 : 0);
356
+ }
357
+ catch (error) {
358
+ console.error('❌ 程序执行出错 (Error executing program):');
359
+ console.error(error);
360
+ process.exit(1);
361
+ }
362
+ }
363
+ // 执行主函数 (Execute main function)
364
+ main();
@@ -0,0 +1,236 @@
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
+ // 删除未使用的国际化键 (Remove unused internationalization keys)
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ // 获取当前文件的目录路径 (Get the directory path of the current file)
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ // 默认配置 (Default configuration)
14
+ const defaultConfig = {
15
+ reportPath: path.join(__dirname, '../i18n-unused-keys-report.json'),
16
+ verbose: false,
17
+ preserveErrorKeys: true,
18
+ dryRun: false
19
+ };
20
+ // 解析命令行参数 (Parse command line arguments)
21
+ function parseCommandLineArgs() {
22
+ const config = { ...defaultConfig };
23
+ const args = process.argv.slice(2);
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ switch (arg) {
27
+ case '--report-path':
28
+ if (i + 1 < args.length) {
29
+ config.reportPath = args[++i];
30
+ }
31
+ break;
32
+ case '--verbose':
33
+ config.verbose = true;
34
+ break;
35
+ case '--no-preserve-error-keys':
36
+ config.preserveErrorKeys = false;
37
+ break;
38
+ case '--dry-run':
39
+ config.dryRun = true;
40
+ break;
41
+ case '--help':
42
+ printHelp();
43
+ process.exit(0);
44
+ break;
45
+ }
46
+ }
47
+ return config;
48
+ }
49
+ // 打印帮助信息 (Print help information)
50
+ function printHelp() {
51
+ console.log(`
52
+ 删除未使用的国际化键工具 (Remove Unused Internationalization Keys Tool)
53
+
54
+ 用法: node --loader ts-node/esm tests/i18n-remove-unused-keys.ts [选项]
55
+
56
+ 选项:
57
+ --report-path <path> 指定报告文件路径 (Specify report file path)
58
+ --verbose 显示详细信息 (Show verbose information)
59
+ --no-preserve-error-keys 不保留错误相关的键 (Don't preserve error-related keys)
60
+ --dry-run 仅显示将要删除的键,不实际删除 (Only show keys to be deleted, don't actually delete)
61
+ --help 显示帮助信息 (Show help information)
62
+ `);
63
+ }
64
+ /**
65
+ * 读取报告文件 (Read report file)
66
+ * @param reportPath 报告文件路径 (Report file path)
67
+ * @returns 报告数据 (Report data)
68
+ */
69
+ function readReport(reportPath) {
70
+ try {
71
+ const reportContent = fs.readFileSync(reportPath, 'utf-8');
72
+ return JSON.parse(reportContent);
73
+ }
74
+ catch (error) {
75
+ console.error(`读取报告文件时出错 (Error reading report file):`, error);
76
+ process.exit(1);
77
+ }
78
+ }
79
+ /**
80
+ * 获取要删除的键 (Get keys to delete)
81
+ * @param report 报告数据 (Report data)
82
+ * @param config 配置 (Configuration)
83
+ * @returns 要删除的键 (Keys to delete)
84
+ */
85
+ function getKeysToDelete(report, config) {
86
+ const { unusedKeys } = report;
87
+ if (!unusedKeys || !Array.isArray(unusedKeys)) {
88
+ console.error('报告中没有未使用的键 (No unused keys in report)');
89
+ return [];
90
+ }
91
+ // 过滤要删除的键 (Filter keys to delete)
92
+ return unusedKeys
93
+ .map(item => item.key)
94
+ .filter(key => {
95
+ // 如果配置为保留错误相关的键,则跳过以 errors. 开头的键
96
+ // (If configured to preserve error-related keys, skip keys starting with errors.)
97
+ if (config.preserveErrorKeys && key.startsWith('errors.')) {
98
+ if (config.verbose) {
99
+ console.log(`保留错误相关的键: ${key} (Preserving error-related key: ${key})`);
100
+ }
101
+ return false;
102
+ }
103
+ return true;
104
+ });
105
+ }
106
+ /**
107
+ * 获取键所在的文件 (Get file containing the key)
108
+ * @param key 键 (Key)
109
+ * @returns 文件路径 (File path)
110
+ */
111
+ function getKeyFile(key) {
112
+ const parts = key.split('.');
113
+ if (parts.length < 1)
114
+ return null;
115
+ const namespace = parts[0];
116
+ // 根据命名空间确定文件路径 (Determine file path based on namespace)
117
+ const enPath = path.join(__dirname, `../src/lib/i18n/locales/en/${namespace}.ts`);
118
+ const zhPath = path.join(__dirname, `../src/lib/i18n/locales/zh/${namespace}.ts`);
119
+ // 检查文件是否存在 (Check if files exist)
120
+ if (fs.existsSync(enPath) && fs.existsSync(zhPath)) {
121
+ return namespace;
122
+ }
123
+ return null;
124
+ }
125
+ /**
126
+ * 从文件中删除键 (Remove key from file)
127
+ * @param namespace 命名空间 (Namespace)
128
+ * @param key 键 (Key)
129
+ * @param config 配置 (Configuration)
130
+ */
131
+ function removeKeyFromFiles(namespace, key, config) {
132
+ const enPath = path.join(__dirname, `../src/lib/i18n/locales/en/${namespace}.ts`);
133
+ const zhPath = path.join(__dirname, `../src/lib/i18n/locales/zh/${namespace}.ts`);
134
+ // 从英文文件中删除键 (Remove key from English file)
135
+ removeKeyFromFile(enPath, key, config);
136
+ // 从中文文件中删除键 (Remove key from Chinese file)
137
+ removeKeyFromFile(zhPath, key, config);
138
+ }
139
+ /**
140
+ * 从文件中删除键 (Remove key from file)
141
+ * @param filePath 文件路径 (File path)
142
+ * @param key 键 (Key)
143
+ * @param config 配置 (Configuration)
144
+ */
145
+ function removeKeyFromFile(filePath, key, config) {
146
+ try {
147
+ // 读取文件内容 (Read file content)
148
+ let content = fs.readFileSync(filePath, 'utf-8');
149
+ // 获取键的完整名称 (Get full key name)
150
+ const parts = key.split('.');
151
+ const keyName = parts.slice(1).join('.');
152
+ // 构建正则表达式 (Build regular expression)
153
+ const regex = new RegExp(`\\s*\\[.*\\.${keyName}\\]:\\s*['"].*['"],?\\n?`);
154
+ // 替换内容 (Replace content)
155
+ const newContent = content.replace(regex, '');
156
+ // 如果内容没有变化,说明没有找到键 (If content hasn't changed, key wasn't found)
157
+ if (newContent === content) {
158
+ if (config.verbose) {
159
+ console.log(`在文件 ${filePath} 中未找到键 ${key} (Key ${key} not found in file ${filePath})`);
160
+ }
161
+ return;
162
+ }
163
+ // 如果是演习模式,只显示要删除的内容 (If dry run, only show content to be deleted)
164
+ if (config.dryRun) {
165
+ if (config.verbose) {
166
+ console.log(`将从文件 ${filePath} 中删除键 ${key} (Will remove key ${key} from file ${filePath})`);
167
+ }
168
+ return;
169
+ }
170
+ // 写入文件 (Write file)
171
+ fs.writeFileSync(filePath, newContent, 'utf-8');
172
+ if (config.verbose) {
173
+ console.log(`已从文件 ${filePath} 中删除键 ${key} (Removed key ${key} from file ${filePath})`);
174
+ }
175
+ }
176
+ catch (error) {
177
+ console.error(`从文件 ${filePath} 中删除键 ${key} 时出错 (Error removing key ${key} from file ${filePath}):`, error);
178
+ }
179
+ }
180
+ /**
181
+ * 主函数 (Main function)
182
+ */
183
+ async function main() {
184
+ console.log('=== 删除未使用的国际化键 (Removing unused internationalization keys) ===');
185
+ try {
186
+ // 解析命令行参数 (Parse command line arguments)
187
+ const config = parseCommandLineArgs();
188
+ // 如果是演习模式,显示提示 (If dry run, show notice)
189
+ if (config.dryRun) {
190
+ console.log('演习模式:不会实际删除键 (Dry run: keys will not be actually deleted)');
191
+ }
192
+ // 读取报告 (Read report)
193
+ const report = readReport(config.reportPath);
194
+ // 获取要删除的键 (Get keys to delete)
195
+ const keysToDelete = getKeysToDelete(report, config);
196
+ console.log(`找到 ${keysToDelete.length} 个要删除的键 (Found ${keysToDelete.length} keys to delete)`);
197
+ // 按命名空间分组键 (Group keys by namespace)
198
+ const keysByNamespace = {};
199
+ for (const key of keysToDelete) {
200
+ const namespace = getKeyFile(key);
201
+ if (namespace) {
202
+ if (!keysByNamespace[namespace]) {
203
+ keysByNamespace[namespace] = [];
204
+ }
205
+ keysByNamespace[namespace].push(key);
206
+ }
207
+ else {
208
+ console.warn(`无法确定键 ${key} 所在的文件 (Cannot determine file for key ${key})`);
209
+ }
210
+ }
211
+ // 删除键 (Delete keys)
212
+ let totalDeleted = 0;
213
+ for (const namespace in keysByNamespace) {
214
+ const keys = keysByNamespace[namespace];
215
+ console.log(`从命名空间 ${namespace} 中删除 ${keys.length} 个键 (Deleting ${keys.length} keys from namespace ${namespace})`);
216
+ for (const key of keys) {
217
+ removeKeyFromFiles(namespace, key, config);
218
+ totalDeleted++;
219
+ }
220
+ }
221
+ // 输出结果 (Output results)
222
+ if (config.dryRun) {
223
+ console.log(`演习模式:将删除 ${totalDeleted} 个键 (Dry run: would delete ${totalDeleted} keys)`);
224
+ }
225
+ else {
226
+ console.log(`已删除 ${totalDeleted} 个键 (Deleted ${totalDeleted} keys)`);
227
+ }
228
+ }
229
+ catch (error) {
230
+ console.error('❌ 程序执行出错 (Error executing program):');
231
+ console.error(error);
232
+ process.exit(1);
233
+ }
234
+ }
235
+ // 执行主函数 (Execute main function)
236
+ main();