i18next-cli 1.41.2 → 1.41.3

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/dist/cjs/cli.js CHANGED
@@ -28,7 +28,7 @@ const program = new commander.Command();
28
28
  program
29
29
  .name('i18next-cli')
30
30
  .description('A unified, high-performance i18next CLI.')
31
- .version('1.41.2'); // This string is replaced with the actual version at build time by rollup
31
+ .version('1.41.3'); // This string is replaced with the actual version at build time by rollup
32
32
  // new: global config override option
33
33
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
34
34
  program
@@ -217,16 +217,14 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
217
217
  }
218
218
  }
219
219
  }
220
- // Get the plural categories for the target language
220
+ // Get the plural categories for the target language (only used for filtering extracted keys)
221
221
  const targetLanguagePluralCategories = new Set();
222
222
  // Track cardinal plural categories separately so we can special-case single-"other" languages
223
223
  let cardinalCategories = [];
224
- let ordinalCategories = [];
225
224
  try {
226
225
  const cardinalRules = new Intl.PluralRules(locale, { type: 'cardinal' });
227
226
  const ordinalRules = new Intl.PluralRules(locale, { type: 'ordinal' });
228
227
  cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
229
- ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
230
228
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
231
229
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
232
230
  }
@@ -236,7 +234,6 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
236
234
  const cardinalRules = new Intl.PluralRules(fallbackLang, { type: 'cardinal' });
237
235
  const ordinalRules = new Intl.PluralRules(fallbackLang, { type: 'ordinal' });
238
236
  cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
239
- ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
240
237
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
241
238
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
242
239
  }
@@ -450,49 +447,104 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
450
447
  continue;
451
448
  }
452
449
  }
