i18next-cli 1.60.0 → 1.61.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/README.md +2 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/status.js +49 -12
- package/dist/esm/cli.js +1 -1
- package/dist/esm/status.js +49 -12
- package/package.json +1 -1
- package/types/status.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -204,6 +204,8 @@ npx i18next-cli extract --watch --with-types
|
|
|
204
204
|
|
|
205
205
|
Displays a health check of your project's translation status. Can run without a config file. Exits with a non-zero status code when translations are missing.
|
|
206
206
|
|
|
207
|
+
The primary language is checked too: any key used in your code but absent from the primary language's translation files (a typo, or `extract` was never run) is reported and causes a non-zero exit code. Empty-string placeholders written by `extract` are considered present and do not fail the check. Running `npx i18next-cli status <primaryLanguage>` shows the absent keys in detail.
|
|
208
|
+
|
|
207
209
|
**Options:**
|
|
208
210
|
- `--namespace <ns>, -n <ns>`: Filter the report by a specific namespace.
|
|
209
211
|
- `--hide-translated`: Hide already translated keys in the detailed view, showing only missing translations.
|
package/dist/cjs/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ const program = new commander.Command();
|
|
|
32
32
|
program
|
|
33
33
|
.name('i18next-cli')
|
|
34
34
|
.description('A unified, high-performance i18next CLI.')
|
|
35
|
-
.version('1.
|
|
35
|
+
.version('1.61.0'); // This string is replaced with the actual version at build time by rollup
|
|
36
36
|
// new: global config override option
|
|
37
37
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
38
38
|
program
|
package/dist/cjs/status.js
CHANGED
|
@@ -56,6 +56,11 @@ async function runStatus(config, options = {}) {
|
|
|
56
56
|
break;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
// The primary language fails the check only on absent keys (used in code but
|
|
60
|
+
// missing from the translation file); empty placeholders are tolerated.
|
|
61
|
+
if (!hasMissing && report.primary && report.primary.totalAbsent > 0) {
|
|
62
|
+
hasMissing = true;
|
|
63
|
+
}
|
|
59
64
|
if (hasMissing) {
|
|
60
65
|
spinner.fail('Error: Incomplete translations detected.');
|
|
61
66
|
process.exit(1);
|
|
@@ -203,7 +208,13 @@ async function generateStatusReport(config) {
|
|
|
203
208
|
if (nestedRefKeys.length > 0)
|
|
204
209
|
nestedReferenceKeysByNs.set(ns, nestedRefKeys);
|
|
205
210
|
}
|
|
206
|
-
|
|
211
|
+
// The primary language is checked first so that keys used in code but absent
|
|
212
|
+
// from the primary translation file (e.g. a typo, or `extract` never run) are
|
|
213
|
+
// surfaced as well. For the primary, an empty value is a deliberate
|
|
214
|
+
// placeholder and counts as present — only truly absent keys are flagged.
|
|
215
|
+
const localesToCheck = [primaryLanguage, ...secondaryLanguages.filter((l) => l !== primaryLanguage)];
|
|
216
|
+
for (const locale of localesToCheck) {
|
|
217
|
+
const isPrimary = locale === primaryLanguage;
|
|
207
218
|
let totalTranslatedForLocale = 0;
|
|
208
219
|
let totalEmptyForLocale = 0;
|
|
209
220
|
let totalAbsentForLocale = 0;
|
|
@@ -264,11 +275,17 @@ async function generateStatusReport(config) {
|
|
|
264
275
|
const primaryState = classifyValue(primaryValue);
|
|
265
276
|
// Only fall back when the key is genuinely absent from the primary file.
|
|
266
277
|
// An empty string is intentional (placeholder from extract) — don't hide it.
|
|
278
|
+
let state = primaryState;
|
|
267
279
|
if (primaryState === 'absent' && fallbackTranslations) {
|
|
268
280
|
const fallbackValue = nestedObject.getNestedValue(fallbackTranslations, key, sep);
|
|
269
|
-
|
|
281
|
+
state = classifyValue(fallbackValue);
|
|
270
282
|
}
|
|
271
|
-
|
|
283
|
+
// For the primary language the file itself is the source of values, so an
|
|
284
|
+
// empty placeholder still means the key is present. Only a truly absent
|
|
285
|
+
// key (used in code, missing from the file) is a problem here.
|
|
286
|
+
if (isPrimary && state === 'empty')
|
|
287
|
+
return 'translated';
|
|
288
|
+
return state;
|
|
272
289
|
};
|
|
273
290
|
const processedKeys = new Set();
|
|
274
291
|
// Combine AST-extracted keys with nested-reference keys discovered in
|
|
@@ -365,13 +382,19 @@ async function generateStatusReport(config) {
|
|
|
365
382
|
totalAbsentForLocale += absentInNs;
|
|
366
383
|
totalKeysForLocale += totalInNs;
|
|
367
384
|
}
|
|
368
|
-
|
|
385
|
+
const localeStatus = {
|
|
369
386
|
totalKeys: totalKeysForLocale,
|
|
370
387
|
totalTranslated: totalTranslatedForLocale,
|
|
371
388
|
totalEmpty: totalEmptyForLocale,
|
|
372
389
|
totalAbsent: totalAbsentForLocale,
|
|
373
390
|
namespaces,
|
|
374
|
-
}
|
|
391
|
+
};
|
|
392
|
+
if (isPrimary) {
|
|
393
|
+
report.primary = localeStatus;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
report.locales.set(locale, localeStatus);
|
|
397
|
+
}
|
|
375
398
|
}
|
|
376
399
|
return report;
|
|
377
400
|
}
|
|
@@ -419,15 +442,12 @@ async function displayStatusReport(report, config, options) {
|
|
|
419
442
|
* ✗ red — absent from file entirely (structural problem)
|
|
420
443
|
*/
|
|
421
444
|
async function displayDetailedLocaleReport(report, config, locale, namespaceFilter, hideTranslated) {
|
|
422
|
-
if (locale === config.extract.primaryLanguage) {
|
|
423
|
-
console.log(node_util.styleText('yellow', `Locale "${locale}" is the primary language. All keys are considered present.`));
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
445
|
if (!config.locales.includes(locale)) {
|
|
427
446
|
console.error(node_util.styleText('red', `Error: Locale "${locale}" is not defined in your configuration.`));
|
|
428
447
|
return;
|
|
429
448
|
}
|
|
430
|
-
const
|
|
449
|
+
const isPrimary = locale === config.extract.primaryLanguage;
|
|
450
|
+
const localeData = isPrimary ? report.primary : report.locales.get(locale);
|
|
431
451
|
if (!localeData) {
|
|
432
452
|
console.error(node_util.styleText('red', `Error: Locale "${locale}" is not a valid secondary language.`));
|
|
433
453
|
return;
|
|
@@ -465,8 +485,16 @@ async function displayDetailedLocaleReport(report, config, locale, namespaceFilt
|
|
|
465
485
|
}
|
|
466
486
|
const missingCount = totalKeysForLocale - localeData.totalTranslated;
|
|
467
487
|
if (missingCount > 0) {
|
|
468
|
-
|
|
469
|
-
|
|
488
|
+
if (isPrimary) {
|
|
489
|
+
console.log(node_util.styleText(['red', 'bold'], `\nSummary: Found ${missingCount} key(s) used in code but absent from the "${locale}" translation files. Run "i18next-cli extract" to add them.`));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const summaryBreakdown = buildBreakdown(localeData.totalEmpty, localeData.totalAbsent);
|
|
493
|
+
console.log(node_util.styleText(['yellow', 'bold'], `\nSummary: Found ${missingCount} incomplete translations for "${locale}" — ${summaryBreakdown}.`));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (isPrimary) {
|
|
497
|
+
console.log(node_util.styleText(['green', 'bold'], `\nSummary: 🎉 All keys used in code are present in the "${locale}" translation files.`));
|
|
470
498
|
}
|
|
471
499
|
else {
|
|
472
500
|
console.log(node_util.styleText(['green', 'bold'], `\nSummary: 🎉 All keys are translated for "${locale}".`));
|
|
@@ -501,6 +529,11 @@ async function displayNamespaceSummaryReport(report, config, namespace) {
|
|
|
501
529
|
console.log(`- ${locale}: ${bar} ${percentage}% (${nsLocaleData.translatedKeys}/${nsLocaleData.totalKeys} keys)${suffix}`);
|
|
502
530
|
}
|
|
503
531
|
}
|
|
532
|
+
const primaryNsData = report.primary?.namespaces.get(namespace);
|
|
533
|
+
if (primaryNsData && primaryNsData.absentKeys > 0) {
|
|
534
|
+
const { primaryLanguage } = config.extract;
|
|
535
|
+
console.log(node_util.styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${primaryNsData.absentKeys} key(s) that are used in code.`));
|
|
536
|
+
}
|
|
504
537
|
await printLocizeFunnel();
|
|
505
538
|
}
|
|
506
539
|
/**
|
|
@@ -531,6 +564,10 @@ async function displayOverallSummaryReport(report, config) {
|
|
|
531
564
|
const suffix = breakdown ? ` — ${breakdown}` : '';
|
|
532
565
|
console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${localeData.totalKeys} keys)${suffix}`);
|
|
533
566
|
}
|
|
567
|
+
if (report.primary && report.primary.totalAbsent > 0) {
|
|
568
|
+
console.log(node_util.styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${report.primary.totalAbsent} key(s) that are used in code.`));
|
|
569
|
+
console.log(node_util.styleText('red', ` Run "i18next-cli status ${primaryLanguage}" for details, or "i18next-cli extract" to add them.`));
|
|
570
|
+
}
|
|
534
571
|
await printLocizeFunnel();
|
|
535
572
|
}
|
|
536
573
|
/**
|
package/dist/esm/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ const program = new Command();
|
|
|
30
30
|
program
|
|
31
31
|
.name('i18next-cli')
|
|
32
32
|
.description('A unified, high-performance i18next CLI.')
|
|
33
|
-
.version('1.
|
|
33
|
+
.version('1.61.0'); // This string is replaced with the actual version at build time by rollup
|
|
34
34
|
// new: global config override option
|
|
35
35
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
36
36
|
program
|
package/dist/esm/status.js
CHANGED
|
@@ -54,6 +54,11 @@ async function runStatus(config, options = {}) {
|
|
|
54
54
|
break;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
// The primary language fails the check only on absent keys (used in code but
|
|
58
|
+
// missing from the translation file); empty placeholders are tolerated.
|
|
59
|
+
if (!hasMissing && report.primary && report.primary.totalAbsent > 0) {
|
|
60
|
+
hasMissing = true;
|
|
61
|
+
}
|
|
57
62
|
if (hasMissing) {
|
|
58
63
|
spinner.fail('Error: Incomplete translations detected.');
|
|
59
64
|
process.exit(1);
|
|
@@ -201,7 +206,13 @@ async function generateStatusReport(config) {
|
|
|
201
206
|
if (nestedRefKeys.length > 0)
|
|
202
207
|
nestedReferenceKeysByNs.set(ns, nestedRefKeys);
|
|
203
208
|
}
|
|
204
|
-
|
|
209
|
+
// The primary language is checked first so that keys used in code but absent
|
|
210
|
+
// from the primary translation file (e.g. a typo, or `extract` never run) are
|
|
211
|
+
// surfaced as well. For the primary, an empty value is a deliberate
|
|
212
|
+
// placeholder and counts as present — only truly absent keys are flagged.
|
|
213
|
+
const localesToCheck = [primaryLanguage, ...secondaryLanguages.filter((l) => l !== primaryLanguage)];
|
|
214
|
+
for (const locale of localesToCheck) {
|
|
215
|
+
const isPrimary = locale === primaryLanguage;
|
|
205
216
|
let totalTranslatedForLocale = 0;
|
|
206
217
|
let totalEmptyForLocale = 0;
|
|
207
218
|
let totalAbsentForLocale = 0;
|
|
@@ -262,11 +273,17 @@ async function generateStatusReport(config) {
|
|
|
262
273
|
const primaryState = classifyValue(primaryValue);
|
|
263
274
|
// Only fall back when the key is genuinely absent from the primary file.
|
|
264
275
|
// An empty string is intentional (placeholder from extract) — don't hide it.
|
|
276
|
+
let state = primaryState;
|
|
265
277
|
if (primaryState === 'absent' && fallbackTranslations) {
|
|
266
278
|
const fallbackValue = getNestedValue(fallbackTranslations, key, sep);
|
|
267
|
-
|
|
279
|
+
state = classifyValue(fallbackValue);
|
|
268
280
|
}
|
|
269
|
-
|
|
281
|
+
// For the primary language the file itself is the source of values, so an
|
|
282
|
+
// empty placeholder still means the key is present. Only a truly absent
|
|
283
|
+
// key (used in code, missing from the file) is a problem here.
|
|
284
|
+
if (isPrimary && state === 'empty')
|
|
285
|
+
return 'translated';
|
|
286
|
+
return state;
|
|
270
287
|
};
|
|
271
288
|
const processedKeys = new Set();
|
|
272
289
|
// Combine AST-extracted keys with nested-reference keys discovered in
|
|
@@ -363,13 +380,19 @@ async function generateStatusReport(config) {
|
|
|
363
380
|
totalAbsentForLocale += absentInNs;
|
|
364
381
|
totalKeysForLocale += totalInNs;
|
|
365
382
|
}
|
|
366
|
-
|
|
383
|
+
const localeStatus = {
|
|
367
384
|
totalKeys: totalKeysForLocale,
|
|
368
385
|
totalTranslated: totalTranslatedForLocale,
|
|
369
386
|
totalEmpty: totalEmptyForLocale,
|
|
370
387
|
totalAbsent: totalAbsentForLocale,
|
|
371
388
|
namespaces,
|
|
372
|
-
}
|
|
389
|
+
};
|
|
390
|
+
if (isPrimary) {
|
|
391
|
+
report.primary = localeStatus;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
report.locales.set(locale, localeStatus);
|
|
395
|
+
}
|
|
373
396
|
}
|
|
374
397
|
return report;
|
|
375
398
|
}
|
|
@@ -417,15 +440,12 @@ async function displayStatusReport(report, config, options) {
|
|
|
417
440
|
* ✗ red — absent from file entirely (structural problem)
|
|
418
441
|
*/
|
|
419
442
|
async function displayDetailedLocaleReport(report, config, locale, namespaceFilter, hideTranslated) {
|
|
420
|
-
if (locale === config.extract.primaryLanguage) {
|
|
421
|
-
console.log(styleText('yellow', `Locale "${locale}" is the primary language. All keys are considered present.`));
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
443
|
if (!config.locales.includes(locale)) {
|
|
425
444
|
console.error(styleText('red', `Error: Locale "${locale}" is not defined in your configuration.`));
|
|
426
445
|
return;
|
|
427
446
|
}
|
|
428
|
-
const
|
|
447
|
+
const isPrimary = locale === config.extract.primaryLanguage;
|
|
448
|
+
const localeData = isPrimary ? report.primary : report.locales.get(locale);
|
|
429
449
|
if (!localeData) {
|
|
430
450
|
console.error(styleText('red', `Error: Locale "${locale}" is not a valid secondary language.`));
|
|
431
451
|
return;
|
|
@@ -463,8 +483,16 @@ async function displayDetailedLocaleReport(report, config, locale, namespaceFilt
|
|
|
463
483
|
}
|
|
464
484
|
const missingCount = totalKeysForLocale - localeData.totalTranslated;
|
|
465
485
|
if (missingCount > 0) {
|
|
466
|
-
|
|
467
|
-
|
|
486
|
+
if (isPrimary) {
|
|
487
|
+
console.log(styleText(['red', 'bold'], `\nSummary: Found ${missingCount} key(s) used in code but absent from the "${locale}" translation files. Run "i18next-cli extract" to add them.`));
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
const summaryBreakdown = buildBreakdown(localeData.totalEmpty, localeData.totalAbsent);
|
|
491
|
+
console.log(styleText(['yellow', 'bold'], `\nSummary: Found ${missingCount} incomplete translations for "${locale}" — ${summaryBreakdown}.`));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (isPrimary) {
|
|
495
|
+
console.log(styleText(['green', 'bold'], `\nSummary: 🎉 All keys used in code are present in the "${locale}" translation files.`));
|
|
468
496
|
}
|
|
469
497
|
else {
|
|
470
498
|
console.log(styleText(['green', 'bold'], `\nSummary: 🎉 All keys are translated for "${locale}".`));
|
|
@@ -499,6 +527,11 @@ async function displayNamespaceSummaryReport(report, config, namespace) {
|
|
|
499
527
|
console.log(`- ${locale}: ${bar} ${percentage}% (${nsLocaleData.translatedKeys}/${nsLocaleData.totalKeys} keys)${suffix}`);
|
|
500
528
|
}
|
|
501
529
|
}
|
|
530
|
+
const primaryNsData = report.primary?.namespaces.get(namespace);
|
|
531
|
+
if (primaryNsData && primaryNsData.absentKeys > 0) {
|
|
532
|
+
const { primaryLanguage } = config.extract;
|
|
533
|
+
console.log(styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${primaryNsData.absentKeys} key(s) that are used in code.`));
|
|
534
|
+
}
|
|
502
535
|
await printLocizeFunnel();
|
|
503
536
|
}
|
|
504
537
|
/**
|
|
@@ -529,6 +562,10 @@ async function displayOverallSummaryReport(report, config) {
|
|
|
529
562
|
const suffix = breakdown ? ` — ${breakdown}` : '';
|
|
530
563
|
console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${localeData.totalKeys} keys)${suffix}`);
|
|
531
564
|
}
|
|
565
|
+
if (report.primary && report.primary.totalAbsent > 0) {
|
|
566
|
+
console.log(styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${report.primary.totalAbsent} key(s) that are used in code.`));
|
|
567
|
+
console.log(styleText('red', ` Run "i18next-cli status ${primaryLanguage}" for details, or "i18next-cli extract" to add them.`));
|
|
568
|
+
}
|
|
532
569
|
await printLocizeFunnel();
|
|
533
570
|
}
|
|
534
571
|
/**
|
package/package.json
CHANGED
package/types/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAOpE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAOpE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAiED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBA4BzF"}
|