@4399ywkf/cli 1.0.8 → 1.0.10
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/templates/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/AppTheme.tsx +136 -0
- package/dist/templates/DIRECTORY_STRUCTURE.md +141 -0
- package/dist/templates/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/GlobalProvider/index.tsx +23 -0
- package/dist/templates/Locale.tsx +55 -56
- package/dist/templates/Query.tsx +12 -0
- package/dist/templates/StyleRegistry.tsx +9 -0
- package/dist/templates/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/app/.i18nrc.js +57 -0
- package/dist/templates/app/config/jwt/index.ts +2 -1
- package/dist/templates/app/docs/DIRECTORY_STRUCTURE.md +141 -0
- package/dist/templates/app/docs/glossary.md +11 -0
- package/dist/templates/app/package.json.tpl +7 -15
- package/dist/templates/app/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/app/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
- package/dist/templates/app/scripts/i18nWorkflow/const.ts +18 -0
- package/dist/templates/app/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
- package/dist/templates/app/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
- package/dist/templates/app/scripts/i18nWorkflow/genDiff.ts +49 -0
- package/dist/templates/app/scripts/i18nWorkflow/i18nConfig.ts +7 -0
- package/dist/templates/app/scripts/i18nWorkflow/index.ts +11 -0
- package/dist/templates/app/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
- package/dist/templates/app/scripts/i18nWorkflow/utils.ts +76 -0
- package/dist/templates/app/src/components/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/app/src/index.tsx +0 -4
- package/dist/templates/app/src/layout/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/app/src/layout/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/app/src/layout/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/app/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/app/src/layout/GlobalProvider/index.tsx +23 -0
- package/dist/templates/app/src/locales/utils.ts +23 -0
- package/dist/templates/app/src/pages/base/index.tsx +170 -79
- package/dist/templates/app/src/routes.tsx +2 -2
- package/dist/templates/app/tsconfig.json +19 -3
- package/dist/templates/base/index.tsx +170 -79
- package/dist/templates/cleanUnusedKeys.ts +344 -0
- package/dist/templates/components/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/config/jwt/index.ts +2 -1
- package/dist/templates/const.ts +18 -0
- package/dist/templates/docs/DIRECTORY_STRUCTURE.md +141 -0
- package/dist/templates/docs/glossary.md +11 -0
- package/dist/templates/flattenLocaleKeys.ts +139 -0
- package/dist/templates/genDefaultLocale.ts +19 -0
- package/dist/templates/genDiff.ts +49 -0
- package/dist/templates/glossary.md +11 -0
- package/dist/templates/i18nConfig.ts +7 -0
- package/dist/templates/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/i18nWorkflow/cleanUnusedKeys.ts +344 -0
- package/dist/templates/i18nWorkflow/const.ts +18 -0
- package/dist/templates/i18nWorkflow/flattenLocaleKeys.ts +139 -0
- package/dist/templates/i18nWorkflow/genDefaultLocale.ts +19 -0
- package/dist/templates/i18nWorkflow/genDiff.ts +49 -0
- package/dist/templates/i18nWorkflow/i18nConfig.ts +7 -0
- package/dist/templates/i18nWorkflow/index.ts +11 -0
- package/dist/templates/i18nWorkflow/protectedPatterns.ts +91 -0
- package/dist/templates/i18nWorkflow/utils.ts +76 -0
- package/dist/templates/index.tsx +170 -79
- package/dist/templates/jwt/index.ts +2 -1
- package/dist/templates/layout/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/layout/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/layout/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/layout/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/layout/GlobalProvider/index.tsx +23 -0
- package/dist/templates/locales/utils.ts +23 -0
- package/dist/templates/package.json.tpl +7 -15
- package/dist/templates/pages/base/index.tsx +170 -79
- package/dist/templates/protectedPatterns.ts +91 -0
- package/dist/templates/routes.tsx +2 -2
- package/dist/templates/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
- package/dist/templates/scripts/i18nWorkflow/const.ts +18 -0
- package/dist/templates/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
- package/dist/templates/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
- package/dist/templates/scripts/i18nWorkflow/genDiff.ts +49 -0
- package/dist/templates/scripts/i18nWorkflow/i18nConfig.ts +7 -0
- package/dist/templates/scripts/i18nWorkflow/index.ts +11 -0
- package/dist/templates/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
- package/dist/templates/scripts/i18nWorkflow/utils.ts +76 -0
- package/dist/templates/src/components/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/src/index.tsx +0 -4
- package/dist/templates/src/layout/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/src/layout/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/src/layout/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/src/layout/GlobalProvider/index.tsx +23 -0
- package/dist/templates/src/locales/utils.ts +23 -0
- package/dist/templates/src/pages/base/index.tsx +170 -79
- package/dist/templates/src/routes.tsx +2 -2
- package/dist/templates/tsconfig.json +19 -3
- package/dist/templates/type.ts +23 -24
- package/dist/templates/utils.ts +23 -0
- package/package.json +19 -21
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/* eslint-disable unicorn/prefer-top-level-await */
|
|
2
|
+
import { consola } from 'consola';
|
|
3
|
+
import { colors } from 'consola/utils';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { IGNORED_FILES, PROTECTED_KEY_PATTERNS } from './protectedPatterns';
|
|
9
|
+
|
|
10
|
+
interface I18nKey {
|
|
11
|
+
fullKey: string;
|
|
12
|
+
key: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UnusedKey extends I18nKey {
|
|
17
|
+
filePath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a key should be protected (considered as "used")
|
|
22
|
+
*/
|
|
23
|
+
function isProtectedKey(namespace: string, key: string): boolean {
|
|
24
|
+
// Check if namespace is in protected list
|
|
25
|
+
if (PROTECTED_KEY_PATTERNS.includes(namespace)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if key matches any protected pattern
|
|
30
|
+
const fullKey = `${namespace}.${key}`;
|
|
31
|
+
return PROTECTED_KEY_PATTERNS.some((pattern) => {
|
|
32
|
+
// Exact namespace match
|
|
33
|
+
if (pattern === namespace) return true;
|
|
34
|
+
// Partial key match (e.g., "error.code" matches "error.code.NOT_FOUND")
|
|
35
|
+
if (fullKey.startsWith(pattern + '.')) return true;
|
|
36
|
+
return false;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Recursively extract all keys from a nested object
|
|
42
|
+
*/
|
|
43
|
+
function extractKeysFromObject(obj: any, namespace: string, prefix: string = ''): I18nKey[] {
|
|
44
|
+
const keys: I18nKey[] = [];
|
|
45
|
+
|
|
46
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
47
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
48
|
+
|
|
49
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
50
|
+
// Recursively extract keys from nested objects
|
|
51
|
+
keys.push(...extractKeysFromObject(value, namespace, fullKey));
|
|
52
|
+
} else {
|
|
53
|
+
// This is a leaf node (actual translation)
|
|
54
|
+
keys.push({
|
|
55
|
+
fullKey: `${namespace}:${fullKey}`,
|
|
56
|
+
key: fullKey,
|
|
57
|
+
namespace,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return keys;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load all i18n keys from src/locales/default
|
|
67
|
+
*/
|
|
68
|
+
function loadAllI18nKeys(): I18nKey[] {
|
|
69
|
+
const defaultLocalesPath = path.join(process.cwd(), 'src/locales/default');
|
|
70
|
+
const allKeys: I18nKey[] = [];
|
|
71
|
+
|
|
72
|
+
// Get all TypeScript files except index.ts and ignored files
|
|
73
|
+
const ignoredFiles: string[] = [...IGNORED_FILES];
|
|
74
|
+
const files = fs
|
|
75
|
+
.readdirSync(defaultLocalesPath)
|
|
76
|
+
.filter((f) => f.endsWith('.ts') && f !== 'index.ts' && !ignoredFiles.includes(f));
|
|
77
|
+
|
|
78
|
+
consola.info(`Found ${files.length} namespace files (ignored: ${ignoredFiles.join(', ')})`);
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const namespace = path.basename(file, '.ts');
|
|
82
|
+
const filePath = path.join(defaultLocalesPath, file);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Use require to load the TypeScript file (after it's compiled)
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
87
|
+
const loadedModule = require(filePath);
|
|
88
|
+
const translations = loadedModule.default || loadedModule;
|
|
89
|
+
|
|
90
|
+
const keys = extractKeysFromObject(translations, namespace);
|
|
91
|
+
allKeys.push(...keys);
|
|
92
|
+
|
|
93
|
+
consola.success(colors.cyan(namespace.padEnd(20)), colors.gray(`${keys.length} keys`));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
consola.error(`Failed to load ${file}:`, error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return allKeys;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Find all t() function calls in the codebase
|
|
104
|
+
*/
|
|
105
|
+
async function findAllTranslationCalls(): Promise<Set<string>> {
|
|
106
|
+
const usedKeys = new Set<string>();
|
|
107
|
+
|
|
108
|
+
// Patterns to search for translation calls
|
|
109
|
+
const patterns = [
|
|
110
|
+
'src/**/*.{ts,tsx,js,jsx}',
|
|
111
|
+
'apps/desktop/src/**/*.{ts,tsx,js,jsx}',
|
|
112
|
+
'packages/**/src/**/*.{ts,tsx,js,jsx}', // Include packages directory
|
|
113
|
+
'!**/*.test.{ts,tsx}',
|
|
114
|
+
'!**/*.spec.{ts,tsx}',
|
|
115
|
+
'!**/node_modules/**',
|
|
116
|
+
'!**/.next/**',
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
consola.start('Scanning codebase for translation calls...');
|
|
120
|
+
|
|
121
|
+
const files = await glob(patterns);
|
|
122
|
+
consola.info(`Found ${files.length} files to scan`);
|
|
123
|
+
|
|
124
|
+
// Regular expressions to match translation calls
|
|
125
|
+
// Mark dynamic patterns with special flag to handle them differently
|
|
126
|
+
const regexPatterns: Array<{
|
|
127
|
+
// Whether this pattern captures dynamic keys
|
|
128
|
+
captureNs?: boolean;
|
|
129
|
+
isDynamic?: boolean;
|
|
130
|
+
pattern: RegExp; // Whether pattern can capture namespace
|
|
131
|
+
}> = [
|
|
132
|
+
// Static patterns
|
|
133
|
+
{ pattern: /\bt[A-Z]?\w*\(\s*["'`]([^"'`]+)["'`]/g },
|
|
134
|
+
{
|
|
135
|
+
captureNs: true,
|
|
136
|
+
pattern: /\bt[A-Z]?\w*\(\s*["'`]([^"'`]+)["'`]\s*,\s*{[^}]*ns:\s*["'`]([^"'`]+)["'`]/g,
|
|
137
|
+
},
|
|
138
|
+
{ pattern: /i18n\.t\(\s*["'`]([^"'`]+)["'`]/g },
|
|
139
|
+
{ pattern: /\bt[A-Z]?\w*\([^)]*\?\s*["'`]([^"'`]+)["'`]/g },
|
|
140
|
+
{ pattern: /\bt[A-Z]?\w*\([^)]*:\s*["'`]([^"'`]+)["'`]/g },
|
|
141
|
+
{ pattern: /<Trans[^>]+i18nKey=["']([^"']+)["']/g },
|
|
142
|
+
{ pattern: /<Trans[^>]+i18nKey={["']([^"']+)["']}/g },
|
|
143
|
+
{ captureNs: true, pattern: /<Trans[^>]+i18nKey=["']([^"']+)["'][\S\s]*?ns=["']([^"']+)["']/g },
|
|
144
|
+
{
|
|
145
|
+
captureNs: true,
|
|
146
|
+
pattern: /<Trans[^>]+i18nKey={["']([^"']+)["']}[\S\s]*?ns={["']([^"']+)["']}/g,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// Dynamic patterns (template strings, concatenations, etc.)
|
|
150
|
+
// Pattern 1: t(`prefix.${var}.suffix`) - variable in the middle
|
|
151
|
+
{ captureNs: false, isDynamic: true, pattern: /\bt[A-Z]?\w*\(\s*`([^$`]+)\${[^}]+}([^`]*)`/g },
|
|
152
|
+
// Pattern 2: t(`${var}.suffix`) - variable at the start
|
|
153
|
+
{ captureNs: false, isDynamic: true, pattern: /\bt[A-Z]?\w*\(\s*`\${[^}]+}([^`]+)`/g },
|
|
154
|
+
// Pattern 3: t(`prefix.${var}.suffix`, { ns: 'namespace' }) - with explicit ns
|
|
155
|
+
{
|
|
156
|
+
captureNs: true,
|
|
157
|
+
isDynamic: true,
|
|
158
|
+
pattern: /\bt[A-Z]?\w*\(\s*`([^$`]*)\${[^}]+}([^`]*)`\s*,\s*{[^}]*ns:\s*["'`]([^"'`]+)["'`]/g,
|
|
159
|
+
},
|
|
160
|
+
// Pattern 4: t(`${var}.suffix`, { ns: 'namespace' }) - variable at start with ns
|
|
161
|
+
{
|
|
162
|
+
captureNs: true,
|
|
163
|
+
isDynamic: true,
|
|
164
|
+
pattern: /\bt[A-Z]?\w*\(\s*`\${[^}]+}([^`]+)`\s*,\s*{[^}]*ns:\s*["'`]([^"'`]+)["'`]/g,
|
|
165
|
+
},
|
|
166
|
+
// Pattern 5: String concatenation
|
|
167
|
+
{ isDynamic: true, pattern: /\bt[A-Z]?\w*\(\s*["'`]([^"'`]+)["'`]\s*\+/g },
|
|
168
|
+
// Pattern 6: <Trans> with dynamic keys
|
|
169
|
+
{ isDynamic: true, pattern: /<Trans[^>]+i18nKey={`([^$`]+)\${[^}]+}([^`]*)`}/g },
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
let totalMatches = 0;
|
|
173
|
+
|
|
174
|
+
for (const file of files) {
|
|
175
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
176
|
+
|
|
177
|
+
// Extract namespace from useTranslation hook
|
|
178
|
+
const useTranslationMatch = content.match(/useTranslation\(\s*["'`]([^"'`]+)["'`]\s*\)/g);
|
|
179
|
+
const useTranslationMultiMatch = content.match(/useTranslation\(\s*\[([^\]]+)]\s*\)/g);
|
|
180
|
+
|
|
181
|
+
// Extract aliases: const { t: tAuth } = useTranslation('auth')
|
|
182
|
+
const aliasPattern =
|
|
183
|
+
/const\s*{\s*t\s*:\s*(\w+)\s*}\s*=\s*useTranslation\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
|
184
|
+
const aliasMatches = content.matchAll(aliasPattern);
|
|
185
|
+
|
|
186
|
+
const namespacesInFile = new Set<string>();
|
|
187
|
+
const aliasToNamespace = new Map<string, string>();
|
|
188
|
+
|
|
189
|
+
// Extract namespaces from useTranslation('namespace')
|
|
190
|
+
if (useTranslationMatch) {
|
|
191
|
+
for (const match of useTranslationMatch) {
|
|
192
|
+
const ns = match.match(/["'`]([^"'`]+)["'`]/)?.[1];
|
|
193
|
+
if (ns) namespacesInFile.add(ns);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Extract namespaces from useTranslation(['ns1', 'ns2'])
|
|
198
|
+
if (useTranslationMultiMatch) {
|
|
199
|
+
for (const match of useTranslationMultiMatch) {
|
|
200
|
+
const nsArray = match.match(/\[([^\]]+)]/)?.[1];
|
|
201
|
+
if (nsArray) {
|
|
202
|
+
const namespaces = nsArray.match(/["'`]([^"'`]+)["'`]/g);
|
|
203
|
+
if (namespaces) {
|
|
204
|
+
for (const ns of namespaces) {
|
|
205
|
+
const cleanNs = ns.replaceAll(/["'`]/g, '');
|
|
206
|
+
namespacesInFile.add(cleanNs);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Extract alias mappings (e.g., tAuth -> 'auth')
|
|
214
|
+
for (const match of aliasMatches) {
|
|
215
|
+
const alias = match[1];
|
|
216
|
+
const namespace = match[2];
|
|
217
|
+
aliasToNamespace.set(alias, namespace);
|
|
218
|
+
namespacesInFile.add(namespace);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Find all t() calls
|
|
222
|
+
for (const { pattern: regex, captureNs, isDynamic } of regexPatterns) {
|
|
223
|
+
const matches = content.matchAll(regex);
|
|
224
|
+
|
|
225
|
+
for (const match of matches) {
|
|
226
|
+
totalMatches++;
|
|
227
|
+
const fullMatch = match[0];
|
|
228
|
+
const key = match[1];
|
|
229
|
+
let explicitNs: string | undefined;
|
|
230
|
+
|
|
231
|
+
// For patterns with captureNs, namespace is in a different position
|
|
232
|
+
if (captureNs && isDynamic) {
|
|
233
|
+
// Dynamic patterns with ns: match[1] + match[2] = key parts, match[3] = ns
|
|
234
|
+
explicitNs = match[3] || match[2]; // Try match[3] first, fall back to match[2]
|
|
235
|
+
} else if (captureNs) {
|
|
236
|
+
// Static patterns with ns: match[1] = key, match[2] = ns
|
|
237
|
+
explicitNs = match[2];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!key) continue;
|
|
241
|
+
|
|
242
|
+
// Extract function name (t, tAuth, tCommon, etc.)
|
|
243
|
+
const funcNameMatch = fullMatch.match(/\b(t[A-Z]?\w*)\(/);
|
|
244
|
+
const funcName = funcNameMatch?.[1] || 't';
|
|
245
|
+
|
|
246
|
+
// Check if it's an alias with known namespace
|
|
247
|
+
let aliasNamespace: string | undefined;
|
|
248
|
+
if (funcName !== 't' && aliasToNamespace.has(funcName)) {
|
|
249
|
+
aliasNamespace = aliasToNamespace.get(funcName);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Handle dynamic keys differently
|
|
253
|
+
if (isDynamic) {
|
|
254
|
+
// For dynamic patterns, extract the static prefix/suffix
|
|
255
|
+
// e.g., t(`mcp.details.${var}.title`) -> "mcp.details." and ".title"
|
|
256
|
+
// e.g., t(`${var}.title`) -> ".title"
|
|
257
|
+
let prefix = '';
|
|
258
|
+
let suffix = '';
|
|
259
|
+
|
|
260
|
+
if (match[2] !== undefined) {
|
|
261
|
+
// Pattern has both prefix and suffix: match[1] = prefix, match[2] = suffix
|
|
262
|
+
prefix = match[1] || '';
|
|
263
|
+
suffix = match[2] || '';
|
|
264
|
+
} else {
|
|
265
|
+
// Pattern has only suffix (var at start): match[1] = suffix
|
|
266
|
+
suffix = match[1] || '';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Combine prefix and suffix for the pattern
|
|
270
|
+
const pattern = (prefix + suffix).trim();
|
|
271
|
+
if (!pattern) continue; // Skip if nothing to protect
|
|
272
|
+
|
|
273
|
+
// Determine the namespace
|
|
274
|
+
let targetNs: string | undefined;
|
|
275
|
+
if (aliasNamespace) {
|
|
276
|
+
targetNs = aliasNamespace;
|
|
277
|
+
} else if (explicitNs) {
|
|
278
|
+
targetNs = explicitNs;
|
|
279
|
+
} else if (namespacesInFile.size === 1) {
|
|
280
|
+
targetNs = [...namespacesInFile][0];
|
|
281
|
+
} else if (namespacesInFile.size > 0) {
|
|
282
|
+
// Multiple namespaces, add prefix pattern for each
|
|
283
|
+
for (const ns of namespacesInFile) {
|
|
284
|
+
usedKeys.add(`${ns}:${pattern}*`);
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (targetNs) {
|
|
290
|
+
usedKeys.add(`${targetNs}:${pattern}*`);
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Handle static keys
|
|
296
|
+
if (explicitNs) {
|
|
297
|
+
// Has explicit namespace
|
|
298
|
+
usedKeys.add(`${explicitNs}:${key}`);
|
|
299
|
+
} else if (aliasNamespace) {
|
|
300
|
+
// Using alias (e.g., tAuth('key'))
|
|
301
|
+
usedKeys.add(`${aliasNamespace}:${key}`);
|
|
302
|
+
} else if (key.includes(':')) {
|
|
303
|
+
// Key already includes namespace (e.g., t('common:key'))
|
|
304
|
+
usedKeys.add(key);
|
|
305
|
+
} else {
|
|
306
|
+
// Use namespaces from useTranslation hook
|
|
307
|
+
if (namespacesInFile.size > 0) {
|
|
308
|
+
for (const ns of namespacesInFile) {
|
|
309
|
+
usedKeys.add(`${ns}:${key}`);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Default to 'common' if no namespace found
|
|
313
|
+
usedKeys.add(`common:${key}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
consola.success(`Found ${totalMatches} translation calls`);
|
|
321
|
+
consola.info(`Extracted ${usedKeys.size} unique keys`);
|
|
322
|
+
|
|
323
|
+
return usedKeys;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Find unused i18n keys
|
|
328
|
+
*/
|
|
329
|
+
function findUnusedKeys(allKeys: I18nKey[], usedKeys: Set<string>): UnusedKey[] {
|
|
330
|
+
const unused: UnusedKey[] = [];
|
|
331
|
+
const protectedKeys: UnusedKey[] = [];
|
|
332
|
+
|
|
333
|
+
// Extract prefix patterns from usedKeys
|
|
334
|
+
// e.g., "discover:mcp.details.*" means any key starting with "mcp.details." in discover namespace
|
|
335
|
+
const prefixPatterns: Array<{ namespace: string; prefix: string }> = [];
|
|
336
|
+
for (const key of usedKeys) {
|
|
337
|
+
if (key.includes('*')) {
|
|
338
|
+
const [namespace, pattern] = key.split(':');
|
|
339
|
+
const prefix = pattern.replace(/\*$/, ''); // Remove trailing *
|
|
340
|
+
prefixPatterns.push({ namespace, prefix });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const keyInfo of allKeys) {
|
|
345
|
+
// Check if key is protected by configuration
|
|
346
|
+
if (isProtectedKey(keyInfo.namespace, keyInfo.key)) {
|
|
347
|
+
protectedKeys.push({
|
|
348
|
+
...keyInfo,
|
|
349
|
+
filePath: `src/locales/default/${keyInfo.namespace}.ts`,
|
|
350
|
+
});
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check if key matches any prefix pattern (from dynamic usage)
|
|
355
|
+
let matchesPrefix = false;
|
|
356
|
+
for (const { namespace, prefix } of prefixPatterns) {
|
|
357
|
+
if (keyInfo.namespace === namespace && keyInfo.key.startsWith(prefix)) {
|
|
358
|
+
matchesPrefix = true;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (matchesPrefix) {
|
|
364
|
+
protectedKeys.push({
|
|
365
|
+
...keyInfo,
|
|
366
|
+
filePath: `src/locales/default/${keyInfo.namespace}.ts`,
|
|
367
|
+
});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check if key is actually used
|
|
372
|
+
if (!usedKeys.has(keyInfo.fullKey)) {
|
|
373
|
+
unused.push({
|
|
374
|
+
...keyInfo,
|
|
375
|
+
filePath: `src/locales/default/${keyInfo.namespace}.ts`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (protectedKeys.length > 0) {
|
|
381
|
+
consola.info('');
|
|
382
|
+
consola.info(colors.cyan('Protected keys (considered as used):'));
|
|
383
|
+
consola.info(
|
|
384
|
+
` ${colors.green(protectedKeys.length.toString())} keys protected by patterns or dynamic usage`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return unused;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Generate report
|
|
393
|
+
*/
|
|
394
|
+
function generateReport(unusedKeys: UnusedKey[], allKeysCount: number, usedKeysCount: number) {
|
|
395
|
+
consola.box('📊 Unused i18n Keys Analysis Report');
|
|
396
|
+
|
|
397
|
+
const actualUsedCount = allKeysCount - unusedKeys.length;
|
|
398
|
+
|
|
399
|
+
consola.info('');
|
|
400
|
+
consola.info(colors.cyan('Statistics:'));
|
|
401
|
+
consola.info(` Total defined keys: ${colors.yellow(allKeysCount.toString())}`);
|
|
402
|
+
consola.info(` Used keys: ${colors.green(actualUsedCount.toString())}`);
|
|
403
|
+
consola.info(` Unused keys: ${colors.red(unusedKeys.length.toString())}`);
|
|
404
|
+
consola.info(
|
|
405
|
+
` Usage rate: ${colors.cyan(((actualUsedCount / allKeysCount) * 100).toFixed(2) + '%')}`,
|
|
406
|
+
);
|
|
407
|
+
consola.info('');
|
|
408
|
+
consola.info(colors.gray('Protected patterns:'));
|
|
409
|
+
consola.info(` ${colors.gray(PROTECTED_KEY_PATTERNS.map((p) => `"${p}"`).join(', '))}`);
|
|
410
|
+
consola.info('');
|
|
411
|
+
|
|
412
|
+
if (unusedKeys.length === 0) {
|
|
413
|
+
consola.success('🎉 All i18n keys are being used!');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Group by namespace
|
|
418
|
+
const byNamespace = new Map<string, UnusedKey[]>();
|
|
419
|
+
for (const key of unusedKeys) {
|
|
420
|
+
if (!byNamespace.has(key.namespace)) {
|
|
421
|
+
byNamespace.set(key.namespace, []);
|
|
422
|
+
}
|
|
423
|
+
byNamespace.get(key.namespace)!.push(key);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
consola.info(colors.yellow('Unused keys by namespace:'));
|
|
427
|
+
consola.info('');
|
|
428
|
+
|
|
429
|
+
for (const [namespace, keys] of byNamespace.entries()) {
|
|
430
|
+
consola.warn(
|
|
431
|
+
`${colors.cyan(namespace.padEnd(20))} ${colors.gray('→')} ${colors.red(keys.length + ' unused keys')}`,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// Show first 10 keys
|
|
435
|
+
const displayKeys = keys.slice(0, 10);
|
|
436
|
+
for (const key of displayKeys) {
|
|
437
|
+
consola.log(` ${colors.gray('•')} ${key.key}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (keys.length > 10) {
|
|
441
|
+
consola.log(` ${colors.gray(`... and ${keys.length - 10} more`)}`);
|
|
442
|
+
}
|
|
443
|
+
consola.info('');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Save detailed report to file
|
|
447
|
+
const reportPath = path.join(process.cwd(), 'i18n-unused-keys-report.json');
|
|
448
|
+
fs.writeFileSync(
|
|
449
|
+
reportPath,
|
|
450
|
+
JSON.stringify(
|
|
451
|
+
{
|
|
452
|
+
generatedAt: new Date().toISOString(),
|
|
453
|
+
statistics: {
|
|
454
|
+
totalKeys: allKeysCount,
|
|
455
|
+
unusedKeys: unusedKeys.length,
|
|
456
|
+
usageRate: ((usedKeysCount / allKeysCount) * 100).toFixed(2) + '%',
|
|
457
|
+
usedKeys: usedKeysCount,
|
|
458
|
+
},
|
|
459
|
+
unusedKeys: unusedKeys.map((k) => ({
|
|
460
|
+
filePath: k.filePath,
|
|
461
|
+
fullKey: k.fullKey,
|
|
462
|
+
key: k.key,
|
|
463
|
+
namespace: k.namespace,
|
|
464
|
+
})),
|
|
465
|
+
unusedKeysByNamespace: Array.from(byNamespace.entries()).map(([ns, keys]) => ({
|
|
466
|
+
count: keys.length,
|
|
467
|
+
keys: keys.map((k) => k.key),
|
|
468
|
+
namespace: ns,
|
|
469
|
+
})),
|
|
470
|
+
},
|
|
471
|
+
null,
|
|
472
|
+
2,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
consola.success(`Detailed report saved to: ${colors.cyan(reportPath)}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Main function
|
|
481
|
+
*/
|
|
482
|
+
async function main() {
|
|
483
|
+
consola.start('Starting i18n unused keys analysis...');
|
|
484
|
+
consola.info('');
|
|
485
|
+
|
|
486
|
+
// Step 1: Load all defined keys
|
|
487
|
+
consola.box('Step 1: Loading all i18n keys');
|
|
488
|
+
const allKeys = loadAllI18nKeys();
|
|
489
|
+
consola.success(`Total keys loaded: ${allKeys.length}`);
|
|
490
|
+
consola.info('');
|
|
491
|
+
|
|
492
|
+
// Step 2: Find all translation calls
|
|
493
|
+
consola.box('Step 2: Finding translation calls in codebase');
|
|
494
|
+
const usedKeys = await findAllTranslationCalls();
|
|
495
|
+
consola.info('');
|
|
496
|
+
|
|
497
|
+
// Step 3: Find unused keys
|
|
498
|
+
consola.box('Step 3: Analyzing unused keys');
|
|
499
|
+
const unusedKeys = findUnusedKeys(allKeys, usedKeys);
|
|
500
|
+
consola.info('');
|
|
501
|
+
|
|
502
|
+
// Step 4: Generate report
|
|
503
|
+
generateReport(unusedKeys, allKeys.length, usedKeys.size);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
main();
|