@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,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,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
|
+
*/
|