i18next-cli 0.9.18 → 0.9.20

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.
@@ -1,6 +1,7 @@
1
1
  import type { Module, Node, CallExpression, VariableDeclarator, JSXElement, ArrowFunctionExpression, ObjectExpression, Expression } from '@swc/core'
2
2
  import type { PluginContext, I18nextToolkitConfig, Logger } from '../../types'
3
3
  import { extractFromTransComponent } from './jsx-parser'
4
+ import { getObjectProperty, getObjectPropValue } from './ast-utils'
4
5
 
5
6
  /**
6
7
  * Represents variable scope information tracked during AST traversal.
@@ -308,7 +309,7 @@ export class ASTVisitors {
308
309
  const optionsArg = callExpr.arguments?.[hookConfig.keyPrefixArg]?.expression
309
310
  let keyPrefix: string | undefined
310
311
  if (optionsArg?.type === 'ObjectExpression') {
311
- const kp = this.getObjectPropValue(optionsArg, 'keyPrefix')
312
+ const kp = getObjectPropValue(optionsArg, 'keyPrefix')
312
313
  keyPrefix = typeof kp === 'string' ? kp : undefined
313
314
  }
314
315
 
@@ -374,7 +375,7 @@ export class ASTVisitors {
374
375
  if (!isFunctionToParse || node.arguments.length === 0) return
375
376
 
376
377
  const firstArg = node.arguments[0].expression
377
- const keysToProcess: string[] = []
378
+ let keysToProcess: string[] = []
378
379
 
379
380
  if (firstArg.type === 'StringLiteral') {
380
381
  keysToProcess.push(firstArg.value)
@@ -390,8 +391,20 @@ export class ASTVisitors {
390
391
  }
391
392
  }
392
393
 
394
+ keysToProcess = keysToProcess.filter(key => !!key)
393
395
  if (keysToProcess.length === 0) return
394
396
 
397
+ let isOrdinalByKey = false
398
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
399
+
400
+ for (let i = 0; i < keysToProcess.length; i++) {
401
+ if (keysToProcess[i].endsWith(`${pluralSeparator}ordinal`)) {
402
+ isOrdinalByKey = true
403
+ // Normalize the key by stripping the suffix
404
+ keysToProcess[i] = keysToProcess[i].slice(0, -8)
405
+ }
406
+ }
407
+
395
408
  let defaultValue: string | undefined
396
409
  let options: ObjectExpression | undefined
397
410
 
@@ -409,7 +422,7 @@ export class ASTVisitors {
409
422
  options = arg3
410
423
  }
411
424
  }
412
- const defaultValueFromOptions = options ? this.getObjectPropValue(options, 'defaultValue') : undefined
425
+ const defaultValueFromOptions = options ? getObjectPropValue(options, 'defaultValue') : undefined
413
426
  const finalDefaultValue = (typeof defaultValueFromOptions === 'string' ? defaultValueFromOptions : defaultValue)
414
427
 
415
428
  // Loop through each key found (could be one or more) and process it
@@ -419,7 +432,7 @@ export class ASTVisitors {
419
432
 
420
433
  // Determine namespace (explicit ns > scope ns > ns:key > default)
421
434
  if (options) {
422
- const nsVal = this.getObjectPropValue(options, 'ns')
435
+ const nsVal = getObjectPropValue(options, 'ns')
423
436
  if (typeof nsVal === 'string') ns = nsVal
424
437
  }
425
438
  if (!ns && scopeInfo?.defaultNs) ns = scopeInfo.defaultNs
@@ -443,7 +456,7 @@ export class ASTVisitors {
443
456
 
444
457
  // Handle plurals, context, and returnObjects
445
458
  if (options) {
446
- const contextProp = this.getObjectProperty(options, 'context')
459
+ const contextProp = getObjectProperty(options, 'context')
447
460
 
448
461
  // 1. Handle Dynamic Context (Ternary) first
449
462
  if (contextProp?.value?.type === 'ConditionalExpression') {
@@ -461,7 +474,7 @@ export class ASTVisitors {
461
474
  }
462
475
 
463
476
  // 2. Handle Static Context
464
- const contextValue = this.getObjectPropValue(options, 'context')
477
+ const contextValue = getObjectPropValue(options, 'context')
465
478
  if (typeof contextValue === 'string' && contextValue) {
466
479
  const contextSeparator = this.config.extract.contextSeparator ?? '_'
467
480
  this.pluginContext.addKey({ key: `${finalKey}${contextSeparator}${contextValue}`, ns, defaultValue: dv })
@@ -469,13 +482,16 @@ export class ASTVisitors {
469
482
  }
470
483
 
471
484
  // 3. Handle Plurals
472
- if (this.getObjectPropValue(options, 'count') !== undefined) {
473
- this.handlePluralKeys(finalKey, ns, options)
485
+ const hasCount = getObjectPropValue(options, 'count') !== undefined
486
+ const isOrdinalByOption = getObjectPropValue(options, 'ordinal') === true
487
+ if (hasCount || isOrdinalByKey) {
488
+ // Pass the combined ordinal flag to the handler
489
+ this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey)
474
490
  continue
475
491
  }
476
492
 
477
493
  // 4. Handle returnObjects
478
- if (this.getObjectPropValue(options, 'returnObjects') === true) {
494
+ if (getObjectPropValue(options, 'returnObjects') === true) {
479
495
  this.objectKeys.add(finalKey)
480
496
  // Fall through to add the base key itself
481
497
  }
@@ -496,43 +512,50 @@ export class ASTVisitors {
496
512
  * @param key - Base key name for pluralization
497
513
  * @param ns - Namespace for the keys
498
514
  * @param options - object expression options
515
+ * @param isOrdinal - isOrdinal flag
499
516
  *
500
517
  * @private
501
518
  */
