i18next-cli 1.34.0 → 1.34.1

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 (63) 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 +364 -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 +941 -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 +362 -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 +939 -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/locize.d.ts.map +1 -1
@@ -1 +1,492 @@
1
- "use strict";var e=require("./jsx-parser.js"),t=require("./ast-utils.js");exports.JSXHandler=class{config;pluginContext;expressionResolver;getCurrentFile;getCurrentCode;lastSearchIndex=0;constructor(e,t,n,o,i){this.config=e,this.pluginContext=t,this.expressionResolver=n,this.getCurrentFile=o,this.getCurrentCode=i}resetSearchIndex(){this.lastSearchIndex=0}getLocationFromNode(e){const t=this.getCurrentCode();let n;if("JSXElement"===e.type&&e.opening){const t=e.opening.name?.value;t&&(n=`<${t}`)}if(!n)return;const o=t.indexOf(n,this.lastSearchIndex);if(-1===o)return;this.lastSearchIndex=o+n.length;const i=t.substring(0,o).split("\n");return{line:i.length,column:i[i.length-1].length}}handleJSXElement(t,n){const o=this.getElementName(t);if(o&&(this.config.extract.transComponents||["Trans"]).includes(o)){let i=null;try{i=e.extractFromTransComponent(t,this.config)}catch(e){const n=this.getLocationFromNode(t),i=n?`${this.getCurrentFile()}:${n.line}:${n.column}`:this.getCurrentFile(),s=e instanceof Error?e.message:("string"==typeof e?e:"")||String(e),a=this.pluginContext?.logger?.warn?.bind(this.pluginContext.logger)??console.warn.bind(console);return a(`Failed to extract <${o}> at ${i}`),void a(` ${s}`)}const s=[];if(i){if(i.keyExpression){const e=this.expressionResolver.resolvePossibleKeyStringValues(i.keyExpression);s.push(...e)}else s.push(i.serializedChildren);let e;const{contextExpression:o,optionsNode:a,defaultValue:l,hasCount:r,isOrdinal:u,serializedChildren:c}=i,f=this.getLocationFromNode(t),d=f?[{file:this.getCurrentFile(),line:f.line,column:f.column}]:void 0;if(i.ns){const{ns:t}=i;e=s.map(e=>({key:e,ns:t,defaultValue:l||c,hasCount:r,isOrdinal:u,locations:d}))}else{e=s.map(e=>{const t=this.config.extract.nsSeparator??":";let n;if(t&&e.includes(t)){let o;[n,...o]=e.split(t),e=o.join(t)}return{key:e,ns:n,defaultValue:l||c,hasCount:r,isOrdinal:u,explicitDefault:i.explicitDefault,locations:d}});const o=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===o?.type&&"JSXExpressionContainer"===o.value?.type&&"Identifier"===o.value.expression.type){const t=n(o.value.expression.value);if(t?.defaultNs&&e.forEach(e=>{e.ns||(e.ns=t.defaultNs)}),t?.keyPrefix){const n=this.config.extract.keySeparator??".";for(const o of e){let e=o.key;if(e=!1!==n?String(t.keyPrefix).endsWith(String(n))?`${t.keyPrefix}${e}`:`${t.keyPrefix}${n}${e}`:`${t.keyPrefix}${e}`,!1!==n){if(String(e).split(String(n)).some(e=>""===e.trim()))continue}o.key=e}}}}if(e.forEach(e=>{e.ns||(e.ns=this.config.extract.defaultNS)}),o&&r)if(this.config.extract.disablePlurals){const t=this.expressionResolver.resolvePossibleContextStringValues(o),n=this.config.extract.contextSeparator??"_";if(t.length>0)if("StringLiteral"===o.type)for(const o of t)for(const t of e){const e=`${t.key}${n}${o}`;this.pluginContext.addKey({key:e,ns:t.ns,defaultValue:t.defaultValue,locations:t.locations})}else{e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue,locations:e.locations,keyAcceptingContext:e.key})});for(const o of t)for(const t of e){const e=`${t.key}${n}${o}`;this.pluginContext.addKey({key:e,ns:t.ns,defaultValue:t.defaultValue,locations:t.locations})}}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue,locations:e.locations,keyAcceptingContext:e.key})})}else{const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),i=!!n,s=this.expressionResolver.resolvePossibleContextStringValues(o),l=this.config.extract.contextSeparator??"_";if(s.length>0){!1!==this.config.extract.generateBasePluralForms&&e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,i,a,void 0,e.locations,e.key));for(const t of s)for(const n of e){const e=`${n.key}${l}${t}`;this.generatePluralKeysForTrans(e,n.defaultValue,n.ns,i,a,n.explicitDefault,n.locations,n.key)}}else e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,i,a,e.explicitDefault,e.locations))}else if(o){const t=this.expressionResolver.resolvePossibleContextStringValues(o),n=this.config.extract.contextSeparator??"_";if(t.length>0){for(const o of t)for(const{key:t,ns:i,defaultValue:s,locations:a}of e)this.pluginContext.addKey({key:`${t}${n}${o}`,ns:i,defaultValue:s,locations:a});"StringLiteral"!==o.type&&e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue,locations:e.locations,keyAcceptingContext:e.key})})}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue,locations:e.locations,keyAcceptingContext:e.key})})}else if(r)if(this.config.extract.disablePlurals)e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue,locations:e.locations})});else{const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),o=!!n;e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,o,a,e.explicitDefault,e.locations))}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue,locations:e.locations})})}}}generatePluralKeysForTrans(e,n,o,i,s,a,l,r){try{const u=i?"ordinal":"cardinal",c=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:u}).resolvedOptions().pluralCategories,f=this.config.extract.pluralSeparator??"_";let d,p;if(s&&(d=t.getObjectPropValue(s,`defaultValue${f}other`),p=t.getObjectPropValue(s,`defaultValue${f}ordinal${f}other`)),1===c.length&&"other"===c[0]){const u=s?t.getObjectPropValue(s,`defaultValue${f}other`):void 0,c="string"==typeof u?u:"string"==typeof n?n:e;return void this.pluginContext.addKey({key:e,ns:o,defaultValue:c,hasCount:!0,isOrdinal:i,explicitDefault:Boolean(a||"string"==typeof u||"string"==typeof d),locations:l,keyAcceptingContext:r})}for(const u of c){const c=i?`defaultValue${f}ordinal${f}${u}`:`defaultValue${f}${u}`,g=s?t.getObjectPropValue(s,c):void 0;let y;y="string"==typeof g?g:"one"===u&&"string"==typeof n?n:i&&"string"==typeof p?p:i||"string"!=typeof d?"string"==typeof n?n:e:d;const h=i?`${e}${f}ordinal${f}${u}`:`${e}${f}${u}`;this.pluginContext.addKey({key:h,ns:o,defaultValue:y,hasCount:!0,isOrdinal:i,explicitDefault:Boolean(a||"string"==typeof g||"string"==typeof d),locations:l,keyAcceptingContext:r})}}catch(t){this.pluginContext.addKey({key:e,ns:o,defaultValue:n,locations:l})}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}};
1
+ 'use strict';
2
+
3
+ var jsxParser = require('./jsx-parser.js');
4
+ var astUtils = require('./ast-utils.js');
5
+
6
+ class JSXHandler {
7
+ config;
8
+ pluginContext;
9
+ expressionResolver;
10
+ getCurrentFile;
11
+ getCurrentCode;
12
+ lastSearchIndex = 0;
13
+ constructor(config, pluginContext, expressionResolver, getCurrentFile, getCurrentCode) {
14
+ this.config = config;
15
+ this.pluginContext = pluginContext;
16
+ this.expressionResolver = expressionResolver;
17
+ this.getCurrentFile = getCurrentFile;
18
+ this.getCurrentCode = getCurrentCode;
19
+ }
20
+ /**
21
+ * Reset the search index when starting to process a new file.
22
+ */
23
+ resetSearchIndex() {
24
+ this.lastSearchIndex = 0;
25
+ }
26
+ /**
27
+ * Helper method to calculate line and column by searching for the JSX element in the code.
28
+ */
29
+ getLocationFromNode(node) {
30
+ const code = this.getCurrentCode();
31
+ // For JSXElement, search for the opening tag
32
+ let searchText;
33
+ if (node.type === 'JSXElement' && node.opening) {
34
+ const tagName = node.opening.name?.value;
35
+ if (tagName) {
36
+ searchText = `<${tagName}`;
37
+ }
38
+ }
39
+ if (!searchText)
40
+ return undefined;
41
+ const position = code.indexOf(searchText, this.lastSearchIndex);
42
+ if (position === -1)
43
+ return undefined;
44
+ this.lastSearchIndex = position + searchText.length;
45
+ const upToPosition = code.substring(0, position);
46
+ const lines = upToPosition.split('\n');
47
+ return {
48
+ line: lines.length,
49
+ column: lines[lines.length - 1].length
50
+ };
51
+ }
52
+ /**
53
+ * Processes JSX elements to extract translation keys from Trans components.
54
+ *
55
+ * Identifies configured Trans components and delegates to the JSX parser
56
+ * for complex children serialization and attribute extraction.
57
+ *
58
+ * @param node - JSX element node to process
59
+ * @param getScopeInfo - Function to retrieve scope information for variables
60
+ */
61
+ handleJSXElement(node, getScopeInfo) {
62
+ const elementName = this.getElementName(node);
63
+ if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
64
+ let extractedAttributes = null;
65
+ try {
66
+ extractedAttributes = jsxParser.extractFromTransComponent(node, this.config);
67
+ }
68
+ catch (err) {
69
+ const loc = this.getLocationFromNode(node);
70
+ const where = loc
71
+ ? `${this.getCurrentFile()}:${loc.line}:${loc.column}`
72
+ : this.getCurrentFile();
73
+ const message = err instanceof Error
74
+ ? err.message
75
+ : (typeof err === 'string' ? err : '') || String(err);
76
+ // Prefer any logger that might exist on pluginContext, else fall back to console.
77
+ const warn = this.pluginContext?.logger?.warn?.bind(this.pluginContext.logger) ??
78
+ console.warn.bind(console);
79
+ warn(`Failed to extract <${elementName}> at ${where}`);
80
+ warn(` ${message}`);
81
+ // IMPORTANT: do not rethrow; keep visiting the rest of the file
82
+ return;
83
+ }
84
+ const keysToProcess = [];
85
+ if (extractedAttributes) {
86
+ if (extractedAttributes.keyExpression) {
87
+ const keyValues = this.expressionResolver.resolvePossibleKeyStringValues(extractedAttributes.keyExpression);
88
+ keysToProcess.push(...keyValues);
89
+ }
90
+ else {
91
+ keysToProcess.push(extractedAttributes.serializedChildren);
92
+ }
93
+ let extractedKeys;
94
+ const { contextExpression, optionsNode, defaultValue, hasCount, isOrdinal, serializedChildren } = extractedAttributes;
95
+ // Extract location information using the helper method
96
+ const location = this.getLocationFromNode(node);
97
+ const locations = location
98
+ ? [{
99
+ file: this.getCurrentFile(),
100
+ line: location.line,
101
+ column: location.column
102
+ }]
103
+ : undefined;
104
+ // If ns is not explicitly set on the component, try to find it from the key
105
+ // or the `t` prop
106
+ if (!extractedAttributes.ns) {
107
+ extractedKeys = keysToProcess.map(key => {
108
+ const nsSeparator = this.config.extract.nsSeparator ?? ':';
109
+ let ns;
110
+ // If the key contains a namespace separator, it takes precedence
111
+ // over the default t ns value
112
+ if (nsSeparator && key.includes(nsSeparator)) {
113
+ let parts;
114
+ ([ns, ...parts] = key.split(nsSeparator));
115
+ key = parts.join(nsSeparator);
116
+ }
117
+ return {
118
+ key,
119
+ ns,
120
+ defaultValue: defaultValue || serializedChildren,
121
+ hasCount,
122
+ isOrdinal,
123
+ explicitDefault: extractedAttributes.explicitDefault,
124
+ locations
125
+ };
126
+ });
127
+ const tProp = node.opening.attributes?.find(attr => attr.type === 'JSXAttribute' &&
128
+ attr.name.type === 'Identifier' &&
129
+ attr.name.value === 't');
130
+ // Check if the prop value is an identifier (e.g., t={t})
131
+ if (tProp?.type === 'JSXAttribute' &&
132
+ tProp.value?.type === 'JSXExpressionContainer' &&
133
+ tProp.value.expression.type === 'Identifier') {
134
+ const tIdentifier = tProp.value.expression.value;
135
+ const scopeInfo = getScopeInfo(tIdentifier);
136
+ if (scopeInfo?.defaultNs) {
137
+ extractedKeys.forEach(key => {
138
+ if (!key.ns) {
139
+ key.ns = scopeInfo.defaultNs;
140
+ }
141
+ });
142
+ }
143
+ // APPLY keyPrefix from useTranslation to Trans component keys
144
+ if (scopeInfo?.keyPrefix) {
145
+ const keySeparator = this.config.extract.keySeparator ?? '.';
146
+ for (const ek of extractedKeys) {
147
+ // only apply prefix to keys that don't already contain a namespace (ek.key is already namespace-stripped)
148
+ let finalKey = ek.key;
149
+ if (keySeparator !== false) {
150
+ if (String(scopeInfo.keyPrefix).endsWith(String(keySeparator))) {
151
+ finalKey = `${scopeInfo.keyPrefix}${finalKey}`;
152
+ }
153
+ else {
154
+ finalKey = `${scopeInfo.keyPrefix}${keySeparator}${finalKey}`;
155
+ }
156
+ }
157
+ else {
158
+ finalKey = `${scopeInfo.keyPrefix}${finalKey}`;
159
+ }
160
+ // validate result does not create empty segments (robustness)
161
+ if (keySeparator !== false) {
162
+ const segments = String(finalKey).split(String(keySeparator));
163
+ if (segments.some(segment => segment.trim() === '')) {
164
+ // this.logger?.warn?.(`Skipping applying keyPrefix due to empty segment: keyPrefix='${scopeInfo.keyPrefix}', key='${ek.key}'`)
165
+ continue;
166
+ }
167
+ }
168
+ ek.key = finalKey;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ else {
174
+ const { ns } = extractedAttributes;
175
+ extractedKeys = keysToProcess.map(key => {
176
+ return {
177
+ key,
178
+ ns,
179
+ defaultValue: defaultValue || serializedChildren,
180
+ hasCount,
181
+ isOrdinal,
182
+ locations
183
+ };
184
+ });
185
+ }
186
+ extractedKeys.forEach(key => {
187
+ // Apply defaultNS from config if no namespace was found on the component and
188
+ // the key does not contain a namespace prefix
189
+ if (!key.ns) {
190
+ key.ns = this.config.extract.defaultNS;
191
+ }
192
+ });
193
+ // Handle the combination of context and count
194
+ if (contextExpression && hasCount) {
195
+ // Check if plurals are disabled
196
+ if (this.config.extract.disablePlurals) {
197
+ // When plurals are disabled, treat count as a regular option
198
+ // Still handle context normally
199
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextExpression);
200
+ const contextSeparator = this.config.extract.contextSeparator ?? '_';
201
+ if (contextValues.length > 0) {
202
+ // For static context (string literal), only add context variants
203
+ if (contextExpression.type === 'StringLiteral') {
204
+ for (const context of contextValues) {
205
+ for (const extractedKey of extractedKeys) {
206
+ const contextKey = `${extractedKey.key}${contextSeparator}${context}`;
207
+ this.pluginContext.addKey({
208
+ key: contextKey,
209
+ ns: extractedKey.ns,
210
+ defaultValue: extractedKey.defaultValue,
211
+ locations: extractedKey.locations
212
+ });
213
+ }
214
+ }
215
+ }
216
+ else {
217
+ // For dynamic context, add both base and context variants
218
+ extractedKeys.forEach(extractedKey => {
219
+ this.pluginContext.addKey({
220
+ key: extractedKey.key,
221
+ ns: extractedKey.ns,
222
+ defaultValue: extractedKey.defaultValue,
223
+ locations: extractedKey.locations,
224
+ keyAcceptingContext: extractedKey.key
225
+ });
226
+ });
227
+ for (const context of contextValues) {
228
+ for (const extractedKey of extractedKeys) {
229
+ const contextKey = `${extractedKey.key}${contextSeparator}${context}`;
230
+ this.pluginContext.addKey({
231
+ key: contextKey,
232
+ ns: extractedKey.ns,
233
+ defaultValue: extractedKey.defaultValue,
234
+ locations: extractedKey.locations
235
+ });
236
+ }
237
+ }
238
+ }
239
+ }
240
+ else {
241
+ // Fallback to just base keys if context resolution fails
242
+ extractedKeys.forEach(extractedKey => {
243
+ this.pluginContext.addKey({
244
+ key: extractedKey.key,
245
+ ns: extractedKey.ns,
246
+ defaultValue: extractedKey.defaultValue,
247
+ locations: extractedKey.locations,
248
+ keyAcceptingContext: extractedKey.key
249
+ });
250
+ });
251
+ }
252
+ }
253
+ else {
254
+ // Original plural handling logic when plurals are enabled
255
+ // Find isOrdinal prop on the <Trans> component
256
+ const ordinalAttr = node.opening.attributes?.find((attr) => attr.type === 'JSXAttribute' &&
257
+ attr.name.type === 'Identifier' &&
258
+ attr.name.value === 'ordinal');
259
+ const isOrdinal = !!ordinalAttr;
260
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextExpression);
261
+ const contextSeparator = this.config.extract.contextSeparator ?? '_';
262
+ // Generate all combinations of context and plural forms
263
+ if (contextValues.length > 0) {
264
+ // Generate base plural forms (no context) - these also accept context
265
+ if (this.config.extract.generateBasePluralForms !== false) {
266
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode, undefined, extractedKey.locations, extractedKey.key));
267
+ }
268
+ // Generate context + plural combinations
269
+ for (const context of contextValues) {
270
+ for (const extractedKey of extractedKeys) {
271
+ const contextKey = `${extractedKey.key}${contextSeparator}${context}`;
272
+ // The base key that accepts context is extractedKey.key (without the context suffix)
273
+ this.generatePluralKeysForTrans(contextKey, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode, extractedKey.explicitDefault, extractedKey.locations, extractedKey.key);
274
+ }
275
+ }
276
+ }
277
+ else {
278
+ // Fallback to just plural forms if context resolution fails
279
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode, extractedKey.explicitDefault, extractedKey.locations));
280
+ }
281
+ }
282
+ }
283
+ else if (contextExpression) {
284
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextExpression);
285
+ const contextSeparator = this.config.extract.contextSeparator ?? '_';
286
+ if (contextValues.length > 0) {
287
+ // Add context variants
288
+ for (const context of contextValues) {
289
+ for (const { key, ns, defaultValue, locations } of extractedKeys) {
290
+ this.pluginContext.addKey({
291
+ key: `${key}${contextSeparator}${context}`,
292
+ ns,
293
+ defaultValue,
294
+ locations,
295
+ });
296
+ }
297
+ }
298
+ // Only add the base key as a fallback if the context is dynamic (i.e., not a simple string).
299
+ if (contextExpression.type !== 'StringLiteral') {
300
+ extractedKeys.forEach(extractedKey => {
301
+ this.pluginContext.addKey({
302
+ key: extractedKey.key,
303
+ ns: extractedKey.ns,
304
+ defaultValue: extractedKey.defaultValue,
305
+ locations: extractedKey.locations,
306
+ keyAcceptingContext: extractedKey.key
307
+ });
308
+ });
309
+ }
310
+ }
311
+ else {
312
+ // If no context values were resolved, just add base keys
313
+ extractedKeys.forEach(extractedKey => {
314
+ this.pluginContext.addKey({
315
+ key: extractedKey.key,
316
+ ns: extractedKey.ns,
317
+ defaultValue: extractedKey.defaultValue,
318
+ locations: extractedKey.locations,
319
+ keyAcceptingContext: extractedKey.key
320
+ });
321
+ });
322
+ }
323
+ }
324
+ else if (hasCount) {
325
+ // Check if plurals are disabled
326
+ if (this.config.extract.disablePlurals) {
327
+ // When plurals are disabled, just add the base keys (no plural forms)
328
+ extractedKeys.forEach(extractedKey => {
329
+ this.pluginContext.addKey({
330
+ key: extractedKey.key,
331
+ ns: extractedKey.ns,
332
+ defaultValue: extractedKey.defaultValue,
333
+ locations: extractedKey.locations
334
+ });
335
+ });
336
+ }
337
+ else {
338
+ // Original plural handling logic when plurals are enabled
339
+ // Find isOrdinal prop on the <Trans> component
340
+ const ordinalAttr = node.opening.attributes?.find((attr) => attr.type === 'JSXAttribute' &&
341
+ attr.name.type === 'Identifier' &&
342
+ attr.name.value === 'ordinal');
343
+ const isOrdinal = !!ordinalAttr;
344
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode, extractedKey.explicitDefault, extractedKey.locations));
345
+ }
346
+ }
347
+ else {
348
+ // No count or context - just add the base keys
349
+ extractedKeys.forEach(extractedKey => {
350
+ this.pluginContext.addKey({
351
+ key: extractedKey.key,
352
+ ns: extractedKey.ns,
353
+ defaultValue: extractedKey.defaultValue,
354
+ locations: extractedKey.locations
355
+ });
356
+ });
357
+ }
358
+ }
359
+ }
360
+ }
361
+ /**
362
+ * Generates plural keys for Trans components, with support for tOptions plural defaults.
363
+ *
364
+ * @param key - Base key name for pluralization
365
+ * @param defaultValue - Default value for the keys
366
+ * @param ns - Namespace for the keys
367
+ * @param isOrdinal - Whether to generate ordinal plural forms
368
+ * @param optionsNode - Optional tOptions object expression for plural-specific defaults
369
+ * @param explicitDefaultFromSource - Whether the default was explicitly provided
370
+ * @param locations - Source location information for this key
371
+ * @param keyAcceptingContext - The base key that accepts context (if this is a context variant)
372
+ */
373
+ generatePluralKeysForTrans(key, defaultValue, ns, isOrdinal, optionsNode, explicitDefaultFromSource, locations, keyAcceptingContext) {
374
+ try {
375
+ const type = isOrdinal ? 'ordinal' : 'cardinal';
376
+ const pluralCategories = new Intl.PluralRules(this.config.extract?.primaryLanguage, { type }).resolvedOptions().pluralCategories;
377
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_';
378
+ // Get plural-specific default values from tOptions if available
379
+ let otherDefault;
380
+ let ordinalOtherDefault;
381
+ if (optionsNode) {
382
+ otherDefault = astUtils.getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}other`);
383
+ ordinalOtherDefault = astUtils.getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`);
384
+ }
385
+ // Special-case single-"other" languages: generate base key (or context variant) instead of key_other
386
+ if (pluralCategories.length === 1 && pluralCategories[0] === 'other') {
387
+ // Determine final default for the base/other form
388
+ const specificDefault = optionsNode ? astUtils.getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}other`) : undefined;
389
+ const finalDefault = typeof specificDefault === 'string' ? specificDefault : (typeof defaultValue === 'string' ? defaultValue : key);
390
+ // add base key (no suffix)
391
+ this.pluginContext.addKey({
392
+ key,
393
+ ns,
394
+ defaultValue: finalDefault,
395
+ hasCount: true,
396
+ isOrdinal,
397
+ explicitDefault: Boolean(explicitDefaultFromSource || typeof specificDefault === 'string' || typeof otherDefault === 'string'),
398
+ locations,
399
+ keyAcceptingContext
400
+ });
401
+ return;
402
+ }
403
+ for (const category of pluralCategories) {
404
+ // Look for the most specific default value (e.g., defaultValue_ordinal_one)
405
+ const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`;
406
+ const specificDefault = optionsNode ? astUtils.getObjectPropValue(optionsNode, specificDefaultKey) : undefined;
407
+ // Determine the final default value using a clear fallback chain
408
+ let finalDefaultValue;
409
+ if (typeof specificDefault === 'string') {
410
+ // 1. Use the most specific default if it exists (e.g., defaultValue_one)
411
+ finalDefaultValue = specificDefault;
412
+ }
413
+ else if (category === 'one' && typeof defaultValue === 'string') {
414
+ // 2. SPECIAL CASE: The 'one' category falls back to the main default value (children content)
415
+ finalDefaultValue = defaultValue;
416
+ }
417
+ else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
418
+ // 3a. Other ordinal categories fall back to 'defaultValue_ordinal_other'
419
+ finalDefaultValue = ordinalOtherDefault;
420
+ }
421
+ else if (!isOrdinal && typeof otherDefault === 'string') {
422
+ // 3b. Other cardinal categories fall back to 'defaultValue_other'
423
+ finalDefaultValue = otherDefault;
424
+ }
425
+ else if (typeof defaultValue === 'string') {
426
+ // 4. If no '_other' is found, all categories can fall back to the main default value
427
+ finalDefaultValue = defaultValue;
428
+ }
429
+ else {
430
+ // 5. Final fallback to the base key itself
431
+ finalDefaultValue = key;
432
+ }
433
+ const finalKey = isOrdinal
434
+ ? `${key}${pluralSeparator}ordinal${pluralSeparator}${category}`
435
+ : `${key}${pluralSeparator}${category}`;
436
+ this.pluginContext.addKey({
437
+ key: finalKey,
438
+ ns,
439
+ defaultValue: finalDefaultValue,
440
+ hasCount: true,
441
+ isOrdinal,
442
+ // Only treat plural/context variant as explicit when:
443
+ // - the extractor indicated the default was explicit on the source element
444
+ // - OR a plural-specific default was provided in tOptions (specificDefault/otherDefault)
445
+ explicitDefault: Boolean(explicitDefaultFromSource || typeof specificDefault === 'string' || typeof otherDefault === 'string'),
446
+ locations,
447
+ // Pass through the base key that accepts context (if any)
448
+ keyAcceptingContext
449
+ });
450
+ }
451
+ }
452
+ catch (e) {
453
+ // Fallback to a simple key if Intl API fails
454
+ this.pluginContext.addKey({
455
+ key,
456
+ ns,
457
+ defaultValue,
458
+ locations
459
+ });
460
+ }
461
+ }
462
+ /**
463
+ * Extracts element name from JSX opening tag.
464
+ *
465
+ * Handles both simple identifiers and member expressions:
466
+ * - `<Trans>` → 'Trans'
467
+ * - `<React.Trans>` → 'React.Trans'
468
+ *
469
+ * @param node - JSX element node
470
+ * @returns Element name or undefined if not extractable
471
+ */
472
+ getElementName(node) {
473
+ if (node.opening.name.type === 'Identifier') {
474
+ return node.opening.name.value;
475
+ }
476
+ else if (node.opening.name.type === 'JSXMemberExpression') {
477
+ let curr = node.opening.name;
478
+ const names = [];
479
+ while (curr.type === 'JSXMemberExpression') {
480
+ if (curr.property.type === 'Identifier')
481
+ names.unshift(curr.property.value);
482
+ curr = curr.object;
483
+ }
484
+ if (curr.type === 'Identifier')
485
+ names.unshift(curr.value);
486
+ return names.join('.');
487
+ }
488
+ return undefined;
489
+ }
490
+ }
491
+
492
+ exports.JSXHandler = JSXHandler;