i18ntk 4.5.3 → 4.6.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 +63 -3
- package/README.md +127 -530
- package/main/i18ntk-analyze.js +77 -42
- package/main/i18ntk-complete.js +56 -37
- package/main/i18ntk-report.js +22 -9
- package/main/i18ntk-scanner.js +151 -29
- package/main/i18ntk-sizing.js +9 -6
- package/main/i18ntk-summary.js +61 -39
- package/main/i18ntk-usage.js +29 -16
- package/main/i18ntk-validate.js +38 -24
- package/main/manage/commands/AnalyzeCommand.js +79 -44
- package/main/manage/commands/ScannerCommand.js +158 -35
- package/main/manage/commands/ValidateCommand.js +37 -23
- package/main/manage/index.js +13 -9
- package/main/manage/services/FrameworkDetectionService.js +67 -9
- package/package.json +27 -36
- package/utils/config-helper.js +42 -32
- package/utils/exit-codes.js +8 -6
- package/utils/framework-detector.js +317 -6
- package/utils/prompt-helper.js +1 -1
- package/utils/report-model.js +17 -4
- package/utils/usage-source.js +2 -1
package/main/i18ntk-scanner.js
CHANGED
|
@@ -22,6 +22,7 @@ const { getUnifiedConfig, displayHelp } = require('../utils/config-helper');
|
|
|
22
22
|
const { loadTranslations } = require('../utils/i18n-helper');
|
|
23
23
|
const SecurityUtils = require('../utils/security');
|
|
24
24
|
const SetupEnforcer = require('../utils/setup-enforcer');
|
|
25
|
+
const { detectProjectFramework, getFrameworkPatterns, getFrameworkSuggestions, SCANNER_EXTENSIONS, WRAPPER_SKIP_PATTERNS, getExcludeDirs } = require('../utils/framework-detector');
|
|
25
26
|
|
|
26
27
|
// Ensure setup is complete before running
|
|
27
28
|
(async () => {
|
|
@@ -133,6 +134,8 @@ class I18nTextScanner {
|
|
|
133
134
|
|
|
134
135
|
switch (key) {
|
|
135
136
|
case 'source-dir':
|
|
137
|
+
case 'code-dir':
|
|
138
|
+
case 'source-code-dir':
|
|
136
139
|
parsed.sourceDir = value || '';
|
|
137
140
|
if (value === args[i + 1]) i++;
|
|
138
141
|
break;
|
|
@@ -167,6 +170,7 @@ class I18nTextScanner {
|
|
|
167
170
|
parsed.includeTests = true;
|
|
168
171
|
break;
|
|
169
172
|
case 'source-language':
|
|
173
|
+
case 'source-locale':
|
|
170
174
|
parsed.sourceLanguage = value || '';
|
|
171
175
|
if (value === args[i + 1]) i++;
|
|
172
176
|
break;
|
|
@@ -222,11 +226,26 @@ class I18nTextScanner {
|
|
|
222
226
|
const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
|
|
223
227
|
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
224
228
|
|
|
229
|
+
if (deps.next || deps['next-intl'] || deps['next-i18next']) return 'next';
|
|
225
230
|
if (deps.react || deps['react-dom']) return 'react';
|
|
226
231
|
if (deps.vue || deps['vue-router']) return 'vue';
|
|
227
232
|
if (deps['@angular/core'] || deps.angular) return 'angular';
|
|
228
|
-
if (deps.next) return 'next';
|
|
229
233
|
if (deps.svelte) return 'svelte';
|
|
234
|
+
if (deps.astro || deps['astro-i18next'] || deps['@astrojs/i18n']) return 'astro';
|
|
235
|
+
if (deps['@remix-run/react'] || deps['remix-i18next']) return 'remix';
|
|
236
|
+
if (deps.gatsby || deps['gatsby-plugin-react-i18next']) return 'gatsby';
|
|
237
|
+
if (deps['@builder.io/qwik'] || deps['qwik-speak']) return 'qwik';
|
|
238
|
+
if (deps['solid-js'] || deps['@solid-primitives/i18n']) return 'solid';
|
|
239
|
+
if (deps['ember-source'] || deps['ember-intl']) return 'ember';
|
|
240
|
+
if (deps.nuxt || deps['@nuxtjs/i18n']) return 'nuxt';
|
|
241
|
+
|
|
242
|
+
// Check for Rust project
|
|
243
|
+
const cargoTomlPath = path.join(projectRoot, 'Cargo.toml');
|
|
244
|
+
if (SecurityUtils.safeExistsSync(cargoTomlPath, projectRoot)) return 'rust';
|
|
245
|
+
|
|
246
|
+
// Check for Go project
|
|
247
|
+
const goModPath = path.join(projectRoot, 'go.mod');
|
|
248
|
+
if (SecurityUtils.safeExistsSync(goModPath, projectRoot)) return 'go';
|
|
230
249
|
|
|
231
250
|
return 'vanilla';
|
|
232
251
|
} catch (error) {
|
|
@@ -252,57 +271,110 @@ class I18nTextScanner {
|
|
|
252
271
|
|
|
253
272
|
const frameworkSpecific = {
|
|
254
273
|
react: [
|
|
255
|
-
// React specific patterns - enhanced for i18next detection
|
|
256
274
|
/children:\s*["']([^"']{2,99})["']/g,
|
|
257
275
|
/dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
|
|
258
|
-
// JSX text content without translation
|
|
259
276
|
/>([^<{][^<>{]*[^}>])</g,
|
|
260
|
-
// Button text
|
|
261
277
|
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
262
|
-
|
|
263
|
-
/<
|
|
278
|
+
/<span[^>]*>([^<]{2,99})<\/span>/g,
|
|
279
|
+
/<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g,
|
|
280
|
+
/<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*\{[']\s*([^'}]+\.[^'}]+)\s*[']/g
|
|
264
281
|
],
|
|
265
282
|
vue: [
|
|
266
|
-
// Vue specific patterns - enhanced for vue-i18n detection
|
|
267
283
|
/v-text=["']([^"']{2,99})["']/g,
|
|
268
284
|
/v-html=["']([^"']{2,99})["']/g,
|
|
269
|
-
// Vue template text
|
|
270
285
|
/>([^<{][^<>{]*[^}>])</g,
|
|
271
|
-
// Button text
|
|
272
286
|
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
/<span[^>]*>([^<]{2,99})<\/span>/g,
|
|
288
|
+
/\$t\(["']([^"']{2,99})["']\)/g,
|
|
289
|
+
/v-t=["']([^"']{2,99})["']/g
|
|
275
290
|
],
|
|
276
291
|
angular: [
|
|
277
|
-
// Angular specific patterns - enhanced for ngx-translate detection
|
|
278
292
|
/\[innerHTML\]=["']([^"']{2,99})["']/g,
|
|
279
293
|
/\[textContent\]=["']([^"']{2,99})["']/g,
|
|
280
|
-
// Angular template text
|
|
281
294
|
/>([^<{][^<>{]*[^}>])</g,
|
|
282
|
-
// Button text
|
|
283
295
|
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
284
|
-
|
|
285
|
-
|
|
296
|
+
/<span[^>]*>([^<]{2,99})<\/span>/g,
|
|
297
|
+
/i18n=["']([^"']{2,99})["']/g,
|
|
298
|
+
/\[attr\.title\]=["']([^"']{2,99})["']/g
|
|
299
|
+
],
|
|
300
|
+
next: [
|
|
301
|
+
/children:\s*["']([^"']{2,99})["']/g,
|
|
302
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
303
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
304
|
+
/<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
|
|
305
|
+
],
|
|
306
|
+
svelte: [
|
|
307
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
308
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
309
|
+
/\$_\(["']([^"']{2,99})["']\)/g,
|
|
310
|
+
/t\.set\(["']([^"']{2,99})["']/g,
|
|
311
|
+
/\{#if[^}]*\}([^<]{2,99})\{\/if\}/g
|
|
312
|
+
],
|
|
313
|
+
astro: [
|
|
314
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
315
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
316
|
+
/t\(["']([^"']{2,99})["']\)/g
|
|
317
|
+
],
|
|
318
|
+
remix: [
|
|
319
|
+
/children:\s*["']([^"']{2,99})["']/g,
|
|
320
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
321
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
322
|
+
/<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
|
|
323
|
+
],
|
|
324
|
+
qwik: [
|
|
325
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
326
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
327
|
+
/t\(["']([^"']{2,99})["']\)/g,
|
|
328
|
+
/useTranslate\(\)/g
|
|
329
|
+
],
|
|
330
|
+
solid: [
|
|
331
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
332
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
333
|
+
/t\(["']([^"']{2,99})["']\)/g,
|
|
334
|
+
/useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g
|
|
335
|
+
],
|
|
336
|
+
ember: [
|
|
337
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
338
|
+
/\{\{t\s+["']([^"']{2,99})["']\s*\}\}/g,
|
|
339
|
+
/\{\{intl\.t\s+["']([^"']{2,99})["']\s*\}\}/g
|
|
340
|
+
],
|
|
341
|
+
gatsby: [
|
|
342
|
+
/children:\s*["']([^"']{2,99})["']/g,
|
|
343
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
344
|
+
/<(?:Trans)[\s\S]*?\bi18nKey\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
|
|
286
345
|
],
|
|
287
346
|
django: [
|
|
288
|
-
// Django template patterns
|
|
289
347
|
/\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
|
|
290
348
|
/\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
|
|
291
349
|
/{{\s*_["']([^"']{2,99})["']\s*}}/g,
|
|
292
350
|
/{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
|
|
293
351
|
],
|
|
294
352
|
flask: [
|
|
295
|
-
// Flask/Jinja2 template patterns
|
|
296
353
|
/\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
|
|
297
354
|
/\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
|
|
298
355
|
/\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
|
|
299
356
|
],
|
|
300
357
|
python: [
|
|
301
|
-
// Python source patterns
|
|
302
358
|
/gettext\(["']([^"']{2,99})["']\)/g,
|
|
303
359
|
/_\(["']([^"']{2,99})["']\)/g,
|
|
304
360
|
/gettext_lazy\(["']([^"']{2,99})["']\)/g,
|
|
305
361
|
/lazy_gettext\(["']([^"']{2,99})["']\)/g
|
|
362
|
+
],
|
|
363
|
+
rust: [
|
|
364
|
+
/bundle\.get_message\(\s*["']([^"']{2,99})["']\)/g,
|
|
365
|
+
/\.get_message\s*\(\s*["']([^"']{2,99})["']\)/g,
|
|
366
|
+
/ts!\s*\(\s*["']([^"']{2,99})["']/g,
|
|
367
|
+
/fluent!\s*\(\s*["']([^"']{2,99})["']/g
|
|
368
|
+
],
|
|
369
|
+
go: [
|
|
370
|
+
/i18n\.Translate\([^,]+,\s*["']([^"']{2,99})["']\)/g,
|
|
371
|
+
/i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
|
|
372
|
+
/t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
|
|
373
|
+
],
|
|
374
|
+
vanilla: [
|
|
375
|
+
/t\(["']([^"']{2,99})["']\)/g,
|
|
376
|
+
/i18n\.t\(["']([^"']{2,99})["']\)/g,
|
|
377
|
+
/translate\(["']([^"']{2,99})["']\)/g
|
|
306
378
|
]
|
|
307
379
|
};
|
|
308
380
|
|
|
@@ -515,7 +587,7 @@ class I18nTextScanner {
|
|
|
515
587
|
key: `ui.${key}`,
|
|
516
588
|
original: text,
|
|
517
589
|
translationKey: `t('ui.${key}')`,
|
|
518
|
-
frameworkSpecific: this.
|
|
590
|
+
frameworkSpecific: getFrameworkSuggestions(this.framework, text)
|
|
519
591
|
};
|
|
520
592
|
}
|
|
521
593
|
|
|
@@ -523,16 +595,51 @@ class I18nTextScanner {
|
|
|
523
595
|
const frameworks = {
|
|
524
596
|
react: {
|
|
525
597
|
hook: `const { t } = useTranslation();`,
|
|
526
|
-
usage: `{t('ui.${
|
|
527
|
-
component: `<Trans i18nKey="ui.${
|
|
598
|
+
usage: `{t('ui.${this._keySnippet(text)}')}`,
|
|
599
|
+
component: `<Trans i18nKey="ui.${this._keySnippet(text)}">${text}</Trans>`
|
|
600
|
+
},
|
|
601
|
+
next: {
|
|
602
|
+
hook: `const t = useTranslations();`,
|
|
603
|
+
usage: `{t('ui.${this._keySnippet(text)}')}`,
|
|
604
|
+
component: `<Trans i18nKey="ui.${this._keySnippet(text)}">${text}</Trans>`
|
|
528
605
|
},
|
|
529
606
|
vue: {
|
|
530
|
-
directive: `{{ $t('ui.${
|
|
531
|
-
method: `this.$t('ui.${
|
|
607
|
+
directive: `{{ $t('ui.${this._keySnippet(text)}') }}`,
|
|
608
|
+
method: `this.$t('ui.${this._keySnippet(text)}')`
|
|
532
609
|
},
|
|
533
610
|
angular: {
|
|
534
611
|
pipe: `{{ '${text}' | translate }}`,
|
|
535
|
-
service: `this.translateService.instant('ui.${
|
|
612
|
+
service: `this.translateService.instant('ui.${this._keySnippet(text)}')`
|
|
613
|
+
},
|
|
614
|
+
svelte: {
|
|
615
|
+
store: `$_(('ui.${this._keySnippet(text)}'))`,
|
|
616
|
+
method: `t.set('ui.${this._keySnippet(text)}', '${text}')`
|
|
617
|
+
},
|
|
618
|
+
astro: {
|
|
619
|
+
import: `import { t } from 'astro-i18next';`,
|
|
620
|
+
usage: `{t('ui.${this._keySnippet(text)}')}`
|
|
621
|
+
},
|
|
622
|
+
remix: {
|
|
623
|
+
hook: `const { t } = useTranslation();`,
|
|
624
|
+
usage: `{t('ui.${this._keySnippet(text)}')}`,
|
|
625
|
+
server: `export const handle = i18next.handle;`
|
|
626
|
+
},
|
|
627
|
+
qwik: {
|
|
628
|
+
hook: `const t = useTranslate();`,
|
|
629
|
+
usage: `{t('ui.${this._keySnippet(text)}')}`
|
|
630
|
+
},
|
|
631
|
+
solid: {
|
|
632
|
+
hook: `const [t] = useI18n();`,
|
|
633
|
+
usage: `{t('ui.${this._keySnippet(text)}')}`
|
|
634
|
+
},
|
|
635
|
+
ember: {
|
|
636
|
+
template: `{{t 'ui.${this._keySnippet(text)}'}}`,
|
|
637
|
+
helper: `this.intl.t('ui.${this._keySnippet(text)}')`
|
|
638
|
+
},
|
|
639
|
+
gatsby: {
|
|
640
|
+
hook: `const { t } = useTranslation();`,
|
|
641
|
+
usage: `{t('ui.${this._keySnippet(text)}')}`,
|
|
642
|
+
plugin: `'gatsby-plugin-react-i18next'`
|
|
536
643
|
},
|
|
537
644
|
django: {
|
|
538
645
|
template: `{% trans '${text}' %}`,
|
|
@@ -549,14 +656,29 @@ class I18nTextScanner {
|
|
|
549
656
|
underscore: `from gettext import gettext as _\n_('${text}')`,
|
|
550
657
|
lazy: `from gettext import gettext_lazy as _\n_('${text}')`
|
|
551
658
|
},
|
|
659
|
+
rust: {
|
|
660
|
+
fluent: `bundle.get_message("ui_${this._keySnippet(text)}")`,
|
|
661
|
+
gettext: `gettext("ui_${this._keySnippet(text)}")`
|
|
662
|
+
},
|
|
663
|
+
go: {
|
|
664
|
+
translate: `i18n.Translate("ui_${this._keySnippet(text)}")`,
|
|
665
|
+
message: `i18n.NewMessage("ui_${this._keySnippet(text)}")`
|
|
666
|
+
},
|
|
552
667
|
vanilla: {
|
|
553
|
-
generic: `t('ui.${
|
|
668
|
+
generic: `t('ui.${this._keySnippet(text)}')`
|
|
554
669
|
}
|
|
555
670
|
};
|
|
556
671
|
|
|
557
672
|
return frameworks[this.framework] || frameworks.vanilla;
|
|
558
673
|
}
|
|
559
674
|
|
|
675
|
+
_keySnippet(text) {
|
|
676
|
+
return text.toLowerCase()
|
|
677
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
678
|
+
.replace(/\s+/g, '_')
|
|
679
|
+
.substring(0, 40);
|
|
680
|
+
}
|
|
681
|
+
|
|
560
682
|
async scanDirectory(dir, options = {}) {
|
|
561
683
|
const {
|
|
562
684
|
patterns = [],
|
|
@@ -571,7 +693,7 @@ class I18nTextScanner {
|
|
|
571
693
|
}
|
|
572
694
|
|
|
573
695
|
const allResults = [];
|
|
574
|
-
const extensions = [
|
|
696
|
+
const extensions = [...SCANNER_EXTENSIONS];
|
|
575
697
|
|
|
576
698
|
const scanRecursive = (currentDir) => {
|
|
577
699
|
const items = SecurityUtils.safeReaddirSync(currentDir, path.dirname(currentDir), { withFileTypes: true });
|
|
@@ -733,7 +855,7 @@ class I18nTextScanner {
|
|
|
733
855
|
} else if (fwPref && fwPref !== 'auto') {
|
|
734
856
|
this.framework = fwPref;
|
|
735
857
|
} else if (fwDetectEnabled) {
|
|
736
|
-
const detected =
|
|
858
|
+
const detected = detectProjectFramework(process.cwd());
|
|
737
859
|
this.framework = detected || fwFallback;
|
|
738
860
|
} else {
|
|
739
861
|
this.framework = fwFallback;
|
|
@@ -759,7 +881,7 @@ class I18nTextScanner {
|
|
|
759
881
|
console.log(this.t('scanner.starting', { framework: this.framework }));
|
|
760
882
|
console.log(this.t('scanner.sourceDirectory', { sourceDir: this.sourceDir }));
|
|
761
883
|
|
|
762
|
-
const patterns =
|
|
884
|
+
const patterns = getFrameworkPatterns(this.framework);
|
|
763
885
|
const exclusions = this.config.exclude || ['node_modules', '.git', 'dist', 'build'];
|
|
764
886
|
const minLength = this.config.minLength || 3;
|
|
765
887
|
const maxLength = this.config.maxLength || 100;
|
package/main/i18ntk-sizing.js
CHANGED
|
@@ -1141,7 +1141,7 @@ Generated: ${new Date().toISOString()}
|
|
|
1141
1141
|
const key = keyValueMatch[1];
|
|
1142
1142
|
const value = keyValueMatch[2];
|
|
1143
1143
|
|
|
1144
|
-
if (key === 'source-dir' || key === 's') {
|
|
1144
|
+
if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir' || key === 's') {
|
|
1145
1145
|
options['source-dir'] = value;
|
|
1146
1146
|
options.s = value;
|
|
1147
1147
|
options.sourceDirExplicit = true;
|
|
@@ -1162,7 +1162,7 @@ Generated: ${new Date().toISOString()}
|
|
|
1162
1162
|
options.threshold = numValue;
|
|
1163
1163
|
options.t = numValue;
|
|
1164
1164
|
}
|
|
1165
|
-
} else if (key === 'source-language') {
|
|
1165
|
+
} else if (key === 'source-language' || key === 'source-locale') {
|
|
1166
1166
|
options['source-language'] = value;
|
|
1167
1167
|
} else if (key === 'detailed' || key === 'd') {
|
|
1168
1168
|
options.detailed = value.toLowerCase() !== 'false';
|
|
@@ -1184,7 +1184,7 @@ Generated: ${new Date().toISOString()}
|
|
|
1184
1184
|
const key = match[1];
|
|
1185
1185
|
const nextArg = args[i + 1];
|
|
1186
1186
|
|
|
1187
|
-
if (key === 'source-dir' || key === 's') {
|
|
1187
|
+
if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir' || key === 's') {
|
|
1188
1188
|
options['source-dir'] = nextArg || options['source-dir'];
|
|
1189
1189
|
options.s = options['source-dir'];
|
|
1190
1190
|
options.sourceDirExplicit = true;
|
|
@@ -1216,7 +1216,7 @@ Generated: ${new Date().toISOString()}
|
|
|
1216
1216
|
options.t = value;
|
|
1217
1217
|
}
|
|
1218
1218
|
if (nextArg && !nextArg.startsWith('-') && !isNaN(parseInt(nextArg))) i++;
|
|
1219
|
-
} else if (key === 'source-language') {
|
|
1219
|
+
} else if (key === 'source-language' || key === 'source-locale') {
|
|
1220
1220
|
options['source-language'] = nextArg || options['source-language'];
|
|
1221
1221
|
if (nextArg && !nextArg.startsWith('-')) i++;
|
|
1222
1222
|
} else if (key === 'detailed' || key === 'd') {
|
|
@@ -1257,12 +1257,15 @@ I18NTK Sizing Analysis Tool
|
|
|
1257
1257
|
Usage: i18ntk-sizing [options]
|
|
1258
1258
|
|
|
1259
1259
|
Options:
|
|
1260
|
-
-s, --
|
|
1260
|
+
-s, --locales-dir <dir> Locale directory containing translation files (default: ./locales)
|
|
1261
|
+
--i18n-dir <dir> Alias for --locales-dir
|
|
1262
|
+
--source-dir <dir> Legacy alias for --locales-dir
|
|
1261
1263
|
-l, --languages <langs> Comma-separated list of languages to analyze
|
|
1262
1264
|
-o, --output-report Generate detailed sizing report (default: true)
|
|
1263
1265
|
-f, --format <format> Output format: json, csv, table (default: table)
|
|
1264
1266
|
-t, --threshold <number> Size difference threshold for warnings (%) (default: 50)
|
|
1265
|
-
--source-
|
|
1267
|
+
--source-locale <code> Source locale baseline for comparisons (default: en)
|
|
1268
|
+
--source-language <code> Legacy alias for --source-locale
|
|
1266
1269
|
-d, --detailed Generate detailed report with more information
|
|
1267
1270
|
--detailed-keys Show detailed key-level analysis
|
|
1268
1271
|
--predict-expansion Predict UI layout expansion risk per language
|
package/main/i18ntk-summary.js
CHANGED
|
@@ -56,11 +56,14 @@ class I18nSummaryReporter {
|
|
|
56
56
|
const uiLanguage = this.config.uiLanguage || 'en';
|
|
57
57
|
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
58
58
|
|
|
59
|
-
this.sourceDir = this.config.sourceDir;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
|
|
60
|
+
this.config.sourceDir = this.sourceDir;
|
|
61
|
+
|
|
62
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir, process.cwd())) {
|
|
63
|
+
const err = new Error(`Locale directory not found: ${this.sourceDir}`);
|
|
64
|
+
err.exitCode = 2;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
64
67
|
} catch (error) {
|
|
65
68
|
console.error(`Error initializing summary reporter: ${error.message}`);
|
|
66
69
|
throw error;
|
|
@@ -523,7 +526,10 @@ class I18nSummaryReporter {
|
|
|
523
526
|
report.push(t('summary.languagesCount', {count: this.stats.languages.length}));
|
|
524
527
|
report.push(t('summary.totalFiles', {count: this.stats.totalFiles}));
|
|
525
528
|
report.push(t('summary.totalKeys', {count: this.stats.totalKeys}));
|
|
526
|
-
|
|
529
|
+
const averageKeysPerLanguage = this.stats.languages.length > 0
|
|
530
|
+
? Math.round(this.stats.totalKeys / this.stats.languages.length)
|
|
531
|
+
: 0;
|
|
532
|
+
report.push(t('summary.avgKeysPerLanguage', {count: averageKeysPerLanguage}));
|
|
527
533
|
|
|
528
534
|
// Calculate total size
|
|
529
535
|
const totalSize = Object.values(this.stats.folderSizes).reduce((sum, size) => sum + size, 0);
|
|
@@ -666,8 +672,13 @@ class I18nSummaryReporter {
|
|
|
666
672
|
}
|
|
667
673
|
report.push('');
|
|
668
674
|
}
|
|
669
|
-
} else {
|
|
670
|
-
report.push(
|
|
675
|
+
} else if (this.stats.languages.length === 0) {
|
|
676
|
+
report.push('⚠️ NO LOCALE LANGUAGES FOUND');
|
|
677
|
+
report.push('='.repeat(30));
|
|
678
|
+
report.push('No locale language files or directories were found in the selected locale root.');
|
|
679
|
+
report.push('');
|
|
680
|
+
} else {
|
|
681
|
+
report.push(t('summary.noIssuesFound'));
|
|
671
682
|
report.push('='.repeat(30));
|
|
672
683
|
report.push(t('summary.allFilesConsistent'));
|
|
673
684
|
report.push('');
|
|
@@ -795,13 +806,20 @@ class I18nSummaryReporter {
|
|
|
795
806
|
const args = process.argv.slice(2);
|
|
796
807
|
const parsed = {};
|
|
797
808
|
|
|
798
|
-
args.
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
809
|
+
for (let i = 0; i < args.length; i++) {
|
|
810
|
+
const arg = args[i];
|
|
811
|
+
if (arg.startsWith('--')) {
|
|
812
|
+
const [key, rawValue] = arg.substring(2).split('=');
|
|
813
|
+
const value = rawValue !== undefined
|
|
814
|
+
? rawValue
|
|
815
|
+
: (args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true);
|
|
816
|
+
if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir') {
|
|
817
|
+
parsed.sourceDir = value;
|
|
818
|
+
parsed.i18nDir = value;
|
|
819
|
+
} else if (key === 'source-language' || key === 'source-locale') {
|
|
820
|
+
parsed.sourceLanguage = value;
|
|
821
|
+
} else if (key === 'code-dir' || key === 'source-code-dir') {
|
|
822
|
+
parsed.codeDir = value;
|
|
805
823
|
} else if (key === 'output-file') {
|
|
806
824
|
parsed.outputFile = value;
|
|
807
825
|
} else if (key === 'verbose') {
|
|
@@ -813,10 +831,10 @@ class I18nSummaryReporter {
|
|
|
813
831
|
} else if (key === 'no-prompt') {
|
|
814
832
|
parsed.noPrompt = true;
|
|
815
833
|
} else if (key === 'help') {
|
|
816
|
-
parsed.help = true;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
834
|
+
parsed.help = true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
820
838
|
|
|
821
839
|
return parsed;
|
|
822
840
|
}
|
|
@@ -827,14 +845,16 @@ class I18nSummaryReporter {
|
|
|
827
845
|
const { fromMenu = false, noPrompt = false } = options;
|
|
828
846
|
|
|
829
847
|
// Always use unified configuration to respect settings
|
|
830
|
-
const baseConfig = await getUnifiedConfig('summary', args);
|
|
831
|
-
this.config = { ...this.config, ...baseConfig };
|
|
848
|
+
const baseConfig = await getUnifiedConfig('summary', args);
|
|
849
|
+
this.config = { ...this.config, ...baseConfig };
|
|
832
850
|
|
|
833
851
|
const uiLanguage = this.config.uiLanguage || 'en';
|
|
834
852
|
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
835
|
-
if (!this.config.sourceDir) {
|
|
836
|
-
this.config.sourceDir = this.detectI18nDirectory();
|
|
837
|
-
}
|
|
853
|
+
if (!this.config.sourceDir) {
|
|
854
|
+
this.config.sourceDir = this.detectI18nDirectory();
|
|
855
|
+
}
|
|
856
|
+
this.config.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
|
|
857
|
+
this.sourceDir = this.config.sourceDir;
|
|
838
858
|
|
|
839
859
|
if (!fromMenu) {
|
|
840
860
|
// Check admin authentication for sensitive operations (only when called directly)
|
|
@@ -866,10 +886,10 @@ class I18nSummaryReporter {
|
|
|
866
886
|
}
|
|
867
887
|
|
|
868
888
|
// Validate source directory exists
|
|
869
|
-
if (!SecurityUtils.safeExistsSync(this.config.sourceDir,
|
|
870
|
-
console.error(t('summary.sourceDirectoryDoesNotExist', { sourceDir: this.config.sourceDir }));
|
|
871
|
-
process.exit(
|
|
872
|
-
}
|
|
889
|
+
if (!SecurityUtils.safeExistsSync(this.config.sourceDir, process.cwd())) {
|
|
890
|
+
console.error(t('summary.sourceDirectoryDoesNotExist', { sourceDir: this.config.sourceDir }));
|
|
891
|
+
process.exit(2);
|
|
892
|
+
}
|
|
873
893
|
}
|
|
874
894
|
|
|
875
895
|
console.log(t('summary.i18nSummaryReportGenerator'));
|
|
@@ -992,17 +1012,19 @@ class I18nSummaryReporter {
|
|
|
992
1012
|
|
|
993
1013
|
if (totalIssues > 0) {
|
|
994
1014
|
console.log(t('summary.foundIssues', {count: totalIssues}));
|
|
995
|
-
} else {
|
|
996
|
-
console.log(
|
|
997
|
-
}
|
|
1015
|
+
} else if (this.stats.languages.length === 0) {
|
|
1016
|
+
console.log('⚠️ No locale languages found in the selected locale root.');
|
|
1017
|
+
} else {
|
|
1018
|
+
console.log(t('summary.noIssuesConsole'));
|
|
1019
|
+
}
|
|
998
1020
|
|
|
999
1021
|
} catch (error) {
|
|
1000
1022
|
console.error(t('summary.errorDuringAnalysis', { error: error.message }));
|
|
1001
1023
|
if (args.verbose) {
|
|
1002
1024
|
console.error(error.stack);
|
|
1003
1025
|
}
|
|
1004
|
-
process.exit(1);
|
|
1005
|
-
}
|
|
1026
|
+
process.exit(error.exitCode || 1);
|
|
1027
|
+
}
|
|
1006
1028
|
process.exit(0);
|
|
1007
1029
|
}
|
|
1008
1030
|
}
|
|
@@ -1014,10 +1036,10 @@ if (require.main === module) {
|
|
|
1014
1036
|
await reporter.initialize();
|
|
1015
1037
|
await reporter.run();
|
|
1016
1038
|
}
|
|
1017
|
-
main().catch(error => {
|
|
1018
|
-
console.error('Error in i18ntk-summary:', error.message);
|
|
1019
|
-
process.exit(1);
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1039
|
+
main().catch(error => {
|
|
1040
|
+
console.error('Error in i18ntk-summary:', error.message);
|
|
1041
|
+
process.exit(error.exitCode || 1);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1022
1044
|
|
|
1023
|
-
module.exports = I18nSummaryReporter;
|
|
1045
|
+
module.exports = I18nSummaryReporter;
|
package/main/i18ntk-usage.js
CHANGED
|
@@ -46,6 +46,7 @@ const JsonOutput = require('../utils/json-output');
|
|
|
46
46
|
const SetupEnforcer = require('../utils/setup-enforcer');
|
|
47
47
|
const { resolveUsageSourceDir } = require('../utils/usage-source');
|
|
48
48
|
const { analyzeSourceForUsageInsights } = require('../utils/usage-insights');
|
|
49
|
+
const { isInteractive } = require('../utils/prompt-helper');
|
|
49
50
|
|
|
50
51
|
// Ensure setup is complete before running
|
|
51
52
|
(async () => {
|
|
@@ -207,9 +208,10 @@ class I18nUsageAnalyzer {
|
|
|
207
208
|
|
|
208
209
|
// Normalize CLI arguments to handle both camelCase and hyphenated flags
|
|
209
210
|
normalizeArgs(a) {
|
|
210
|
-
return {
|
|
211
|
-
sourceDir: a.sourceDir ?? a['source-dir'],
|
|
212
|
-
i18nDir: a.i18nDir ?? a['i18n-dir'],
|
|
211
|
+
return {
|
|
212
|
+
sourceDir: a.sourceDir ?? a['source-dir'] ?? a['code-dir'] ?? a['source-code-dir'],
|
|
213
|
+
i18nDir: a.i18nDir ?? a['i18n-dir'] ?? a['locales-dir'],
|
|
214
|
+
sourceLanguage: a.sourceLanguage ?? a['source-language'] ?? a['source-locale'],
|
|
213
215
|
outputReport: a.outputReport ?? a['output-report'],
|
|
214
216
|
outputDir: a.outputDir ?? a['output-dir'],
|
|
215
217
|
uiLanguage: a.uiLanguage ?? a['ui-language'],
|
|
@@ -231,11 +233,18 @@ class I18nUsageAnalyzer {
|
|
|
231
233
|
const args = process.argv.slice(2);
|
|
232
234
|
const parsed = {};
|
|
233
235
|
|
|
234
|
-
for (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
for (let i = 0; i < args.length; i++) {
|
|
237
|
+
const arg = args[i];
|
|
238
|
+
if (arg.startsWith('--')) {
|
|
239
|
+
let [key, value] = arg.substring(2).split('=');
|
|
240
|
+
if (value === undefined && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
241
|
+
value = args[++i];
|
|
242
|
+
}
|
|
243
|
+
if (key === 'code-dir' || key === 'source-code-dir') key = 'source-dir';
|
|
244
|
+
if (key === 'locales-dir') key = 'i18n-dir';
|
|
245
|
+
if (key === 'source-locale') key = 'source-language';
|
|
246
|
+
if (value !== undefined) {
|
|
247
|
+
parsed[key] = value;
|
|
239
248
|
} else {
|
|
240
249
|
parsed[key] = true;
|
|
241
250
|
}
|
|
@@ -513,11 +522,15 @@ class I18nUsageAnalyzer {
|
|
|
513
522
|
this.sourceDir = path.resolve(args.sourceDir);
|
|
514
523
|
}
|
|
515
524
|
|
|
516
|
-
if (args.i18nDir) {
|
|
517
|
-
this.config.i18nDir = args.i18nDir;
|
|
518
|
-
this.i18nDir = path.resolve(args.i18nDir);
|
|
519
|
-
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
520
|
-
}
|
|
525
|
+
if (args.i18nDir) {
|
|
526
|
+
this.config.i18nDir = args.i18nDir;
|
|
527
|
+
this.i18nDir = path.resolve(args.i18nDir);
|
|
528
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
529
|
+
}
|
|
530
|
+
if (args.sourceLanguage) {
|
|
531
|
+
this.config.sourceLanguage = args.sourceLanguage;
|
|
532
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
533
|
+
}
|
|
521
534
|
|
|
522
535
|
if (this.sourceDir || this.i18nDir) {
|
|
523
536
|
await configManager.updateConfig({
|
|
@@ -722,9 +735,9 @@ class I18nUsageAnalyzer {
|
|
|
722
735
|
|
|
723
736
|
console.log('\n' + t('usage.analysisCompletedSuccessfully'));
|
|
724
737
|
|
|
725
|
-
if (require.main === module &&
|
|
726
|
-
await this.prompt('\nPress Enter to continue...');
|
|
727
|
-
}
|
|
738
|
+
if (require.main === module && isInteractive(args)) {
|
|
739
|
+
await this.prompt('\nPress Enter to continue...');
|
|
740
|
+
}
|
|
728
741
|
this.closeReadline();
|
|
729
742
|
|
|
730
743
|
} catch (error) {
|