502
- private handlePluralKeys (key: string, ns: string | undefined, options: ObjectExpression): void {
519
+ private handlePluralKeys (key: string, ns: string | undefined, options: ObjectExpression, isOrdinal: boolean): void {
503
520
  try {
504
- const isOrdinal = this.getObjectPropValue(options, 'ordinal') === true
505
521
  const type = isOrdinal ? 'ordinal' : 'cardinal'
506
522
 
507
523
  const pluralCategories = new Intl.PluralRules(this.config.extract?.primaryLanguage, { type }).resolvedOptions().pluralCategories
508
524
  const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
509
525
 
510
- const defaultValue = this.getObjectPropValue(options, 'defaultValue')
511
- const defaultValueOther = this.getObjectPropValue(options, 'defaultValue_other')
526
+ // Get all possible default values once at the start
527
+ const defaultValue = getObjectPropValue(options, 'defaultValue')
528
+ const otherDefault = getObjectPropValue(options, `defaultValue${pluralSeparator}other`)
529
+ const ordinalOtherDefault = getObjectPropValue(options, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`)
512
530
 
513
531
  for (const category of pluralCategories) {
514
- // Construct the specific defaultValue key, e.g., `defaultValue_ordinal_one` or `defaultValue_one`
515
- const specificDefaultKey = isOrdinal ? `defaultValue_ordinal_${category}` : `defaultValue_${category}`
516
- const specificDefault = this.getObjectPropValue(options, specificDefaultKey)
532
+ // 1. Look for the most specific default value (e.g., defaultValue_ordinal_one)
533
+ const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`
534
+ const specificDefault = getObjectPropValue(options, specificDefaultKey)
517
535
 
536
+ // 2. Determine the final default value using a clear fallback chain
518
537
  let finalDefaultValue: string | undefined
519
-
520
538
  if (typeof specificDefault === 'string') {
539
+ // 1. Use the most specific default if it exists (e.g., defaultValue_one)
521
540
  finalDefaultValue = specificDefault
522
541
  } else if (category === 'one' && typeof defaultValue === 'string') {
523
- // 'one' specifically falls back to the main `defaultValue` prop
542
+ // 2. SPECIAL CASE: The 'one' category falls back to the main 'defaultValue' prop
524
543
  finalDefaultValue = defaultValue
525
- } else if (typeof defaultValueOther === 'string') {
526
- // All other categories fall back to `defaultValue_other`
527
- finalDefaultValue = defaultValueOther
544
+ } else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
545
+ // 3a. Other ordinal categories fall back to 'defaultValue_ordinal_other'
546
+ finalDefaultValue = ordinalOtherDefault
547
+ } else if (!isOrdinal && typeof otherDefault === 'string') {
548
+ // 3b. Other cardinal categories fall back to 'defaultValue_other'
549
+ finalDefaultValue = otherDefault
528
550
  } else if (typeof defaultValue === 'string') {
529
- // If all else fails, use the main `defaultValue` as a last resort
551
+ // 4. If no '_other' is found, all categories can fall back to the main 'defaultValue'
530
552
  finalDefaultValue = defaultValue
531
553
  } else {
532
- finalDefaultValue = key // Fallback to the key itself
554
+ // 5. Final fallback to the base key itself
555
+ finalDefaultValue = key
533
556
  }
534
557
 
535
- // Construct the final key, e.g., `key_ordinal_one` or `key_one`
558
+ // 3. Construct the final plural key
536
559
  const finalKey = isOrdinal
537
560
  ? `${key}${pluralSeparator}ordinal${pluralSeparator}${category}`
538
561
  : `${key}${pluralSeparator}${category}`
@@ -542,12 +565,13 @@ export class ASTVisitors {
542
565
  ns,
543
566
  defaultValue: finalDefaultValue,
544
567
  hasCount: true,
568
+ isOrdinal
545
569
  })
546
570
  }
547
571
  } catch (e) {
548
572
  this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`)
549
573
  // Fallback to a simple key if Intl API fails
550
- const defaultValue = this.getObjectPropValue(options, 'defaultValue')
574
+ const defaultValue = getObjectPropValue(options, 'defaultValue')
551
575
  this.pluginContext.addKey({ key, ns, defaultValue: typeof defaultValue === 'string' ? defaultValue : key })
552
576
  }
553
577
  }
@@ -595,20 +619,20 @@ export class ASTVisitors {
595
619
  if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
596
620
  const extractedKey = extractFromTransComponent(node, this.config)
597
621
  if (extractedKey) {
598
- // If ns is not explicitly set on the component, try to find it from the `t` prop
622
+ // If ns is not explicitly set on the component, try to find it from the `t` prop
599
623
  if (!extractedKey.ns) {
600
624
  const tProp = node.opening.attributes?.find(
601
625
  attr =>
602
626
  attr.type === 'JSXAttribute' &&
603
- attr.name.type === 'Identifier' &&
604
- attr.name.value === 't'
627
+ attr.name.type === 'Identifier' &&
628
+ attr.name.value === 't'
605
629
  )
606
630
 
607
631
  // Check if the prop value is an identifier (e.g., t={t})
608
632
  if (
609
633
  tProp?.type === 'JSXAttribute' &&
610
- tProp.value?.type === 'JSXExpressionContainer' &&
611
- tProp.value.expression.type === 'Identifier'
634
+ tProp.value?.type === 'JSXExpressionContainer' &&
635
+ tProp.value.expression.type === 'Identifier'
612
636
  ) {
613
637
  const tIdentifier = tProp.value.expression.value
614
638
  const scopeInfo = this.getVarFromScope(tIdentifier)
@@ -631,15 +655,35 @@ export class ASTVisitors {
631
655
  for (const context of contextValues) {
632
656
  this.pluginContext.addKey({ key: `${extractedKey.key}${contextSeparator}${context}`, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
633
657
  }
634
- // Add the base key as well
635
- this.pluginContext.addKey(extractedKey)
658
+ // Only add the base key as a fallback if the context is dynamic (i.e., not a simple string).
659
+ if (extractedKey.contextExpression.type !== 'StringLiteral') {
660
+ this.pluginContext.addKey(extractedKey)
661
+ }
636
662
  }
637
663
  } else if (extractedKey.hasCount) {
638
- this.handleSimplePluralKeys(extractedKey.key, extractedKey.defaultValue, extractedKey.ns)
664
+ // Find isOrdinal prop on the <Trans> component
665
+ const ordinalAttr = node.opening.attributes?.find(
666
+ (attr) =>
667
+ attr.type === 'JSXAttribute' &&
668
+ attr.name.type === 'Identifier' &&
669
+ attr.name.value === 'ordinal'
670
+ )
671
+ const isOrdinal = !!ordinalAttr
672
+
673
+ // If tOptions are provided, use the advanced plural handler
674
+ const optionsNode = extractedKey.optionsNode ?? { type: 'ObjectExpression', properties: [], span: { start: 0, end: 0, ctxt: 0 } }
675
+
676
+ // Inject the defaultValue from children into the options object
677
+ optionsNode.properties.push({
678
+ type: 'KeyValueProperty',
679
+ key: { type: 'Identifier', value: 'defaultValue', optional: false, span: { start: 0, end: 0, ctxt: 0 } },
680
+ value: { type: 'StringLiteral', value: extractedKey.defaultValue!, span: { start: 0, end: 0, ctxt: 0 } }
681
+ })
682
+
683
+ this.handlePluralKeys(extractedKey.key, extractedKey.ns, optionsNode, isOrdinal)
639
684
  } else {
640
685
  this.pluginContext.addKey(extractedKey)
641
686
  }
642
- // The duplicated addKey call has been removed.
643
687
  }
644
688
  }
645
689
  }
@@ -672,47 +716,6 @@ export class ASTVisitors {
672
716
  return undefined
673
717
  }
674
718
 
675
- /**
676
- * Extracts string value from object property.
677
- *
678
- * Looks for properties by name and returns their string values.
679
- * Used for extracting options like 'ns', 'defaultValue', 'context', etc.
680
- *
681
- * @param object - Object expression to search
682
- * @param propName - Property name to find
683
- * @returns String value if found, empty string if property exists but isn't a string, undefined if not found
684
- *
685
- * @private
686
- */
687
- private getObjectPropValue (object: ObjectExpression, propName: string): string | boolean | number | undefined {
688
- const prop = (object.properties).find(
689
- (p) =>
690
- p.type === 'KeyValueProperty' &&
691
- (
692
- (p.key?.type === 'Identifier' && p.key.value === propName) ||
693
- (p.key?.type === 'StringLiteral' && p.key.value === propName)
694
- )
695
- )
696
-
697
- if (prop?.type === 'KeyValueProperty') {
698
- const val = prop.value
699
- // Return concrete literal values when possible
700
- if (val.type === 'StringLiteral') {
701
- return val.value
702
- }
703
- if (val.type === 'BooleanLiteral') {
704
- return val.value
705
- }
706
- if (val.type === 'NumericLiteral') {
707
- return val.value
708
- }
709
- // For other expression types (identifier, member expr, etc.) we only care that the prop exists.
710
- // Return an empty string to indicate presence.
711
- return ''
712
- }
713
- return undefined
714
- }
715
-
716
719
  /**
717
720
  * Extracts translation key from selector API arrow function.
718
721
  *
@@ -807,32 +810,6 @@ export class ASTVisitors {
807
810
  return []
808
811
  }
809
812
 
810
- /**
811
- * Finds and returns the full property node (KeyValueProperty) for the given
812
- * property name from an ObjectExpression.
813
- *
814
- * Matches both identifier keys (e.g., { ns: 'value' }) and string literal keys
815
- * (e.g., { 'ns': 'value' }).
816
- *
817
- * This helper returns the full property node rather than just its primitive
818
- * value so callers can inspect expression types (ConditionalExpression, etc.).
819
- *
820
- * @private
821
- * @param object - The SWC ObjectExpression to search
822
- * @param propName - The property name to locate
823
- * @returns The matching KeyValueProperty node if found, otherwise undefined.
824
- */
825
- private getObjectProperty (object: ObjectExpression, propName: string): any {
826
- return (object.properties).find(
827
- (p) =>
828
- p.type === 'KeyValueProperty' &&
829
- (
830
- (p.key?.type === 'Identifier' && p.key.value === propName) ||
831
- (p.key?.type === 'StringLiteral' && p.key.value === propName)
832
- )
833
- )
834
- }
835
-
836
813
  /**
837
814
  * Finds the configuration for a given useTranslation function name.
838
815
  * Applies default argument positions if none are specified.
@@ -1,5 +1,6 @@
1
1
  import type { JSXElement } from '@swc/core'
2
2
  import type { ExtractedKey, I18nextToolkitConfig } from '../../types'
3
+ import { getObjectProperty, getObjectPropValue } from './ast-utils'
3
4
 
4
5
  /**
5
6
  * Extracts translation keys from JSX Trans components.
@@ -55,13 +56,23 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
55
56
  )
56
57
  const hasCount = !!countAttr
57
58
 
59
+ const tOptionsAttr = node.opening.attributes?.find(
60
+ (attr) =>
61
+ attr.type === 'JSXAttribute' &&
62
+ attr.name.type === 'Identifier' &&
63
+ attr.name.value === 'tOptions'
64
+ )
65
+ const optionsNode = (tOptionsAttr?.type === 'JSXAttribute' && tOptionsAttr.value?.type === 'JSXExpressionContainer' && tOptionsAttr.value.expression.type === 'ObjectExpression')
66
+ ? tOptionsAttr.value.expression
67
+ : undefined
68
+
58
69
  const contextAttr = node.opening.attributes?.find(
59
70
  (attr) =>
60
71
  attr.type === 'JSXAttribute' &&
61
72
  attr.name.type === 'Identifier' &&
62
73
  attr.name.value === 'context'
63
74
  )
64
- const contextExpression = (contextAttr?.type === 'JSXAttribute' && contextAttr.value?.type === 'JSXExpressionContainer')
75
+ let contextExpression = (contextAttr?.type === 'JSXAttribute' && contextAttr.value?.type === 'JSXExpressionContainer')
65
76
  ? contextAttr.value.expression
66
77
  : undefined
67
78
 
@@ -76,13 +87,27 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
76
87
  return null
77
88
  }
78
89
 
79
- const nsAttr = node.opening.attributes?.find(
80
- (attr) =>
81
- attr.type === 'JSXAttribute' && attr.name.type === 'Identifier' && attr.name.value === 'ns'
82
- )
83
- const ns = nsAttr?.type === 'JSXAttribute' && nsAttr.value?.type === 'StringLiteral'
84
- ? nsAttr.value.value
85
- : undefined
90
+ // 1. Prioritize direct props for 'ns' and 'context'
91
+ const nsAttr = node.opening.attributes?.find(attr => attr.type === 'JSXAttribute' && attr.name.type === 'Identifier' && attr.name.value === 'ns')
92
+ let ns: string | undefined
93
+ if (nsAttr?.type === 'JSXAttribute' && nsAttr.value?.type === 'StringLiteral') {
94
+ ns = nsAttr.value.value
95
+ } else {
96
+ ns = undefined
97
+ }
98
+
99
+ // 2. If not found, fall back to looking inside tOptions
100
+ if (optionsNode) {
101
+ if (ns === undefined) {
102
+ ns = getObjectPropValue(optionsNode, 'ns') as string | undefined
103
+ }
104
+ if (contextExpression === undefined) {
105
+ const contextPropFromOptions = getObjectProperty(optionsNode, 'context')
106
+ if (contextPropFromOptions?.value) {
107
+ contextExpression = contextPropFromOptions.value
108
+ }
109
+ }
110
+ }
86
111
 
87
112
  let defaultValue = config.extract.defaultValue || ''
88
113
  if (defaultsAttr?.type === 'JSXAttribute' && defaultsAttr.value?.type === 'StringLiteral') {
@@ -91,7 +116,7 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
91
116
  defaultValue = serializeJSXChildren(node.children, config)
92
117
  }
93
118
 
94
- return { key, ns, defaultValue: defaultValue || key, hasCount, contextExpression }
119
+ return { key, ns, defaultValue: defaultValue || key, hasCount, contextExpression, optionsNode }
95
120
  }
96
121
 
97
122
  /**
package/src/locize.ts CHANGED
@@ -81,13 +81,6 @@ async function interactiveCredentialSetup (config: I18nextToolkitConfig): Promis
81
81
  return undefined
82
82
  }
83
83
 
84
- // Use the entered credentials for the current run
85
- config.locize = {
86
- projectId: answers.projectId,
87
- apiKey: answers.apiKey,
88
- version: answers.version,
89
- }
90
-
91
84
  const { save } = await inquirer.prompt([{
92
85
  type: 'confirm',
93
86
  name: 'save',
@@ -116,33 +109,29 @@ LOCIZE_API_KEY=${answers.apiKey}
116
109
  console.log(chalk.green(configSnippet))
117
110
  }
118
111
 
119
- return config.locize
112
+ return {
113
+ projectId: answers.projectId,
114
+ apiKey: answers.apiKey,
115
+ version: answers.version,
116
+ }
120
117
  }
121
118
 
122
119
  /**
123
- * Converts CLI options and configuration into locize-cli command arguments.
124
- *
125
- * Maps toolkit configuration and CLI flags to the appropriate locize-cli arguments:
126
- * - `updateValues` → `--update-values`
127
- * - `sourceLanguageOnly` → `--reference-language-only`
128
- * - `compareModificationTime` → `--compare-modification-time`
129
- * - `dryRun` → `--dry`
130
- *
131
- * @param command - The locize command being executed
132
- * @param cliOptions - CLI options passed to the command
133
- * @param locizeConfig - Locize configuration from the config file
134
- * @returns Array of command-line arguments
135
- *
136
- * @example
137
- * ```typescript
138
- * const args = cliOptionsToArgs('sync', { updateValues: true }, { dryRun: false })
139
- * // Returns: ['--update-values', 'true']
140
- * ```
120
+ * Helper function to build the array of arguments for the execa call.
121
+ * This ensures the logic is consistent for both the initial run and the retry.
141
122
  */
142
- function cliOptionsToArgs (command: 'sync' | 'download' | 'migrate', cliOptions: any = {}, locizeConfig: any = {}) {
143
- const commandArgs: string[] = []
123
+ function buildArgs (command: string, config: I18nextToolkitConfig, cliOptions: any): string[] {
124
+ const { locize: locizeConfig = {}, extract } = config
125
+ const { projectId, apiKey, version } = locizeConfig
126
+
127
+ const commandArgs: string[] = [command]
128
+
129
+ if (projectId) commandArgs.push('--project-id', projectId)
130
+ if (apiKey) commandArgs.push('--api-key', apiKey)
131
+ if (version) commandArgs.push('--ver', version)
132
+ // TODO: there might be more configurable locize-cli options in future
144
133
 
145
- // Pass-through options
134
+ // Pass-through options from the CLI
146
135
  if (command === 'sync') {
147
136
  const updateValues = cliOptions.updateValues ?? locizeConfig.updateValues
148
137
  if (updateValues) commandArgs.push('--update-values', 'true')
@@ -154,6 +143,9 @@ function cliOptionsToArgs (command: 'sync' | 'download' | 'migrate', cliOptions:
154
143
  if (dryRun) commandArgs.push('--dry', 'true')
155
144
  }
156
145
 
146
+ const basePath = resolve(process.cwd(), extract.output.split('/{{language}}/')[0])
147
+ commandArgs.push('--path', basePath)
148
+
157
149
  return commandArgs
158
150
  }
159
151
 
@@ -189,44 +181,33 @@ async function runLocizeCommand (command: 'sync' | 'download' | 'migrate', confi
189
181
 
190
182
  const spinner = ora(`Running 'locize ${command}'...\n`).start()
191
183
 
192
- const locizeConfig = config.locize || {}
193
- const { projectId, apiKey, version } = locizeConfig
194
- let commandArgs: string[] = [command]
195
-
196
- if (projectId) commandArgs.push('--project-id', projectId)
197
- if (apiKey) commandArgs.push('--api-key', apiKey)
198
- if (version) commandArgs.push('--ver', version)
199
- // TODO: there might be more configurable locize-cli options in future
200
-
201
- commandArgs.push(...cliOptionsToArgs(command, cliOptions, locizeConfig))
202
-
203
- const basePath = resolve(process.cwd(), config.extract.output.split('/{{language}}/')[0])
204
- commandArgs.push('--path', basePath)
184
+ let effectiveConfig = config
205
185
 
206
186
  try {
207
- console.log(chalk.cyan(`\nRunning 'locize ${commandArgs.join(' ')}'...`))
208
- const result = await execa('locize', commandArgs, { stdio: 'pipe' })
187
+ // 1. First attempt
188
+ const initialArgs = buildArgs(command, effectiveConfig, cliOptions)
189
+ console.log(chalk.cyan(`\nRunning 'locize ${initialArgs.join(' ')}'...`))
190
+ const result = await execa('locize', initialArgs, { stdio: 'pipe' })
191
+
209
192
  spinner.succeed(chalk.green(`'locize ${command}' completed successfully.`))
210
193
  if (result?.stdout) console.log(result.stdout) // Print captured output on success
211
194
  } catch (error: any) {
212
195
  const stderr = error.stderr || ''
213
196
  if (stderr.includes('missing required argument')) {
214
- // Fallback to interactive setup
215
- const newCredentials = await interactiveCredentialSetup(config)
197
+ // 2. Auth failure, trigger interactive setup
198
+ const newCredentials = await interactiveCredentialSetup(effectiveConfig)
216
199
  if (newCredentials) {
217
- // Retry the command with the new credentials
218
- commandArgs = [command]
219
- if (newCredentials.projectId) commandArgs.push('--project-id', newCredentials.projectId)
220
- if (newCredentials.apiKey) commandArgs.push('--api-key', newCredentials.apiKey)
221
- if (newCredentials.version) commandArgs.push('--ver', newCredentials.version)
222
- // TODO: there might be more configurable locize-cli options in future
223
- commandArgs.push(...cliOptionsToArgs(command, cliOptions, locizeConfig))
224
- commandArgs.push('--path', basePath)
200
+ effectiveConfig = { ...effectiveConfig, locize: newCredentials }
201
+
202
+ spinner.start('Retrying with new credentials...')
225
203
  try {
226
- spinner.start('Retrying with new credentials...') // Restart spinner
227
- const result = await execa('locize', commandArgs, { stdio: 'pipe' })
204
+ // 3. Retry attempt, rebuilding args with the NOW-UPDATED currentConfig object
205
+ const retryArgs = buildArgs(command, effectiveConfig, cliOptions)
206
+ console.log(chalk.cyan(`\nRunning 'locize ${retryArgs.join(' ')}'...`))
207
+ const result = await execa('locize', retryArgs, { stdio: 'pipe' })
208
+
228
209
  spinner.succeed(chalk.green('Retry successful!'))
229
- if (result?.stdout) console.log(result.stdout) // Print captured output on success
210
+ if (result?.stdout) console.log(result.stdout)
230
211
  } catch (retryError: any) {
231
212
  spinner.fail(chalk.red('Error during retry.'))
232
213
  console.error(retryError.stderr || retryError.message)