i18next-cli 1.56.4 → 1.56.6

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
@@ -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.56.4'); // This string is replaced with the actual version at build time by rollup
35
+ .version('1.56.6'); // 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
@@ -224,6 +224,11 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
224
224
  const cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
225
225
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
226
226
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
227
+ // Plural categories of the primary language — used to recognise locale-specific
228
+ // plural variants (e.g. French `_many` when primary is English) so we don't
229
+ // treat their absence from the primary file as a "divergence" during --sync-all.
230
+ const primaryCardinalCategoriesSet = new Set(pluralRules.safePluralRules(primaryLanguage, { type: 'cardinal' }).resolvedOptions().pluralCategories);
231
+ const primaryOrdinalCategoriesSet = new Set(pluralRules.safePluralRules(primaryLanguage, { type: 'ordinal' }).resolvedOptions().pluralCategories);
227
232
  // When allPluralForms is enabled, compute the union of cardinal categories across all configured locales.
228
233
  // This ensures every locale gets the same set of plural keys — but only the forms actually needed by at least one locale.
229
234
  const allLocalesCardinalCategories = config.extract.allPluralForms
@@ -762,8 +767,26 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
762
767
  else {
763
768
  // Non-primary locale behavior
764
769
  const isVariantKey = key.includes(pluralSeparator) || key.includes(contextSeparator);
770
+ // A plural variant whose category exists in the current locale but not in the
771
+ // primary language (e.g. French `_many` vs English `one`/`other`) will always be
772
+ // absent from the primary file by CLDR design. Treat that absence as expected —
773
+ // not as the primary "diverging" from the default — so --sync-all preserves the
774
+ // locale-specific translation instead of clearing it on every run. (issue #248)
775
+ const isLocaleSpecificPluralVariant = (() => {
776
+ if (!hasCount)
777
+ return false;
778
+ const parts = String(key).split(pluralSeparator);
779
+ if (parts.length < 2)
780
+ return false;
781
+ const lastPart = parts[parts.length - 1];
782
+ if (isOrdinal && parts.length >= 3 && parts[parts.length - 2] === 'ordinal') {
783
+ return !primaryOrdinalCategoriesSet.has(lastPart);
784
+ }
785
+ return !primaryCardinalCategoriesSet.has(lastPart);
786
+ })();
765
787
  const primaryDivergedFromDefault = Boolean(defaultValue$1 &&
766
788
  !primaryShouldPreserveObject &&
789
+ !isLocaleSpecificPluralVariant &&
767
790
  (primaryExistingValue === undefined ||
768
791
  primaryIsStaleObject ||
769
792
  ((!isVariantKey || explicitDefault) &&
@@ -608,7 +608,48 @@ class CallExpressionHandler {
608
608
  return [];
609
609
  }
610
610
  }
611
- let current = body;
611
+ return this.extractKeysFromSelectorExpression(body);
612
+ }
613
+ /**
614
+ * Recursively extracts key paths from a selector-body expression.
615
+ *
616
+ * On top of the straight MemberExpression walk this also descends into
617
+ * branching forms so keys referenced in either branch are preserved
618
+ * (see #247): ternaries (`cond ? $.a : $.b`), nullish/logical
619
+ * short-circuits (`x ?? $.a`, `x && $.a`, `x || $.a`), and common
620
+ * wrapper nodes (parentheses, TS type assertions).
621
+ */
622
+ extractKeysFromSelectorExpression(expr) {
623
+ if (!expr)
624
+ return [];
625
+ // Unwrap wrappers that don't change the expressed key path.
626
+ if (expr.type === 'ParenthesisExpression' ||
627
+ expr.type === 'TsAsExpression' ||
628
+ expr.type === 'TsSatisfiesExpression' ||
629
+ expr.type === 'TsNonNullExpression' ||
630
+ expr.type === 'TsConstAssertion') {
631
+ return this.extractKeysFromSelectorExpression(expr.expression);
632
+ }
633
+ // Ternary: union of both branches.
634
+ if (expr.type === 'ConditionalExpression') {
635
+ return [
636
+ ...this.extractKeysFromSelectorExpression(expr.consequent),
637
+ ...this.extractKeysFromSelectorExpression(expr.alternate),
638
+ ];
639
+ }
640
+ // Short-circuit logicals (`||`, `&&`, `??`): union of both sides. SWC
641
+ // may emit these as LogicalExpression or BinaryExpression depending on
642
+ // version, so accept both.
643
+ if ((expr.type === 'LogicalExpression' || expr.type === 'BinaryExpression') &&
644
+ (expr.operator === '||' || expr.operator === '&&' || expr.operator === '??')) {
645
+ return [
646
+ ...this.extractKeysFromSelectorExpression(expr.left),
647
+ ...this.extractKeysFromSelectorExpression(expr.right),
648
+ ];
649
+ }
650
+ if (expr.type !== 'MemberExpression')
651
+ return [];
652
+ let current = expr;
612
653
  // Each element is an array of possible values for that position in the key path
613
654
  const parts = [];
614
655
  // Recursively walk down MemberExpressions
@@ -73,7 +73,7 @@ class JSXHandler {
73
73
  const where = loc
74
74
  ? `${this.getCurrentFile()}:${loc.line}:${loc.column}`
75
75
  : this.getCurrentFile();
76
- emit(`Error: <${elementName}> child {${name}} at ${where} won't match at runtime — react-i18next inlines the value (e.g. "<1>meow</1>"), but extraction produces "<1>{{${name}}}</1>". Use {{${name}}} (double braces) with values={{ ${name} }} for interpolation, or inline the value if it isn't meant to be translated.`);
76
+ emit(`Error: <${elementName}> child {${name}} at ${where} won't match at runtime — react-i18next inlines the value (e.g. "<1>meow</1>"), but extraction produces "<1>{{${name}}}</1>". Use {{${name}}} (double braces) with values={{ ${name} }} for interpolation.`);
77
77
  }
78
78
  }
79
79
  /**
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.56.4'); // This string is replaced with the actual version at build time by rollup
33
+ .version('1.56.6'); // 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
@@ -222,6 +222,11 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
222
222
  const cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
223
223
  cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
224
224
  ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
225
+ // Plural categories of the primary language — used to recognise locale-specific
226
+ // plural variants (e.g. French `_many` when primary is English) so we don't
227
+ // treat their absence from the primary file as a "divergence" during --sync-all.
228
+ const primaryCardinalCategoriesSet = new Set(safePluralRules(primaryLanguage, { type: 'cardinal' }).resolvedOptions().pluralCategories);
229
+ const primaryOrdinalCategoriesSet = new Set(safePluralRules(primaryLanguage, { type: 'ordinal' }).resolvedOptions().pluralCategories);
225
230
  // When allPluralForms is enabled, compute the union of cardinal categories across all configured locales.
226
231
  // This ensures every locale gets the same set of plural keys — but only the forms actually needed by at least one locale.
227
232
  const allLocalesCardinalCategories = config.extract.allPluralForms
@@ -760,8 +765,26 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
760
765
  else {
761
766
  // Non-primary locale behavior
762
767
  const isVariantKey = key.includes(pluralSeparator) || key.includes(contextSeparator);
768
+ // A plural variant whose category exists in the current locale but not in the
769
+ // primary language (e.g. French `_many` vs English `one`/`other`) will always be
770
+ // absent from the primary file by CLDR design. Treat that absence as expected —
771
+ // not as the primary "diverging" from the default — so --sync-all preserves the
772
+ // locale-specific translation instead of clearing it on every run. (issue #248)
773
+ const isLocaleSpecificPluralVariant = (() => {
774
+ if (!hasCount)
775
+ return false;
776
+ const parts = String(key).split(pluralSeparator);
777
+ if (parts.length < 2)
778
+ return false;
779
+ const lastPart = parts[parts.length - 1];
780
+ if (isOrdinal && parts.length >= 3 && parts[parts.length - 2] === 'ordinal') {
781
+ return !primaryOrdinalCategoriesSet.has(lastPart);
782
+ }
783
+ return !primaryCardinalCategoriesSet.has(lastPart);
784
+ })();
763
785
  const primaryDivergedFromDefault = Boolean(defaultValue &&
764
786
  !primaryShouldPreserveObject &&
787
+ !isLocaleSpecificPluralVariant &&
765
788
  (primaryExistingValue === undefined ||
766
789
  primaryIsStaleObject ||
767
790
  ((!isVariantKey || explicitDefault) &&
@@ -606,7 +606,48 @@ class CallExpressionHandler {
606
606
  return [];
607
607
  }
608
608
  }
609
- let current = body;
609
+ return this.extractKeysFromSelectorExpression(body);
610
+ }
611
+ /**
612
+ * Recursively extracts key paths from a selector-body expression.
613
+ *
614
+ * On top of the straight MemberExpression walk this also descends into
615
+ * branching forms so keys referenced in either branch are preserved
616
+ * (see #247): ternaries (`cond ? $.a : $.b`), nullish/logical
617
+ * short-circuits (`x ?? $.a`, `x && $.a`, `x || $.a`), and common
618
+ * wrapper nodes (parentheses, TS type assertions).
619
+ */
620
+ extractKeysFromSelectorExpression(expr) {
621
+ if (!expr)
622
+ return [];
623
+ // Unwrap wrappers that don't change the expressed key path.
624
+ if (expr.type === 'ParenthesisExpression' ||
625
+ expr.type === 'TsAsExpression' ||
626
+ expr.type === 'TsSatisfiesExpression' ||
627
+ expr.type === 'TsNonNullExpression' ||
628
+ expr.type === 'TsConstAssertion') {
629
+ return this.extractKeysFromSelectorExpression(expr.expression);
630
+ }
631
+ // Ternary: union of both branches.
632
+ if (expr.type === 'ConditionalExpression') {
633
+ return [
634
+ ...this.extractKeysFromSelectorExpression(expr.consequent),
635
+ ...this.extractKeysFromSelectorExpression(expr.alternate),
636
+ ];
637
+ }
638
+ // Short-circuit logicals (`||`, `&&`, `??`): union of both sides. SWC
639
+ // may emit these as LogicalExpression or BinaryExpression depending on
640
+ // version, so accept both.
641
+ if ((expr.type === 'LogicalExpression' || expr.type === 'BinaryExpression') &&
642
+ (expr.operator === '||' || expr.operator === '&&' || expr.operator === '??')) {
643
+ return [
644
+ ...this.extractKeysFromSelectorExpression(expr.left),
645
+ ...this.extractKeysFromSelectorExpression(expr.right),
646
+ ];
647
+ }
648
+ if (expr.type !== 'MemberExpression')
649
+ return [];
650
+ let current = expr;
610
651
  // Each element is an array of possible values for that position in the key path
611
652
  const parts = [];
612
653
  // Recursively walk down MemberExpressions
@@ -71,7 +71,7 @@ class JSXHandler {
71
71
  const where = loc
72
72
  ? `${this.getCurrentFile()}:${loc.line}:${loc.column}`
73
73
  : this.getCurrentFile();
74
- emit(`Error: <${elementName}> child {${name}} at ${where} won't match at runtime — react-i18next inlines the value (e.g. "<1>meow</1>"), but extraction produces "<1>{{${name}}}</1>". Use {{${name}}} (double braces) with values={{ ${name} }} for interpolation, or inline the value if it isn't meant to be translated.`);
74
+ emit(`Error: <${elementName}> child {${name}} at ${where} won't match at runtime — react-i18next inlines the value (e.g. "<1>meow</1>"), but extraction produces "<1>{{${name}}}</1>". Use {{${name}}} (double braces) with values={{ ${name} }} for interpolation.`);
75
75
  }
76
76
  }
77
77
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.56.4",
3
+ "version": "1.56.6",
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;AAohC9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,oBAA4B,EAC5B,MAA4B,EAC7B,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAiK9B"}
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;AA8iC9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,oBAA4B,EAC5B,MAA4B,EAC7B,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAiK9B"}
@@ -67,6 +67,16 @@ export declare class CallExpressionHandler {
67
67
  * @returns Extracted key paths, or empty array if not statically analyzable
68
68
  */
69
69
  private extractKeysFromSelector;
70
+ /**
71
+ * Recursively extracts key paths from a selector-body expression.
72
+ *
73
+ * On top of the straight MemberExpression walk this also descends into
74
+ * branching forms so keys referenced in either branch are preserved
75
+ * (see #247): ternaries (`cond ? $.a : $.b`), nullish/logical
76
+ * short-circuits (`x ?? $.a`, `x && $.a`, `x || $.a`), and common
77
+ * wrapper nodes (parentheses, TS type assertions).
78
+ */
79
+ private extractKeysFromSelectorExpression;
70
80
  /**
71
81
  * Generates plural form keys based on the primary language's plural rules.
72
82
  *
@@ -1 +1 @@
1
- {"version":3,"file":"call-expression-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/call-expression-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6C,MAAM,WAAW,CAAA;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAgB,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1G,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAa7D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,kBAAkB,CAAoB;IACvC,UAAU,cAAoB;IACrC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,iBAAiB,CAAsC;gBAG7D,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM,EAC5B,iBAAiB,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAA2B;IAW3E;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;IA0ZxG;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,wBAAwB;IAyEhC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,uBAAuB;IAgE/B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,gBAAgB;IAyMxB;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;CA2BxB"}
1
+ {"version":3,"file":"call-expression-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/call-expression-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6C,MAAM,WAAW,CAAA;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAgB,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1G,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAa7D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,kBAAkB,CAAoB;IACvC,UAAU,cAAoB;IACrC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,iBAAiB,CAAsC;gBAG7D,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM,EAC5B,iBAAiB,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAA2B;IAW3E;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;IA0ZxG;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,wBAAwB;IAyEhC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,uBAAuB;IAgB/B;;;;;;;;OAQG;IACH,OAAO,CAAC,iCAAiC;IAwFzC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,gBAAgB;IAyMxB;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;CA2BxB"}