453
- // If this is a base plural key (no explicit suffix) and the locale is NOT the primary,
454
- // expand it into locale-specific plural variants (e.g. key_one, key_other).
455
- // Use the extracted defaultValue (fallback to base) for variant values.
450
+ // If this is a base plural key (no explicit suffix), expand it into locale-specific plural variants.
451
+ // For non-primary locales, we generate forms for that specific locale from CLDR.
452
+ // Additionally, we generate empty placeholders for ALL other CLDR forms not in the target locale
453
+ // (so translators can add them manually if needed).
456
454
  if (hasCount && !isExpandedPlural) {
457
455
  const parts = String(key).split(pluralSeparator);
458
456
  const isBaseKey = parts.length === 1;
459
- if (isBaseKey && locale !== primaryLanguage) {
457
+ if (isBaseKey) {
460
458
  // If explicit expanded variants exist, do not expand the base.
461
459
  const base = key;
462
460
  if (expandedBases.has(base)) ;
463
461
  else {
464
- // choose categories based on ordinal flag
465
- const categories = isOrdinal ? ordinalCategories : cardinalCategories;
466
- for (const category of categories) {
467
- const finalKey = isOrdinal
468
- ? `${base}${pluralSeparator}ordinal${pluralSeparator}${category}`
469
- : `${base}${pluralSeparator}${category}`;
470
- // If the key looks like a serialized Trans component (starts with <), treat it as a flat key
471
- // to prevent splitting on dots that appear within the content.
472
- const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
473
- // Preserve existing translation if present; otherwise set a sensible default
474
- const existingVariantValue = nestedObject.getNestedValue(existingTranslations, finalKey, separator);
475
- if (existingVariantValue === undefined) {
476
- // Prefer explicit defaultValue extracted for this key; fall back to configured defaultValue
477
- // (resolved via resolveDefaultValue which handles functions or strings and accepts the full parameter set).
478
- let resolvedValue;
479
- if (typeof defaultValue$1 === 'string') {
480
- resolvedValue = defaultValue$1;
462
+ // Determine which plural forms to generate
463
+ let formsToGenerate;
464
+ if (locale !== primaryLanguage) {
465
+ // For non-primary locales:
466
+ // 1. Generate the forms that locale actually needs
467
+ formsToGenerate = cardinalCategories;
468
+ // 2. Also prepare empty placeholders for all OTHER CLDR forms not in this locale
469
+ // so translators can add them manually without --sync-primary removing them
470
+ const otherForms = pluralForms.filter(f => !cardinalCategories.includes(f));
471
+ // Process the locale-specific forms normally
472
+ for (const form of formsToGenerate) {
473
+ const finalKey = isOrdinal
474
+ ? `${base}${pluralSeparator}${form}`
475
+ : `${base}${pluralSeparator}${form}`;
476
+ const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
477
+ const existingVariantValue = nestedObject.getNestedValue(existingTranslations, finalKey, separator);
478
+ if (existingVariantValue === undefined) {
479
+ // Use the default value for secondary locale forms
480
+ let resolvedValue;
481
+ if (typeof defaultValue$1 === 'string') {
482
+ resolvedValue = defaultValue$1;
483
+ }
484
+ else {
485
+ resolvedValue = defaultValue.resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
486
+ }
487
+ nestedObject.setNestedValue(newTranslations, finalKey, resolvedValue, separator);
481
488
  }
482
489
  else {
483
- // Use resolveDefaultValue to compute a sensible default, providing namespace and locale context.
484
- resolvedValue = defaultValue.resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
490
+ nestedObject.setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
485
491
  }
486
- nestedObject.setNestedValue(newTranslations, finalKey, resolvedValue, separator);
492
+ }
493
+ // Now process other CLDR forms: set empty placeholders for forms this locale doesn't use
494
+ // but preserve any that were manually added by translators
495
+ for (const form of otherForms) {
496
+ const finalKey = isOrdinal
497
+ ? `${base}${pluralSeparator}${form}`
498
+ : `${base}${pluralSeparator}${form}`;
499
+ const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
500
+ const existingVariantValue = nestedObject.getNestedValue(existingTranslations, finalKey, separator);
501
+ if (existingVariantValue !== undefined) {
502
+ // Preserve manually-added forms
503
+ nestedObject.setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
504
+ }
505
+ // Don't generate empty placeholders - only generate what the locale needs and preserve what's manual
506
+ }
507
+ }
508
+ else {
509
+ // For primary language, only expand if it has multiple plural forms
510
+ // Single-"other" languages (ja, zh, ko) should NOT expand the base key
511
+ if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other') {
512
+ // Single-"other" language - don't expand, keep just the base key
513
+ formsToGenerate = [];
487
514
  }
488
515
  else {
489
- // Keep existing translation
490
- nestedObject.setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
516
+ // Multi-form language - expand to its plural forms
517
+ formsToGenerate = cardinalCategories;
518
+ for (const form of formsToGenerate) {
519
+ const finalKey = isOrdinal
520
+ ? `${base}${pluralSeparator}${form}`
521
+ : `${base}${pluralSeparator}${form}`;
522
+ const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
523
+ const existingVariantValue = nestedObject.getNestedValue(existingTranslations, finalKey, separator);
524
+ if (existingVariantValue === undefined) {
525
+ // Prefer explicit defaultValue extracted for this key; fall back to configured defaultValue
526
+ let resolvedValue;
527
+ if (typeof defaultValue$1 === 'string') {
528
+ resolvedValue = defaultValue$1;
529
+ }
530
+ else {
531
+ resolvedValue = defaultValue.resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
532
+ }
533
+ nestedObject.setNestedValue(newTranslations, finalKey, resolvedValue, separator);
534
+ }
535
+ else {
536
+ nestedObject.setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
537
+ }
538
+ }
491
539
  }
492
540
  }
541
+ if (formsToGenerate && formsToGenerate.length > 0) {
542
+ // We've handled expansion for this base key; skip the normal single-key handling.
543
+ continue;
544
+ }
545
+ // else: formsToGenerate is empty (single-"other" primary language)
546
+ // Fall through to normal key handling below
493
547
  }
494
- // We've expanded variants for this base key; skip the normal single-key handling.
495
- continue;
496
548
  }
497
549
  }
498
550
  // If the key looks like a serialized Trans component (starts with <), treat it as a flat key
package/dist/esm/cli.js CHANGED
@@ -26,7 +26,7 @@ const program = new Command();
26
26
  program
27
27
  .name('i18next-cli')
28
28
  .description('A unified, high-performance i18next CLI.')
29
- .version('1.41.2'); // This string is replaced with the actual version at build time by rollup
29
+ .version('1.41.3'); // This string is replaced with the actual version at build time by rollup
30
30
  // new: global config override option
31
31
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
32
32
  program
@@ -215,16 +215,14 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
215
215
  }
216
216
  }
217
217
  }
