@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.
Files changed (96) hide show
  1. package/dist/templates/AntdStaticMethods/index.tsx +20 -0
  2. package/dist/templates/AppTheme.tsx +136 -0
  3. package/dist/templates/DIRECTORY_STRUCTURE.md +141 -0
  4. package/dist/templates/GlobalProvider/AppTheme.tsx +136 -0
  5. package/dist/templates/GlobalProvider/Locale.tsx +84 -0
  6. package/dist/templates/GlobalProvider/Query.tsx +12 -0
  7. package/dist/templates/GlobalProvider/StyleRegistry.tsx +9 -0
  8. package/dist/templates/GlobalProvider/index.tsx +23 -0
  9. package/dist/templates/Locale.tsx +55 -56
  10. package/dist/templates/Query.tsx +12 -0
  11. package/dist/templates/StyleRegistry.tsx +9 -0
  12. package/dist/templates/analyzeUnusedKeys.ts +506 -0
  13. package/dist/templates/app/.i18nrc.js +57 -0
  14. package/dist/templates/app/config/jwt/index.ts +2 -1
  15. package/dist/templates/app/docs/DIRECTORY_STRUCTURE.md +141 -0
  16. package/dist/templates/app/docs/glossary.md +11 -0
  17. package/dist/templates/app/package.json.tpl +7 -15
  18. package/dist/templates/app/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
  19. package/dist/templates/app/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
  20. package/dist/templates/app/scripts/i18nWorkflow/const.ts +18 -0
  21. package/dist/templates/app/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
  22. package/dist/templates/app/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
  23. package/dist/templates/app/scripts/i18nWorkflow/genDiff.ts +49 -0
  24. package/dist/templates/app/scripts/i18nWorkflow/i18nConfig.ts +7 -0
  25. package/dist/templates/app/scripts/i18nWorkflow/index.ts +11 -0
  26. package/dist/templates/app/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
  27. package/dist/templates/app/scripts/i18nWorkflow/utils.ts +76 -0
  28. package/dist/templates/app/src/components/AntdStaticMethods/index.tsx +20 -0
  29. package/dist/templates/app/src/index.tsx +0 -4
  30. package/dist/templates/app/src/layout/GlobalProvider/AppTheme.tsx +136 -0
  31. package/dist/templates/app/src/layout/GlobalProvider/Locale.tsx +84 -0
  32. package/dist/templates/app/src/layout/GlobalProvider/Query.tsx +12 -0
  33. package/dist/templates/app/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
  34. package/dist/templates/app/src/layout/GlobalProvider/index.tsx +23 -0
  35. package/dist/templates/app/src/locales/utils.ts +23 -0
  36. package/dist/templates/app/src/pages/base/index.tsx +170 -79
  37. package/dist/templates/app/src/routes.tsx +2 -2
  38. package/dist/templates/app/tsconfig.json +19 -3
  39. package/dist/templates/base/index.tsx +170 -79
  40. package/dist/templates/cleanUnusedKeys.ts +344 -0
  41. package/dist/templates/components/AntdStaticMethods/index.tsx +20 -0
  42. package/dist/templates/config/jwt/index.ts +2 -1
  43. package/dist/templates/const.ts +18 -0
  44. package/dist/templates/docs/DIRECTORY_STRUCTURE.md +141 -0
  45. package/dist/templates/docs/glossary.md +11 -0
  46. package/dist/templates/flattenLocaleKeys.ts +139 -0
  47. package/dist/templates/genDefaultLocale.ts +19 -0
  48. package/dist/templates/genDiff.ts +49 -0
  49. package/dist/templates/glossary.md +11 -0
  50. package/dist/templates/i18nConfig.ts +7 -0
  51. package/dist/templates/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
  52. package/dist/templates/i18nWorkflow/cleanUnusedKeys.ts +344 -0
  53. package/dist/templates/i18nWorkflow/const.ts +18 -0
  54. package/dist/templates/i18nWorkflow/flattenLocaleKeys.ts +139 -0
  55. package/dist/templates/i18nWorkflow/genDefaultLocale.ts +19 -0
  56. package/dist/templates/i18nWorkflow/genDiff.ts +49 -0
  57. package/dist/templates/i18nWorkflow/i18nConfig.ts +7 -0
  58. package/dist/templates/i18nWorkflow/index.ts +11 -0
  59. package/dist/templates/i18nWorkflow/protectedPatterns.ts +91 -0
  60. package/dist/templates/i18nWorkflow/utils.ts +76 -0
  61. package/dist/templates/index.tsx +170 -79
  62. package/dist/templates/jwt/index.ts +2 -1
  63. package/dist/templates/layout/GlobalProvider/AppTheme.tsx +136 -0
  64. package/dist/templates/layout/GlobalProvider/Locale.tsx +84 -0
  65. package/dist/templates/layout/GlobalProvider/Query.tsx +12 -0
  66. package/dist/templates/layout/GlobalProvider/StyleRegistry.tsx +9 -0
  67. package/dist/templates/layout/GlobalProvider/index.tsx +23 -0
  68. package/dist/templates/locales/utils.ts +23 -0
  69. package/dist/templates/package.json.tpl +7 -15
  70. package/dist/templates/pages/base/index.tsx +170 -79
  71. package/dist/templates/protectedPatterns.ts +91 -0
  72. package/dist/templates/routes.tsx +2 -2
  73. package/dist/templates/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
  74. package/dist/templates/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
  75. package/dist/templates/scripts/i18nWorkflow/const.ts +18 -0
  76. package/dist/templates/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
  77. package/dist/templates/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
  78. package/dist/templates/scripts/i18nWorkflow/genDiff.ts +49 -0
  79. package/dist/templates/scripts/i18nWorkflow/i18nConfig.ts +7 -0
  80. package/dist/templates/scripts/i18nWorkflow/index.ts +11 -0
  81. package/dist/templates/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
  82. package/dist/templates/scripts/i18nWorkflow/utils.ts +76 -0
  83. package/dist/templates/src/components/AntdStaticMethods/index.tsx +20 -0
  84. package/dist/templates/src/index.tsx +0 -4
  85. package/dist/templates/src/layout/GlobalProvider/AppTheme.tsx +136 -0
  86. package/dist/templates/src/layout/GlobalProvider/Locale.tsx +84 -0
  87. package/dist/templates/src/layout/GlobalProvider/Query.tsx +12 -0
  88. package/dist/templates/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
  89. package/dist/templates/src/layout/GlobalProvider/index.tsx +23 -0
  90. package/dist/templates/src/locales/utils.ts +23 -0
  91. package/dist/templates/src/pages/base/index.tsx +170 -79
  92. package/dist/templates/src/routes.tsx +2 -2
  93. package/dist/templates/tsconfig.json +19 -3
  94. package/dist/templates/type.ts +23 -24
  95. package/dist/templates/utils.ts +23 -0
  96. package/package.json +19 -21
