i18next-cli 1.10.1 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +3 -0
  3. package/dist/cjs/cli.js +1 -1
  4. package/dist/cjs/extractor/core/ast-visitors.js +1 -0
  5. package/dist/cjs/extractor/core/key-finder.js +1 -1
  6. package/dist/cjs/extractor/core/translation-manager.js +1 -1
  7. package/dist/cjs/extractor/parsers/call-expression-handler.js +1 -0
  8. package/dist/cjs/extractor/parsers/expression-resolver.js +1 -0
  9. package/dist/cjs/extractor/parsers/jsx-handler.js +1 -0
  10. package/dist/cjs/extractor/parsers/scope-manager.js +1 -0
  11. package/dist/esm/cli.js +1 -1
  12. package/dist/esm/extractor/core/ast-visitors.js +1 -0
  13. package/dist/esm/extractor/core/key-finder.js +1 -1
  14. package/dist/esm/extractor/core/translation-manager.js +1 -1
  15. package/dist/esm/extractor/parsers/call-expression-handler.js +1 -0
  16. package/dist/esm/extractor/parsers/expression-resolver.js +1 -0
  17. package/dist/esm/extractor/parsers/jsx-handler.js +1 -0
  18. package/dist/esm/extractor/parsers/scope-manager.js +1 -0
  19. package/package.json +1 -1
  20. package/src/cli.ts +1 -1
  21. package/src/extractor/core/ast-visitors.ts +170 -0
  22. package/src/extractor/core/extractor.ts +1 -1
  23. package/src/extractor/core/key-finder.ts +2 -2
  24. package/src/extractor/core/translation-manager.ts +88 -8
  25. package/src/extractor/index.ts +1 -1
  26. package/src/extractor/parsers/call-expression-handler.ts +506 -0
  27. package/src/extractor/parsers/expression-resolver.ts +178 -0
  28. package/src/extractor/parsers/jsx-handler.ts +358 -0
  29. package/src/extractor/parsers/scope-manager.ts +327 -0
  30. package/src/extractor.ts +1 -1
  31. package/src/types.ts +82 -0
  32. package/types/extractor/core/ast-visitors.d.ts +75 -0
  33. package/types/extractor/core/ast-visitors.d.ts.map +1 -0
  34. package/types/extractor/core/extractor.d.ts +1 -1
  35. package/types/extractor/core/extractor.d.ts.map +1 -1
  36. package/types/extractor/core/key-finder.d.ts.map +1 -1
  37. package/types/extractor/core/translation-manager.d.ts.map +1 -1
  38. package/types/extractor/index.d.ts +1 -1
  39. package/types/extractor/index.d.ts.map +1 -1
  40. package/types/extractor/parsers/call-expression-handler.d.ts +74 -0
  41. package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -0
  42. package/types/extractor/parsers/expression-resolver.d.ts +62 -0
  43. package/types/extractor/parsers/expression-resolver.d.ts.map +1 -0
  44. package/types/extractor/parsers/jsx-handler.d.ts +44 -0
  45. package/types/extractor/parsers/jsx-handler.d.ts.map +1 -0
  46. package/types/extractor/parsers/scope-manager.d.ts +99 -0
  47. package/types/extractor/parsers/scope-manager.d.ts.map +1 -0
  48. package/types/extractor.d.ts +1 -1
  49. package/types/extractor.d.ts.map +1 -1
  50. package/types/types.d.ts +77 -0
  51. package/types/types.d.ts.map +1 -1
  52. package/dist/cjs/extractor/parsers/ast-visitors.js +0 -1
  53. package/dist/esm/extractor/parsers/ast-visitors.js +0 -1
  54. package/src/extractor/parsers/ast-visitors.ts +0 -1510
  55. package/types/extractor/parsers/ast-visitors.d.ts +0 -352
  56. package/types/extractor/parsers/ast-visitors.d.ts.map +0 -1