218
- // Get the plural categories for the target language
218
+ // Get the plural categories for the target language (only used for filtering extracted keys)
219
219
  const targetLanguagePluralCategories = new Set();
220
220
  // Track cardinal plural categories separately so we can special-case single-"other" languages
221
221
  let cardinalCategories = [];
222
- let ordinalCategories = [];
223
222
  try {
224
223
  const cardinalRules = new Intl.PluralRules(locale, { type: 'cardinal' });
225
224
  const ordinalRules = new Intl.PluralRules(locale, { type: 'ordinal' });
226
225
  cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
227
- ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
228
226
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
229
227
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
230
228
  }
@@ -234,7 +232,6 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
234
232
  const cardinalRules = new Intl.PluralRules(fallbackLang, { type: 'cardinal' });
235
233
  const ordinalRules = new Intl.PluralRules(fallbackLang, { type: 'ordinal' });
236
234
  cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
237
- ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
238
235
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
239
236
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
240
237
  }
@@ -448,49 +445,104 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
448
445
  continue;
449
446
  }
450
447
  }
451
- // If this is a base plural key (no explicit suffix) and the locale is NOT the primary,
452
- // expand it into locale-specific plural variants (e.g. key_one, key_other).
453
- // Use the extracted defaultValue (fallback to base) for variant values.
448
+ // If this is a base plural key (no explicit suffix), expand it into locale-specific plural variants.
449
+ // For non-primary locales, we generate forms for that specific locale from CLDR.
450
+ // Additionally, we generate empty placeholders for ALL other CLDR forms not in the target locale
451
+ // (so translators can add them manually if needed).
454
452
  if (hasCount && !isExpandedPlural) {
455
453
  const parts = String(key).split(pluralSeparator);
456
454
  const isBaseKey = parts.length === 1;
457
- if (isBaseKey && locale !== primaryLanguage) {
455
+ if (isBaseKey) {
458
456
  // If explicit expanded variants exist, do not expand the base.
459
457
  const base = key;
460
458
  if (expandedBases.has(base)) ;
461
459
  else {
462
- // choose categories based on ordinal flag
463
- const categories = isOrdinal ? ordinalCategories : cardinalCategories;
464
- for (const category of categories) {
465
- const finalKey = isOrdinal
466
- ? `${base}${pluralSeparator}ordinal${pluralSeparator}${category}`
467
- : `${base}${pluralSeparator}${category}`;
468
- // If the key looks like a serialized Trans component (starts with <), treat it as a flat key
469
- // to prevent splitting on dots that appear within the content.
470
- const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
471
- // Preserve existing translation if present; otherwise set a sensible default
472
- const existingVariantValue = getNestedValue(existingTranslations, finalKey, separator);
473
- if (existingVariantValue === undefined) {
474
- // Prefer explicit defaultValue extracted for this key; fall back to configured defaultValue
475
- // (resolved via resolveDefaultValue which handles functions or strings and accepts the full parameter set).
476
- let resolvedValue;
477
- if (typeof defaultValue === 'string') {
478
- resolvedValue = defaultValue;
460
+ // Determine which plural forms to generate
461
+ let formsToGenerate;
462
+ if (locale !== primaryLanguage) {
463
+ // For non-primary locales:
464
+ // 1. Generate the forms that locale actually needs
465
+ formsToGenerate = cardinalCategories;
466
+ // 2. Also prepare empty placeholders for all OTHER CLDR forms not in this locale
467
+ // so translators can add them manually without --sync-primary removing them
468
+ const otherForms = pluralForms.filter(f => !cardinalCategories.includes(f));
469
+ // Process the locale-specific forms normally
470
+ for (const form of formsToGenerate) {
471
+ const finalKey = isOrdinal
472
+ ? `${base}${pluralSeparator}${form}`
473
+ : `${base}${pluralSeparator}${form}`;
474
+ const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
475
+ const existingVariantValue = getNestedValue(existingTranslations, finalKey, separator);
476
+ if (existingVariantValue === undefined) {
477
+ // Use the default value for secondary locale forms
478
+ let resolvedValue;
479
+ if (typeof defaultValue === 'string') {
480
+ resolvedValue = defaultValue;
481
+ }
482
+ else {
483
+ resolvedValue = resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
484
+ }
485
+ setNestedValue(newTranslations, finalKey, resolvedValue, separator);
479
486
  }
480
487
  else {
481
- // Use resolveDefaultValue to compute a sensible default, providing namespace and locale context.
482
- resolvedValue = resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
488
+ setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
483
489
  }
484
- setNestedValue(newTranslations, finalKey, resolvedValue, separator);
490
+ }
491
+ // Now process other CLDR forms: set empty placeholders for forms this locale doesn't use
492
+ // but preserve any that were manually added by translators
493
+ for (const form of otherForms) {
494
+ const finalKey = isOrdinal
495
+ ? `${base}${pluralSeparator}${form}`
496
+ : `${base}${pluralSeparator}${form}`;
497
+ const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
498
+ const existingVariantValue = getNestedValue(existingTranslations, finalKey, separator);
499
+ if (existingVariantValue !== undefined) {
500
+ // Preserve manually-added forms
501
+ setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
502
+ }
503
+ // Don't generate empty placeholders - only generate what the locale needs and preserve what's manual
504
+ }
505
+ }
506
+ else {
507
+ // For primary language, only expand if it has multiple plural forms
508
+ // Single-"other" languages (ja, zh, ko) should NOT expand the base key
509
+ if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other') {
510
+ // Single-"other" language - don't expand, keep just the base key
511
+ formsToGenerate = [];
485
512
  }
486
513
  else {
487
- // Keep existing translation
488
- setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
514
+ // Multi-form language - expand to its plural forms
515
+ formsToGenerate = cardinalCategories;
516
+ for (const form of formsToGenerate) {
517
+ const finalKey = isOrdinal
518
+ ? `${base}${pluralSeparator}${form}`
519
+ : `${base}${pluralSeparator}${form}`;
520
+ const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
521
+ const existingVariantValue = getNestedValue(existingTranslations, finalKey, separator);
522
+ if (existingVariantValue === undefined) {
523
+ // Prefer explicit defaultValue extracted for this key; fall back to configured defaultValue
524
+ let resolvedValue;
525
+ if (typeof defaultValue === 'string') {
526
+ resolvedValue = defaultValue;
527
+ }
528
+ else {
529
+ resolvedValue = resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
530
+ }
531
+ setNestedValue(newTranslations, finalKey, resolvedValue, separator);
532
+ }
533
+ else {
534
+ setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
535
+ }
536
+ }
489
537
  }
490
538
  }
539
+ if (formsToGenerate && formsToGenerate.length > 0) {
540
+ // We've handled expansion for this base key; skip the normal single-key handling.
541
+ continue;
542
+ }
543
+ // else: formsToGenerate is empty (single-"other" primary language)
544
+ // Fall through to normal key handling below
491
545
  }
492
- // We've expanded variants for this base key; skip the normal single-key handling.
493
- continue;
494
546
  }
495
547
  }
496
548
  // If the key looks like a serialized Trans component (starts with <), treat it as a flat key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.41.2",
3
+ "version": "1.41.3",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AA2uBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA0I9B"}
1
+ {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgyBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA0I9B"}