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.
- package/CHANGELOG.md +10 -2
- package/README.md +15 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/parsers/ast-utils.js +1 -0
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -1
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -1
- package/dist/cjs/locize.js +1 -1
- package/dist/cjs/status.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/parsers/ast-utils.js +1 -0
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -1
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -1
- package/dist/esm/locize.js +1 -1
- package/dist/esm/status.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/extractor/parsers/ast-utils.ts +52 -0
- package/src/extractor/parsers/ast-visitors.ts +78 -101
- package/src/extractor/parsers/jsx-parser.ts +34 -9
- package/src/locize.ts +38 -57
- package/src/status.ts +43 -28
- package/src/types.ts +7 -1
- package/types/extractor/parsers/ast-utils.d.ts +31 -0
- package/types/extractor/parsers/ast-utils.d.ts.map +1 -0
- package/types/extractor/parsers/ast-visitors.d.ts +1 -29
- package/types/extractor/parsers/ast-visitors.d.ts.map +1 -1
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -1
- package/types/locize.d.ts.map +1 -1
- package/types/status.d.ts.map +1 -1
- package/types/types.d.ts +5 -1
- package/types/types.d.ts.map +1 -1
- package/vitest.config.ts +4 -0
- package/tryme.js +0 -8
|
@@ -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 =
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
473
|
-
|
|
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 (
|
|
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
|
-
|
|
511
|
-
const
|
|
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
|
-
//
|
|
515
|
-
const specificDefaultKey = isOrdinal ? `
|
|
516
|
-
const specificDefault =
|
|
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'
|
|
542
|
+
// 2. SPECIAL CASE: The 'one' category falls back to the main 'defaultValue' prop
|
|
524
543
|
finalDefaultValue = defaultValue
|
|
525
|
-
} else if (typeof
|
|
526
|
-
//
|
|
527
|
-
finalDefaultValue =
|
|
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
|
|
551
|
+
// 4. If no '_other' is found, all categories can fall back to the main 'defaultValue'
|
|
530
552
|
finalDefaultValue = defaultValue
|
|
531
553
|
} else {
|
|
532
|
-
|
|
554
|
+
// 5. Final fallback to the base key itself
|
|
555
|
+
finalDefaultValue = key
|
|
533
556
|
}
|
|
534
557
|
|
|
535
|
-
// Construct the final key
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
611
|
-
|
|
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
|
-
//
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
112
|
+
return {
|
|
113
|
+
projectId: answers.projectId,
|
|
114
|
+
apiKey: answers.apiKey,
|
|
115
|
+
version: answers.version,
|
|
116
|
+
}
|
|
120
117
|
}
|
|
121
118
|
|
|
122
119
|
/**
|
|
123
|
-
*
|
|
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
|
|
143
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
const
|
|
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
|
-
//
|
|
215
|
-
const newCredentials = await interactiveCredentialSetup(
|
|
197
|
+
// 2. Auth failure, trigger interactive setup
|
|
198
|
+
const newCredentials = await interactiveCredentialSetup(effectiveConfig)
|
|
216
199
|
if (newCredentials) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
227
|
-
const
|
|
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)
|
|
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)
|