@@ -0,0 +1,178 @@
1
+ import type { Expression, TsType, TemplateLiteral, TsTemplateLiteralType } from '@swc/core'
2
+ import type { ASTVisitorHooks } from '../../types'
3
+
4
+ export class ExpressionResolver {
5
+ private hooks: ASTVisitorHooks
6
+
7
+ constructor (hooks: ASTVisitorHooks) {
8
+ this.hooks = hooks
9
+ }
10
+
11
+ /**
12
+ * Resolves an expression to one or more possible context string values that can be
13
+ * determined statically from the AST. This is a wrapper around the plugin hook
14
+ * `extractContextFromExpression` and {@link resolvePossibleStringValuesFromExpression}.
15
+ *
16
+ * @param expression - The SWC AST expression node to resolve
17
+ * @returns An array of possible context string values that the expression may produce.
18
+ */
19
+ resolvePossibleContextStringValues (expression: Expression): string[] {
20
+ const strings = this.hooks.resolvePossibleContextStringValues?.(expression) ?? []
21
+ return [...strings, ...this.resolvePossibleStringValuesFromExpression(expression)]
22
+ }
23
+
24
+ /**
25
+ * Resolves an expression to one or more possible key string values that can be
26
+ * determined statically from the AST. This is a wrapper around the plugin hook
27
+ * `extractKeysFromExpression` and {@link resolvePossibleStringValuesFromExpression}.
28
+ *
29
+ * @param expression - The SWC AST expression node to resolve
30
+ * @returns An array of possible key string values that the expression may produce.
31
+ */
32
+ resolvePossibleKeyStringValues (expression: Expression): string[] {
33
+ const strings = this.hooks.resolvePossibleKeyStringValues?.(expression) ?? []
34
+ return [...strings, ...this.resolvePossibleStringValuesFromExpression(expression)]
35
+ }
36
+
37
+ /**
38
+ * Resolves an expression to one or more possible string values that can be
39
+ * determined statically from the AST.
40
+ *
41
+ * Supports:
42
+ * - StringLiteral -> single value (filtered to exclude empty strings for context)
43
+ * - NumericLiteral -> single value
44
+ * - BooleanLiteral -> single value
45
+ * - ConditionalExpression (ternary) -> union of consequent and alternate resolved values
46
+ * - TemplateLiteral -> union of all possible string values
47
+ * - The identifier `undefined` -> empty array
48
+ *
49
+ * For any other expression types (identifiers, function calls, member expressions,
50
+ * etc.) the value cannot be determined statically and an empty array is returned.
51
+ *
52
+ * @param expression - The SWC AST expression node to resolve
53
+ * @param returnEmptyStrings - Whether to include empty strings in the result
54
+ * @returns An array of possible string values that the expression may produce.
55
+ */
56
+ private resolvePossibleStringValuesFromExpression (expression: Expression, returnEmptyStrings = false): string[] {
57
+ if (expression.type === 'StringLiteral') {
58
+ // Filter out empty strings as they should be treated as "no context" like i18next does
59
+ return expression.value || returnEmptyStrings ? [expression.value] : []
60
+ }
61
+
62
+ if (expression.type === 'ConditionalExpression') { // This is a ternary operator
63
+ const consequentValues = this.resolvePossibleStringValuesFromExpression(expression.consequent, returnEmptyStrings)
64
+ const alternateValues = this.resolvePossibleStringValuesFromExpression(expression.alternate, returnEmptyStrings)
65
+ return [...consequentValues, ...alternateValues]
66
+ }
67
+
68
+ if (expression.type === 'Identifier' && expression.value === 'undefined') {
69
+ return [] // Handle the `undefined` case
70
+ }
71
+
72
+ if (expression.type === 'TemplateLiteral') {
73
+ return this.resolvePossibleStringValuesFromTemplateString(expression)
74
+ }
75
+
76
+ if (expression.type === 'NumericLiteral' || expression.type === 'BooleanLiteral') {
77
+ return [`${expression.value}`] // Handle literals like 5 or true
78
+ }
79
+
80
+ // Support building translation keys for
81
+ // `variable satisfies 'coaching' | 'therapy'`
82
+ if (expression.type === 'TsSatisfiesExpression' || expression.type === 'TsAsExpression') {
83
+ const annotation = expression.typeAnnotation
84
+ return this.resolvePossibleStringValuesFromType(annotation, returnEmptyStrings)
85
+ }
86
+
87
+ // We can't statically determine the value of other expressions (e.g., variables, function calls)
88
+ return []
89
+ }
90
+
91
+ private resolvePossibleStringValuesFromType (type: TsType, returnEmptyStrings = false): string[] {
92
+ if (type.type === 'TsUnionType') {
93
+ return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings))
94
+ }
95
+
96
+ if (type.type === 'TsLiteralType') {
97
+ if (type.literal.type === 'StringLiteral') {
98
+ // Filter out empty strings as they should be treated as "no context" like i18next does
99
+ return type.literal.value || returnEmptyStrings ? [type.literal.value] : []
100
+ }
101
+
102
+ if (type.literal.type === 'TemplateLiteral') {
103
+ return this.resolvePossibleStringValuesFromTemplateLiteralType(type.literal)
104
+ }
105
+
106
+ if (type.literal.type === 'NumericLiteral' || type.literal.type === 'BooleanLiteral') {
107
+ return [`${type.literal.value}`] // Handle literals like 5 or true
108
+ }
109
+ }
110
+
111
+ // We can't statically determine the value of other expressions (e.g., variables, function calls)
112
+ return []
113
+ }
114
+
115
+ /**
116
+ * Resolves a template literal string to one or more possible strings that can be
117
+ * determined statically from the AST.
118
+ *
119
+ * @param templateString - The SWC AST template literal string to resolve
120
+ * @returns An array of possible string values that the template may produce.
121
+ */
122
+ private resolvePossibleStringValuesFromTemplateString (templateString: TemplateLiteral): string[] {
123
+ // If there are no expressions, we can just return the cooked value
124
+ if (templateString.quasis.length === 1 && templateString.expressions.length === 0) {
125
+ // Ex. `translation.key.no.substitution`
126
+ return [templateString.quasis[0].cooked || '']
127
+ }
128
+
129
+ // Ex. `translation.key.with.expression.${x ? 'title' : 'description'}`
130
+ const [firstQuasis, ...tails] = templateString.quasis
131
+
132
+ const stringValues = templateString.expressions.reduce(
133
+ (heads, expression, i) => {
134
+ return heads.flatMap((head) => {
135
+ const tail = tails[i]?.cooked ?? ''
136
+ return this.resolvePossibleStringValuesFromExpression(expression, true).map(
137
+ (expressionValue) => `${head}${expressionValue}${tail}`
138
+ )
139
+ })
140
+ },
141
+ [firstQuasis.cooked ?? '']
142
+ )
143
+
144
+ return stringValues
145
+ }
146
+
147
+ /**
148
+ * Resolves a template literal type to one or more possible strings that can be
149
+ * determined statically from the AST.
150
+ *
151
+ * @param templateLiteralType - The SWC AST template literal type to resolve
152
+ * @returns An array of possible string values that the template may produce.
153
+ */
154
+ private resolvePossibleStringValuesFromTemplateLiteralType (templateLiteralType: TsTemplateLiteralType): string[] {
155
+ // If there are no types, we can just return the cooked value
156
+ if (templateLiteralType.quasis.length === 1 && templateLiteralType.types.length === 0) {
157
+ // Ex. `translation.key.no.substitution`
158
+ return [templateLiteralType.quasis[0].cooked || '']
159
+ }
160
+
161
+ // Ex. `translation.key.with.expression.${'title' | 'description'}`
162
+ const [firstQuasis, ...tails] = templateLiteralType.quasis
163
+
164
+ const stringValues = templateLiteralType.types.reduce(
165
+ (heads, type, i) => {
166
+ return heads.flatMap((head) => {
167
+ const tail = tails[i]?.cooked ?? ''
168
+ return this.resolvePossibleStringValuesFromType(type, true).map(
169
+ (expressionValue) => `${head}${expressionValue}${tail}`
170
+ )
171
+ })
172
+ },
173
+ [firstQuasis.cooked ?? '']
174
+ )
175
+
176
+ return stringValues
177
+ }
178
+ }
@@ -0,0 +1,358 @@
1
+ import type { JSXElement, ObjectExpression } from '@swc/core'
2
+ import type { PluginContext, I18nextToolkitConfig, ExtractedKey } from '../../types'
3
+ import { ExpressionResolver } from './expression-resolver'
4
+ import { extractFromTransComponent } from './jsx-parser'
5
+ import { getObjectPropValue } from './ast-utils'
6
+
7
+ export class JSXHandler {
8
+ private config: Omit<I18nextToolkitConfig, 'plugins'>
9
+ private pluginContext: PluginContext
10
+ private expressionResolver: ExpressionResolver
11
+
12
+ constructor (
13
+ config: Omit<I18nextToolkitConfig, 'plugins'>,
14
+ pluginContext: PluginContext,
15
+ expressionResolver: ExpressionResolver
16
+ ) {
17
+ this.config = config
18
+ this.pluginContext = pluginContext
19
+ this.expressionResolver = expressionResolver
20
+ }
21
+
22
+ /**
23
+ * Processes JSX elements to extract translation keys from Trans components.
24
+ *
25
+ * Identifies configured Trans components and delegates to the JSX parser
26
+ * for complex children serialization and attribute extraction.
27
+ *
28
+ * @param node - JSX element node to process
29
+ * @param getScopeInfo - Function to retrieve scope information for variables
30
+ */
31
+ handleJSXElement (node: JSXElement, getScopeInfo: (name: string) => { defaultNs?: string; keyPrefix?: string } | undefined): void {
32
+ const elementName = this.getElementName(node)
33
+
34
+ if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
35
+ const extractedAttributes = extractFromTransComponent(node, this.config)
36
+
37
+ const keysToProcess: string[] = []
38
+
39
+ if (extractedAttributes) {
40
+ if (extractedAttributes.keyExpression) {
41
+ const keyValues = this.expressionResolver.resolvePossibleKeyStringValues(extractedAttributes.keyExpression)
42
+ keysToProcess.push(...keyValues)
43
+ } else {
44
+ keysToProcess.push(extractedAttributes.serializedChildren)
45
+ }
46
+
47
+ let extractedKeys: ExtractedKey[]
48
+
49
+ const { contextExpression, optionsNode, defaultValue, hasCount, isOrdinal, serializedChildren } = extractedAttributes
50
+
51
+ // If ns is not explicitly set on the component, try to find it from the key
52
+ // or the `t` prop
53
+ if (!extractedAttributes.ns) {
54
+ extractedKeys = keysToProcess.map(key => {
55
+ const nsSeparator = this.config.extract.nsSeparator ?? ':'
56
+ let ns: string | undefined
57
+
58
+ // If the key contains a namespace separator, it takes precedence
59
+ // over the default t ns value
60
+ if (nsSeparator && key.includes(nsSeparator)) {
61
+ let parts: string[]
62
+ ([ns, ...parts] = key.split(nsSeparator))
63
+
64
+ key = parts.join(nsSeparator)
65
+ }
66
+
67
+ return {
68
+ key,
69
+ ns,
70
+ defaultValue: defaultValue || serializedChildren,
71
+ hasCount,
72
+ isOrdinal,
73
+ }
74
+ })
75
+
76
+ const tProp = node.opening.attributes?.find(
77
+ attr =>
78
+ attr.type === 'JSXAttribute' &&
79
+ attr.name.type === 'Identifier' &&
80
+ attr.name.value === 't'
81
+ )
82
+
83
+ // Check if the prop value is an identifier (e.g., t={t})
84
+ if (
85
+ tProp?.type === 'JSXAttribute' &&
86
+ tProp.value?.type === 'JSXExpressionContainer' &&
87
+ tProp.value.expression.type === 'Identifier'
88
+ ) {
89
+ const tIdentifier = tProp.value.expression.value
90
+ const scopeInfo = getScopeInfo(tIdentifier)
91
+ if (scopeInfo?.defaultNs) {
92
+ extractedKeys.forEach(key => {
93
+ if (!key.ns) {
94
+ key.ns = scopeInfo.defaultNs
95
+ }
96
+ })
97
+ }
98
+ }
99
+ } else {
100
+ const { ns } = extractedAttributes
101
+ extractedKeys = keysToProcess.map(key => {
102
+ return {
103
+ key,
104
+ ns,
105
+ defaultValue: defaultValue || serializedChildren,
106
+ hasCount,
107
+ isOrdinal,
108
+ }
109
+ })
110
+ }
111
+
112
+ extractedKeys.forEach(key => {
113
+ // Apply defaultNS from config if no namespace was found on the component and
114
+ // the key does not contain a namespace prefix
115
+ if (!key.ns) {
116
+ key.ns = this.config.extract.defaultNS
117
+ }
118
+ })
119
+
120
+ // Handle the combination of context and count
121
+ if (contextExpression && hasCount) {
122
+ // Check if plurals are disabled
123
+ if (this.config.extract.disablePlurals) {
124
+ // When plurals are disabled, treat count as a regular option
125
+ // Still handle context normally
126
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextExpression)
127
+ const contextSeparator = this.config.extract.contextSeparator ?? '_'
128
+
129
+ if (contextValues.length > 0) {
130
+ // For static context (string literal), only add context variants
131
+ if (contextExpression.type === 'StringLiteral') {
132
+ for (const context of contextValues) {
133
+ for (const extractedKey of extractedKeys) {
134
+ const contextKey = `${extractedKey.key}${contextSeparator}${context}`
135
+ this.pluginContext.addKey({ key: contextKey, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
136
+ }
137
+ }
138
+ } else {
139
+ // For dynamic context, add both base and context variants
140
+ extractedKeys.forEach(extractedKey => {
141
+ this.pluginContext.addKey({
142
+ key: extractedKey.key,
143
+ ns: extractedKey.ns,
144
+ defaultValue: extractedKey.defaultValue
145
+ })
146
+ })
147
+ for (const context of contextValues) {
148
+ for (const extractedKey of extractedKeys) {
149
+ const contextKey = `${extractedKey.key}${contextSeparator}${context}`
150
+ this.pluginContext.addKey({ key: contextKey, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
151
+ }
152
+ }
153
+ }
154
+ } else {
155
+ // Fallback to just base keys if context resolution fails
156
+ extractedKeys.forEach(extractedKey => {
157
+ this.pluginContext.addKey({
158
+ key: extractedKey.key,
159
+ ns: extractedKey.ns,
160
+ defaultValue: extractedKey.defaultValue
161
+ })
162
+ })
163
+ }
164
+ } else {
165
+ // Original plural handling logic when plurals are enabled
166
+ // Find isOrdinal prop on the <Trans> component
167
+ const ordinalAttr = node.opening.attributes?.find(
168
+ (attr) =>
169
+ attr.type === 'JSXAttribute' &&
170
+ attr.name.type === 'Identifier' &&
171
+ attr.name.value === 'ordinal'
172
+ )
173
+ const isOrdinal = !!ordinalAttr
174
+
175
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextExpression)
176
+ const contextSeparator = this.config.extract.contextSeparator ?? '_'
177
+
178
+ // Generate all combinations of context and plural forms
179
+ if (contextValues.length > 0) {
180
+ // Generate base plural forms (no context)
181
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
182
+
183
+ // Generate context + plural combinations
184
+ for (const context of contextValues) {
185
+ for (const extractedKey of extractedKeys) {
186
+ const contextKey = `${extractedKey.key}${contextSeparator}${context}`
187
+ this.generatePluralKeysForTrans(contextKey, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode)
188
+ }
189
+ }
190
+ } else {
191
+ // Fallback to just plural forms if context resolution fails
192
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
193
+ }
194
+ }
195
+ } else if (contextExpression) {
196
+ const contextValues = this.expressionResolver.resolvePossibleContextStringValues(contextExpression)
197
+ const contextSeparator = this.config.extract.contextSeparator ?? '_'
198
+
199
+ if (contextValues.length > 0) {
200
+ // Add context variants
201
+ for (const context of contextValues) {
202
+ for (const { key, ns, defaultValue } of extractedKeys) {
203
+ this.pluginContext.addKey({ key: `${key}${contextSeparator}${context}`, ns, defaultValue })
204
+ }
205
+ }
206
+ // Only add the base key as a fallback if the context is dynamic (i.e., not a simple string).
207
+ if (contextExpression.type !== 'StringLiteral') {
208
+ extractedKeys.forEach(extractedKey => {
209
+ this.pluginContext.addKey({
210
+ key: extractedKey.key,
211
+ ns: extractedKey.ns,
212
+ defaultValue: extractedKey.defaultValue
213
+ })
214
+ })
215
+ }
216
+ } else {
217
+ // If no context values were resolved, just add base keys
218
+ extractedKeys.forEach(extractedKey => {
219
+ this.pluginContext.addKey({
220
+ key: extractedKey.key,
221
+ ns: extractedKey.ns,
222
+ defaultValue: extractedKey.defaultValue
223
+ })
224
+ })
225
+ }
226
+ } else if (hasCount) {
227
+ // Check if plurals are disabled
228
+ if (this.config.extract.disablePlurals) {
229
+ // When plurals are disabled, just add the base keys (no plural forms)
230
+ extractedKeys.forEach(extractedKey => {
231
+ this.pluginContext.addKey({
232
+ key: extractedKey.key,
233
+ ns: extractedKey.ns,
234
+ defaultValue: extractedKey.defaultValue
235
+ })
236
+ })
237
+ } else {
238
+ // Original plural handling logic when plurals are enabled
239
+ // Find isOrdinal prop on the <Trans> component
240
+ const ordinalAttr = node.opening.attributes?.find(
241
+ (attr) =>
242
+ attr.type === 'JSXAttribute' &&
243
+ attr.name.type === 'Identifier' &&
244
+ attr.name.value === 'ordinal'
245
+ )
246
+ const isOrdinal = !!ordinalAttr
247
+
248
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
249
+ }
250
+ } else {
251
+ // No count or context - just add the base keys
252
+ extractedKeys.forEach(extractedKey => {
253
+ this.pluginContext.addKey({
254
+ key: extractedKey.key,
255
+ ns: extractedKey.ns,
256
+ defaultValue: extractedKey.defaultValue
257
+ })
258
+ })
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Generates plural keys for Trans components, with support for tOptions plural defaults.
266
+ *
267
+ * @param key - Base key name for pluralization
268
+ * @param defaultValue - Default value for the keys
269
+ * @param ns - Namespace for the keys
270
+ * @param isOrdinal - Whether to generate ordinal plural forms
271
+ * @param optionsNode - Optional tOptions object expression for plural-specific defaults
272
+ */
273
+ private generatePluralKeysForTrans (key: string, defaultValue: string | undefined, ns: string | undefined, isOrdinal: boolean, optionsNode?: ObjectExpression): void {
274
+ try {
275
+ const type = isOrdinal ? 'ordinal' : 'cardinal'
276
+ const pluralCategories = new Intl.PluralRules(this.config.extract?.primaryLanguage, { type }).resolvedOptions().pluralCategories
277
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
278
+
279
+ // Get plural-specific default values from tOptions if available
280
+ let otherDefault: string | undefined
281
+ let ordinalOtherDefault: string | undefined
282
+
283
+ if (optionsNode) {
284
+ otherDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}other`) as string | undefined
285
+ ordinalOtherDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`) as string | undefined
286
+ }
287
+
288
+ for (const category of pluralCategories) {
289
+ // Look for the most specific default value (e.g., defaultValue_ordinal_one)
290
+ const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`
291
+ const specificDefault = optionsNode ? getObjectPropValue(optionsNode, specificDefaultKey) as string | undefined : undefined
292
+
293
+ // Determine the final default value using a clear fallback chain
294
+ let finalDefaultValue: string | undefined
295
+ if (typeof specificDefault === 'string') {
296
+ // 1. Use the most specific default if it exists (e.g., defaultValue_one)
297
+ finalDefaultValue = specificDefault
298
+ } else if (category === 'one' && typeof defaultValue === 'string') {
299
+ // 2. SPECIAL CASE: The 'one' category falls back to the main default value (children content)
300
+ finalDefaultValue = defaultValue
301
+ } else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
302
+ // 3a. Other ordinal categories fall back to 'defaultValue_ordinal_other'
303
+ finalDefaultValue = ordinalOtherDefault
304
+ } else if (!isOrdinal && typeof otherDefault === 'string') {
305
+ // 3b. Other cardinal categories fall back to 'defaultValue_other'
306
+ finalDefaultValue = otherDefault
307
+ } else if (typeof defaultValue === 'string') {
308
+ // 4. If no '_other' is found, all categories can fall back to the main default value
309
+ finalDefaultValue = defaultValue
310
+ } else {
311
+ // 5. Final fallback to the base key itself
312
+ finalDefaultValue = key
313
+ }
314
+
315
+ const finalKey = isOrdinal
316
+ ? `${key}${pluralSeparator}ordinal${pluralSeparator}${category}`
317
+ : `${key}${pluralSeparator}${category}`
318
+
319
+ this.pluginContext.addKey({
320
+ key: finalKey,
321
+ ns,
322
+ defaultValue: finalDefaultValue,
323
+ hasCount: true,
324
+ isOrdinal
325
+ })
326
+ }
327
+ } catch (e) {
328
+ // Fallback to a simple key if Intl API fails
329
+ this.pluginContext.addKey({ key, ns, defaultValue })
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Extracts element name from JSX opening tag.
335
+ *
336
+ * Handles both simple identifiers and member expressions:
337
+ * - `<Trans>` → 'Trans'
338
+ * - `<React.Trans>` → 'React.Trans'
339
+ *
340
+ * @param node - JSX element node
341
+ * @returns Element name or undefined if not extractable
342
+ */
343
+ private getElementName (node: JSXElement): string | undefined {
344
+ if (node.opening.name.type === 'Identifier') {
345
+ return node.opening.name.value
346
+ } else if (node.opening.name.type === 'JSXMemberExpression') {
347
+ let curr: any = node.opening.name
348
+ const names: string[] = []
349
+ while (curr.type === 'JSXMemberExpression') {
350
+ if (curr.property.type === 'Identifier') names.unshift(curr.property.value)
351
+ curr = curr.object
352
+ }
353
+ if (curr.type === 'Identifier') names.unshift(curr.value)
354
+ return names.join('.')
355
+ }
356
+ return undefined
357
+ }
358
+ }