@@ -1,85 +1,84 @@
1
- import { ConfigProvider } from "antd";
2
- import type { Locale as AntdLocale } from "antd/es/locale";
3
- import dayjs from "dayjs";
4
- import React, {
5
- type PropsWithChildren,
6
- memo,
7
- useEffect,
8
- useState,
9
- } from "react";
1
+ import { ConfigProvider } from 'antd';
2
+ import dayjs from 'dayjs';
3
+ import { type PropsWithChildren, memo, useEffect, useState } from 'react';
4
+ import { isRtlLang } from 'rtl-detect';
10
5
 
11
- import { isRtlLang } from "rtl-detect";
6
+ import { createI18nNext } from '@/locales/create';
7
+ import { getAntdLocale } from '@/utils/locale';
12
8
 
13
- import { createI18nNext } from "@/locales/create";
14
- import { getAntdLocale } from "@/utils/locale";
15
9
 
16
10
  const updateDayjs = async (lang: string) => {
17
- // load default lang
18
- let dayJSLocale: any;
11
+ let dayJSLocale;
19
12
  try {
20
13
  // dayjs locale is using `en` instead of `en-US`
21
14
  // refs: https://github.com/lobehub/lobe-chat/issues/3396
22
- const locale = lang.toLowerCase() === "en-us" ? "en" : lang.toLowerCase();
15
+ const locale = lang!.toLowerCase() === 'en-us' ? 'en' : lang!.toLowerCase();
23
16
 
24
17
  dayJSLocale = await import(`dayjs/locale/${locale}.js`);
25
18
  } catch {
26
19
  console.warn(`dayjs locale for ${lang} not found, fallback to en`);
27
- dayJSLocale = await import("dayjs/locale/en.js");
20
+ dayJSLocale = await import(`dayjs/locale/en.js`);
28
21
  }
29
22
 
30
23
  dayjs.locale(dayJSLocale.default);
31
24
  };
