i18next-cli 1.51.0 → 1.51.2

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
@@ -649,10 +649,10 @@ export default defineConfig({
649
649
  // The count option can still be used for {{count}} interpolation in the translation value
650
650
  disablePlurals: false, // Default: false
651
651
 
652
- // Force generation of all 6 CLDR plural forms (zero, one, two, few, many, other)
653
- // for every language, including the primary language.
654
- // Useful when secondary languages need plural forms that the primary language doesn't have,
655
- // and you want a consistent set of keys across all locales.
652
+ // Generate the union of all configured locales' plural forms for every language.
653
+ // For example, if your locales are ['en', 'pl'], English normally only gets _one/_other,
654
+ // but with this option it also gets _few/_many (needed by Polish).
655
+ // Useful when you want a consistent set of plural keys across all locales.
656
656
  allPluralForms: false, // Default: false
657
657
 
658
658
  // Prefix for nested translations.
package/dist/cjs/cli.js CHANGED
@@ -31,7 +31,7 @@ const program = new commander.Command();
31
31
  program
32
32
  .name('i18next-cli')
33
33
  .description('A unified, high-performance i18next CLI.')
34
- .version('1.51.0'); // This string is replaced with the actual version at build time by rollup
34
+ .version('1.51.2'); // This string is replaced with the actual version at build time by rollup
35
35
  // new: global config override option
36
36
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
37
37
  program
@@ -273,6 +273,17 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
273
273
  const cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
274
274
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
275
275
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
276
+ // When allPluralForms is enabled, compute the union of cardinal categories across all configured locales.
277
+ // This ensures every locale gets the same set of plural keys — but only the forms actually needed by at least one locale.
278
+ const allLocalesCardinalCategories = config.extract.allPluralForms
279
+ ? (() => {
280
+ const union = new Set();
281
+ for (const loc of config.locales) {
282
+ pluralRules.safePluralRules(loc, { type: 'cardinal' }).resolvedOptions().pluralCategories.forEach(c => union.add(c));
283
+ }
284
+ return [...union];
285
+ })()
286
+ : null;
276
287
  // Prepare namespace pattern checking helpers
277
288
  const rawPreserve = config.extract.preservePatterns || [];
278
289
  // Helper to check if a key should be filtered out during extraction
@@ -364,7 +375,7 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
364
375
  return true;
365
376
  }
366
377
  // When allPluralForms is enabled, include all CLDR plural forms regardless of the target language
367
- if (config.extract.allPluralForms && pluralForms.includes(lastPart)) {
378
+ if (allLocalesCardinalCategories && allLocalesCardinalCategories.includes(lastPart)) {
368
379
  return true;
369
380
  }
370
381
  if (isOrdinal && keyParts.includes('ordinal')) {
@@ -511,8 +522,8 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
511
522
  let formsToGenerate;
512
523
  if (locale !== primaryLanguage) {
513
524
  // For non-primary locales:
514
- // 1. Generate the forms that locale actually needs (or all forms if allPluralForms is enabled)
515
- formsToGenerate = config.extract.allPluralForms ? [...pluralForms] : cardinalCategories;
525
+ // 1. Generate the forms that locale actually needs (or union of all locales' forms if allPluralForms is enabled)
526
+ formsToGenerate = allLocalesCardinalCategories ?? cardinalCategories;
516
527
  // 2. Also prepare empty placeholders for all OTHER CLDR forms not in this locale
517
528
  // so translators can add them manually without --sync-primary removing them
518
529
  const otherForms = pluralForms.filter(f => !cardinalCategories.includes(f));
@@ -556,13 +567,13 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
556
567
  else {
557
568
  // For primary language, only expand if it has multiple plural forms
558
569
  // Single-"other" languages (ja, zh, ko) should NOT expand the base key (unless allPluralForms is enabled)
559
- if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other' && !config.extract.allPluralForms) {
570
+ if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other' && !allLocalesCardinalCategories) {
560
571
  // Single-"other" language - don't expand, keep just the base key
561
572
  formsToGenerate = [];
562
573
  }
563
574
  else {
564
- // Multi-form language - expand to its plural forms (or all forms if allPluralForms is enabled)
565
- formsToGenerate = config.extract.allPluralForms ? [...pluralForms] : cardinalCategories;
575
+ // Multi-form language - expand to its plural forms (or union of all locales' forms if allPluralForms is enabled)
576
+ formsToGenerate = allLocalesCardinalCategories ?? cardinalCategories;
566
577
  for (const form of formsToGenerate) {
567
578
  const finalKey = isOrdinal
568
579
  ? `${base}${pluralSeparator}${form}`
@@ -710,11 +721,11 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
710
721
  }
711
722
  nestedObject.setNestedValue(newTranslations, key, valueToSet, separator);
712
723
  }
713
- // When allPluralForms is enabled, ensure all 6 CLDR plural forms exist for every plural base key.
724
+ // When allPluralForms is enabled, ensure all union plural forms exist for every plural base key.
714
725
  // The extractor only generates forms for the configured locales' categories, so we need to fill in the rest.
715
- if (config.extract.allPluralForms && !config.extract.disablePlurals) {
726
+ if (allLocalesCardinalCategories && !config.extract.disablePlurals) {
716
727
  for (const base of expandedBases) {
717
- for (const form of pluralForms) {
728
+ for (const form of allLocalesCardinalCategories) {
718
729
  const finalKey = `${base}${pluralSeparator}${form}`;
719
730
  const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
720
731
  const existingInNew = nestedObject.getNestedValue(newTranslations, finalKey, separator);
@@ -904,9 +915,12 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
904
915
  }
905
916
  // When nsSeparator is false, keys resolved to the defaultNS (e.g. from
906
917
  // useTranslation() with no args) should be treated as top-level, not
907
- // wrapped under the namespace name.
918
+ // wrapped under the namespace name — but only when there are no other
919
+ // explicit namespaces. If multiple namespaces exist, we must keep the
920
+ // default namespace wrapper to avoid flattening it into the top level (#227).
908
921
  const defaultNs = String(config.extract.defaultNS ?? 'translation');
909
- const isTopLevel = (nsKey) => nsKey === NO_NS_TOKEN || (config.extract.nsSeparator === false && nsKey === defaultNs);
922
+ const hasOtherNamespaces = [...keysByNS.keys()].some(k => k !== NO_NS_TOKEN && k !== defaultNs);
923
+ const isTopLevel = (nsKey) => nsKey === NO_NS_TOKEN || (config.extract.nsSeparator === false && nsKey === defaultNs && !hasOtherNamespaces);
910
924
  for (const nsKey of namespacesToProcess) {
911
925
  const nsKeys = keysByNS.get(nsKey) || [];
912
926
  if (isTopLevel(nsKey)) {
package/dist/esm/cli.js CHANGED
@@ -29,7 +29,7 @@ const program = new Command();
29
29
  program
30
30
  .name('i18next-cli')
31
31
  .description('A unified, high-performance i18next CLI.')
32
- .version('1.51.0'); // This string is replaced with the actual version at build time by rollup
32
+ .version('1.51.2'); // This string is replaced with the actual version at build time by rollup
33
33
  // new: global config override option
34
34
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
35
35
  program
@@ -271,6 +271,17 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
271
271
  const cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
272
272
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
273
273
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
274
+ // When allPluralForms is enabled, compute the union of cardinal categories across all configured locales.
275
+ // This ensures every locale gets the same set of plural keys — but only the forms actually needed by at least one locale.
276
+ const allLocalesCardinalCategories = config.extract.allPluralForms
277
+ ? (() => {
278
+ const union = new Set();
279
+ for (const loc of config.locales) {
280
+ safePluralRules(loc, { type: 'cardinal' }).resolvedOptions().pluralCategories.forEach(c => union.add(c));
281
+ }
282
+ return [...union];
283
+ })()
284
+ : null;
274
285
  // Prepare namespace pattern checking helpers
275
286
  const rawPreserve = config.extract.preservePatterns || [];
276
287
  // Helper to check if a key should be filtered out during extraction
@@ -362,7 +373,7 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
362
373
  return true;
363
374
  }
364
375
  // When allPluralForms is enabled, include all CLDR plural forms regardless of the target language
365
- if (config.extract.allPluralForms && pluralForms.includes(lastPart)) {
376
+ if (allLocalesCardinalCategories && allLocalesCardinalCategories.includes(lastPart)) {
366
377
  return true;
367
378
  }
368
379
  if (isOrdinal && keyParts.includes('ordinal')) {
@@ -509,8 +520,8 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
509
520
  let formsToGenerate;
510
521
  if (locale !== primaryLanguage) {
511
522
  // For non-primary locales:
512
- // 1. Generate the forms that locale actually needs (or all forms if allPluralForms is enabled)
513
- formsToGenerate = config.extract.allPluralForms ? [...pluralForms] : cardinalCategories;
523
+ // 1. Generate the forms that locale actually needs (or union of all locales' forms if allPluralForms is enabled)
524
+ formsToGenerate = allLocalesCardinalCategories ?? cardinalCategories;
514
525
  // 2. Also prepare empty placeholders for all OTHER CLDR forms not in this locale
515
526
  // so translators can add them manually without --sync-primary removing them
516
527
  const otherForms = pluralForms.filter(f => !cardinalCategories.includes(f));
@@ -554,13 +565,13 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
554
565
  else {
555
566
  // For primary language, only expand if it has multiple plural forms
556
567
  // Single-"other" languages (ja, zh, ko) should NOT expand the base key (unless allPluralForms is enabled)
557
- if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other' && !config.extract.allPluralForms) {
568
+ if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other' && !allLocalesCardinalCategories) {
558
569
  // Single-"other" language - don't expand, keep just the base key
559
570
  formsToGenerate = [];
560
571
  }
561
572
  else {
562
- // Multi-form language - expand to its plural forms (or all forms if allPluralForms is enabled)
563
- formsToGenerate = config.extract.allPluralForms ? [...pluralForms] : cardinalCategories;
573
+ // Multi-form language - expand to its plural forms (or union of all locales' forms if allPluralForms is enabled)
574
+ formsToGenerate = allLocalesCardinalCategories ?? cardinalCategories;
564
575
  for (const form of formsToGenerate) {
565
576
  const finalKey = isOrdinal
566
577
  ? `${base}${pluralSeparator}${form}`
@@ -708,11 +719,11 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
708
719
  }
709
720
  setNestedValue(newTranslations, key, valueToSet, separator);
710
721
  }
711
- // When allPluralForms is enabled, ensure all 6 CLDR plural forms exist for every plural base key.
722
+ // When allPluralForms is enabled, ensure all union plural forms exist for every plural base key.
712
723
  // The extractor only generates forms for the configured locales' categories, so we need to fill in the rest.
713
- if (config.extract.allPluralForms && !config.extract.disablePlurals) {
724
+ if (allLocalesCardinalCategories && !config.extract.disablePlurals) {
714
725
  for (const base of expandedBases) {
715
- for (const form of pluralForms) {
726
+ for (const form of allLocalesCardinalCategories) {
716
727
  const finalKey = `${base}${pluralSeparator}${form}`;
717
728
  const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
718
729
  const existingInNew = getNestedValue(newTranslations, finalKey, separator);
@@ -902,9 +913,12 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
902
913
  }
903
914
  // When nsSeparator is false, keys resolved to the defaultNS (e.g. from
904
915
  // useTranslation() with no args) should be treated as top-level, not
905
- // wrapped under the namespace name.
916
+ // wrapped under the namespace name — but only when there are no other
917
+ // explicit namespaces. If multiple namespaces exist, we must keep the
918
+ // default namespace wrapper to avoid flattening it into the top level (#227).
906
919
  const defaultNs = String(config.extract.defaultNS ?? 'translation');
907
- const isTopLevel = (nsKey) => nsKey === NO_NS_TOKEN || (config.extract.nsSeparator === false && nsKey === defaultNs);
920
+ const hasOtherNamespaces = [...keysByNS.keys()].some(k => k !== NO_NS_TOKEN && k !== defaultNs);
921
+ const isTopLevel = (nsKey) => nsKey === NO_NS_TOKEN || (config.extract.nsSeparator === false && nsKey === defaultNs && !hasOtherNamespaces);
908
922
  for (const nsKey of namespacesToProcess) {
909
923
  const nsKeys = keysByNS.get(nsKey) || [];
910
924
  if (isTopLevel(nsKey)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.51.0",
3
+ "version": "1.51.2",
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,EAAE,MAAM,gBAAgB,CAAA;AAi6B9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,EACf,MAA4B,EAC7B,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAoJ9B"}
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,EAAE,MAAM,gBAAgB,CAAA;AA66B9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,EACf,MAA4B,EAC7B,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAuJ9B"}
package/types/types.d.ts CHANGED
@@ -135,10 +135,10 @@ export interface I18nextToolkitConfig {
135
135
  generateBasePluralForms?: boolean;
136
136
  disablePlurals?: boolean;
137
137
  /**
138
- * When true, generates all 6 CLDR plural forms (zero, one, two, few, many, other)
139
- * for every language, including the primary language.
140
- * Useful when secondary languages need plural forms that the primary language doesn't have,
141
- * and you want a consistent set of keys across all locales.
138
+ * When true, generates the union of all configured locales' plural forms for every language.
139
+ * For example, with locales ['en', 'pl'], English normally only gets _one/_other,
140
+ * but with this option it also gets _few/_many (needed by Polish).
141
+ * Only forms required by at least one configured locale are generated.
142
142
  * (default: false)
143
143
  */
144
144
  allPluralForms?: boolean;