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