i18next-cli 1.34.0 → 1.35.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.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/cli.js +271 -1
  3. package/dist/cjs/config.js +211 -1
  4. package/dist/cjs/extractor/core/ast-visitors.js +372 -1
  5. package/dist/cjs/extractor/core/extractor.js +245 -1
  6. package/dist/cjs/extractor/core/key-finder.js +132 -1
  7. package/dist/cjs/extractor/core/translation-manager.js +745 -1
  8. package/dist/cjs/extractor/parsers/ast-utils.js +85 -1
  9. package/dist/cjs/extractor/parsers/call-expression-handler.js +942 -1
  10. package/dist/cjs/extractor/parsers/comment-parser.js +375 -1
  11. package/dist/cjs/extractor/parsers/expression-resolver.js +362 -1
  12. package/dist/cjs/extractor/parsers/jsx-handler.js +492 -1
  13. package/dist/cjs/extractor/parsers/jsx-parser.js +355 -1
  14. package/dist/cjs/extractor/parsers/scope-manager.js +408 -1
  15. package/dist/cjs/extractor/plugin-manager.js +106 -1
  16. package/dist/cjs/heuristic-config.js +99 -1
  17. package/dist/cjs/index.js +28 -1
  18. package/dist/cjs/init.js +174 -1
  19. package/dist/cjs/linter.js +431 -1
  20. package/dist/cjs/locize.js +269 -1
  21. package/dist/cjs/migrator.js +196 -1
  22. package/dist/cjs/rename-key.js +354 -1
  23. package/dist/cjs/status.js +336 -1
  24. package/dist/cjs/syncer.js +120 -1
  25. package/dist/cjs/types-generator.js +165 -1
  26. package/dist/cjs/utils/default-value.js +43 -1
  27. package/dist/cjs/utils/file-utils.js +136 -1
  28. package/dist/cjs/utils/funnel-msg-tracker.js +75 -1
  29. package/dist/cjs/utils/logger.js +36 -1
  30. package/dist/cjs/utils/nested-object.js +124 -1
  31. package/dist/cjs/utils/validation.js +71 -1
  32. package/dist/esm/cli.js +269 -1
  33. package/dist/esm/config.js +206 -1
  34. package/dist/esm/extractor/core/ast-visitors.js +370 -1
  35. package/dist/esm/extractor/core/extractor.js +241 -1
  36. package/dist/esm/extractor/core/key-finder.js +130 -1
  37. package/dist/esm/extractor/core/translation-manager.js +743 -1
  38. package/dist/esm/extractor/parsers/ast-utils.js +80 -1
  39. package/dist/esm/extractor/parsers/call-expression-handler.js +940 -1
  40. package/dist/esm/extractor/parsers/comment-parser.js +373 -1
  41. package/dist/esm/extractor/parsers/expression-resolver.js +360 -1
  42. package/dist/esm/extractor/parsers/jsx-handler.js +490 -1
  43. package/dist/esm/extractor/parsers/jsx-parser.js +334 -1
  44. package/dist/esm/extractor/parsers/scope-manager.js +406 -1
  45. package/dist/esm/extractor/plugin-manager.js +103 -1
  46. package/dist/esm/heuristic-config.js +97 -1
  47. package/dist/esm/index.js +11 -1
  48. package/dist/esm/init.js +172 -1
  49. package/dist/esm/linter.js +425 -1
  50. package/dist/esm/locize.js +265 -1
  51. package/dist/esm/migrator.js +194 -1
  52. package/dist/esm/rename-key.js +352 -1
  53. package/dist/esm/status.js +334 -1
  54. package/dist/esm/syncer.js +118 -1
  55. package/dist/esm/types-generator.js +163 -1
  56. package/dist/esm/utils/default-value.js +41 -1
  57. package/dist/esm/utils/file-utils.js +131 -1
  58. package/dist/esm/utils/funnel-msg-tracker.js +72 -1
  59. package/dist/esm/utils/logger.js +34 -1
  60. package/dist/esm/utils/nested-object.js +120 -1
  61. package/dist/esm/utils/validation.js +68 -1
  62. package/package.json +2 -2
  63. package/types/extractor/core/ast-visitors.d.ts.map +1 -1
  64. package/types/extractor/parsers/call-expression-handler.d.ts +3 -2
  65. package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
  66. package/types/locize.d.ts.map +1 -1
