@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
@@ -0,0 +1,344 @@
1
+ /* eslint-disable unicorn/prefer-top-level-await */
2
+ import { consola } from 'consola';
3
+ import { colors } from 'consola/utils';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+
7
+ import { IGNORED_FILES } from './protectedPatterns';
8
+
9
+ interface UnusedKey {
10
+ filePath: string;
11
+ fullKey: string;
12
+ key: string;
13
+ namespace: string;
14
+ }
15
+
16
+ interface ReportData {
17
+ generatedAt: string;
18
+ statistics: {
19
+ totalKeys: number;
20
+ unusedKeys: number;
21
+ usageRate: string;
22
+ usedKeys: number;
23
+ };
24
+ unusedKeys: UnusedKey[];
25
+ unusedKeysByNamespace: Array<{
26
+ count: number;
27
+ keys: string[];
28
+ namespace: string;
29
+ }>;
30
+ }
31
+
32
+ /**
33
+ * Remove a key from a nested object
34
+ */
35
+ function removeKeyFromObject(obj: any, keyPath: string): boolean {
36
+ const keys = keyPath.split('.');
37
+ const lastKey = keys.pop()!;
38
+
39
+ let current = obj;
40
+ const parents: Array<{ key: string; obj: any }> = [];
41
+
42
+ // Navigate to the parent of the target key
43
+ for (const key of keys) {
44
+ if (!current[key]) {
45
+ return false; // Key path doesn't exist
46
+ }
47
+ parents.push({ key, obj: current });
48
+ current = current[key];
49
+ }
50
+
51
+ // Remove the key
52
+ if (lastKey in current) {
53
+ delete current[lastKey];
54
+
55
+ // Clean up empty parent objects
56
+ for (let i = parents.length - 1; i >= 0; i--) {
57
+ const { obj, key } = parents[i];
58
+ if (Object.keys(obj[key]).length === 0) {
59
+ delete obj[key];
60
+ } else {
61
+ break; // Stop if parent still has other keys
62
+ }
63
+ }
64
+
65
+ return true;
66
+ }
67
+
68
+ return false;
69
+ }
70
+
71
+ /**
72
+ * Clean unused keys from TypeScript default locale files
73
+ */
74
+ function cleanDefaultLocaleFiles(unusedKeys: UnusedKey[], dryRun: boolean = true) {
75
+ const defaultLocalesPath = path.join(process.cwd(), 'src/locales/default');
76
+
77
+ // Get ignored namespace names from IGNORED_FILES (remove .ts extension)
78
+ const ignoredNamespaces = new Set(IGNORED_FILES.map((f) => f.replace('.ts', '')));
79
+
80
+ // Group by namespace
81
+ const byNamespace = new Map<string, string[]>();
82
+ for (const key of unusedKeys) {
83
+ // Skip ignored namespaces (from IGNORED_FILES)
84
+ if (ignoredNamespaces.has(key.namespace)) {
85
+ continue;
86
+ }
87
+
88
+ if (!byNamespace.has(key.namespace)) {
89
+ byNamespace.set(key.namespace, []);
90
+ }
91
+ byNamespace.get(key.namespace)!.push(key.key);
92
+ }
93
+
94
+ consola.info(`Processing ${byNamespace.size} namespace files...`);
95
+ consola.info('');
96
+
97
+ let totalRemoved = 0;
98
+
99
+ for (const [namespace, keys] of byNamespace.entries()) {
100
+ const filePath = path.join(defaultLocalesPath, `${namespace}.ts`);
101
+
102
+ if (!fs.existsSync(filePath)) {
103
+ consola.warn(`File not found: ${filePath}`);
104
+ continue;
105
+ }
106
+
107
+ try {
108
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
109
+ const loadedModule = require(filePath);
110
+ const translations = loadedModule.default || loadedModule;
111
+
112
+ // Create a deep copy to avoid modifying the original
113
+ const updatedTranslations = structuredClone(translations);
114
+
115
+ let removedCount = 0;
116
+
117
+ // Remove each unused key
118
+ for (const key of keys) {
119
+ if (removeKeyFromObject(updatedTranslations, key)) {
120
+ removedCount++;
121
+ totalRemoved++;
122
+ }
123
+ }
124
+
125
+ if (removedCount > 0) {
126
+ consola.info(
127
+ colors.cyan(namespace.padEnd(20)),
128
+ colors.gray('→'),
129
+ colors.red(`${removedCount} keys to remove`),
130
+ );
131
+
132
+ if (!dryRun) {
133
+ // Generate new content
134
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
135
+ const newContent = generateTypeScriptContent(updatedTranslations);
136
+
137
+ // Write back to file
138
+ fs.writeFileSync(filePath, newContent, 'utf8');
139
+ consola.success(` ✓ Updated ${filePath}`);
140
+ } else {
141
+ consola.info(` ${colors.gray('(dry run - no changes made)')}`);
142
+ }
143
+ }
144
+ } catch (error) {
145
+ consola.error(`Failed to process ${namespace}:`, error);
146
+ }
147
+ }
148
+
149
+ return totalRemoved;
150
+ }
151
+
152
+ /**
153
+ * Clean unused keys from JSON locale files
154
+ */
155
+ function cleanLocaleJsonFiles(unusedKeys: UnusedKey[], dryRun: boolean = true) {
156
+ const localesPath = path.join(process.cwd(), 'locales');
157
+ const locales = fs
158
+ .readdirSync(localesPath)
159
+ .filter((f) => fs.statSync(path.join(localesPath, f)).isDirectory());
160
+
161
+ consola.info(`Processing ${locales.length} locale directories...`);
162
+ consola.info('');
163
+
164
+ // Get ignored namespace names from IGNORED_FILES (remove .ts extension)
165
+ const ignoredNamespaces = new Set(IGNORED_FILES.map((f) => f.replace('.ts', '')));
166
+
167
+ // Group by namespace
168
+ const byNamespace = new Map<string, string[]>();
169
+ for (const key of unusedKeys) {
170
+ // Skip ignored namespaces (from IGNORED_FILES)
171
+ if (ignoredNamespaces.has(key.namespace)) {
172
+ continue;
173
+ }
174
+
175
+ if (!byNamespace.has(key.namespace)) {
176
+ byNamespace.set(key.namespace, []);
177
+ }
178
+ byNamespace.get(key.namespace)!.push(key.key);
179
+ }
180
+
181
+ let totalRemoved = 0;
182
+
183
+ for (const locale of locales) {
184
+ consola.info(colors.cyan(`Locale: ${locale}`));
185
+
186
+ for (const [namespace, keys] of byNamespace.entries()) {
187
+ const filePath = path.join(localesPath, locale, `${namespace}.json`);
188
+
189
+ if (!fs.existsSync(filePath)) {
190
+ continue;
191
+ }
192
+
193
+ try {
194
+ const content = fs.readFileSync(filePath, 'utf8');
195
+ const translations = JSON.parse(content);
196
+
197
+ let removedCount = 0;
198
+
199
+ // Remove each unused key
200
+ for (const key of keys) {
201
+ if (removeKeyFromObject(translations, key)) {
202
+ removedCount++;
203
+ totalRemoved++;
204
+ }
205
+ }
206
+
207
+ if (removedCount > 0) {
208
+ consola.info(
209
+ ` ${colors.gray(namespace.padEnd(20))} → ${colors.red(removedCount + ' keys removed')}`,
210
+ );
211
+
212
+ if (!dryRun) {
213
+ // Write back to file with pretty formatting
214
+ fs.writeFileSync(filePath, JSON.stringify(translations, null, 2) + '\n', 'utf8');
215
+ }
216
+ }
217
+ } catch (error) {
218
+ consola.error(`Failed to process ${locale}/${namespace}:`, error);
219
+ }
220
+ }
221
+
222
+ consola.info('');
223
+ }
224
+
225
+ return totalRemoved;
226
+ }
227
+
228
+ /**
229
+ * Check if a key needs quotes in TypeScript object notation
230
+ */
231
+ function needsQuotes(key: string): boolean {
232
+ // Keys that need quotes:
233
+ // - Contains special characters (-, ., spaces, etc.)
234
+ // - Starts with a number
235
+ // - Is a reserved keyword
236
+ return !/^[$A-Z_a-z][\w$]*$/.test(key);
237
+ }
238
+
239
+ /**
240
+ * Generate TypeScript file content from object
241
+ */
242
+ function generateTypeScriptContent(obj: any): string {
243
+ const jsonString = JSON.stringify(obj, null, 2);
244
+
245
+ // Convert JSON to TypeScript object notation
246
+ // Handle keys that need quotes vs those that don't
247
+ let tsContent = jsonString.replaceAll(/"([^"]+)":/g, (match, key) => {
248
+ if (needsQuotes(key)) {
249
+ // Keep quotes for keys with special characters
250
+ return `'${key}':`;
251
+ }
252
+ // Remove quotes for valid identifiers
253
+ return `${key}:`;
254
+ });
255
+
256
+ // Use single quotes for string values
257
+ tsContent = tsContent.replaceAll(/: "([^"]*)"/g, ": '$1'");
258
+
259
+ return `export default ${tsContent};\n`;
260
+ }
261
+
262
+ /**
263
+ * Main function
264
+ */
265
+ async function main() {
266
+ const reportPath = path.join(process.cwd(), 'i18n-unused-keys-report.json');
267
+
268
+ // Check if report exists
269
+ if (!fs.existsSync(reportPath)) {
270
+ consola.error(
271
+ `Report file not found: ${reportPath}\n` +
272
+ 'Please run "bun run workflow:i18n-analyze" first to generate the report.',
273
+ );
274
+ throw new Error('Report file not found');
275
+ }
276
+
277
+ // Load report
278
+ const reportContent = fs.readFileSync(reportPath, 'utf8');
279
+ const report: ReportData = JSON.parse(reportContent);
280
+
281
+ consola.box('🧹 Clean Unused i18n Keys');
282
+ consola.info('');
283
+
284
+ // Show statistics
285
+ consola.info(colors.cyan('Statistics from report:'));
286
+ consola.info(` Total keys: ${report.statistics.totalKeys}`);
287
+ consola.info(` Used keys: ${report.statistics.usedKeys}`);
288
+ consola.info(` Unused keys: ${colors.red(report.statistics.unusedKeys.toString())}`);
289
+ consola.info(` Usage rate: ${report.statistics.usageRate}`);
290
+ consola.info('');
291
+
292
+ if (report.unusedKeys.length === 0) {
293
+ consola.success('No unused keys to clean!');
294
+ return;
295
+ }
296
+
297
+ // Ask for confirmation
298
+ const args = process.argv.slice(2);
299
+ const dryRun = !args.includes('--no-dry-run');
300
+
301
+ if (dryRun) {
302
+ consola.warn('Running in DRY RUN mode - no files will be modified');
303
+ consola.info('To actually clean the files, run: bun run workflow:i18n-clean --no-dry-run');
304
+ consola.info('');
305
+ } else {
306
+ consola.warn('⚠️ WARNING: This will modify your locale files!');
307
+ consola.info('Make sure you have committed your changes or have a backup.');
308
+ consola.info('');
309
+ }
310
+
311
+ // Clean default locale files (TypeScript)
312
+ consola.box('Step 1: Cleaning default locale files (TypeScript)');
313
+ const removedFromDefault = cleanDefaultLocaleFiles(report.unusedKeys, dryRun);
314
+ consola.info('');
315
+
316
+ // Clean locale JSON files
317
+ consola.box('Step 2: Cleaning locale JSON files');
318
+ const removedFromJson = cleanLocaleJsonFiles(report.unusedKeys, dryRun);
319
+ consola.info('');
320
+
321
+ // Summary
322
+ consola.box('Summary');
323
+ consola.info(`Keys marked for removal: ${colors.red(report.unusedKeys.length.toString())}`);
324
+ consola.info(
325
+ `Total operations: ${colors.yellow((removedFromDefault + removedFromJson).toString())}`,
326
+ );
327
+
328
+ if (dryRun) {
329
+ consola.info('');
330
+ consola.warn('This was a DRY RUN - no files were modified');
331
+ consola.info('To actually clean the files, run:');
332
+ consola.info(colors.cyan(' bun run workflow:i18n-clean --no-dry-run'));
333
+ } else {
334
+ consola.success('✓ Cleanup completed!');
335
+ consola.info('');
336
+ consola.info('Next steps:');
337
+ consola.info(' 1. Review the changes with git diff');
338
+ consola.info(' 2. Run "bun run i18n" to regenerate all locale files');
339
+ consola.info(' 3. Test your application');
340
+ consola.info(' 4. Commit the changes');
341
+ }
342
+ }
343
+
344
+ main();
@@ -0,0 +1,18 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import i18nConfig from './i18nConfig';
5
+
6
+ export const root = resolve(__dirname, '../..');
7
+ export const localesDir = resolve(root, i18nConfig.output);
8
+ export const localeDir = (locale: string) => resolve(localesDir, locale);
9
+ export const localeDirJsonList = (locale: string) =>
10
+ readdirSync(localeDir(locale)).filter((name) => name.includes('.json'));
11
+ export const srcLocalesDir = resolve(root, './src/locales');
12
+ export const entryLocaleJsonFilepath = (file: string) =>
13
+ resolve(localesDir, i18nConfig.entryLocale, file);
14
+ export const outputLocaleJsonFilepath = (locale: string, file: string) =>
15
+ resolve(localesDir, locale, file);
16
+ export const srcDefaultLocales = resolve(root, srcLocalesDir, 'default');
17
+
18
+ export { default as i18nConfig } from './i18nConfig';
@@ -0,0 +1,139 @@
1
+ /* eslint-disable unicorn/prefer-top-level-await */
2
+ import prettier from '@prettier/sync';
3
+ import { consola } from 'consola';
4
+ import { colors } from 'consola/utils';
5
+ import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+
9
+ import { toLodashPath } from '../../src/locales/utils';
10
+ import { localeDir, localeDirJsonList, localesDir, srcDefaultLocales } from './const';
11
+
12
+ const prettierOptions = prettier.resolveConfig(resolve(__dirname, '../../.prettierrc.js')) ?? {};
13
+
14
+ const DEFAULT_SKIP_FILES = new Set(['index.ts', 'models.ts', 'providers.ts']);
15
+
16
+ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
17
+ if (!value || typeof value !== 'object') return false;
18
+ return Object.prototype.toString.call(value) === '[object Object]';
19
+ };
20
+
21
+ const shouldPreserveObject = (value: Record<string, unknown>) => {
22
+ const keys = Object.keys(value);
23
+ if (keys.length === 0) return true;
24
+ return keys.every((key) => /^\d+$/.test(key));
25
+ };
26
+
27
+ const flattenObject = (input: Record<string, unknown>) => {
28
+ const output: Record<string, unknown> = {};
29
+
30
+ const addEntry = (pathSegments: Array<number | string>, value: unknown) => {
31
+ const key = toLodashPath(pathSegments);
32
+ if (Object.prototype.hasOwnProperty.call(output, key)) {
33
+ throw new Error(`Duplicate i18n key detected: ${key}`);
34
+ }
35
+ output[key] = value;
36
+ };
37
+
38
+ const visit = (value: unknown, pathSegments: Array<number | string>) => {
39
+ if (Array.isArray(value)) {
40
+ addEntry(pathSegments, value);
41
+ return;
42
+ }
43
+
44
+ if (isPlainObject(value)) {
45
+ if (shouldPreserveObject(value)) {
46
+ addEntry(pathSegments, value);
47
+ return;
48
+ }
49
+
50
+ const entries = Object.entries(value);
51
+ if (entries.length === 0) {
52
+ addEntry(pathSegments, value);
53
+ return;
54
+ }
55
+
56
+ for (const [childKey, childValue] of entries) {
57
+ visit(childValue, [...pathSegments, childKey]);
58
+ }
59
+ return;
60
+ }
61
+
62
+ addEntry(pathSegments, value);
63
+ };
64
+
65
+ for (const [key, value] of Object.entries(input)) {
66
+ visit(value, [key]);
67
+ }
68
+
69
+ return output;
70
+ };
71
+
72
+ const writeTs = (filePath: string, data: Record<string, unknown>) => {
73
+ const content = `export default ${JSON.stringify(data, null, 2)};\n`;
74
+ const formatted = prettier.format(content, {
75
+ ...prettierOptions,
76
+ parser: 'typescript',
77
+ });
78
+ writeFileSync(filePath, formatted, 'utf8');
79
+ };
80
+
81
+ const writeJson = (filePath: string, data: Record<string, unknown>) => {
82
+ const json = JSON.stringify(data, null, 2);
83
+ const formatted = prettier.format(json, {
84
+ ...prettierOptions,
85
+ parser: 'json',
86
+ });
87
+ writeFileSync(filePath, formatted, 'utf8');
88
+ };
89
+
90
+ const flattenDefaultLocales = async () => {
91
+ const files = readdirSync(srcDefaultLocales).filter((file) => file.endsWith('.ts'));
92
+
93
+ for (const file of files) {
94
+ if (DEFAULT_SKIP_FILES.has(file)) continue;
95
+
96
+ const filePath = resolve(srcDefaultLocales, file);
97
+ const fileUrl = pathToFileURL(filePath).href;
98
+ const loaded = await import(fileUrl);
99
+ const data = loaded.default ?? loaded;
100
+
101
+ const flat = flattenObject(data as Record<string, unknown>);
102
+ writeTs(filePath, flat);
103
+ consola.success(colors.cyan(file), colors.gray('flattened'));
104
+ }
105
+ };
106
+
107
+ const flattenLocaleJsons = () => {
108
+ const localeFolders = readdirSync(localesDir).filter((dir) =>
109
+ statSync(localeDir(dir)).isDirectory(),
110
+ );
111
+
112
+ for (const locale of localeFolders) {
113
+ const jsonFiles = localeDirJsonList(locale);
114
+ for (const jsonFile of jsonFiles) {
115
+ const filePath = resolve(localeDir(locale), jsonFile);
116
+ const raw = readFileSync(filePath, 'utf8');
117
+ const data = JSON.parse(raw);
118
+ const flat = flattenObject(data);
119
+ writeJson(filePath, flat);
120
+ consola.success(colors.cyan(`${locale}/${jsonFile}`), colors.gray('flattened'));
121
+ }
122
+ }
123
+ };
124
+
125
+ const run = async () => {
126
+ consola.start('Flattening src/locales/default...');
127
+ await flattenDefaultLocales();
128
+
129
+ consola.start('Flattening locales JSON files...');
130
+ flattenLocaleJsons();
131
+
132
+ consola.success('Flattening completed.');
133
+ };
134
+
135
+ run().catch((error) => {
136
+ consola.error(error);
137
+ // eslint-disable-next-line unicorn/no-process-exit
138
+ process.exit(1);
139
+ });
@@ -0,0 +1,19 @@
1
+ import { consola } from 'consola';
2
+ import { colors } from 'consola/utils';
3
+
4
+ import { entryLocaleJsonFilepath, i18nConfig, srcDefaultLocales } from './const';
5
+ import { tagWhite, writeJSONWithPrettier } from './utils';
6
+
7
+ export const genDefaultLocale = () => {
8
+ consola.info(`Default locale is ${i18nConfig.entryLocale}...`);
9
+
10
+ const resources = require(srcDefaultLocales);
11
+ const data = Object.entries(resources.default);
12
+ consola.start(`Generate default locale json, found ${data.length} namespaces...`);
13
+
14
+ for (const [ns, value] of data) {
15
+ const filepath = entryLocaleJsonFilepath(`${ns}.json`);
16
+ writeJSONWithPrettier(filepath, value);
17
+ consola.success(tagWhite(ns), colors.gray(filepath));
18
+ }
19
+ };
@@ -0,0 +1,49 @@
1
+ import { consola } from 'consola';
2
+ import { colors } from 'consola/utils';
3
+ import { unset } from 'es-toolkit/compat';
4
+ import { diff } from 'just-diff';
5
+ import { existsSync } from 'node:fs';
6
+
7
+ import {
8
+ entryLocaleJsonFilepath,
9
+ i18nConfig,
10
+ outputLocaleJsonFilepath,
11
+ srcDefaultLocales,
12
+ } from './const';
13
+ import { readJSON, tagWhite, writeJSONWithPrettier } from './utils';
14
+
15
+ export const genDiff = () => {
16
+ consola.start(`Remove diff analysis...`);
17
+
18
+ const resources = require(srcDefaultLocales);
19
+ const data = Object.entries(resources.default);
20
+
21
+ for (const [ns, devJSON] of data) {
22
+ const filepath = entryLocaleJsonFilepath(`${ns}.json`);
23
+ if (!existsSync(filepath)) continue;
24
+ const previousProdJSON = readJSON(filepath);
25
+
26
+ const diffResult = diff(previousProdJSON, devJSON as any);
27
+ if (diffResult.length === 0) {
28
+ consola.success(tagWhite(ns), colors.gray(filepath));
29
+ continue;
30
+ }
31
+
32
+ const clearLocals = [];
33
+
34
+ for (const locale of i18nConfig.outputLocales) {
35
+ const localeFilepath = outputLocaleJsonFilepath(locale, `${ns}.json`);
36
+ if (!existsSync(localeFilepath)) continue;
37
+ const localeJSON = readJSON(localeFilepath);
38
+
39
+ for (const item of diffResult) {
40
+ unset(localeJSON, item.path);
41
+ }
42
+
43
+ writeJSONWithPrettier(localeFilepath, localeJSON);
44
+ clearLocals.push(locale);
45
+ }
46
+ consola.info('clear', clearLocals);
47
+ consola.success(tagWhite(ns), colors.gray(filepath));
48
+ }
49
+ };
@@ -0,0 +1,7 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+
5
+ const config = require('../../.i18nrc');
6
+
7
+ export default config;
@@ -0,0 +1,11 @@
1
+ import { genDefaultLocale } from './genDefaultLocale';
2
+ import { genDiff } from './genDiff';
3
+ import { split } from './utils';
4
+
5
+ split('DIFF ANALYSIS');
6
+ genDiff();
7
+
8
+ split('GENERATE DEFAULT LOCALE');
9
+ genDefaultLocale();
10
+
11
+ split('GENERATE I18N FILES');
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Protected patterns for i18n keys
3
+ *
4
+ * Keys matching these patterns will be considered "used" even if not found in static analysis.
5
+ * This is useful for dynamically generated keys like:
6
+ * - t(`modelProvider.${providerId}.title`)
7
+ * - t('error.' + errorCode)
8
+ */
9
+
10
+ /**
11
+ * Files to ignore at file level (won't be scanned at all)
12
+ */
13
+ export const IGNORED_FILES = [
14
+ 'providers.ts', // Dynamically generated from DEFAULT_MODEL_PROVIDER_LIST
15
+ 'models.ts', // Dynamically generated from LOBE_DEFAULT_MODEL_LIST
16
+ 'auth.ts', // Auth-related dynamic keys
17
+ 'authError.ts', // Auth error dynamic keys
18
+ 'error.ts', // Error messages with dynamic codes
19
+ 'migration.ts', // Migration-related dynamic keys
20
+ 'subscription.ts',
21
+ 'electron.ts', // Electron-specific dynamic keys
22
+ 'editor.ts', // Editor-related dynamic keys
23
+ 'changelog.ts', // Changelog dynamic keys
24
+ 'ragEval.ts',
25
+ 'plugin.ts',
26
+ 'tools.ts',
27
+ 'oauth.ts',
28
+ ];
29
+
30
+ /**
31
+ * Namespace patterns to protect (keys won't be marked as unused)
32
+ */
33
+ export const PROTECTED_KEY_PATTERNS = [
34
+ // === Namespaces with extensive dynamic usage ===
35
+ 'modelProvider', // t(`modelProvider.${providerId}.title`)
36
+
37
+ // Discover namespace has many dynamic keys
38
+ 'discover', // t(`assistants.status.${statusKey}.subtitle`)
39
+
40
+ // Setting namespace with dynamic agent configurations
41
+ 'setting', // t(`systemAgent.${key}.label`)
42
+
43
+ // Hotkey namespace uses dynamic key construction
44
+ 'hotkey', // t(`${item.id}.desc`, { ns: 'hotkey' })
45
+
46
+ // Home namespace with dynamic starter keys
47
+ 'home', // t(`starter.${key}`)
48
+
49
+ // Welcome namespace with returnObjects usage
50
+ 'welcome', // t('welcomeMessages', { returnObjects: true })
51
+
52
+ // Chat namespace has dynamic input keys
53
+ 'chat', // t(`input.${key}`)
54
+
55
+ // File namespace - used in hooks that receive t as parameter
56
+ 'file', // TFunction<'file'> passed as parameter
57
+
58
+ // MarketAuth namespace - has Trans components with dynamic keys
59
+ 'marketAuth', // <Trans i18nKey="authorize.footer.agreement" />
60
+
61
+ // Onboarding namespace - has various dynamic usage patterns
62
+ 'onboarding', // Onboarding flow with complex Trans usage
63
+ 'error',
64
+ 'errors',
65
+ 'consent.error',
66
+ 'builtins',
67
+
68
+ // === Add your custom patterns here ===
69
+ // Examples:
70
+ // 'error.code', // Protects all error.code.* keys
71
+ // 'plugin.settings', // Protects all plugin.settings.* keys
72
+ // 'tool', // Protects entire 'tool' namespace
73
+ ];
74
+
75
+ /**
76
+ * How to use:
77
+ *
78
+ * 1. IGNORED_FILES - Files to completely skip during analysis:
79
+ * Add filename with .ts extension (e.g., 'auth.ts')
80
+ * These files won't be scanned at all
81
+ *
82
+ * 2. PROTECTED_KEY_PATTERNS - Namespace/patterns to protect:
83
+ * - Full namespace: 'myNamespace' protects all keys under that namespace
84
+ * - Prefix pattern: 'namespace.prefix' protects keys starting with that prefix
85
+ * (e.g., 'error.code' protects 'error.code.NOT_FOUND', 'error.code.TIMEOUT', etc.)
86
+ *
87
+ * 3. After modifying this file:
88
+ * - Run `bun run workflow:i18n-analyze` to regenerate the report
89
+ * - Run `bun run workflow:i18n-clean` to preview cleanup (both use same config)
90
+ * - Check the console output for "Protected patterns" to verify your config
91
+ */