kythia-core 0.10.1-beta → 0.11.1-beta
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/README.md +87 -1
- package/package.json +11 -1
- package/src/Kythia.js +9 -7
- package/src/KythiaClient.js +1 -2
- package/src/cli/Command.js +68 -0
- package/src/cli/commands/CacheClearCommand.js +136 -0
- package/src/cli/commands/LangCheckCommand.js +396 -0
- package/src/cli/commands/LangTranslateCommand.js +336 -0
- package/src/cli/commands/MakeMigrationCommand.js +82 -0
- package/src/cli/commands/MakeModelCommand.js +81 -0
- package/src/cli/commands/MigrateCommand.js +259 -0
- package/src/cli/commands/NamespaceCommand.js +112 -0
- package/src/cli/commands/StructureCommand.js +70 -0
- package/src/cli/commands/UpversionCommand.js +94 -0
- package/src/cli/index.js +69 -0
- package/src/cli/utils/db.js +117 -0
- package/src/database/KythiaMigrator.js +1 -1
- package/src/database/KythiaModel.js +76 -48
- package/src/database/KythiaSequelize.js +1 -1
- package/src/database/KythiaStorage.js +1 -1
- package/src/database/ModelLoader.js +1 -1
- package/src/managers/AddonManager.js +10 -1
- package/src/managers/EventManager.js +1 -1
- package/src/managers/InteractionManager.js +56 -2
- package/src/managers/ShutdownManager.js +1 -1
- package/.husky/pre-commit +0 -4
- package/biome.json +0 -40
- package/bun.lock +0 -445
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🕵️♂️ Translation Integrity Linter
|
|
3
|
+
*
|
|
4
|
+
* @file src/cli/commands/LangCheckCommand.js
|
|
5
|
+
* @copyright © 2025 kenndeclouv
|
|
6
|
+
* @assistant chaa & graa
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
|
+
*
|
|
9
|
+
* @description
|
|
10
|
+
* Performs a deep AST analysis of the codebase to find `t()` translation function calls.
|
|
11
|
+
* Verifies that every used key exists in the language files (JSON) and reports usage errors.
|
|
12
|
+
*
|
|
13
|
+
* ✨ Core Features:
|
|
14
|
+
* - AST Parsing: Uses Babel parser for accurate key detection (handles dynamic patterns).
|
|
15
|
+
* - Key Verification: Recursively checks nested JSON structures.
|
|
16
|
+
* - Unused Key Detection: Reports keys defined in JSON but never used in code.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const Command = require('../Command');
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const glob = require('glob');
|
|
23
|
+
const parser = require('@babel/parser');
|
|
24
|
+
const traverse = require('@babel/traverse').default;
|
|
25
|
+
|
|
26
|
+
function deepMerge(target, source) {
|
|
27
|
+
if (typeof target !== 'object' || target === null) return source;
|
|
28
|
+
if (typeof source !== 'object' || source === null) return source;
|
|
29
|
+
|
|
30
|
+
for (const key of Object.keys(source)) {
|
|
31
|
+
if (
|
|
32
|
+
source[key] instanceof Object &&
|
|
33
|
+
target[key] instanceof Object &&
|
|
34
|
+
!Array.isArray(source[key])
|
|
35
|
+
) {
|
|
36
|
+
target[key] = deepMerge(target[key], source[key]);
|
|
37
|
+
} else {
|
|
38
|
+
target[key] = source[key];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getAllKeys(obj, allDefinedKeys, prefix = '') {
|
|
45
|
+
Object.keys(obj).forEach((key) => {
|
|
46
|
+
if (key === '_value' || key === 'text') {
|
|
47
|
+
if (Object.keys(obj).length === 1) return;
|
|
48
|
+
if (prefix) allDefinedKeys.add(prefix);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
52
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
53
|
+
if (key !== 'jobs' && key !== 'shop') {
|
|
54
|
+
getAllKeys(obj[key], allDefinedKeys, fullKey);
|
|
55
|
+
} else {
|
|
56
|
+
allDefinedKeys.add(fullKey);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
allDefinedKeys.add(fullKey);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class LangCheckCommand extends Command {
|
|
65
|
+
signature = 'lang:check';
|
|
66
|
+
description =
|
|
67
|
+
'Lint translation key usage in code and language files (AST-based)';
|
|
68
|
+
|
|
69
|
+
async handle() {
|
|
70
|
+
const PROJECT_ROOT = process.cwd();
|
|
71
|
+
const SCAN_DIRECTORIES = ['addons', 'src'];
|
|
72
|
+
const DEFAULT_LANG = 'en';
|
|
73
|
+
const IGNORE_PATTERNS = [
|
|
74
|
+
'**/node_modules/**',
|
|
75
|
+
'**/dist/**',
|
|
76
|
+
'**/tests/**',
|
|
77
|
+
'**/assets/**',
|
|
78
|
+
'**/dashboard/web/public/**',
|
|
79
|
+
'**/temp/**',
|
|
80
|
+
'**/leetMap.js',
|
|
81
|
+
'**/generate_*.js',
|
|
82
|
+
'**/refactor_*.js',
|
|
83
|
+
'**/undo_*.js',
|
|
84
|
+
'**/*.d.ts',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const locales = {};
|
|
88
|
+
const usedStaticKeys = new Set();
|
|
89
|
+
const usedDynamicKeys = new Set();
|
|
90
|
+
const unanalyzableKeys = new Set();
|
|
91
|
+
let filesScanned = 0;
|
|
92
|
+
let filesWithErrors = 0;
|
|
93
|
+
|
|
94
|
+
console.log('--- Kythia AST Translation Linter ---');
|
|
95
|
+
|
|
96
|
+
function hasNestedKey(obj, pathExpr) {
|
|
97
|
+
if (!obj || !pathExpr) return false;
|
|
98
|
+
const parts = pathExpr.split('.');
|
|
99
|
+
let current = obj;
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
if (
|
|
102
|
+
typeof current !== 'object' ||
|
|
103
|
+
current === null ||
|
|
104
|
+
!Object.hasOwn(current, part)
|
|
105
|
+
) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
current = current[part];
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _loadLocales() {
|
|
114
|
+
console.log(`\n🔍 Searching for language files in: ${PROJECT_ROOT}`);
|
|
115
|
+
try {
|
|
116
|
+
const langFiles = glob.sync('**/lang/*.json', {
|
|
117
|
+
cwd: PROJECT_ROOT,
|
|
118
|
+
ignore: ['**/node_modules/**', '**/dist/**'],
|
|
119
|
+
absolute: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (langFiles.length === 0) {
|
|
123
|
+
console.error(
|
|
124
|
+
'\x1b[31m%s\x1b[0m',
|
|
125
|
+
'❌ No .json files found in any lang folder.',
|
|
126
|
+
);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let loadedCount = 0;
|
|
131
|
+
for (const file of langFiles) {
|
|
132
|
+
if (file.includes('_flat') || file.includes('_FLAT')) continue;
|
|
133
|
+
|
|
134
|
+
const filename = path.basename(file);
|
|
135
|
+
const lang = filename.replace('.json', '');
|
|
136
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(content);
|
|
140
|
+
if (!locales[lang]) {
|
|
141
|
+
locales[lang] = parsed;
|
|
142
|
+
} else {
|
|
143
|
+
// Merge with existing locale data
|
|
144
|
+
locales[lang] = deepMerge(locales[lang], parsed);
|
|
145
|
+
}
|
|
146
|
+
loadedCount++;
|
|
147
|
+
} catch (jsonError) {
|
|
148
|
+
console.error(
|
|
149
|
+
`\x1b[31m%s\x1b[0m`,
|
|
150
|
+
`❌ Failed to parse JSON: ${path.relative(PROJECT_ROOT, file)} - ${jsonError.message}`,
|
|
151
|
+
);
|
|
152
|
+
filesWithErrors++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(` > Successfully loaded ${loadedCount} language files.`);
|
|
156
|
+
|
|
157
|
+
if (!locales[DEFAULT_LANG]) {
|
|
158
|
+
console.error(
|
|
159
|
+
`\x1b[31m%s\x1b[0m`,
|
|
160
|
+
`❌ Default language (${DEFAULT_LANG}) not found in any loaded files!`,
|
|
161
|
+
);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(
|
|
167
|
+
'\x1b[31m%s\x1b[0m',
|
|
168
|
+
`❌ Failed to load language files: ${error.message}`,
|
|
169
|
+
);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!_loadLocales()) {
|
|
175
|
+
console.error('\x1b[31mCannot proceed (language files invalid).\x1b[0m');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(
|
|
180
|
+
`\nScanning .js/.ts files in: ${SCAN_DIRECTORIES.join(', ')}...`,
|
|
181
|
+
);
|
|
182
|
+
SCAN_DIRECTORIES.forEach((dirName) => {
|
|
183
|
+
const dirPath = path.join(PROJECT_ROOT, dirName);
|
|
184
|
+
const files = glob.sync(`${dirPath}/**/*.{js,ts}`, {
|
|
185
|
+
ignore: IGNORE_PATTERNS,
|
|
186
|
+
dot: true,
|
|
187
|
+
});
|
|
188
|
+
files.forEach((filePath) => {
|
|
189
|
+
filesScanned++;
|
|
190
|
+
process.stdout.write(`\rScanning: ${filesScanned} files...`);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const code = fs.readFileSync(filePath, 'utf8');
|
|
194
|
+
const ast = parser.parse(code, {
|
|
195
|
+
sourceType: 'module',
|
|
196
|
+
plugins: [
|
|
197
|
+
'typescript',
|
|
198
|
+
'jsx',
|
|
199
|
+
'classProperties',
|
|
200
|
+
'objectRestSpread',
|
|
201
|
+
],
|
|
202
|
+
errorRecovery: true,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
traverse(ast, {
|
|
206
|
+
CallExpression(nodePath) {
|
|
207
|
+
const node = nodePath.node;
|
|
208
|
+
if (
|
|
209
|
+
node.callee.type === 'Identifier' &&
|
|
210
|
+
node.callee.name === 't'
|
|
211
|
+
) {
|
|
212
|
+
if (node.arguments.length >= 2) {
|
|
213
|
+
const keyArg = node.arguments[1];
|
|
214
|
+
if (keyArg.type === 'StringLiteral') {
|
|
215
|
+
usedStaticKeys.add(keyArg.value);
|
|
216
|
+
} else if (keyArg.type === 'TemplateLiteral') {
|
|
217
|
+
let pattern = '';
|
|
218
|
+
keyArg.quasis.forEach((quasi, _i) => {
|
|
219
|
+
pattern += quasi.value.raw;
|
|
220
|
+
if (!quasi.tail) {
|
|
221
|
+
pattern += '*';
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
pattern = pattern.replace(/_/g, '.');
|
|
225
|
+
usedDynamicKeys.add(pattern);
|
|
226
|
+
} else if (
|
|
227
|
+
keyArg.type === 'BinaryExpression' &&
|
|
228
|
+
keyArg.operator === '+'
|
|
229
|
+
) {
|
|
230
|
+
if (keyArg.left.type === 'StringLiteral') {
|
|
231
|
+
const pattern = `${keyArg.left.value.replace(/_/g, '.')}*`;
|
|
232
|
+
usedDynamicKeys.add(pattern);
|
|
233
|
+
} else {
|
|
234
|
+
unanalyzableKeys.add(
|
|
235
|
+
`Complex (+) at ${path.relative(PROJECT_ROOT, filePath)}:${node.loc?.start.line}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
unanalyzableKeys.add(
|
|
240
|
+
`Variable/Other at ${path.relative(PROJECT_ROOT, filePath)}:${node.loc?.start.line}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
} catch (parseError) {
|
|
248
|
+
if (parseError.message.includes('Unexpected token')) {
|
|
249
|
+
console.warn(
|
|
250
|
+
`\n\x1b[33m[WARN] Syntax Error parsing ${path.relative(
|
|
251
|
+
PROJECT_ROOT,
|
|
252
|
+
filePath,
|
|
253
|
+
)}:${parseError.loc?.line} - ${parseError.message}\x1b[0m`,
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
console.error(
|
|
257
|
+
`\n\x1b[31m[ERROR] Failed to parse ${path.relative(
|
|
258
|
+
PROJECT_ROOT,
|
|
259
|
+
filePath,
|
|
260
|
+
)}: ${parseError.message}\x1b[0m`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
filesWithErrors++;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
process.stdout.write(`${'\r'.padEnd(process.stdout.columns || 60)}\r`);
|
|
268
|
+
|
|
269
|
+
console.log(`\nScan completed. Total ${filesScanned} files processed.`);
|
|
270
|
+
console.log(` > Found \x1b[33m${usedStaticKeys.size}\x1b[0m static keys.`);
|
|
271
|
+
console.log(
|
|
272
|
+
` > Found \x1b[33m${usedDynamicKeys.size}\x1b[0m dynamic key patterns (check manually!).`,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (unanalyzableKeys.size > 0) {
|
|
276
|
+
console.log(
|
|
277
|
+
` > \x1b[31m${unanalyzableKeys.size}\x1b[0m t() calls could not be analyzed (variable/complex).`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log('\nVerifying static keys against language files...');
|
|
282
|
+
|
|
283
|
+
let totalMissingStatic = 0;
|
|
284
|
+
for (const lang in locales) {
|
|
285
|
+
const missingInLang = [];
|
|
286
|
+
for (const staticKey of usedStaticKeys) {
|
|
287
|
+
if (!hasNestedKey(locales[lang], staticKey)) {
|
|
288
|
+
missingInLang.push(staticKey);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (missingInLang.length > 0) {
|
|
292
|
+
console.log(
|
|
293
|
+
`\n❌ \x1b[31m[${lang.toUpperCase()}] Found ${missingInLang.length} missing static keys:\x1b[0m`,
|
|
294
|
+
);
|
|
295
|
+
missingInLang.sort().forEach((key) => {
|
|
296
|
+
console.log(` - ${key}`);
|
|
297
|
+
});
|
|
298
|
+
totalMissingStatic += missingInLang.length;
|
|
299
|
+
filesWithErrors++;
|
|
300
|
+
} else {
|
|
301
|
+
console.log(
|
|
302
|
+
`\n✅ \x1b[32m[${lang.toUpperCase()}] All static keys found!\x1b[0m`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (usedDynamicKeys.size > 0) {
|
|
308
|
+
console.log(
|
|
309
|
+
`\n\n⚠️ \x1b[33mDynamic Key Patterns Detected (Check Manually):\x1b[0m`,
|
|
310
|
+
);
|
|
311
|
+
[...usedDynamicKeys].sort().forEach((pattern) => {
|
|
312
|
+
console.log(` - ${pattern}`);
|
|
313
|
+
});
|
|
314
|
+
console.log(
|
|
315
|
+
` (Ensure all possible keys from these patterns exist in the language files)`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (unanalyzableKeys.size > 0) {
|
|
320
|
+
console.log(
|
|
321
|
+
`\n\n⚠️ \x1b[31mComplex/Unanalyzable t() Calls (Check Manually):\x1b[0m`,
|
|
322
|
+
);
|
|
323
|
+
[...unanalyzableKeys].sort().forEach((loc) => {
|
|
324
|
+
console.log(` - ${loc}`);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`\nChecking UNUSED keys (based on ${DEFAULT_LANG}.json)...`);
|
|
329
|
+
|
|
330
|
+
const defaultLocale = locales[DEFAULT_LANG];
|
|
331
|
+
const allDefinedKeys = new Set();
|
|
332
|
+
|
|
333
|
+
if (defaultLocale) {
|
|
334
|
+
try {
|
|
335
|
+
getAllKeys(defaultLocale, allDefinedKeys);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.error('Error collecting defined keys:', e);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const unusedKeys = [];
|
|
341
|
+
for (const definedKey of allDefinedKeys) {
|
|
342
|
+
if (!usedStaticKeys.has(definedKey)) {
|
|
343
|
+
let matchedByDynamic = false;
|
|
344
|
+
for (const dynamicPattern of usedDynamicKeys) {
|
|
345
|
+
const regexPattern = `^${dynamicPattern
|
|
346
|
+
.replace(/\./g, '\\.')
|
|
347
|
+
.replace(/\*/g, '[^.]+?')}$`;
|
|
348
|
+
if (new RegExp(regexPattern).test(definedKey)) {
|
|
349
|
+
matchedByDynamic = true;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (!matchedByDynamic) {
|
|
354
|
+
unusedKeys.push(definedKey);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (unusedKeys.length > 0) {
|
|
359
|
+
console.log(
|
|
360
|
+
`\n⚠️ \x1b[33mFound ${unusedKeys.length} UNUSED keys in ${DEFAULT_LANG}.json (don't match static/dynamic patterns):\x1b[0m`,
|
|
361
|
+
);
|
|
362
|
+
unusedKeys.sort().forEach((key) => {
|
|
363
|
+
console.log(` - ${key}`);
|
|
364
|
+
});
|
|
365
|
+
} else {
|
|
366
|
+
console.log(
|
|
367
|
+
`\n✅ \x1b[32m[${DEFAULT_LANG.toUpperCase()}] No unused keys found.\x1b[0m`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
console.warn(
|
|
372
|
+
`\n\x1b[33m[WARN] Cannot check unused keys because ${DEFAULT_LANG}.json failed to load.\x1b[0m`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log('\n--- Done ---');
|
|
377
|
+
|
|
378
|
+
if (filesWithErrors > 0 || totalMissingStatic > 0) {
|
|
379
|
+
console.log(
|
|
380
|
+
`\x1b[31mTotal ${totalMissingStatic} missing static key errors + ${filesWithErrors - totalMissingStatic} file errors found. Please fix them.\x1b[0m`,
|
|
381
|
+
);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
} else {
|
|
384
|
+
console.log(
|
|
385
|
+
'\x1b[32mCongratulations! Language files (for static keys) are already synced with the code.\x1b[0m',
|
|
386
|
+
);
|
|
387
|
+
if (usedDynamicKeys.size > 0 || unanalyzableKeys.size > 0) {
|
|
388
|
+
console.log(
|
|
389
|
+
"\x1b[33mHowever, don't forget to manually check the reported dynamic/complex keys above!\x1b[0m",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = LangCheckCommand;
|