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 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.60.0'); // This string is replaced with the actual version at build time by rollup
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
@@ -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
- for (const locale of secondaryLanguages) {
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
- return classifyValue(fallbackValue);
281
+ state = classifyValue(fallbackValue);
270
282
  }
271
- return primaryState;
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
- report.locales.set(locale, {
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 localeData = report.locales.get(locale);
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
- const summaryBreakdown = buildBreakdown(localeData.totalEmpty, localeData.totalAbsent);
469
- console.log(node_util.styleText(['yellow', 'bold'], `\nSummary: Found ${missingCount} incomplete translations for "${locale}" ${summaryBreakdown}.`));
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.60.0'); // This string is replaced with the actual version at build time by rollup
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
@@ -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
- for (const locale of secondaryLanguages) {
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
- return classifyValue(fallbackValue);
279
+ state = classifyValue(fallbackValue);
268
280
  }
269
- return primaryState;
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
- report.locales.set(locale, {
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 localeData = report.locales.get(locale);
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
- const summaryBreakdown = buildBreakdown(localeData.totalEmpty, localeData.totalAbsent);
467
- console.log(styleText(['yellow', 'bold'], `\nSummary: Found ${missingCount} incomplete translations for "${locale}" ${summaryBreakdown}.`));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.60.0",
3
+ "version": "1.61.0",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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;AAqDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuBzF"}
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"}