@@ -1 +1,942 @@
1
- "use strict";var e=require("./ast-utils.js");const t=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");exports.CallExpressionHandler=class{pluginContext;config;logger;expressionResolver;objectKeys=new Set;getCurrentFile;getCurrentCode;lastSearchIndex=0;constructor(e,t,n,r,s,i){this.config=e,this.pluginContext=t,this.logger=n,this.expressionResolver=r,this.getCurrentFile=s,this.getCurrentCode=i}resetSearchIndex(){this.lastSearchIndex=0}getLocationFromNode(e){const t=this.getCurrentCode();let n;if("CallExpression"===e.type&&e.arguments.length>0){const t=e.arguments[0].expression;"StringLiteral"===t.type?n=t.raw??`'${t.value}'`:"TemplateLiteral"===t.type?n="`":"ArrowFunctionExpression"===t.type&&(n="=>")}if(!n)return;const r=t.indexOf(n,this.lastSearchIndex);if(-1===r)return;this.lastSearchIndex=r+n.length;const s=t.substring(0,r).split("\n");return{line:s.length,column:s[s.length-1].length}}handleCallExpression(n,r){const s=this.getFunctionName(n.callee);if(!s)return;const i=r(s),o=this.config.extract.functions||["t","*.t"];let l=void 0!==i;if(!l)for(const e of o)if(e.startsWith("*.")){if(s.endsWith(e.substring(1))){l=!0;break}}else if(e===s){l=!0;break}if(!l||0===n.arguments.length)return;const{keysToProcess:a,isSelectorAPI:c}=this.handleCallExpressionArgument(n,0);if(0===a.length)return;let u=!1;const f=this.config.extract.pluralSeparator??"_";for(let e=0;e<a.length;e++)a[e].endsWith(`${f}ordinal`)&&(u=!0,a[e]=a[e].slice(0,-8));let p,y;if(n.arguments.length>1){const t=n.arguments[1].expression;"ObjectExpression"===t.type?y=t:"StringLiteral"===t.type?p=t.value:"TemplateLiteral"===t.type&&e.isSimpleTemplateLiteral(t)&&(p=t.quasis[0].cooked)}if(n.arguments.length>2){const e=n.arguments[2].expression;"ObjectExpression"===e.type&&(y=e)}const g=y?e.getObjectPropValue(y,"defaultValue"):void 0,h="string"==typeof g?g:p,d=e=>{if(!e||!Array.isArray(e.properties))return!1;for(const t of e.properties)if(t&&"KeyValueProperty"===t.type&&t.key){const e="Identifier"===t.key.type&&t.key.value||"StringLiteral"===t.key.type&&t.key.value;if("string"==typeof e&&e.startsWith("defaultValue"))return!0}return!1},x="string"==typeof h||d(y),$=d(y),k=Boolean($||"string"==typeof h&&!(e=>{if("string"!=typeof e)return!1;const n=this.config.extract.interpolationPrefix??"{{",r=this.config.extract.interpolationSuffix??"}}";return new RegExp(`${t(n)}\\s*count\\s*${t(r)}`).test(e)})(h));for(let t=0;t<a.length;t++){const r=a[t];let s,o=a[t];if(y){const t=e.getObjectPropValue(y,"ns");"string"==typeof t&&(s=t)}const l=this.config.extract.nsSeparator??":";if(!s&&l&&o.includes(l)){const e=o.split(l);if(s=e.shift(),o=e.join(l),!o||""===o.trim()){this.logger.warn(`Skipping key that became empty after namespace removal: '${s}${l}'`);continue}}!s&&i?.defaultNs&&(s=i.defaultNs),s||(s=this.config.extract.defaultNS);let f=o;if(i?.keyPrefix){const e=this.config.extract.keySeparator??".";if(f=!1!==e?i.keyPrefix.endsWith(e)?`${i.keyPrefix}${o}`:`${i.keyPrefix}${e}${o}`:`${i.keyPrefix}${o}`,!1!==e){if(f.split(e).some(e=>""===e.trim())){this.logger.warn(`Skipping key with empty segments: '${f}' (keyPrefix: '${i.keyPrefix}', key: '${o}')`);continue}}}const p=t===a.length-1?"string"==typeof h?h:l&&r.includes(l||":")?r:o:o;if(y){const t=e.getObjectPropValueExpression(y,"context"),n=[];if("StringLiteral"===t?.type||"NumericLiteral"===t?.type||"BooleanLiteral"===t?.type){const e=`${t.value}`,r=this.config.extract.contextSeparator??"_";""!==e&&n.push({key:`${f}${r}${e}`,ns:s,defaultValue:p,explicitDefault:x})}else if(t){const e=this.expressionResolver.resolvePossibleContextStringValues(t),r=this.config.extract.contextSeparator??"_";e.length>0&&e.forEach(e=>{n.push({key:`${f}${r}${e}`,ns:s,defaultValue:p,explicitDefault:x})}),n.push({key:f,ns:s,defaultValue:p,explicitDefault:x,keyAcceptingContext:f})}const r=e=>{if(e){if("KeyValueProperty"===e.type&&e.key){if("Identifier"===e.key.type)return e.key.value;if("StringLiteral"===e.key.type)return e.key.value}return"KeyValueProperty"===e.type&&e.value&&"Identifier"===e.value.type?e.key&&"Identifier"===e.key.type?e.key.value:void 0:"ShorthandProperty"!==e.type&&"Identifier"!==e.type||!e.value?e.key&&"string"==typeof e.key?e.key:void 0:e.value}},i=(()=>{if(!y||!Array.isArray(y.properties))return!1;for(const e of y.properties){if("count"===r(e))return!0}return!1})(),o=(()=>{if(!y||!Array.isArray(y.properties))return!1;for(const e of y.properties){if("ordinal"===r(e))return!("KeyValueProperty"!==e.type||!e.value||"BooleanLiteral"!==e.value.type)&&Boolean(e.value.value)}return!1})();if(i||u){try{const e=u?"ordinal":"cardinal",t=this.config.extract?.primaryLanguage||(Array.isArray(this.config.locales)?this.config.locales[0]:void 0)||"en";let r=!1;try{const n=new Intl.PluralRules(t,{type:e}).resolvedOptions().pluralCategories;1===n.length&&"other"===n[0]&&(r=!0)}catch{}if(!r){const t=new Set;for(const n of this.config.locales)try{new Intl.PluralRules(n,{type:e}).resolvedOptions().pluralCategories.forEach(e=>t.add(e))}catch{new Intl.PluralRules("en",{type:e}).resolvedOptions().pluralCategories.forEach(e=>t.add(e))}const n=Array.from(t).sort();1===n.length&&"other"===n[0]&&(r=!0)}if(r){if(n.length>0)for(const e of n)this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue,hasCount:!0,isOrdinal:u});else this.pluginContext.addKey({key:f,ns:s,defaultValue:p,hasCount:!0,isOrdinal:u});continue}}catch(e){}this.config.extract.disablePlurals?n.length>0?n.forEach(this.pluginContext.addKey):this.pluginContext.addKey({key:f,ns:s,defaultValue:p,explicitDefault:x}):this.handlePluralKeys(f,s,y,o||u,h,k);continue}if(n.length>0){n.forEach(this.pluginContext.addKey);continue}!0===e.getObjectPropValue(y,"returnObjects")&&this.objectKeys.add(f)}c&&this.objectKeys.add(f);{const e=this.getLocationFromNode(n);this.pluginContext.addKey({key:f,ns:s,defaultValue:p,explicitDefault:x,locations:e?[{file:this.getCurrentFile(),line:e.line,column:e.column}]:void 0}),this.extractNestedKeys(f,s)}"string"==typeof h&&this.extractNestedKeys(h,s)}}extractNestedKeys(e,n){if(!e||"string"!=typeof e)return;const r=this.config.extract.nestingPrefix??"$t(",s=this.config.extract.nestingSuffix??")",i=t(r),o=t(s),l=new RegExp(`${i}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${o}`,"g");let a;for(;null!==(a=l.exec(e));)a[1]&&this.processNestedContent(a[1],void 0)}processNestedContent(e,n){let r=e,s="";const i=this.config.extract.nestingOptionsSeparator??",";if(e.indexOf(i)<0)r=e.trim();else{const n=new RegExp(`${t(i)}[ ]*{`),o=e.split(n);if(o.length>1)r=o[0].trim(),s=`{${o.slice(1).join(i+" {")}`;else{const t=e.indexOf(i);r=e.substring(0,t).trim(),s=e.substring(t+1).trim()}}if((r.startsWith("'")&&r.endsWith("'")||r.startsWith('"')&&r.endsWith('"'))&&(r=r.slice(1,-1)),!r)return;let o;const l=this.config.extract.nsSeparator??":";if(l&&r.includes(l)){const e=r.split(l);if(o=e.shift(),r=e.join(l),!r||""===r.trim())return}else o=this.config.extract.defaultNS;let a,c=!1;if(s){/['"]?count['"]?\s*:/.test(s)&&(c=!0);const e=/['"]?context['"]?\s*:\s*(['"])(.*?)\1/.exec(s);e&&(a=e[2])}c||void 0!==a?this.generateNestedPluralKeys(r,o,c,a):this.pluginContext.addKey({key:r,ns:o})}generateNestedPluralKeys(e,t,n,r){try{const s="cardinal";if(!n&&void 0!==r)return this.pluginContext.addKey({key:e,ns:t}),void this.pluginContext.addKey({key:`${e}_${r}`,ns:t});const i=new Set,o=this.config.locales||["en"];for(const e of o)try{const t=new Intl.PluralRules(e,{type:s});t.resolvedOptions().pluralCategories.forEach(e=>i.add(e))}catch(e){const t=new Intl.PluralRules("en",{type:s});t.resolvedOptions().pluralCategories.forEach(e=>i.add(e))}const l=Array.from(i).sort(),a=this.config.extract.pluralSeparator??"_",c=this.config.extract.contextSeparator??"_",u=this.config.extract?.primaryLanguage||(Array.isArray(this.config.locales)?this.config.locales[0]:void 0)||"en";let f=!1;try{const e=new Intl.PluralRules(u,{type:s}).resolvedOptions().pluralCategories;1===e.length&&"other"===e[0]&&(f=!0)}catch{f=!1}const p=f||1===l.length&&"other"===l[0],y=[];if(void 0!==r?y.push({key:e,context:r}):y.push({key:e}),p){for(const{key:e,context:n}of y){const r=n?`${e}${c}${n}`:e;this.pluginContext.addKey({key:r,ns:t,hasCount:!0})}return}for(const{key:e,context:n}of y)for(const r of l){let s;s=n?`${e}${c}${n}${a}${r}`:`${e}${a}${r}`,this.pluginContext.addKey({key:s,ns:t,hasCount:!0})}}catch(n){this.pluginContext.addKey({key:e,ns:t})}}handleCallExpressionArgument(e,t){const n=e.arguments[t].expression,r=[];let s=!1;if("ArrowFunctionExpression"===n.type){const e=this.extractKeyFromSelector(n);e&&(r.push(e),s=!0)}else if("ArrayExpression"===n.type)for(const e of n.elements)e?.expression&&r.push(...this.expressionResolver.resolvePossibleKeyStringValues(e.expression));else r.push(...this.expressionResolver.resolvePossibleKeyStringValues(n));return{keysToProcess:r.filter(e=>!!e),isSelectorAPI:s}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const r=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)r.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;r.unshift(e.expression.value)}n=n.object}if(r.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return r.join(t)}return null}handlePluralKeys(t,n,r,s,i,o){try{const l=s?"ordinal":"cardinal",a=new Set;for(const e of this.config.locales)try{const t=new Intl.PluralRules(e,{type:l});t.resolvedOptions().pluralCategories.forEach(e=>a.add(e))}catch(e){const t=new Intl.PluralRules("en",{type:l});t.resolvedOptions().pluralCategories.forEach(e=>a.add(e))}const c=Array.from(a).sort(),u=this.config.extract.pluralSeparator??"_",f=e.getObjectPropValue(r,"defaultValue"),p=e.getObjectPropValue(r,`defaultValue${u}other`),y=e.getObjectPropValue(r,`defaultValue${u}ordinal${u}other`),g=e.getObjectPropValueExpression(r,"context"),h=[];if(g){const e=this.expressionResolver.resolvePossibleContextStringValues(g);if(e.length>0)if("StringLiteral"===g.type)for(const n of e)n.length>0&&h.push({key:t,context:n});else{for(const n of e)n.length>0&&h.push({key:t,context:n});!1!==this.config.extract?.generateBasePluralForms&&h.push({key:t})}else h.push({key:t})}else h.push({key:t});const d=this.config.extract?.primaryLanguage||(Array.isArray(this.config.locales)?this.config.locales[0]:void 0)||"en";let x=!1;try{const e=new Intl.PluralRules(d,{type:l}).resolvedOptions().pluralCategories;1===e.length&&"other"===e[0]&&(x=!0)}catch{x=!1}if(x||1===c.length&&"other"===c[0]){for(const{key:t,context:l}of h){const a=e.getObjectPropValue(r,`defaultValue${u}other`);let c;c="string"==typeof a?a:"string"==typeof f?f:"string"==typeof i?i:l?`${t}_${l}`:t;const p=this.config.extract.contextSeparator??"_",y=l?`${t}${p}${l}`:t;this.pluginContext.addKey({key:y,ns:n,defaultValue:c,hasCount:!0,isOrdinal:s,explicitDefault:Boolean(o||"string"==typeof a)})}return}for(const{key:l,context:a}of h)for(const g of c){const c=s?`defaultValue${u}ordinal${u}${g}`:`defaultValue${u}${g}`,h=e.getObjectPropValue(r,c);let d,x;if(d="string"==typeof h?h:"one"===g&&"string"==typeof f?f:"one"===g&&"string"==typeof i?i:s&&"string"==typeof y?y:s||"string"!=typeof p?"string"==typeof f?f:"string"==typeof i?i:l:p,a){const e=this.config.extract.contextSeparator??"_";x=s?`${l}${e}${a}${u}ordinal${u}${g}`:`${l}${e}${a}${u}${g}`}else x=s?`${l}${u}ordinal${u}${g}`:`${l}${u}${g}`;this.pluginContext.addKey({key:x,ns:n,defaultValue:d,hasCount:!0,isOrdinal:s,explicitDefault:Boolean(o||"string"==typeof h||"string"==typeof p),keyAcceptingContext:void 0!==a?t:void 0})}}catch(s){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const o=i||e.getObjectPropValue(r,"defaultValue");this.pluginContext.addKey({key:t,ns:n,defaultValue:"string"==typeof o?o:t})}}getFunctionName(e){if("Identifier"===e.type)return e.value;if("MemberExpression"===e.type){const t=[];let n=e;for(;"MemberExpression"===n.type;){if("Identifier"!==n.property.type)return null;t.unshift(n.property.value),n=n.object}if("ThisExpression"===n.type)t.unshift("this");else{if("Identifier"!==n.type)return null;t.unshift(n.value)}return t.join(".")}return null}};
1
+ 'use strict';
2
+
3
+ var astUtils = require('./ast-utils.js');
4
+
5
+ // Helper to escape regex characters
6
+ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ class CallExpressionHandler {
8
+ pluginContext;
9
+ config;
10
+ logger;
11
+ expressionResolver;
12
+ objectKeys = new Set();
13
+ getCurrentFile;
14
+ getCurrentCode;
15
+ lastSearchIndex = 0;
16
+ constructor(config, pluginContext, logger, expressionResolver, getCurrentFile, getCurrentCode) {
17
+ this.config = config;
18
+ this.pluginContext = pluginContext;
19
+ this.logger = logger;
20
+ this.expressionResolver = expressionResolver;
21
+ this.getCurrentFile = getCurrentFile;
22
+ this.getCurrentCode = getCurrentCode;
23
+ }
24
+ /**
25
+ * Reset the search index when starting to process a new file.
26
+ * This should be called before processing each file.
27
+ */
28
+ resetSearchIndex() {
29
+ this.lastSearchIndex = 0;
30
+ }
31
+ /**
32
+ * Helper method to calculate line and column from a position in the code.
33
+ * Uses string searching instead of SWC span offsets to avoid accumulation bugs.
34
+ */
35
+ getLocationFromNode(node) {
36
+ const code = this.getCurrentCode();
37
+ // Extract searchable text from the node
38
+ // For CallExpression and NewExpression, we can search for the key argument
39
+ let searchText;
40
+ if ((node.type === 'CallExpression' || node.type === 'NewExpression') && node.arguments.length > 0) {
41
+ const firstArg = node.arguments[0].expression;
42
+ if (firstArg.type === 'StringLiteral') {
43
+ // Search for the string literal including quotes
44
+ searchText = firstArg.raw ?? `'${firstArg.value}'`;
45
+ }
46
+ else if (firstArg.type === 'TemplateLiteral') {
47
+ // For template literals, search for the backtick
48
+ searchText = '`';
49
+ }
50
+ else if (firstArg.type === 'ArrowFunctionExpression') {
51
+ searchText = '=>';
52
+ }
53
+ }
54
+ if (!searchText)
55
+ return undefined;
56
+ // Search for the text starting from last known position
57
+ const position = code.indexOf(searchText, this.lastSearchIndex);
58
+ if (position === -1) {
59
+ // Not found - might be a parsing issue, skip location tracking
60
+ return undefined;
61
+ }
62
+ // Update last search position for next search
63
+ this.lastSearchIndex = position + searchText.length;
64
+ // Calculate line and column from the position
65
+ const upToPosition = code.substring(0, position);
66
+ const lines = upToPosition.split('\n');
67
+ return {
68
+ line: lines.length,
69
+ column: lines[lines.length - 1].length
70
+ };
71
+ }
72
+ /**
73
+ * Processes function call expressions and new expressions to extract translation keys.
74
+ *
75
+ * This is the core extraction method that handles:
76
+ * - Standard t() calls with string literals
77
+ * - NewExpression calls like new TranslatedError(...)
78
+ * - Selector API calls with arrow functions: `t($ => $.path.to.key)`
79
+ * - Namespace resolution from multiple sources
80
+ * - Default value extraction from various argument patterns
81
+ * - Pluralization and context handling
82
+ * - Key prefix application from scope
83
+ *
84
+ * @param node - Call expression or new expression node to process
85
+ * @param getScopeInfo - Function to retrieve scope information for variables
86
+ */
87
+ handleCallExpression(node, getScopeInfo) {
88
+ const functionName = this.getFunctionName(node.callee);
89
+ if (!functionName)
90
+ return;
91
+ // The scope lookup will only work for simple identifiers, which is okay for this fix.
92
+ const scopeInfo = getScopeInfo(functionName);
93
+ const configuredFunctions = this.config.extract.functions || ['t', '*.t'];
94
+ let isFunctionToParse = scopeInfo !== undefined; // A scoped variable (from useTranslation, etc.) is always parsed.
95
+ if (!isFunctionToParse) {
96
+ for (const pattern of configuredFunctions) {
97
+ if (pattern.startsWith('*.')) {
98
+ // Handle wildcard suffix (e.g., '*.t' matches 'i18n.t')
99
+ if (functionName.endsWith(pattern.substring(1))) {
100
+ isFunctionToParse = true;
101
+ break;
102
+ }
103
+ }
104
+ else {
105
+ // Handle exact match
106
+ if (pattern === functionName) {
107
+ isFunctionToParse = true;
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ if (!isFunctionToParse || node.arguments.length === 0)
114
+ return;
115
+ const { keysToProcess, isSelectorAPI } = this.handleCallExpressionArgument(node, 0);
116
+ if (keysToProcess.length === 0)
117
+ return;
118
+ let isOrdinalByKey = false;
119
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_';
120
+ for (let i = 0; i < keysToProcess.length; i++) {
121
+ if (keysToProcess[i].endsWith(`${pluralSeparator}ordinal`)) {
122
+ isOrdinalByKey = true;
123
+ // Normalize the key by stripping the suffix
124
+ keysToProcess[i] = keysToProcess[i].slice(0, -8);
125
+ }
126
+ }
127
+ let defaultValue;
128
+ let options;
129
+ if (node.arguments.length > 1) {
130
+ const arg2 = node.arguments[1].expression;
131
+ if (arg2.type === 'ObjectExpression') {
132
+ options = arg2;
133
+ }
134
+ else if (arg2.type === 'StringLiteral') {
135
+ defaultValue = arg2.value;
136
+ }
137
+ else if (arg2.type === 'TemplateLiteral' && astUtils.isSimpleTemplateLiteral(arg2)) {
138
+ defaultValue = arg2.quasis[0].cooked;
139
+ }
140
+ }
141
+ if (node.arguments.length > 2) {
142
+ const arg3 = node.arguments[2].expression;
143
+ if (arg3.type === 'ObjectExpression') {
144
+ options = arg3;
145
+ }
146
+ }
147
+ const defaultValueFromOptions = options ? astUtils.getObjectPropValue(options, 'defaultValue') : undefined;
148
+ const finalDefaultValue = (typeof defaultValueFromOptions === 'string' ? defaultValueFromOptions : defaultValue);
149
+ // Helper: detect if options object contains any defaultValue* properties
150
+ const optionsHasDefaultProps = (opts) => {
151
+ if (!opts || !Array.isArray(opts.properties))
152
+ return false;
153
+ for (const p of opts.properties) {
154
+ if (p && p.type === 'KeyValueProperty' && p.key) {
155
+ const keyName = (p.key.type === 'Identifier' && p.key.value) || (p.key.type === 'StringLiteral' && p.key.value);
156
+ if (typeof keyName === 'string' && keyName.startsWith('defaultValue'))
157
+ return true;
158
+ }
159
+ }
160
+ return false;
161
+ };
162
+ // explicit for base key when a string default was provided OR explicit plural defaults are present
163
+ const explicitDefaultForBase = typeof finalDefaultValue === 'string' || optionsHasDefaultProps(options);
164
+ // detect if options contain plural-specific defaultValue_* props
165
+ const explicitPluralDefaultsInOptions = optionsHasDefaultProps(options);
166
+ // If a base default string exists, consider it explicit for plural VARIANTS only when
167
+ // it does NOT contain a count interpolation like '{{count}}' — templates with count
168
+ // are often the runtime interpolation form and should NOT overwrite existing variant forms.
169
+ const containsCountPlaceholder = (s) => {
170
+ if (typeof s !== 'string')
171
+ return false;
172
+ const ip = this.config.extract.interpolationPrefix ?? '{{';
173
+ const is = this.config.extract.interpolationSuffix ?? '}}';
174
+ const re = new RegExp(`${escapeRegex(ip)}\\s*count\\s*${escapeRegex(is)}`);
175
+ return re.test(s);
176
+ };
177
+ const explicitPluralForVariants = Boolean(explicitPluralDefaultsInOptions || (typeof finalDefaultValue === 'string' && !containsCountPlaceholder(finalDefaultValue)));
178
+ // Loop through each key found (could be one or more) and process it
179
+ for (let i = 0; i < keysToProcess.length; i++) {
180
+ const originalKey = keysToProcess[i]; // preserve original (possibly namespaced) form
181
+ let key = keysToProcess[i];
182
+ let ns;
183
+ // Determine namespace (explicit ns > ns:key > scope ns > default)
184
+ // See https://www.i18next.com/overview/api#getfixedt
185
+ if (options) {
186
+ const nsVal = astUtils.getObjectPropValue(options, 'ns');
187
+ if (typeof nsVal === 'string')
188
+ ns = nsVal;
189
+ }
190
+ const nsSeparator = this.config.extract.nsSeparator ?? ':';
191
+ if (!ns && nsSeparator && key.includes(nsSeparator)) {
192
+ const parts = key.split(nsSeparator);
193
+ ns = parts.shift();
194
+ key = parts.join(nsSeparator);
195
+ if (!key || key.trim() === '') {
196
+ this.logger.warn(`Skipping key that became empty after namespace removal: '${ns}${nsSeparator}'`);
197
+ continue;
198
+ }
199
+ }
200
+ if (!ns && scopeInfo?.defaultNs)
201
+ ns = scopeInfo.defaultNs;
202
+ if (!ns)
203
+ ns = this.config.extract.defaultNS;
204
+ let finalKey = key;
205
+ // Apply keyPrefix AFTER namespace extraction
206
+ if (scopeInfo?.keyPrefix) {
207
+ const keySeparator = this.config.extract.keySeparator ?? '.';
208
+ // Apply keyPrefix - handle case where keyPrefix already ends with separator
209
+ if (keySeparator !== false) {
210
+ if (scopeInfo.keyPrefix.endsWith(keySeparator)) {
211
+ finalKey = `${scopeInfo.keyPrefix}${key}`;
212
+ }
213
+ else {
214
+ finalKey = `${scopeInfo.keyPrefix}${keySeparator}${key}`;
215
+ }
216
+ }
217
+ else {
218
+ finalKey = `${scopeInfo.keyPrefix}${key}`;
219
+ }
220
+ // Validate keyPrefix combinations that create problematic keys
221
+ if (keySeparator !== false) {
222
+ // Check for patterns that would create empty segments in the nested key structure
223
+ const segments = finalKey.split(keySeparator);
224
+ const hasEmptySegment = segments.some(segment => segment.trim() === '');
225
+ if (hasEmptySegment) {
226
+ this.logger.warn(`Skipping key with empty segments: '${finalKey}' (keyPrefix: '${scopeInfo.keyPrefix}', key: '${key}')`);
227
+ continue;
228
+ }
229
+ }
230
+ }
231
+ const isLastKey = i === keysToProcess.length - 1;
232
+ // Use the original (possibly namespaced) key as the default when no explicit
233
+ // default was provided and the source key contained a namespace prefix.
234
+ const dv = isLastKey
235
+ ? (typeof finalDefaultValue === 'string'
236
+ ? finalDefaultValue
237
+ : (nsSeparator && originalKey.includes(nsSeparator || ':') ? originalKey : key))
238
+ : key;
239
+ // Handle plurals, context, and returnObjects
240
+ if (options) {
241
+ const contextPropValue = astUtils.getObjectPropValueExpression(options, 'context');
242
+ const keysWithContext = [];
243
+ // 1. Handle Context
244
+ if (contextPropValue?.type === 'StringLiteral' || contextPropValue?.type === 'NumericLiteral' || contextPropValue?.type === 'BooleanLiteral') {
245
+ // If the context is static, we don't need to add the base key
246
+ const contextValue = `${contextPropValue.value}`;
247
+ const contextSeparator = this.config.extract.contextSeparator ?? '_';
248
+ // Ignore context: ''
249
+ if (contextValue !== '') {
250
+ keysWithContext.push({ key: `${finalKey}${contextSeparator}${contextValue}`, ns, defaultValue: dv, explicitDefault: explicitDefaultForBase });
251
+ }
252
+ }
253
+ else if (contextPropValue) {
254
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextPropValue);
255
+ const contextSeparator = this.config.extract.contextSeparator ?? '_';
256
+ if (contextValues.length > 0) {
257
+ contextValues.forEach(context => {
258
+ keysWithContext.push({ key: `${finalKey}${contextSeparator}${context}`, ns, defaultValue: dv, explicitDefault: explicitDefaultForBase });
259
+ });
260
+ }
261
+ // For dynamic context, also add the base key as a fallback
262
+ keysWithContext.push({
263
+ key: finalKey,
264
+ ns,
265
+ defaultValue: dv,
266
+ explicitDefault: explicitDefaultForBase,
267
+ keyAcceptingContext: finalKey
268
+ });
269
+ }
270
+ // 2. Handle Plurals
271
+ // Robust detection for `{ count }`, `{ count: x }`, `{ 'count': x }` etc.
272
+ // Support KeyValueProperty and common shorthand forms that SWC may emit.
273
+ const propNameFromNode = (p) => {
274
+ if (!p)
275
+ return undefined;
276
+ // Standard key:value property
277
+ if (p.type === 'KeyValueProperty' && p.key) {
278
+ if (p.key.type === 'Identifier')
279
+ return p.key.value;
280
+ if (p.key.type === 'StringLiteral')
281
+ return p.key.value;
282
+ }
283
+ // SWC may represent shorthand properties differently (no explicit key node).
284
+ // Try common shapes: property with `value` being an Identifier (shorthand).
285
+ if (p.type === 'KeyValueProperty' && p.value && p.value.type === 'Identifier') {
286
+ // e.g. { count: count } - already covered above, but keep safe fallback
287
+ return p.key && p.key.type === 'Identifier' ? p.key.value : undefined;
288
+ }
289
+ // Some AST variants use 'ShorthandProperty' or keep the Identifier directly.
290
+ if ((p.type === 'ShorthandProperty' || p.type === 'Identifier') && p.value) {
291
+ return p.value;
292
+ }
293
+ // Fallback: if node has an 'id' or 'key' string value
294
+ if (p.key && typeof p.key === 'string')
295
+ return p.key;
296
+ return undefined;
297
+ };
298
+ const hasCount = (() => {
299
+ if (!options || !Array.isArray(options.properties))
300
+ return false;
301
+ for (const p of options.properties) {
302
+ const name = propNameFromNode(p);
303
+ if (name === 'count')
304
+ return true;
305
+ }
306
+ return false;
307
+ })();
308
+ const isOrdinalByOption = (() => {
309
+ if (!options || !Array.isArray(options.properties))
310
+ return false;
311
+ for (const p of options.properties) {
312
+ const name = propNameFromNode(p);
313
+ if (name === 'ordinal') {
314
+ // If it's a key:value pair with a BooleanLiteral true, respect it.
315
+ if (p.type === 'KeyValueProperty' && p.value && p.value.type === 'BooleanLiteral') {
316
+ return Boolean(p.value.value);
317
+ }
318
+ // shorthand `ordinal` without explicit true -> treat as false
319
+ return false;
320
+ }
321
+ }
322
+ return false;
323
+ })();
324
+ if (hasCount || isOrdinalByKey) {
325
+ // QUICK PATH: If ALL target locales only have the "other" category,
326
+ // emit base/context keys directly (avoid generating *_other). This
327
+ // mirrors the special-case in handlePluralKeys but is placed here as a
328
+ // defensive guard to ensure keys are always emitted.
329
+ try {
330
+ const typeForCheck = isOrdinalByKey ? 'ordinal' : 'cardinal';
331
+ // Prefer the configured primaryLanguage as the deciding signal for
332
+ // "single-other" languages (ja/zh/ko). Fall back to union of locales.
333
+ const primaryLang = this.config.extract?.primaryLanguage || (Array.isArray(this.config.locales) ? this.config.locales[0] : undefined) || 'en';
334
+ let isSingleOther = false;
335
+ try {
336
+ const primaryCategories = new Intl.PluralRules(primaryLang, { type: typeForCheck }).resolvedOptions().pluralCategories;
337
+ if (primaryCategories.length === 1 && primaryCategories[0] === 'other') {
338
+ isSingleOther = true;
339
+ }
340
+ }
341
+ catch {
342
+ // ignore and fall back to union-of-locales check below
343
+ }
344
+ if (!isSingleOther) {
345
+ const allPluralCategoriesCheck = new Set();
346
+ for (const locale of this.config.locales) {
347
+ try {
348
+ const rules = new Intl.PluralRules(locale, { type: typeForCheck });
349
+ rules.resolvedOptions().pluralCategories.forEach(c => allPluralCategoriesCheck.add(c));
350
+ }
351
+ catch {
352
+ new Intl.PluralRules('en', { type: typeForCheck }).resolvedOptions().pluralCategories.forEach(c => allPluralCategoriesCheck.add(c));
353
+ }
354
+ }
355
+ const pluralCategoriesCheck = Array.from(allPluralCategoriesCheck).sort();
356
+ if (pluralCategoriesCheck.length === 1 && pluralCategoriesCheck[0] === 'other') {
357
+ isSingleOther = true;
358
+ }
359
+ }
360
+ if (isSingleOther) {
361
+ // Emit only base/context keys (no _other) and skip the heavy plural path.
362
+ if (keysWithContext.length > 0) {
363
+ for (const k of keysWithContext) {
364
+ this.pluginContext.addKey({
365
+ key: k.key,
366
+ ns: k.ns,
367
+ defaultValue: k.defaultValue,
368
+ hasCount: true,
369
+ isOrdinal: isOrdinalByKey
370
+ });
371
+ }
372
+ }
373
+ else {
374
+ this.pluginContext.addKey({
375
+ key: finalKey,
376
+ ns,
377
+ defaultValue: dv,
378
+ hasCount: true,
379
+ isOrdinal: isOrdinalByKey
380
+ });
381
+ }
382
+ continue;
383
+ }
384
+ }
385
+ catch (e) {
386
+ // Ignore Intl failures here and fall through to normal logic
387
+ }
388
+ // Check if plurals are disabled
389
+ if (this.config.extract.disablePlurals) {
390
+ // When plurals are disabled, treat count as a regular option (for interpolation only)
391
+ // Still handle context normally
392
+ if (keysWithContext.length > 0) {
393
+ keysWithContext.forEach(this.pluginContext.addKey);
394
+ }
395
+ else {
396
+ this.pluginContext.addKey({ key: finalKey, ns, defaultValue: dv, explicitDefault: explicitDefaultForBase });
397
+ }
398
+ }
399
+ else {
400
+ // Original plural handling logic when plurals are enabled
401
+ // Always pass the base key to handlePluralKeys - it will handle context internally.
402
+ // Pass explicitDefaultForBase so that when a call-site provided an explicit
403
+ // base default (e.g. t('key', 'Default', { count })), plural variant keys
404
+ // are treated as explicit and may be synced to that default.
405
+ this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey, finalDefaultValue, explicitPluralForVariants);
406
+ }
407
+ continue; // This key is fully handled
408
+ }
409
+ if (keysWithContext.length > 0) {
410
+ keysWithContext.forEach(this.pluginContext.addKey);
411
+ continue; // This key is now fully handled
412
+ }
413
+ // 3. Handle returnObjects
414
+ if (astUtils.getObjectPropValue(options, 'returnObjects') === true) {
415
+ this.objectKeys.add(finalKey);
416
+ // Fall through to add the base key itself
417
+ }
418
+ }
419
+ // 4. Handle selector API as implicit returnObjects
420
+ if (isSelectorAPI) {
421
+ this.objectKeys.add(finalKey);
422
+ // Fall through to add the base key itself
423
+ }
424
+ // 5. Default case: Add the simple key
425
+ {
426
+ // ✅ Use the helper method to find location by searching the code
427
+ const location = this.getLocationFromNode(node);
428
+ this.pluginContext.addKey({
429
+ key: finalKey,
430
+ ns,
431
+ defaultValue: dv,
432
+ explicitDefault: explicitDefaultForBase,
433
+ locations: location
434
+ ? [{
435
+ file: this.getCurrentFile(),
436
+ line: location.line,
437
+ column: location.column
438
+ }]
439
+ : undefined
440
+ });
441
+ // Check for nested translations in the key itself
442
+ this.extractNestedKeys(finalKey, ns);
443
+ }
444
+ // Check for nested translations in the default value
445
+ if (typeof finalDefaultValue === 'string') {
446
+ this.extractNestedKeys(finalDefaultValue, ns);
447
+ }
448
+ }
449
+ }
450
+ /**
451
+ * Scans a string for nested translations like $t(key, options) and extracts them.
452
+ */
453
+ extractNestedKeys(text, ns) {
454
+ if (!text || typeof text !== 'string')
455
+ return;
456
+ const prefix = this.config.extract.nestingPrefix ?? '$t(';
457
+ const suffix = this.config.extract.nestingSuffix ?? ')';
458
+ const escapedPrefix = escapeRegex(prefix);
459
+ const escapedSuffix = escapeRegex(suffix);
460
+ // Regex adapted from i18next Interpolator.js
461
+ // Matches nested calls like $t(key) or $t(key, { options })
462
+ // It handles balanced parentheses to some extent and quoted strings
463
+ const nestingRegexp = new RegExp(`${escapedPrefix}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${escapedSuffix}`, 'g');
464
+ let match;
465
+ while ((match = nestingRegexp.exec(text)) !== null) {
466
+ if (match[1]) {
467
+ // Do NOT trust the outer `ns` blindly — compute namespace from the nested key itself
468
+ // inside processNestedContent. Pass `undefined` so processNestedContent resolves ns
469
+ // deterministically (either from key "ns:key" or from defaultNS).
470
+ this.processNestedContent(match[1], undefined);
471
+ }
472
+ }
473
+ }
474
+ processNestedContent(content, ns) {
475
+ let key = content;
476
+ let optionsString = '';
477
+ const separator = this.config.extract.nestingOptionsSeparator ?? ',';
478
+ // Logic adapted from i18next Interpolator.js handleHasOptions
479
+ if (content.indexOf(separator) < 0) {
480
+ key = content.trim();
481
+ }
482
+ else {
483
+ // Split by separator, but be careful about objects
484
+ // i18next does: const c = key.split(new RegExp(`${sep}[ ]*{`));
485
+ // This assumes options start with {
486
+ const sepRegex = new RegExp(`${escapeRegex(separator)}[ ]*{`);
487
+ const parts = content.split(sepRegex);
488
+ if (parts.length > 1) {
489
+ key = parts[0].trim();
490
+ // Reconstruct the options part: add back the '{' that was consumed by split
491
+ optionsString = `{${parts.slice(1).join(separator + ' {')}`;
492
+ }
493
+ else {
494
+ // Fallback for simple split if no object pattern found
495
+ const sepIdx = content.indexOf(separator);
496
+ key = content.substring(0, sepIdx).trim();
497
+ optionsString = content.substring(sepIdx + 1).trim();
498
+ }
499
+ }
500
+ // Remove quotes from key if present
501
+ if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith('"') && key.endsWith('"'))) {
502
+ key = key.slice(1, -1);
503
+ }
504
+ if (!key)
505
+ return;
506
+ // Resolve namespace for the nested key:
507
+ // If nested key contains nsSeparator (e.g. "ns:key"), extract namespace,
508
+ // otherwise use configured defaultNS.
509
+ let nestedNs;
510
+ const nsSeparator = this.config.extract.nsSeparator ?? ':';
511
+ if (nsSeparator && key.includes(nsSeparator)) {
512
+ const parts = key.split(nsSeparator);
513
+ nestedNs = parts.shift();
514
+ key = parts.join(nsSeparator);
515
+ if (!key || key.trim() === '')
516
+ return;
517
+ }
518
+ else {
519
+ nestedNs = this.config.extract.defaultNS;
520
+ }
521
+ let hasCount = false;
522
+ let context;
523
+ if (optionsString) {
524
+ // Simple regex check for count and context in the options string
525
+ // This is an approximation since we don't have a full JSON parser here that handles JS objects perfectly
526
+ // but it should cover most static cases.
527
+ // Check for count: ...
528
+ if (/['"]?count['"]?\s*:/.test(optionsString)) {
529
+ hasCount = true;
530
+ }
531
+ // Check for context: ...
532
+ const contextMatch = /['"]?context['"]?\s*:\s*(['"])(.*?)\1/.exec(optionsString);
533
+ if (contextMatch) {
534
+ context = contextMatch[2];
535
+ }
536
+ }
537
+ if (hasCount || context !== undefined) {
538
+ this.generateNestedPluralKeys(key, nestedNs, hasCount, context);
539
+ }
540
+ else {
541
+ this.pluginContext.addKey({ key, ns: nestedNs });
542
+ }
543
+ }
544
+ generateNestedPluralKeys(key, ns, hasCount, context) {
545
+ try {
546
+ const type = 'cardinal';
547
+ // If only context, no plural
548
+ if (!hasCount && context !== undefined) {
549
+ this.pluginContext.addKey({ key, ns });
550
+ this.pluginContext.addKey({ key: `${key}_${context}`, ns });
551
+ return;
552
+ }
553
+ // If hasCount, generate plurals
554
+ const allPluralCategories = new Set();
555
+ const locales = this.config.locales || ['en'];
556
+ for (const locale of locales) {
557
+ try {
558
+ const pluralRules = new Intl.PluralRules(locale, { type });
559
+ const categories = pluralRules.resolvedOptions().pluralCategories;
560
+ categories.forEach(cat => allPluralCategories.add(cat));
561
+ }
562
+ catch (e) {
563
+ const englishRules = new Intl.PluralRules('en', { type });
564
+ const categories = englishRules.resolvedOptions().pluralCategories;
565
+ categories.forEach(cat => allPluralCategories.add(cat));
566
+ }
567
+ }
568
+ const pluralCategories = Array.from(allPluralCategories).sort();
569
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_';
570
+ const contextSeparator = this.config.extract.contextSeparator ?? '_';
571
+ const primaryLang = this.config.extract?.primaryLanguage || (Array.isArray(this.config.locales) ? this.config.locales[0] : undefined) || 'en';
572
+ let primaryIsSingleOther = false;
573
+ try {
574
+ const primaryCats = new Intl.PluralRules(primaryLang, { type }).resolvedOptions().pluralCategories;
575
+ if (primaryCats.length === 1 && primaryCats[0] === 'other')
576
+ primaryIsSingleOther = true;
577
+ }
578
+ catch {
579
+ primaryIsSingleOther = false;
580
+ }
581
+ const isSingleOther = primaryIsSingleOther || (pluralCategories.length === 1 && pluralCategories[0] === 'other');
582
+ const keysToGenerate = [];
583
+ if (context !== undefined) {
584
+ keysToGenerate.push({ key, context });
585
+ }
586
+ else {
587
+ keysToGenerate.push({ key });
588
+ }
589
+ if (isSingleOther) {
590
+ for (const { key: baseKey, context } of keysToGenerate) {
591
+ const finalKey = context ? `${baseKey}${contextSeparator}${context}` : baseKey;
592
+ this.pluginContext.addKey({ key: finalKey, ns, hasCount: true });
593
+ }
594
+ return;
595
+ }
596
+ for (const { key: baseKey, context } of keysToGenerate) {
597
+ for (const category of pluralCategories) {
598
+ let finalKey;
599
+ if (context) {
600
+ finalKey = `${baseKey}${contextSeparator}${context}${pluralSeparator}${category}`;
601
+ }
602
+ else {
603
+ finalKey = `${baseKey}${pluralSeparator}${category}`;
604
+ }
605
+ this.pluginContext.addKey({ key: finalKey, ns, hasCount: true });
606
+ }
607
+ }
608
+ }
609
+ catch (e) {
610
+ this.pluginContext.addKey({ key, ns });
611
+ }
612
+ }
613
+ /**
614
+ * Processed a call expression to extract keys from the specified argument.
615
+ *
616
+ * @param node - The call expression node
617
+ * @param argIndex - The index of the argument to process
618
+ * @returns An object containing the keys to process and a flag indicating if the selector API is used
619
+ */
620
+ handleCallExpressionArgument(node, argIndex) {
621
+ const firstArg = node.arguments[argIndex].expression;
622
+ const keysToProcess = [];
623
+ let isSelectorAPI = false;
624
+ if (firstArg.type === 'ArrowFunctionExpression') {
625
+ const key = this.extractKeyFromSelector(firstArg);
626
+ if (key) {
627
+ keysToProcess.push(key);
628
+ isSelectorAPI = true;
629
+ }
630
+ }
631
+ else if (firstArg.type === 'ArrayExpression') {
632
+ for (const element of firstArg.elements) {
633
+ if (element?.expression) {
634
+ keysToProcess.push(...this.expressionResolver.resolvePossibleKeyStringValues(element.expression));
635
+ }
636
+ }
637
+ }
638
+ else {
639
+ keysToProcess.push(...this.expressionResolver.resolvePossibleKeyStringValues(firstArg));
640
+ }
641
+ return {
642
+ keysToProcess: keysToProcess.filter((key) => !!key),
643
+ isSelectorAPI,
644
+ };
645
+ }
646
+ /**
647
+ * Extracts translation key from selector API arrow function.
648
+ *
649
+ * Processes selector expressions like:
650
+ * - `$ => $.path.to.key` → 'path.to.key'
651
+ * - `$ => $.app['title'].main` → 'app.title.main'
652
+ * - `$ => { return $.nested.key; }` → 'nested.key'
653
+ *
654
+ * Handles both dot notation and bracket notation, respecting
655
+ * the configured key separator or flat key structure.
656
+ *
657
+ * @param node - Arrow function expression from selector call
658
+ * @returns Extracted key path or null if not statically analyzable
659
+ */
660
+ extractKeyFromSelector(node) {
661
+ let body = node.body;
662
+ // Handle block bodies, e.g., $ => { return $.key; }
663
+ if (body.type === 'BlockStatement') {
664
+ const returnStmt = body.stmts.find(s => s.type === 'ReturnStatement');
665
+ if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
666
+ body = returnStmt.argument;
667
+ }
668
+ else {
669
+ return null;
670
+ }
671
+ }
672
+ let current = body;
673
+ const parts = [];
674
+ // Recursively walk down MemberExpressions
675
+ while (current.type === 'MemberExpression') {
676
+ const prop = current.property;
677
+ if (prop.type === 'Identifier') {
678
+ // This handles dot notation: .key
679
+ parts.unshift(prop.value);
680
+ }
681
+ else if (prop.type === 'Computed' && prop.expression.type === 'StringLiteral') {
682
+ // This handles bracket notation: ['key']
683
+ parts.unshift(prop.expression.value);
684
+ }
685
+ else {
686
+ // This is a dynamic property like [myVar] or a private name, which we cannot resolve.
687
+ return null;
688
+ }
689
+ current = current.object;
690
+ }
691
+ if (parts.length > 0) {
692
+ const keySeparator = this.config.extract.keySeparator;
693
+ const joiner = typeof keySeparator === 'string' ? keySeparator : '.';
694
+ return parts.join(joiner);
695
+ }
696
+ return null;
697
+ }
698
+ /**
699
+ * Generates plural form keys based on the primary language's plural rules.
700
+ *
701
+ * Uses Intl.PluralRules to determine the correct plural categories
702
+ * for the configured primary language and generates suffixed keys
703
+ * for each category (e.g., 'item_one', 'item_other').
704
+ *
705
+ * @param key - Base key name for pluralization
706
+ * @param ns - Namespace for the keys
707
+ * @param options - object expression options
708
+ * @param isOrdinal - isOrdinal flag
709
+ */
710
+ handlePluralKeys(key, ns, options, isOrdinal, defaultValueFromCall, explicitDefaultFromSource) {
711
+ try {
712
+ const type = isOrdinal ? 'ordinal' : 'cardinal';
713
+ // Generate plural forms for ALL target languages to ensure we have all necessary keys
714
+ const allPluralCategories = new Set();
715
+ for (const locale of this.config.locales) {
716
+ try {
717
+ const pluralRules = new Intl.PluralRules(locale, { type });
718
+ const categories = pluralRules.resolvedOptions().pluralCategories;
719
+ categories.forEach(cat => allPluralCategories.add(cat));
720
+ }
721
+ catch (e) {
722
+ // If a locale is invalid, fall back to English rules
723
+ const englishRules = new Intl.PluralRules('en', { type });
724
+ const categories = englishRules.resolvedOptions().pluralCategories;
725
+ categories.forEach(cat => allPluralCategories.add(cat));
726
+ }
727
+ }
728
+ const pluralCategories = Array.from(allPluralCategories).sort();
729
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_';
730
+ // Get all possible default values once at the start
731
+ const defaultValue = astUtils.getObjectPropValue(options, 'defaultValue');
732
+ const otherDefault = astUtils.getObjectPropValue(options, `defaultValue${pluralSeparator}other`);
733
+ const ordinalOtherDefault = astUtils.getObjectPropValue(options, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`);
734
+ // Handle context - both static and dynamic
735
+ const contextPropValue = astUtils.getObjectPropValueExpression(options, 'context');
736
+ const keysToGenerate = [];
737
+ if (contextPropValue) {
738
+ // Handle dynamic context by resolving all possible values
739
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextPropValue);
740
+ if (contextValues.length > 0) {
741
+ // For static context (string literal), only generate context variants
742
+ if (contextPropValue.type === 'StringLiteral') {
743
+ // Only generate context-specific plural forms, no base forms
744
+ for (const contextValue of contextValues) {
745
+ if (contextValue.length > 0) {
746
+ keysToGenerate.push({ key, context: contextValue });
747
+ }
748
+ }
749
+ }
750
+ else {
751
+ // For dynamic context, generate context variants AND base forms
752
+ for (const contextValue of contextValues) {
753
+ if (contextValue.length > 0) {
754
+ keysToGenerate.push({ key, context: contextValue });
755
+ }
756
+ }
757
+ // Only generate base plural forms if generateBasePluralForms is not disabled
758
+ const shouldGenerateBaseForms = this.config.extract?.generateBasePluralForms !== false;
759
+ if (shouldGenerateBaseForms) {
760
+ keysToGenerate.push({ key });
761
+ }
762
+ }
763
+ }
764
+ else {
765
+ // Couldn't resolve context, fall back to base key only
766
+ keysToGenerate.push({ key });
767
+ }
768
+ }
769
+ else {
770
+ // No context, always generate base plural forms
771
+ keysToGenerate.push({ key });
772
+ }
773
+ // If the only plural category across configured locales is "other",
774
+ // prefer the base key (no "_other" suffix) as it's more natural for languages
775
+ // with no grammatical plural forms (ja/zh/ko).
776
+ // Prefer the configured primaryLanguage as signal for single-"other" languages.
777
+ // If primaryLanguage indicates single-"other", treat as that case; otherwise
778
+ // fall back to earlier union-of-locales check that produced `pluralCategories`.
779
+ const primaryLang = this.config.extract?.primaryLanguage || (Array.isArray(this.config.locales) ? this.config.locales[0] : undefined) || 'en';
780
+ let primaryIsSingleOther = false;
781
+ try {
782
+ const primaryCats = new Intl.PluralRules(primaryLang, { type }).resolvedOptions().pluralCategories;
783
+ if (primaryCats.length === 1 && primaryCats[0] === 'other')
784
+ primaryIsSingleOther = true;
785
+ }
786
+ catch {
787
+ primaryIsSingleOther = false;
788
+ }
789
+ if (primaryIsSingleOther || (pluralCategories.length === 1 && pluralCategories[0] === 'other')) {
790
+ for (const { key: baseKey, context } of keysToGenerate) {
791
+ const specificOther = astUtils.getObjectPropValue(options, `defaultValue${pluralSeparator}other`);
792
+ // Final default resolution:
793
+ // 1) plural-specific defaultValue_other
794
+ // 2) general defaultValue (from options)
795
+ // 3) defaultValueFromCall (string arg)
796
+ // 4) fallback to key (or context-key for context variants)
797
+ let finalDefaultValue;
798
+ if (typeof specificOther === 'string') {
799
+ finalDefaultValue = specificOther;
800
+ }
801
+ else if (typeof defaultValue === 'string') {
802
+ finalDefaultValue = defaultValue;
803
+ }
804
+ else if (typeof defaultValueFromCall === 'string') {
805
+ finalDefaultValue = defaultValueFromCall;
806
+ }
807
+ else {
808
+ finalDefaultValue = context ? `${baseKey}_${context}` : baseKey;
809
+ }
810
+ const ctxSep = this.config.extract.contextSeparator ?? '_';
811
+ const finalKey = context ? `${baseKey}${ctxSep}${context}` : baseKey;
812
+ this.pluginContext.addKey({
813
+ key: finalKey,
814
+ ns,
815
+ defaultValue: finalDefaultValue,
816
+ hasCount: true,
817
+ isOrdinal,
818
+ explicitDefault: Boolean(explicitDefaultFromSource || typeof specificOther === 'string')
819
+ });
820
+ }
821
+ return;
822
+ }
823
+ // Generate plural forms for each key variant
824
+ for (const { key: baseKey, context } of keysToGenerate) {
825
+ for (const category of pluralCategories) {
826
+ // 1. Look for the most specific default value
827
+ const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`;
828
+ const specificDefault = astUtils.getObjectPropValue(options, specificDefaultKey);
829
+ // 2. Determine the final default value using the ORIGINAL fallback chain with corrections
830
+ let finalDefaultValue;
831
+ if (typeof specificDefault === 'string') {
832
+ // Most specific: defaultValue_one, defaultValue_ordinal_other, etc.
833
+ finalDefaultValue = specificDefault;
834
+ }
835
+ else if (category === 'one' && typeof defaultValue === 'string') {
836
+ // For "one" category, prefer the general defaultValue
837
+ finalDefaultValue = defaultValue;
838
+ }
839
+ else if (category === 'one' && typeof defaultValueFromCall === 'string') {
840
+ // For "one" category, also consider defaultValueFromCall
841
+ finalDefaultValue = defaultValueFromCall;
842
+ }
843
+ else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
844
+ // For ordinals (non-one categories), fall back to ordinal_other
845
+ finalDefaultValue = ordinalOtherDefault;
846
+ }
847
+ else if (!isOrdinal && typeof otherDefault === 'string') {
848
+ // For cardinals (non-one categories), fall back to _other
849
+ finalDefaultValue = otherDefault;
850
+ }
851
+ else if (typeof defaultValue === 'string') {
852
+ // General defaultValue as fallback
853
+ finalDefaultValue = defaultValue;
854
+ }
855
+ else if (typeof defaultValueFromCall === 'string') {
856
+ // defaultValueFromCall as fallback
857
+ finalDefaultValue = defaultValueFromCall;
858
+ }
859
+ else {
860
+ // Final fallback to the base key itself
861
+ finalDefaultValue = baseKey;
862
+ }
863
+ // 3. Construct the final plural key
864
+ let finalKey;
865
+ if (context) {
866
+ const contextSeparator = this.config.extract.contextSeparator ?? '_';
867
+ finalKey = isOrdinal
868
+ ? `${baseKey}${contextSeparator}${context}${pluralSeparator}ordinal${pluralSeparator}${category}`
869
+ : `${baseKey}${contextSeparator}${context}${pluralSeparator}${category}`;
870
+ }
871
+ else {
872
+ finalKey = isOrdinal
873
+ ? `${baseKey}${pluralSeparator}ordinal${pluralSeparator}${category}`
874
+ : `${baseKey}${pluralSeparator}${category}`;
875
+ }
876
+ this.pluginContext.addKey({
877
+ key: finalKey,
878
+ ns,
879
+ defaultValue: finalDefaultValue,
880
+ hasCount: true,
881
+ isOrdinal,
882
+ // Only treat plural/context variant as explicit when:
883
+ // - the extractor marked the source as explicitly providing plural defaults
884
+ // - OR a plural-specific default was provided in the options (specificDefault/otherDefault)
885
+ // Do NOT treat the presence of a general base defaultValueFromCall as making variants explicit.
886
+ explicitDefault: Boolean(explicitDefaultFromSource || typeof specificDefault === 'string' || typeof otherDefault === 'string'),
887
+ // If this is a context variant, track the base key (without context or plural suffixes)
888
+ keyAcceptingContext: context !== undefined ? key : undefined
889
+ });
890
+ }
891
+ }
892
+ }
893
+ catch (e) {
894
+ this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);
895
+ // Fallback to a simple key if Intl API fails
896
+ const defaultValue = defaultValueFromCall || astUtils.getObjectPropValue(options, 'defaultValue');
897
+ this.pluginContext.addKey({ key, ns, defaultValue: typeof defaultValue === 'string' ? defaultValue : key });
898
+ }
899
+ }
900
+ /**
901
+ * Serializes a callee node (Identifier or MemberExpression) into a string.
902
+ *
903
+ * Produces a dotted name for simple callees that can be used for scope lookups
904
+ * or configuration matching.
905
+ *
906
+ * @param callee - The CallExpression callee node to serialize
907
+ * @returns A dotted string name for supported callees, or null when the callee
908
+ * is a computed/unsupported expression.
909
+ */
910
+ getFunctionName(callee) {
911
+ if (callee.type === 'Identifier') {
912
+ return callee.value;
913
+ }
914
+ if (callee.type === 'MemberExpression') {
915
+ const parts = [];
916
+ let current = callee;
917
+ while (current.type === 'MemberExpression') {
918
+ if (current.property.type === 'Identifier') {
919
+ parts.unshift(current.property.value);
920
+ }
921
+ else {
922
+ return null; // Cannot handle computed properties like i18n['t']
923
+ }
924
+ current = current.object;
925
+ }
926
+ // Handle `this` as the base of the expression (e.g., this._i18n.t)
927
+ if (current.type === 'ThisExpression') {
928
+ parts.unshift('this');
929
+ }
930
+ else if (current.type === 'Identifier') {
931
+ parts.unshift(current.value);
932
+ }
933
+ else {
934
+ return null; // Base of the expression is not a simple identifier
935
+ }
936
+ return parts.join('.');
937
+ }
938
+ return null;
939
+ }
940
+ }
941
+
942
+ exports.CallExpressionHandler = CallExpressionHandler;