32
25
 
33
26
  interface LocaleLayoutProps extends PropsWithChildren {
34
- antdLocale?: unknown;
27
+ antdLocale?: any;
35
28
  defaultLang?: string;
36
29
  }
37
30
 
38
- const Locale = memo<LocaleLayoutProps>(
39
- ({ children, defaultLang, antdLocale }) => {
40
- const [i18n] = useState(createI18nNext(defaultLang));
41
- const [lang, setLang] = useState(defaultLang);
42
- const [locale, setLocale] = useState(antdLocale);
31
+ const Locale = memo<LocaleLayoutProps>(({ children, defaultLang, antdLocale }) => {
32
+ const [i18n] = useState(() => createI18nNext(defaultLang));
33
+ const [lang, setLang] = useState(defaultLang);
34
+ const [locale, setLocale] = useState(antdLocale);
43
35
 
44
- // if on browser side, init i18n instance only once
45
- if (!i18n.instance.isInitialized)
46
- // console.debug('locale', lang);
36
+ if (!i18n.instance.isInitialized)
47
37
  i18n.init().then(async () => {
48
38
  if (!lang) return;
49
39
 
50
40
  await updateDayjs(lang);
51
41
  });
52
42
 
53
- // handle i18n instance language change
54
- useEffect(() => {
55
- const handleLang = async (lng: string) => {
56
- setLang(lng);
57
-
58
- if (lang === lng) return;
59
-
60
- const newLocale = await getAntdLocale(lng);
61
- setLocale(newLocale);
62
-
63
- await updateDayjs(lng);
64
- };
65
-
66
- i18n.instance.on("languageChanged", handleLang);
67
- return () => {
68
- i18n.instance.off("languageChanged", handleLang);
69
- };
70
- }, [i18n, lang]);
71
-
72
- // detect document direction
73
- const documentDir = isRtlLang(lang) ? "rtl" : "ltr";
74
-
75
- return (
76
- <ConfigProvider direction={documentDir} locale={locale as AntdLocale}>
77
- {children}
78
- </ConfigProvider>
79
- );
80
- }
81
- );
82
-
83
- Locale.displayName = "Locale";
43
+ // handle i18n instance language change
44
+ useEffect(() => {
45
+ const handleLang = async (lng: string) => {
46
+ setLang(lng);
47
+
48
+ if (lang === lng) return;
49
+
50
+ const newLocale = await getAntdLocale(lng);
51
+ setLocale(newLocale);
52
+
53
+ await updateDayjs(lng);
54
+ };
55
+
56
+ i18n.instance.on('languageChanged', handleLang);
57
+ return () => {
58
+ i18n.instance.off('languageChanged', handleLang);
59
+ };
60
+ }, [i18n, lang]);
61
+
62
+ // detect document direction
63
+ const documentDir = isRtlLang(lang!) ? 'rtl' : 'ltr';
64
+
65
+ return (
66
+ <ConfigProvider
67
+ direction={documentDir}
68
+ locale={locale}
69
+ theme={{
70
+ components: {
71
+ Button: {
72
+ contentFontSizeSM: 12,
73
+ },
74
+ },
75
+ }}
76
+ >
77
+ {children}
78
+ </ConfigProvider>
79
+ );
80
+ });
81
+
82
+ Locale.displayName = 'Locale';
84
83
 
85
84
  export default Locale;
@@ -0,0 +1,12 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import React, { type PropsWithChildren, useEffect, useState } from 'react';
3
+
4
+ const QueryProvider = ({ children }: PropsWithChildren) => {
5
+ const [queryClient] = useState(() => new QueryClient());
6
+
7
+ return (
8
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
9
+ );
10
+ };
11
+
12
+ export default QueryProvider;
@@ -0,0 +1,9 @@
1
+ import { StyleProvider } from 'antd-style';
2
+ import { type PropsWithChildren } from 'react';
3
+
4
+
5
+ const StyleRegistry = ({ children }: PropsWithChildren) => {
6
+ return <StyleProvider>{children}</StyleProvider>;
7
+ };
8
+
9
+ export default StyleRegistry;
@@ -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();