i18ntk 3.3.0 → 4.1.0
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/CHANGELOG.md +84 -16
- package/README.md +160 -15
- package/SECURITY.md +16 -8
- package/main/i18ntk-backup.js +370 -73
- package/main/i18ntk-scanner.js +190 -49
- package/main/i18ntk-sizing.js +241 -79
- package/main/i18ntk-usage.js +221 -46
- package/main/i18ntk-validate.js +114 -5
- package/main/manage/commands/FixerCommand.js +23 -21
- package/main/manage/index.js +13 -7
- package/main/manage/services/FileManagementService.js +12 -6
- package/package.json +46 -2
- package/runtime/i18ntk.d.ts +22 -16
- package/runtime/index.d.ts +9 -7
- package/runtime/index.js +246 -50
- package/ui-locales/en.json +1 -1
- package/utils/translate/protection.js +153 -7
- package/utils/watch-locales.js +194 -36
package/main/i18ntk-usage.js
CHANGED
|
@@ -61,6 +61,10 @@ async function getConfig() {
|
|
|
61
61
|
return await getUnifiedConfig('usage');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function toBool(v) {
|
|
65
|
+
return v === true || v === 'true' || v === '1';
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
class I18nUsageAnalyzer {
|
|
65
69
|
constructor(config = {}) {
|
|
66
70
|
this.config = config;
|
|
@@ -84,6 +88,12 @@ class I18nUsageAnalyzer {
|
|
|
84
88
|
this.startTime = Date.now(); // Track performance metrics
|
|
85
89
|
this.version = '1.10.1'; // Version tracking
|
|
86
90
|
|
|
91
|
+
// Dead key detection properties
|
|
92
|
+
this.deadKeys = new Map();
|
|
93
|
+
this.cleanupMode = false;
|
|
94
|
+
this.dryRunDelete = false;
|
|
95
|
+
this._sourceCommentsSet = null;
|
|
96
|
+
|
|
87
97
|
// Use global translation function
|
|
88
98
|
this.rl = null;
|
|
89
99
|
}
|
|
@@ -187,7 +197,9 @@ class I18nUsageAnalyzer {
|
|
|
187
197
|
help: a.help || a.h,
|
|
188
198
|
noPrompt: a.noPrompt ?? a['no-prompt'],
|
|
189
199
|
strict: a.strict,
|
|
190
|
-
debug: a.debug
|
|
200
|
+
debug: a.debug,
|
|
201
|
+
cleanup: a.cleanup ?? a['cleanup'],
|
|
202
|
+
dryRunDelete: a.dryRunDelete ?? a['dry-run-delete']
|
|
191
203
|
};
|
|
192
204
|
}
|
|
193
205
|
|
|
@@ -376,6 +388,13 @@ class I18nUsageAnalyzer {
|
|
|
376
388
|
console.log('🔍 Debug mode enabled');
|
|
377
389
|
}
|
|
378
390
|
|
|
391
|
+
if (toBool(args.cleanup)) {
|
|
392
|
+
this.cleanupMode = true;
|
|
393
|
+
}
|
|
394
|
+
if (toBool(args.dryRunDelete)) {
|
|
395
|
+
this.dryRunDelete = true;
|
|
396
|
+
}
|
|
397
|
+
|
|
379
398
|
try {
|
|
380
399
|
// Ensure config is always initialized
|
|
381
400
|
if (!this.config) {
|
|
@@ -519,9 +538,6 @@ class I18nUsageAnalyzer {
|
|
|
519
538
|
// Load available translation keys first
|
|
520
539
|
await this.loadAvailableKeys();
|
|
521
540
|
|
|
522
|
-
// NEW: Detect framework patterns before analysis
|
|
523
|
-
await this.detectFrameworkPatterns();
|
|
524
|
-
|
|
525
541
|
// Perform usage analysis with enhanced features
|
|
526
542
|
await this.analyzeUsage();
|
|
527
543
|
|
|
@@ -599,6 +615,34 @@ class I18nUsageAnalyzer {
|
|
|
599
615
|
}));
|
|
600
616
|
}
|
|
601
617
|
|
|
618
|
+
if (this.cleanupMode) {
|
|
619
|
+
this._buildSourceCommentsSet();
|
|
620
|
+
const deadKeys = this.findDeadKeys();
|
|
621
|
+
console.log('\n' + t('usage.deadKeysDetectionTitle'));
|
|
622
|
+
console.log(t('usage.deadKeysCount', { count: deadKeys.length }));
|
|
623
|
+
|
|
624
|
+
const highConfidence = deadKeys.filter(dk => dk.confidence >= 0.8).length;
|
|
625
|
+
const mediumConfidence = deadKeys.filter(dk => dk.confidence >= 0.4 && dk.confidence < 0.8).length;
|
|
626
|
+
const lowConfidence = deadKeys.filter(dk => dk.confidence < 0.4).length;
|
|
627
|
+
|
|
628
|
+
console.log(t('usage.deadKeysConfidenceBreakdown', { high: highConfidence, medium: mediumConfidence, low: lowConfidence }));
|
|
629
|
+
|
|
630
|
+
if (deadKeys.length > 0) {
|
|
631
|
+
console.log('\n' + t('usage.deadKeysSample'));
|
|
632
|
+
deadKeys.slice(0, 10).forEach(dk => {
|
|
633
|
+
console.log(` ${dk.key} [${(dk.confidence * 100).toFixed(0)}%] - ${dk.reason}`);
|
|
634
|
+
});
|
|
635
|
+
if (deadKeys.length > 10) {
|
|
636
|
+
console.log(t('usage.deadKeysMore', { count: deadKeys.length - 10 }));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (this.dryRunDelete) {
|
|
641
|
+
const reportPath = this.saveDeadKeysReport(deadKeys, args.outputDir || this.config.outputDir || './i18ntk-reports/usage');
|
|
642
|
+
console.log(t('usage.deadKeysReportSaved', { path: reportPath }));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
602
646
|
if (args.outputReport) {
|
|
603
647
|
const report = this.generateUsageReport();
|
|
604
648
|
await this.saveReport(report, args.outputDir);
|
|
@@ -625,7 +669,7 @@ class I18nUsageAnalyzer {
|
|
|
625
669
|
// Show help message
|
|
626
670
|
showHelp() {
|
|
627
671
|
console.log(`
|
|
628
|
-
📊 i18ntk usage - Translation key usage analysis (v1.
|
|
672
|
+
📊 i18ntk usage - Translation key usage analysis (v1.10.1)
|
|
629
673
|
|
|
630
674
|
Usage:
|
|
631
675
|
node i18ntk-usage.js [options]
|
|
@@ -642,14 +686,17 @@ Options:
|
|
|
642
686
|
--validate-placeholders Enable placeholder key validation
|
|
643
687
|
--framework-detect Enable framework-specific pattern detection
|
|
644
688
|
--performance-mode Enable performance metrics tracking
|
|
689
|
+
--cleanup Enable dead key detection for cleanup mode
|
|
690
|
+
--dry-run-delete Save dead keys report without deleting (requires --cleanup)
|
|
645
691
|
--help, -h Show this help message
|
|
646
692
|
|
|
647
693
|
Examples:
|
|
648
694
|
node i18ntk-usage.js --source-dir=./src --i18n-dir=./translations --output-report
|
|
649
695
|
npm run i18ntk:usage -- --strict --debug --validate-placeholders
|
|
650
696
|
node i18ntk-usage.js --no-prompt --performance-mode --output-dir=./reports
|
|
697
|
+
node i18ntk-usage.js --cleanup --dry-run-delete
|
|
651
698
|
|
|
652
|
-
Analysis Features (v1.
|
|
699
|
+
Analysis Features (v1.10.1):
|
|
653
700
|
• Detects unused translation keys
|
|
654
701
|
• Identifies missing translation keys
|
|
655
702
|
• Shows translation completeness by language
|
|
@@ -662,6 +709,7 @@ Analysis Features (v1.8.3):
|
|
|
662
709
|
• Key complexity analysis
|
|
663
710
|
• Security-enhanced path validation
|
|
664
711
|
• Detailed reporting with validation errors
|
|
712
|
+
• Dead key detection with confidence scoring
|
|
665
713
|
`);
|
|
666
714
|
}
|
|
667
715
|
|
|
@@ -1133,6 +1181,172 @@ Analysis Features (v1.8.3):
|
|
|
1133
1181
|
return missing;
|
|
1134
1182
|
}
|
|
1135
1183
|
|
|
1184
|
+
findDeadKeys() {
|
|
1185
|
+
const unusedKeys = this.findUnusedKeys();
|
|
1186
|
+
const deadKeys = [];
|
|
1187
|
+
|
|
1188
|
+
for (const key of unusedKeys) {
|
|
1189
|
+
let confidence = 0.9;
|
|
1190
|
+
let reason = 'Key not found in any source file';
|
|
1191
|
+
|
|
1192
|
+
if (this._matchesDynamicPattern(key)) {
|
|
1193
|
+
confidence = 0.3;
|
|
1194
|
+
reason = 'Key matches dynamic template pattern (likely used)';
|
|
1195
|
+
} else if (this._keyInSourceComments(key)) {
|
|
1196
|
+
confidence = 0.5;
|
|
1197
|
+
reason = 'Key referenced in comments/JSDoc';
|
|
1198
|
+
} else if (this._parentFileRecentlyModified(key)) {
|
|
1199
|
+
confidence = 0.4;
|
|
1200
|
+
reason = 'Translation file modified within last 30 days';
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
deadKeys.push({ key, confidence, reason });
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
deadKeys.sort((a, b) => b.confidence - a.confidence);
|
|
1207
|
+
return deadKeys;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
_matchesDynamicPattern(key) {
|
|
1211
|
+
const keyParts = key.split('.');
|
|
1212
|
+
if (keyParts.length < 2) return false;
|
|
1213
|
+
|
|
1214
|
+
const dynamicPatterns = [
|
|
1215
|
+
/t\(`[^`]*\$\{[^}]*\}[^`]*`\)/g,
|
|
1216
|
+
/i18n\.t\(`[^`]*\$\{[^}]*\}[^`]*`\)/g,
|
|
1217
|
+
/useTranslation\(\)\.t\(`[^`]*\$\{[^}]*\}[^`]*`\)/g
|
|
1218
|
+
];
|
|
1219
|
+
|
|
1220
|
+
try {
|
|
1221
|
+
const sourceFiles = Array.from(this.fileUsage.keys());
|
|
1222
|
+
for (const filePath of sourceFiles) {
|
|
1223
|
+
const fullPath = path.join(this.sourceDir, filePath);
|
|
1224
|
+
if (!SecurityUtils.safeExistsSync(fullPath, this.sourceDir)) continue;
|
|
1225
|
+
|
|
1226
|
+
const content = SecurityUtils.safeReadFileSync(fullPath, this.sourceDir, 'utf8');
|
|
1227
|
+
if (!content) continue;
|
|
1228
|
+
|
|
1229
|
+
for (const pattern of dynamicPatterns) {
|
|
1230
|
+
const matches = content.match(pattern);
|
|
1231
|
+
if (matches) {
|
|
1232
|
+
for (const match of matches) {
|
|
1233
|
+
const matchLower = match.toLowerCase();
|
|
1234
|
+
if (keyParts.some(part => matchLower.includes(part.toLowerCase()))) {
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
// Silently fail - dynamic pattern detection is best-effort
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
_buildSourceCommentsSet() {
|
|
1249
|
+
if (this._sourceCommentsSet !== null) return;
|
|
1250
|
+
this._sourceCommentsSet = new Set();
|
|
1251
|
+
|
|
1252
|
+
const commentPatterns = [
|
|
1253
|
+
/\/\/[^\n]*/g,
|
|
1254
|
+
/\/\*[\s\S]*?\*\//g
|
|
1255
|
+
];
|
|
1256
|
+
|
|
1257
|
+
try {
|
|
1258
|
+
const sourceFiles = Array.from(this.fileUsage.keys());
|
|
1259
|
+
for (const filePath of sourceFiles) {
|
|
1260
|
+
const fullPath = path.join(this.sourceDir, filePath);
|
|
1261
|
+
if (!SecurityUtils.safeExistsSync(fullPath, this.sourceDir)) continue;
|
|
1262
|
+
|
|
1263
|
+
const content = SecurityUtils.safeReadFileSync(fullPath, this.sourceDir, 'utf8');
|
|
1264
|
+
if (!content) continue;
|
|
1265
|
+
|
|
1266
|
+
for (const pattern of commentPatterns) {
|
|
1267
|
+
const comments = content.match(pattern);
|
|
1268
|
+
if (comments) {
|
|
1269
|
+
for (const comment of comments) {
|
|
1270
|
+
this._sourceCommentsSet.add(comment);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
this._sourceCommentsSet = new Set();
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
_keyInSourceComments(key) {
|
|
1281
|
+
try {
|
|
1282
|
+
if (!this._sourceCommentsSet || this._sourceCommentsSet.size === 0) return false;
|
|
1283
|
+
for (const comment of this._sourceCommentsSet) {
|
|
1284
|
+
if (comment.includes(key)) return true;
|
|
1285
|
+
}
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
// Silently fail - comment detection is best-effort
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
_parentFileRecentlyModified(key) {
|
|
1294
|
+
const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
|
|
1295
|
+
const now = Date.now();
|
|
1296
|
+
|
|
1297
|
+
try {
|
|
1298
|
+
for (const [filePath] of this.translationFiles) {
|
|
1299
|
+
if (!SecurityUtils.safeExistsSync(filePath, this.i18nDir)) continue;
|
|
1300
|
+
|
|
1301
|
+
const content = SecurityUtils.safeReadFileSync(filePath, this.i18nDir, 'utf8');
|
|
1302
|
+
if (!content) continue;
|
|
1303
|
+
|
|
1304
|
+
if (content.includes(key)) {
|
|
1305
|
+
const stats = SecurityUtils.safeStatSync(filePath, this.i18nDir);
|
|
1306
|
+
if (stats && stats.mtime) {
|
|
1307
|
+
const mtimeMs = new Date(stats.mtime).getTime();
|
|
1308
|
+
if ((now - mtimeMs) <= thirtyDaysMs) {
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
// Silently fail - file stat is best-effort
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
generateDeadKeysReport(deadKeys) {
|
|
1322
|
+
const allCleanupReady = deadKeys.length === 0 || deadKeys.every(dk => dk.confidence >= 0.8);
|
|
1323
|
+
|
|
1324
|
+
return {
|
|
1325
|
+
deadKeys,
|
|
1326
|
+
totalAvailableKeys: this.availableKeys.size,
|
|
1327
|
+
totalUsedKeys: this.usedKeys.size,
|
|
1328
|
+
deadKeyCount: deadKeys.length,
|
|
1329
|
+
cleanupReady: allCleanupReady,
|
|
1330
|
+
generatedAt: new Date().toISOString(),
|
|
1331
|
+
version: this.version
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
saveDeadKeysReport(deadKeys, outputDir) {
|
|
1336
|
+
const report = this.generateDeadKeysReport(deadKeys);
|
|
1337
|
+
const resolvedDir = path.resolve(outputDir || './i18ntk-reports/usage');
|
|
1338
|
+
|
|
1339
|
+
if (!SecurityUtils.safeExistsSync(resolvedDir, process.cwd())) {
|
|
1340
|
+
SecurityUtils.safeMkdirSync(resolvedDir, process.cwd(), { recursive: true });
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const filename = '.dead-keys.json';
|
|
1344
|
+
const filepath = path.join(resolvedDir, filename);
|
|
1345
|
+
|
|
1346
|
+
SecurityUtils.safeWriteFileSync(filepath, JSON.stringify(report, null, 2), resolvedDir, 'utf8');
|
|
1347
|
+
return filepath;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1136
1350
|
// Find files that use specific keys
|
|
1137
1351
|
findKeyUsage(searchKey) {
|
|
1138
1352
|
const usage = [];
|
|
@@ -1736,9 +1950,6 @@ Analysis Features (v1.8.3):
|
|
|
1736
1950
|
// Close readline interface to prevent hanging
|
|
1737
1951
|
this.closeReadline();
|
|
1738
1952
|
|
|
1739
|
-
// Return instead of force exit to allow proper cleanup
|
|
1740
|
-
return;
|
|
1741
|
-
|
|
1742
1953
|
return {
|
|
1743
1954
|
success: true,
|
|
1744
1955
|
stats: {
|
|
@@ -1813,40 +2024,4 @@ if (require.main === module) {
|
|
|
1813
2024
|
}
|
|
1814
2025
|
}
|
|
1815
2026
|
|
|
1816
|
-
module.exports = I18nUsageAnalyzer;
|
|
1817
|
-
|
|
1818
|
-
// Run if called directly
|
|
1819
|
-
if (require.main === module) {
|
|
1820
|
-
async function main() {
|
|
1821
|
-
try {
|
|
1822
|
-
const cliArgs = parseCommonArgs(process.argv.slice(2));
|
|
1823
|
-
|
|
1824
|
-
if (cliArgs.help) {
|
|
1825
|
-
displayHelp('usage');
|
|
1826
|
-
process.exit(0);
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
// Let run() handle full initialization to avoid duplicate setup output
|
|
1830
|
-
const analyzer = new I18nUsageAnalyzer();
|
|
1831
|
-
await analyzer.run();
|
|
1832
|
-
} catch (error) {
|
|
1833
|
-
console.error('Error:', error.message);
|
|
1834
|
-
process.exit(1);
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
// Check if we're being called from the menu system (stdin has data)
|
|
1839
|
-
const hasStdinData = !process.stdin.isTTY;
|
|
1840
|
-
|
|
1841
|
-
if (hasStdinData) {
|
|
1842
|
-
// When called from menu, consume stdin data and run with defaults
|
|
1843
|
-
process.stdin.resume();
|
|
1844
|
-
process.stdin.on('data', () => {});
|
|
1845
|
-
process.stdin.on('end', () => {
|
|
1846
|
-
main();
|
|
1847
|
-
});
|
|
1848
|
-
} else {
|
|
1849
|
-
// Normal direct execution
|
|
1850
|
-
main();
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
2027
|
+
module.exports = I18nUsageAnalyzer;
|
package/main/i18ntk-validate.js
CHANGED
|
@@ -68,6 +68,7 @@ class I18nValidator {
|
|
|
68
68
|
this.config = config;
|
|
69
69
|
this.errors = [];
|
|
70
70
|
this.warnings = [];
|
|
71
|
+
this.keyNamingViolations = [];
|
|
71
72
|
this.rl = null;
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -163,7 +164,10 @@ class I18nValidator {
|
|
|
163
164
|
const args = process.argv.slice(2);
|
|
164
165
|
args.forEach(arg => {
|
|
165
166
|
const sanitizedArg = SecurityUtils.sanitizeInput(arg);
|
|
166
|
-
if (sanitizedArg.startsWith('--')
|
|
167
|
+
if (sanitizedArg.startsWith('--enforce-key-style')) {
|
|
168
|
+
const val = arg.split('=')[1];
|
|
169
|
+
baseArgs.enforceKeyStyle = val === undefined ? true : val !== 'false';
|
|
170
|
+
} else if (sanitizedArg.startsWith('--') && !sanitizedArg.includes('=')) {
|
|
167
171
|
const key = sanitizedArg.substring(2);
|
|
168
172
|
if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(key)) {
|
|
169
173
|
baseArgs.uiLanguage = key;
|
|
@@ -239,7 +243,7 @@ class I18nValidator {
|
|
|
239
243
|
const files = items
|
|
240
244
|
.filter(item => {
|
|
241
245
|
return item.isFile() && item.name.endsWith('.json') &&
|
|
242
|
-
!this.config.excludeFiles.includes(item.name);
|
|
246
|
+
(!Array.isArray(this.config.excludeFiles) || !this.config.excludeFiles.includes(item.name));
|
|
243
247
|
}).map(item => item.name);
|
|
244
248
|
|
|
245
249
|
return files;
|
|
@@ -573,6 +577,25 @@ class I18nValidator {
|
|
|
573
577
|
// Validate structure
|
|
574
578
|
const structural = this.validateStructure(sourceContent, targetContent, language, fileName);
|
|
575
579
|
|
|
580
|
+
// Check key naming conventions
|
|
581
|
+
if (this.config.enforceKeyStyle) {
|
|
582
|
+
const keyNamingResult = this.validateKeyNaming(sourceContent);
|
|
583
|
+
keyNamingResult.violations.forEach(v => {
|
|
584
|
+
this.addWarning(
|
|
585
|
+
`Key naming violation in ${language}/${fileName}`,
|
|
586
|
+
{ key: v.key, suggestedFix: v.suggestedFix, reason: v.reason, style: keyNamingResult.style }
|
|
587
|
+
);
|
|
588
|
+
this.keyNamingViolations.push({
|
|
589
|
+
language,
|
|
590
|
+
fileName,
|
|
591
|
+
key: v.key,
|
|
592
|
+
suggestedFix: v.suggestedFix,
|
|
593
|
+
reason: v.reason,
|
|
594
|
+
style: keyNamingResult.style
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
576
599
|
// Validate translations
|
|
577
600
|
const translations = this.validateTranslation(targetContent, language, fileName);
|
|
578
601
|
this.checkPlaceholders(sourceContent, targetContent, language, fileName);
|
|
@@ -652,7 +675,69 @@ class I18nValidator {
|
|
|
652
675
|
return warnings;
|
|
653
676
|
}
|
|
654
677
|
|
|
655
|
-
|
|
678
|
+
validateKeyNaming(sourceObj, style) {
|
|
679
|
+
const keyStyle = style || this.config.keyStyle || 'dot.notation';
|
|
680
|
+
const allKeys = this.getAllKeys(sourceObj);
|
|
681
|
+
const validators = {
|
|
682
|
+
'dot.notation': /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/,
|
|
683
|
+
'snake_case': /^[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*$/,
|
|
684
|
+
'camelCase': /^[a-z][a-zA-Z0-9]*$/,
|
|
685
|
+
'kebab-case': /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/,
|
|
686
|
+
'flat': /^[a-zA-Z][a-zA-Z0-9]*$/
|
|
687
|
+
};
|
|
688
|
+
const regex = validators[keyStyle];
|
|
689
|
+
if (!regex) {
|
|
690
|
+
return { violations: [], totalKeys: allKeys.size, violationCount: 0, style: keyStyle };
|
|
691
|
+
}
|
|
692
|
+
const violations = [];
|
|
693
|
+
for (const key of allKeys) {
|
|
694
|
+
const sanitizedKey = SecurityUtils.sanitizeInput(key);
|
|
695
|
+
const testKey = keyStyle === 'flat' ? sanitizedKey.split('.').pop() : sanitizedKey;
|
|
696
|
+
if (!regex.test(testKey)) {
|
|
697
|
+
violations.push({
|
|
698
|
+
key: sanitizedKey,
|
|
699
|
+
suggestedFix: this.suggestKeyFix(sanitizedKey, keyStyle),
|
|
700
|
+
reason: `Key "${sanitizedKey}" does not match "${keyStyle}" naming convention`
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
violations,
|
|
706
|
+
totalKeys: allKeys.size,
|
|
707
|
+
violationCount: violations.length,
|
|
708
|
+
style: keyStyle
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
suggestKeyFix(key, style) {
|
|
713
|
+
const sanitizedKey = SecurityUtils.sanitizeInput(key);
|
|
714
|
+
const segments = [];
|
|
715
|
+
const rawTokens = sanitizedKey.split(/[._\-]/);
|
|
716
|
+
for (const token of rawTokens) {
|
|
717
|
+
if (!token) continue;
|
|
718
|
+
const camelTokens = token.split(/(?=[A-Z])/).filter(Boolean);
|
|
719
|
+
segments.push(...camelTokens);
|
|
720
|
+
}
|
|
721
|
+
if (segments.length === 0) {
|
|
722
|
+
return sanitizedKey;
|
|
723
|
+
}
|
|
724
|
+
switch (style) {
|
|
725
|
+
case 'dot.notation':
|
|
726
|
+
return segments.map(s => s.toLowerCase()).join('.');
|
|
727
|
+
case 'snake_case':
|
|
728
|
+
return segments.map(s => s.toLowerCase()).join('_');
|
|
729
|
+
case 'camelCase':
|
|
730
|
+
return segments.map((s, i) => i === 0 ? s.toLowerCase() : s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()).join('');
|
|
731
|
+
case 'kebab-case':
|
|
732
|
+
return segments.map(s => s.toLowerCase()).join('-');
|
|
733
|
+
case 'flat':
|
|
734
|
+
return segments.map(s => s.toLowerCase()).join('');
|
|
735
|
+
default:
|
|
736
|
+
return sanitizedKey;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Show help message
|
|
656
741
|
showHelp() {
|
|
657
742
|
console.log(t('validate.help_message'));
|
|
658
743
|
}
|
|
@@ -895,8 +980,29 @@ class I18nValidator {
|
|
|
895
980
|
console.log('');
|
|
896
981
|
});
|
|
897
982
|
}
|
|
898
|
-
|
|
899
|
-
//
|
|
983
|
+
|
|
984
|
+
// Key naming violations summary
|
|
985
|
+
if (this.keyNamingViolations.length > 0) {
|
|
986
|
+
console.log('');
|
|
987
|
+
console.log(t('validate.separator'));
|
|
988
|
+
console.log('🔑 Key Naming Convention Violations');
|
|
989
|
+
console.log('');
|
|
990
|
+
const displayStyle = this.config.keyStyle || 'dot.notation';
|
|
991
|
+
console.log(` Expected style: ${displayStyle}`);
|
|
992
|
+
console.log(` Violations found: ${this.keyNamingViolations.length}`);
|
|
993
|
+
console.log('');
|
|
994
|
+
console.log(' Suggested fixes:');
|
|
995
|
+
this.keyNamingViolations.slice(0, 10).forEach((v, i) => {
|
|
996
|
+
console.log(` ${i + 1}. "${v.key}" → "${v.suggestedFix}" (${v.language}/${v.fileName})`);
|
|
997
|
+
});
|
|
998
|
+
if (this.keyNamingViolations.length > 10) {
|
|
999
|
+
console.log(` ... and ${this.keyNamingViolations.length - 10} more`);
|
|
1000
|
+
}
|
|
1001
|
+
console.log('');
|
|
1002
|
+
console.log(' 💡 Tip: Use i18ntk:fix-keys to auto-fix or manually rename keys.');
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Recommendations
|
|
900
1006
|
console.log('');
|
|
901
1007
|
console.log(t('validate.separator'));
|
|
902
1008
|
console.log(t('validate.recommendationsSection'));
|
|
@@ -950,6 +1056,7 @@ class I18nValidator {
|
|
|
950
1056
|
|
|
951
1057
|
const args = this.parseArgs();
|
|
952
1058
|
|
|
1059
|
+
try {
|
|
953
1060
|
// Ensure config is always initialized
|
|
954
1061
|
if (!this.config) {
|
|
955
1062
|
this.config = {};
|
|
@@ -967,6 +1074,7 @@ class I18nValidator {
|
|
|
967
1074
|
} else {
|
|
968
1075
|
await this.initialize();
|
|
969
1076
|
}
|
|
1077
|
+
this.config.enforceKeyStyle = args.enforceKeyStyle !== undefined ? args.enforceKeyStyle : this.config.enforceKeyStyle;
|
|
970
1078
|
|
|
971
1079
|
// Skip admin authentication when called from menu
|
|
972
1080
|
if (!fromMenu) {
|
|
@@ -1042,6 +1150,7 @@ class I18nValidator {
|
|
|
1042
1150
|
);
|
|
1043
1151
|
throw error;
|
|
1044
1152
|
}
|
|
1153
|
+
}
|
|
1045
1154
|
}
|
|
1046
1155
|
|
|
1047
1156
|
|
|
@@ -530,20 +530,20 @@ class FixerCommand {
|
|
|
530
530
|
this.dryRun = args.dryRun || false;
|
|
531
531
|
this.force = args.force || false;
|
|
532
532
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
console.log(t('fixer.
|
|
537
|
-
|
|
533
|
+
const languages = this.getAvailableLanguages();
|
|
534
|
+
|
|
535
|
+
if (!args.json) {
|
|
536
|
+
console.log(t('fixer.starting', { languages: languages.join(', ') || 'none' }));
|
|
537
|
+
console.log(t('fixer.sourceDirectory', { sourceDir: path.resolve(this.sourceDir) }));
|
|
538
|
+
console.log(t('fixer.dryRunMode', { mode: this.dryRun ? 'ON' : 'OFF' }));
|
|
539
|
+
}
|
|
538
540
|
|
|
539
541
|
// Create backup unless disabled
|
|
540
542
|
if (!args.noBackup && !this.dryRun && this.config?.backup?.enabled === true) {
|
|
541
543
|
await this.createBackup();
|
|
542
544
|
}
|
|
543
545
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (languages.length === 0) {
|
|
546
|
+
if (languages.length === 0) {
|
|
547
547
|
const error = t('fixer.noLanguages') || 'No target languages found.';
|
|
548
548
|
if (args.json) {
|
|
549
549
|
jsonOutput.setStatus('error', error);
|
|
@@ -573,14 +573,16 @@ class FixerCommand {
|
|
|
573
573
|
totalIssues += fixes.totalIssues;
|
|
574
574
|
totalFixed += fixes.fixedIssues;
|
|
575
575
|
|
|
576
|
-
if (!args.json) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
576
|
+
if (!args.json) {
|
|
577
|
+
const skipped = Math.max(0, fixes.totalIssues - fixes.fixedIssues);
|
|
578
|
+
console.log(t('fixer.languageFixed', {
|
|
579
|
+
language,
|
|
580
|
+
issues: fixes.totalIssues,
|
|
581
|
+
fixed: fixes.fixedIssues,
|
|
582
|
+
skipped
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
584
586
|
|
|
585
587
|
// Prepare JSON output
|
|
586
588
|
if (args.json) {
|
|
@@ -595,11 +597,11 @@ class FixerCommand {
|
|
|
595
597
|
return { success: true, totalIssues, totalFixed, results };
|
|
596
598
|
}
|
|
597
599
|
|
|
598
|
-
// Summary
|
|
599
|
-
console.log(t('fixer.summary'));
|
|
600
|
-
console.log('='.repeat(50));
|
|
601
|
-
console.log(t('fixer.totalIssues', {
|
|
602
|
-
console.log(t('fixer.totalFixed', { count: totalFixed }));
|
|
600
|
+
// Summary
|
|
601
|
+
console.log(t('fixer.summary'));
|
|
602
|
+
console.log('='.repeat(50));
|
|
603
|
+
console.log(t('fixer.totalIssues', { totalIssues }));
|
|
604
|
+
console.log(t('fixer.totalFixed', { count: totalFixed }));
|
|
603
605
|
|
|
604
606
|
if (this.backupDir && !args.noBackup && this.config?.backup?.enabled === true) {
|
|
605
607
|
console.log(t('fixer.backupLocation', { dir: this.backupDir }));
|
package/main/manage/index.js
CHANGED
|
@@ -1220,7 +1220,10 @@ class I18nManager {
|
|
|
1220
1220
|
{ path: path.join(process.cwd(), 'scripts', 'debug', 'logs'), name: 'Debug Logs', type: 'logs' },
|
|
1221
1221
|
{ path: path.join(process.cwd(), 'scripts', 'debug', 'reports'), name: 'Debug Reports', type: 'reports' },
|
|
1222
1222
|
{ path: path.join(process.cwd(), 'settings', 'backups'), name: 'Settings Backups', type: 'backups' },
|
|
1223
|
-
{ path: path.join(process.cwd(), 'utils', 'i18ntk-reports'), name: 'Utils Reports', type: 'reports' }
|
|
1223
|
+
{ path: path.join(process.cwd(), 'utils', 'i18ntk-reports'), name: 'Utils Reports', type: 'reports' },
|
|
1224
|
+
{ path: path.join(process.cwd(), '.cache'), name: 'Cache', type: 'cache', includeAllFiles: true },
|
|
1225
|
+
{ path: path.join(process.cwd(), 'settings', '.cache'), name: 'Settings Cache', type: 'cache', includeAllFiles: true },
|
|
1226
|
+
{ path: path.join(process.cwd(), '.cache-ultra'), name: 'Performance Cache', type: 'cache', includeAllFiles: true }
|
|
1224
1227
|
].filter(dir => dir.path && typeof dir.path === 'string');
|
|
1225
1228
|
|
|
1226
1229
|
try {
|
|
@@ -1233,7 +1236,9 @@ class I18nManager {
|
|
|
1233
1236
|
for (const dir of targetDirs) {
|
|
1234
1237
|
const validatedDirPath = SecurityUtils.validatePath(dir.path, projectRoot);
|
|
1235
1238
|
if (validatedDirPath && SecurityUtils.safeExistsSync(validatedDirPath, projectRoot)) {
|
|
1236
|
-
const files = this.getAllReportFiles(validatedDirPath, validatedDirPath
|
|
1239
|
+
const files = this.getAllReportFiles(validatedDirPath, validatedDirPath, {
|
|
1240
|
+
includeAllFiles: dir.includeAllFiles === true
|
|
1241
|
+
});
|
|
1237
1242
|
if (files.length > 0) {
|
|
1238
1243
|
availableDirs.push({
|
|
1239
1244
|
...dir,
|
|
@@ -1368,12 +1373,13 @@ class I18nManager {
|
|
|
1368
1373
|
await this.showInteractiveMenu();
|
|
1369
1374
|
}
|
|
1370
1375
|
|
|
1371
|
-
getAllReportFiles(dir, rootDir = dir) {
|
|
1376
|
+
getAllReportFiles(dir, rootDir = dir, options = {}) {
|
|
1372
1377
|
if (!dir || typeof dir !== 'string') {
|
|
1373
1378
|
return [];
|
|
1374
1379
|
}
|
|
1375
1380
|
|
|
1376
|
-
let files = [];
|
|
1381
|
+
let files = [];
|
|
1382
|
+
const includeAllFiles = options.includeAllFiles === true;
|
|
1377
1383
|
|
|
1378
1384
|
try {
|
|
1379
1385
|
const validatedDir = SecurityUtils.validatePath(dir, rootDir);
|
|
@@ -1399,8 +1405,8 @@ class I18nManager {
|
|
|
1399
1405
|
const stat = fs.statSync(safeFullPath);
|
|
1400
1406
|
|
|
1401
1407
|
if (stat.isDirectory()) {
|
|
1402
|
-
files.push(...this.getAllReportFiles(safeFullPath, rootDir));
|
|
1403
|
-
} else if (
|
|
1408
|
+
files.push(...this.getAllReportFiles(safeFullPath, rootDir, options));
|
|
1409
|
+
} else if (includeAllFiles || (
|
|
1404
1410
|
// Common report file extensions
|
|
1405
1411
|
item.endsWith('.json') ||
|
|
1406
1412
|
item.endsWith('.html') ||
|
|
@@ -1415,7 +1421,7 @@ class I18nManager {
|
|
|
1415
1421
|
item.includes('report_') ||
|
|
1416
1422
|
item.includes('analysis-') ||
|
|
1417
1423
|
item.includes('validation-')
|
|
1418
|
-
) {
|
|
1424
|
+
)) {
|
|
1419
1425
|
files.push(safeFullPath);
|
|
1420
1426
|
}
|
|
1421
1427
|
} catch (error) {
|