i18next-cli 1.8.0 → 1.10.0
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 +20 -0
- package/README.md +10 -8
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +1 -1
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -1
- package/dist/cjs/extractor/parsers/comment-parser.js +1 -1
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -1
- package/dist/cjs/syncer.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +1 -1
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -1
- package/dist/esm/extractor/parsers/comment-parser.js +1 -1
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -1
- package/dist/esm/syncer.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/extractor/core/translation-manager.ts +44 -3
- package/src/extractor/parsers/ast-visitors.ts +315 -86
- package/src/extractor/parsers/comment-parser.ts +78 -18
- package/src/extractor/parsers/jsx-parser.ts +16 -1
- package/src/syncer.ts +1 -1
- package/src/types.ts +7 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/extractor/parsers/ast-visitors.d.ts +13 -3
- package/types/extractor/parsers/ast-visitors.d.ts.map +1 -1
- package/types/extractor/parsers/comment-parser.d.ts.map +1 -1
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -1
- package/types/types.d.ts +3 -4
- package/types/types.d.ts.map +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Module, Node, CallExpression, VariableDeclarator, JSXElement, ArrowFunctionExpression, ObjectExpression, Expression, TemplateLiteral } from '@swc/core'
|
|
1
|
+
import type { Module, Node, CallExpression, VariableDeclarator, JSXElement, ArrowFunctionExpression, ObjectExpression, Expression, TemplateLiteral, TsType, TsTemplateLiteralType } from '@swc/core'
|
|
2
2
|
import type { PluginContext, I18nextToolkitConfig, Logger, ExtractedKey, ScopeInfo } from '../../types'
|
|
3
3
|
import { extractFromTransComponent } from './jsx-parser'
|
|
4
4
|
import { getObjectProperty, getObjectPropValue } from './ast-utils'
|
|
@@ -547,15 +547,44 @@ export class ASTVisitors {
|
|
|
547
547
|
const parts = key.split(nsSeparator)
|
|
548
548
|
ns = parts.shift()
|
|
549
549
|
key = parts.join(nsSeparator)
|
|
550
|
+
|
|
551
|
+
if (!key || key.trim() === '') {
|
|
552
|
+
this.logger.warn(`Skipping key that became empty after namespace removal: '${ns}${nsSeparator}'`)
|
|
553
|
+
continue
|
|
554
|
+
}
|
|
550
555
|
}
|
|
551
556
|
|
|
552
557
|
if (!ns && scopeInfo?.defaultNs) ns = scopeInfo.defaultNs
|
|
553
558
|
if (!ns) ns = this.config.extract.defaultNS
|
|
554
559
|
|
|
555
560
|
let finalKey = key
|
|
561
|
+
|
|
562
|
+
// Apply keyPrefix AFTER namespace extraction
|
|
556
563
|
if (scopeInfo?.keyPrefix) {
|
|
557
564
|
const keySeparator = this.config.extract.keySeparator ?? '.'
|
|
558
|
-
|
|
565
|
+
|
|
566
|
+
// Apply keyPrefix - handle case where keyPrefix already ends with separator
|
|
567
|
+
if (keySeparator !== false) {
|
|
568
|
+
if (scopeInfo.keyPrefix.endsWith(keySeparator)) {
|
|
569
|
+
finalKey = `${scopeInfo.keyPrefix}${key}`
|
|
570
|
+
} else {
|
|
571
|
+
finalKey = `${scopeInfo.keyPrefix}${keySeparator}${key}`
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
finalKey = `${scopeInfo.keyPrefix}${key}`
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Validate keyPrefix combinations that create problematic keys
|
|
578
|
+
if (keySeparator !== false) {
|
|
579
|
+
// Check for patterns that would create empty segments in the nested key structure
|
|
580
|
+
const segments = finalKey.split(keySeparator)
|
|
581
|
+
const hasEmptySegment = segments.some(segment => segment.trim() === '')
|
|
582
|
+
|
|
583
|
+
if (hasEmptySegment) {
|
|
584
|
+
this.logger.warn(`Skipping key with empty segments: '${finalKey}' (keyPrefix: '${scopeInfo.keyPrefix}', key: '${key}')`)
|
|
585
|
+
continue
|
|
586
|
+
}
|
|
587
|
+
}
|
|
559
588
|
}
|
|
560
589
|
|
|
561
590
|
const isLastKey = i === keysToProcess.length - 1
|
|
@@ -594,14 +623,19 @@ export class ASTVisitors {
|
|
|
594
623
|
const hasCount = getObjectPropValue(options, 'count') !== undefined
|
|
595
624
|
const isOrdinalByOption = getObjectPropValue(options, 'ordinal') === true
|
|
596
625
|
if (hasCount || isOrdinalByKey) {
|
|
597
|
-
//
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
626
|
+
// Check if plurals are disabled
|
|
627
|
+
if (this.config.extract.disablePlurals) {
|
|
628
|
+
// When plurals are disabled, treat count as a regular option (for interpolation only)
|
|
629
|
+
// Still handle context normally
|
|
630
|
+
if (keysWithContext.length > 0) {
|
|
631
|
+
keysWithContext.forEach(this.pluginContext.addKey)
|
|
632
|
+
} else {
|
|
633
|
+
// No context, just add the base key (no plurals even if count is present)
|
|
634
|
+
this.pluginContext.addKey({ key: finalKey, ns, defaultValue: dv })
|
|
602
635
|
}
|
|
603
636
|
} else {
|
|
604
|
-
//
|
|
637
|
+
// Original plural handling logic when plurals are enabled
|
|
638
|
+
// Always pass the base key to handlePluralKeys - it will handle context internally
|
|
605
639
|
this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey, finalDefaultValue)
|
|
606
640
|
}
|
|
607
641
|
|
|
@@ -687,7 +721,23 @@ export class ASTVisitors {
|
|
|
687
721
|
try {
|
|
688
722
|
const type = isOrdinal ? 'ordinal' : 'cardinal'
|
|
689
723
|
|
|
690
|
-
|
|
724
|
+
// Generate plural forms for ALL target languages to ensure we have all necessary keys
|
|
725
|
+
const allPluralCategories = new Set<string>()
|
|
726
|
+
|
|
727
|
+
for (const locale of this.config.locales) {
|
|
728
|
+
try {
|
|
729
|
+
const pluralRules = new Intl.PluralRules(locale, { type })
|
|
730
|
+
const categories = pluralRules.resolvedOptions().pluralCategories
|
|
731
|
+
categories.forEach(cat => allPluralCategories.add(cat))
|
|
732
|
+
} catch (e) {
|
|
733
|
+
// If a locale is invalid, fall back to English rules
|
|
734
|
+
const englishRules = new Intl.PluralRules('en', { type })
|
|
735
|
+
const categories = englishRules.resolvedOptions().pluralCategories
|
|
736
|
+
categories.forEach(cat => allPluralCategories.add(cat))
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const pluralCategories = Array.from(allPluralCategories).sort()
|
|
691
741
|
const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
|
|
692
742
|
|
|
693
743
|
// Get all possible default values once at the start
|
|
@@ -709,49 +759,82 @@ export class ASTVisitors {
|
|
|
709
759
|
}
|
|
710
760
|
}
|
|
711
761
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const specificDefault = getObjectPropValue(options, specificDefaultKey)
|
|
762
|
+
// Handle context - both static and dynamic
|
|
763
|
+
const contextProp = getObjectProperty(options, 'context')
|
|
764
|
+
const keysToGenerate: Array<{ key: string, context?: string }> = []
|
|
716
765
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
} else if (defaultValueFromCall && targetCategory === category) {
|
|
735
|
-
// 5. LOWER PRIORITY: If we have a default value from the t() call and this category matches the count,
|
|
736
|
-
// use the default value from the call (only if no other defaults exist)
|
|
737
|
-
finalDefaultValue = defaultValueFromCall
|
|
766
|
+
if (contextProp?.value) {
|
|
767
|
+
// Handle dynamic context by resolving all possible values
|
|
768
|
+
const contextValues = this.resolvePossibleContextStringValues(contextProp.value)
|
|
769
|
+
|
|
770
|
+
if (contextValues.length > 0) {
|
|
771
|
+
// Generate keys for each context value
|
|
772
|
+
for (const contextValue of contextValues) {
|
|
773
|
+
if (contextValue.length > 0) { // Skip empty contexts
|
|
774
|
+
keysToGenerate.push({ key, context: contextValue })
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// For dynamic context, also generate base plural forms if generateBasePluralForms is not disabled
|
|
779
|
+
const shouldGenerateBaseForms = this.config.extract?.generateBasePluralForms !== false
|
|
780
|
+
if (shouldGenerateBaseForms) {
|
|
781
|
+
keysToGenerate.push({ key })
|
|
782
|
+
}
|
|
738
783
|
} else {
|
|
739
|
-
//
|
|
740
|
-
|
|
784
|
+
// Couldn't resolve context, fall back to base key only
|
|
785
|
+
keysToGenerate.push({ key })
|
|
741
786
|
}
|
|
787
|
+
} else {
|
|
788
|
+
// No context, always generate base plural forms
|
|
789
|
+
keysToGenerate.push({ key })
|
|
790
|
+
}
|
|
742
791
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
792
|
+
// Generate plural forms for each key variant
|
|
793
|
+
for (const { key: baseKey, context } of keysToGenerate) {
|
|
794
|
+
for (const category of pluralCategories) {
|
|
795
|
+
// 1. Look for the most specific default value
|
|
796
|
+
const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`
|
|
797
|
+
const specificDefault = getObjectPropValue(options, specificDefaultKey)
|
|
798
|
+
|
|
799
|
+
// 2. Determine the final default value using a clear fallback chain
|
|
800
|
+
let finalDefaultValue: string | undefined
|
|
801
|
+
if (typeof specificDefault === 'string') {
|
|
802
|
+
finalDefaultValue = specificDefault
|
|
803
|
+
} else if (category === 'one' && typeof defaultValue === 'string') {
|
|
804
|
+
finalDefaultValue = defaultValue
|
|
805
|
+
} else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
|
|
806
|
+
finalDefaultValue = ordinalOtherDefault
|
|
807
|
+
} else if (!isOrdinal && typeof otherDefault === 'string') {
|
|
808
|
+
finalDefaultValue = otherDefault
|
|
809
|
+
} else if (typeof defaultValue === 'string') {
|
|
810
|
+
finalDefaultValue = defaultValue
|
|
811
|
+
} else if (defaultValueFromCall && targetCategory === category) {
|
|
812
|
+
finalDefaultValue = defaultValueFromCall
|
|
813
|
+
} else {
|
|
814
|
+
finalDefaultValue = baseKey
|
|
815
|
+
}
|
|
747
816
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
817
|
+
// 3. Construct the final plural key
|
|
818
|
+
let finalKey: string
|
|
819
|
+
if (context) {
|
|
820
|
+
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
821
|
+
finalKey = isOrdinal
|
|
822
|
+
? `${baseKey}${contextSeparator}${context}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
823
|
+
: `${baseKey}${contextSeparator}${context}${pluralSeparator}${category}`
|
|
824
|
+
} else {
|
|
825
|
+
finalKey = isOrdinal
|
|
826
|
+
? `${baseKey}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
827
|
+
: `${baseKey}${pluralSeparator}${category}`
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
this.pluginContext.addKey({
|
|
831
|
+
key: finalKey,
|
|
832
|
+
ns,
|
|
833
|
+
defaultValue: finalDefaultValue,
|
|
834
|
+
hasCount: true,
|
|
835
|
+
isOrdinal
|
|
836
|
+
})
|
|
837
|
+
}
|
|
755
838
|
}
|
|
756
839
|
} catch (e) {
|
|
757
840
|
this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`)
|
|
@@ -862,39 +945,85 @@ export class ASTVisitors {
|
|
|
862
945
|
|
|
863
946
|
// Handle the combination of context and count
|
|
864
947
|
if (contextExpression && hasCount) {
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
948
|
+
// Check if plurals are disabled
|
|
949
|
+
if (this.config.extract.disablePlurals) {
|
|
950
|
+
// When plurals are disabled, treat count as a regular option
|
|
951
|
+
// Still handle context normally
|
|
952
|
+
const contextValues = this.resolvePossibleContextStringValues(contextExpression)
|
|
953
|
+
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
954
|
+
|
|
955
|
+
if (contextValues.length > 0) {
|
|
956
|
+
// For static context (string literal), only add context variants
|
|
957
|
+
if (contextExpression.type === 'StringLiteral') {
|
|
958
|
+
for (const context of contextValues) {
|
|
959
|
+
for (const extractedKey of extractedKeys) {
|
|
960
|
+
const contextKey = `${extractedKey.key}${contextSeparator}${context}`
|
|
961
|
+
this.pluginContext.addKey({ key: contextKey, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
} else {
|
|
965
|
+
// For dynamic context, add both base and context variants
|
|
966
|
+
extractedKeys.forEach(extractedKey => {
|
|
967
|
+
this.pluginContext.addKey({
|
|
968
|
+
key: extractedKey.key,
|
|
969
|
+
ns: extractedKey.ns,
|
|
970
|
+
defaultValue: extractedKey.defaultValue
|
|
971
|
+
})
|
|
972
|
+
})
|
|
973
|
+
for (const context of contextValues) {
|
|
974
|
+
for (const extractedKey of extractedKeys) {
|
|
975
|
+
const contextKey = `${extractedKey.key}${contextSeparator}${context}`
|
|
976
|
+
this.pluginContext.addKey({ key: contextKey, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
|
|
977
|
+
}
|
|
978
|
+
}
|
|
887
979
|
}
|
|
980
|
+
} else {
|
|
981
|
+
// Fallback to just base keys if context resolution fails
|
|
982
|
+
extractedKeys.forEach(extractedKey => {
|
|
983
|
+
this.pluginContext.addKey({
|
|
984
|
+
key: extractedKey.key,
|
|
985
|
+
ns: extractedKey.ns,
|
|
986
|
+
defaultValue: extractedKey.defaultValue
|
|
987
|
+
})
|
|
988
|
+
})
|
|
888
989
|
}
|
|
889
990
|
} else {
|
|
890
|
-
//
|
|
891
|
-
|
|
991
|
+
// Original plural handling logic when plurals are enabled
|
|
992
|
+
// Find isOrdinal prop on the <Trans> component
|
|
993
|
+
const ordinalAttr = node.opening.attributes?.find(
|
|
994
|
+
(attr) =>
|
|
995
|
+
attr.type === 'JSXAttribute' &&
|
|
996
|
+
attr.name.type === 'Identifier' &&
|
|
997
|
+
attr.name.value === 'ordinal'
|
|
998
|
+
)
|
|
999
|
+
const isOrdinal = !!ordinalAttr
|
|
1000
|
+
|
|
1001
|
+
const contextValues = this.resolvePossibleContextStringValues(contextExpression)
|
|
1002
|
+
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
1003
|
+
|
|
1004
|
+
// Generate all combinations of context and plural forms
|
|
1005
|
+
if (contextValues.length > 0) {
|
|
1006
|
+
// Generate base plural forms (no context)
|
|
1007
|
+
extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
|
|
1008
|
+
|
|
1009
|
+
// Generate context + plural combinations
|
|
1010
|
+
for (const context of contextValues) {
|
|
1011
|
+
for (const extractedKey of extractedKeys) {
|
|
1012
|
+
const contextKey = `${extractedKey.key}${contextSeparator}${context}`
|
|
1013
|
+
this.generatePluralKeysForTrans(contextKey, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode)
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
} else {
|
|
1017
|
+
// Fallback to just plural forms if context resolution fails
|
|
1018
|
+
extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
|
|
1019
|
+
}
|
|
892
1020
|
}
|
|
893
1021
|
} else if (contextExpression) {
|
|
894
1022
|
const contextValues = this.resolvePossibleContextStringValues(contextExpression)
|
|
895
1023
|
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
896
1024
|
|
|
897
1025
|
if (contextValues.length > 0) {
|
|
1026
|
+
// Add context variants
|
|
898
1027
|
for (const context of contextValues) {
|
|
899
1028
|
for (const { key, ns, defaultValue } of extractedKeys) {
|
|
900
1029
|
this.pluginContext.addKey({ key: `${key}${contextSeparator}${context}`, ns, defaultValue })
|
|
@@ -902,22 +1031,57 @@ export class ASTVisitors {
|
|
|
902
1031
|
}
|
|
903
1032
|
// Only add the base key as a fallback if the context is dynamic (i.e., not a simple string).
|
|
904
1033
|
if (contextExpression.type !== 'StringLiteral') {
|
|
905
|
-
extractedKeys.forEach(
|
|
1034
|
+
extractedKeys.forEach(extractedKey => {
|
|
1035
|
+
this.pluginContext.addKey({
|
|
1036
|
+
key: extractedKey.key,
|
|
1037
|
+
ns: extractedKey.ns,
|
|
1038
|
+
defaultValue: extractedKey.defaultValue
|
|
1039
|
+
})
|
|
1040
|
+
})
|
|
906
1041
|
}
|
|
1042
|
+
} else {
|
|
1043
|
+
// If no context values were resolved, just add base keys
|
|
1044
|
+
extractedKeys.forEach(extractedKey => {
|
|
1045
|
+
this.pluginContext.addKey({
|
|
1046
|
+
key: extractedKey.key,
|
|
1047
|
+
ns: extractedKey.ns,
|
|
1048
|
+
defaultValue: extractedKey.defaultValue
|
|
1049
|
+
})
|
|
1050
|
+
})
|
|
907
1051
|
}
|
|
908
1052
|
} else if (hasCount) {
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
(
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1053
|
+
// Check if plurals are disabled
|
|
1054
|
+
if (this.config.extract.disablePlurals) {
|
|
1055
|
+
// When plurals are disabled, just add the base keys (no plural forms)
|
|
1056
|
+
extractedKeys.forEach(extractedKey => {
|
|
1057
|
+
this.pluginContext.addKey({
|
|
1058
|
+
key: extractedKey.key,
|
|
1059
|
+
ns: extractedKey.ns,
|
|
1060
|
+
defaultValue: extractedKey.defaultValue
|
|
1061
|
+
})
|
|
1062
|
+
})
|
|
1063
|
+
} else {
|
|
1064
|
+
// Original plural handling logic when plurals are enabled
|
|
1065
|
+
// Find isOrdinal prop on the <Trans> component
|
|
1066
|
+
const ordinalAttr = node.opening.attributes?.find(
|
|
1067
|
+
(attr) =>
|
|
1068
|
+
attr.type === 'JSXAttribute' &&
|
|
1069
|
+
attr.name.type === 'Identifier' &&
|
|
1070
|
+
attr.name.value === 'ordinal'
|
|
1071
|
+
)
|
|
1072
|
+
const isOrdinal = !!ordinalAttr
|
|
917
1073
|
|
|
918
|
-
|
|
1074
|
+
extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
|
|
1075
|
+
}
|
|
919
1076
|
} else {
|
|
920
|
-
|
|
1077
|
+
// No count or context - just add the base keys
|
|
1078
|
+
extractedKeys.forEach(extractedKey => {
|
|
1079
|
+
this.pluginContext.addKey({
|
|
1080
|
+
key: extractedKey.key,
|
|
1081
|
+
ns: extractedKey.ns,
|
|
1082
|
+
defaultValue: extractedKey.defaultValue
|
|
1083
|
+
})
|
|
1084
|
+
})
|
|
921
1085
|
}
|
|
922
1086
|
}
|
|
923
1087
|
}
|
|
@@ -1158,17 +1322,49 @@ export class ASTVisitors {
|
|
|
1158
1322
|
return [`${expression.value}`] // Handle literals like 5 or true
|
|
1159
1323
|
}
|
|
1160
1324
|
|
|
1325
|
+
// Support building translation keys for
|
|
1326
|
+
// `variable satisfies 'coaching' | 'therapy'`
|
|
1327
|
+
if (expression.type === 'TsSatisfiesExpression' || expression.type === 'TsAsExpression') {
|
|
1328
|
+
const annotation = expression.typeAnnotation
|
|
1329
|
+
|
|
1330
|
+
return this.resolvePossibleStringValuesFromType(annotation, returnEmptyStrings)
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
1334
|
+
return []
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private resolvePossibleStringValuesFromType (type: TsType, returnEmptyStrings = false): string[] {
|
|
1338
|
+
if (type.type === 'TsUnionType') {
|
|
1339
|
+
return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings))
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (type.type === 'TsLiteralType') {
|
|
1343
|
+
if (type.literal.type === 'StringLiteral') {
|
|
1344
|
+
// Filter out empty strings as they should be treated as "no context" like i18next does
|
|
1345
|
+
return type.literal.value || returnEmptyStrings ? [type.literal.value] : []
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (type.literal.type === 'TemplateLiteral') {
|
|
1349
|
+
return this.resolvePossibleStringValuesFromTemplateLiteralType(type.literal)
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (type.literal.type === 'NumericLiteral' || type.literal.type === 'BooleanLiteral') {
|
|
1353
|
+
return [`${type.literal.value}`] // Handle literals like 5 or true
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1161
1357
|
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
1162
1358
|
return []
|
|
1163
1359
|
}
|
|
1164
1360
|
|
|
1165
1361
|
/**
|
|
1166
|
-
* Resolves a template literal to one or more possible
|
|
1362
|
+
* Resolves a template literal string to one or more possible strings that can be
|
|
1167
1363
|
* determined statically from the AST.
|
|
1168
1364
|
*
|
|
1169
1365
|
* @private
|
|
1170
|
-
* @param templateString - The SWC AST template literal
|
|
1171
|
-
* @returns An array of possible string values that the template
|
|
1366
|
+
* @param templateString - The SWC AST template literal string to resolve
|
|
1367
|
+
* @returns An array of possible string values that the template may produce.
|
|
1172
1368
|
*/
|
|
1173
1369
|
private resolvePossibleStringValuesFromTemplateString (templateString: TemplateLiteral): string[] {
|
|
1174
1370
|
// If there are no expressions, we can just return the cooked value
|
|
@@ -1177,7 +1373,7 @@ export class ASTVisitors {
|
|
|
1177
1373
|
return [templateString.quasis[0].cooked || '']
|
|
1178
1374
|
}
|
|
1179
1375
|
|
|
1180
|
-
// Ex. `translation.key.with.expression.${x ? 'title' : 'description}`
|
|
1376
|
+
// Ex. `translation.key.with.expression.${x ? 'title' : 'description'}`
|
|
1181
1377
|
const [firstQuasis, ...tails] = templateString.quasis
|
|
1182
1378
|
|
|
1183
1379
|
const stringValues = templateString.expressions.reduce(
|
|
@@ -1195,6 +1391,39 @@ export class ASTVisitors {
|
|
|
1195
1391
|
return stringValues
|
|
1196
1392
|
}
|
|
1197
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* Resolves a template literal type to one or more possible strings that can be
|
|
1396
|
+
* determined statically from the AST.
|
|
1397
|
+
*
|
|
1398
|
+
* @private
|
|
1399
|
+
* @param templateLiteralType - The SWC AST template literal type to resolve
|
|
1400
|
+
* @returns An array of possible string values that the template may produce.
|
|
1401
|
+
*/
|
|
1402
|
+
private resolvePossibleStringValuesFromTemplateLiteralType (templateLiteralType: TsTemplateLiteralType): string[] {
|
|
1403
|
+
// If there are no types, we can just return the cooked value
|
|
1404
|
+
if (templateLiteralType.quasis.length === 1 && templateLiteralType.types.length === 0) {
|
|
1405
|
+
// Ex. `translation.key.no.substitution`
|
|
1406
|
+
return [templateLiteralType.quasis[0].cooked || '']
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Ex. `translation.key.with.expression.${'title' | 'description'}`
|
|
1410
|
+
const [firstQuasis, ...tails] = templateLiteralType.quasis
|
|
1411
|
+
|
|
1412
|
+
const stringValues = templateLiteralType.types.reduce(
|
|
1413
|
+
(heads, type, i) => {
|
|
1414
|
+
return heads.flatMap((head) => {
|
|
1415
|
+
const tail = tails[i]?.cooked ?? ''
|
|
1416
|
+
return this.resolvePossibleStringValuesFromType(type, true).map(
|
|
1417
|
+
(expressionValue) => `${head}${expressionValue}${tail}`
|
|
1418
|
+
)
|
|
1419
|
+
})
|
|
1420
|
+
},
|
|
1421
|
+
[firstQuasis.cooked ?? '']
|
|
1422
|
+
)
|
|
1423
|
+
|
|
1424
|
+
return stringValues
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1198
1427
|
/**
|
|
1199
1428
|
* Finds the configuration for a given useTranslation function name.
|
|
1200
1429
|
* Applies default argument positions if none are specified.
|
|
@@ -39,6 +39,12 @@ export function extractKeysFromComments (
|
|
|
39
39
|
let match: RegExpExecArray | null
|
|
40
40
|
while ((match = keyRegex.exec(text)) !== null) {
|
|
41
41
|
let key = match[2]
|
|
42
|
+
|
|
43
|
+
// Validate that the key is not empty or whitespace-only
|
|
44
|
+
if (!key || key.trim() === '') {
|
|
45
|
+
continue // Skip empty keys
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
let ns: string | undefined
|
|
43
49
|
const remainder = text.slice(match.index + match[0].length)
|
|
44
50
|
|
|
@@ -54,6 +60,11 @@ export function extractKeysFromComments (
|
|
|
54
60
|
isOrdinalByKey = true
|
|
55
61
|
// Normalize the key by stripping the suffix
|
|
56
62
|
key = key.slice(0, -(pluralSeparator.length + 7)) // Remove "_ordinal"
|
|
63
|
+
|
|
64
|
+
// Validate that the key is still not empty after normalization
|
|
65
|
+
if (!key || key.trim() === '') {
|
|
66
|
+
continue // Skip keys that become empty after normalization
|
|
67
|
+
}
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
const isOrdinal = ordinal === true || isOrdinalByKey
|
|
@@ -81,21 +92,38 @@ export function extractKeysFromComments (
|
|
|
81
92
|
// 4. Final fallback to configured default namespace
|
|
82
93
|
if (!ns) ns = config.extract.defaultNS
|
|
83
94
|
|
|
84
|
-
// 5. Handle context and count combinations
|
|
85
|
-
if (
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Just plural variants
|
|
95
|
-
generatePluralKeys(key, defaultValue ?? key, ns, pluginContext, config, isOrdinal)
|
|
95
|
+
// 5. Handle context and count combinations based on disablePlurals setting
|
|
96
|
+
if (config.extract.disablePlurals) {
|
|
97
|
+
// When plurals are disabled, ignore count for key generation
|
|
98
|
+
if (context) {
|
|
99
|
+
// Only generate context variants (no base key when context is static)
|
|
100
|
+
pluginContext.addKey({ key: `${key}_${context}`, ns, defaultValue: defaultValue ?? key })
|
|
101
|
+
} else {
|
|
102
|
+
// Simple key (ignore count)
|
|
103
|
+
pluginContext.addKey({ key, ns, defaultValue: defaultValue ?? key })
|
|
104
|
+
}
|
|
96
105
|
} else {
|
|
97
|
-
//
|
|
98
|
-
|
|
106
|
+
// Original plural handling logic when plurals are enabled
|
|
107
|
+
if (context && count) {
|
|
108
|
+
// Generate context+plural combinations
|
|
109
|
+
generateContextPluralKeys(key, defaultValue ?? key, ns, context, pluginContext, config, isOrdinal)
|
|
110
|
+
|
|
111
|
+
// Only generate base plural forms if generateBasePluralForms is not disabled
|
|
112
|
+
const shouldGenerateBaseForms = config.extract?.generateBasePluralForms !== false
|
|
113
|
+
if (shouldGenerateBaseForms) {
|
|
114
|
+
generatePluralKeys(key, defaultValue ?? key, ns, pluginContext, config, isOrdinal)
|
|
115
|
+
}
|
|
116
|
+
} else if (context) {
|
|
117
|
+
// Just context variants
|
|
118
|
+
pluginContext.addKey({ key, ns, defaultValue: defaultValue ?? key })
|
|
119
|
+
pluginContext.addKey({ key: `${key}_${context}`, ns, defaultValue: defaultValue ?? key })
|
|
120
|
+
} else if (count) {
|
|
121
|
+
// Just plural variants
|
|
122
|
+
generatePluralKeys(key, defaultValue ?? key, ns, pluginContext, config, isOrdinal)
|
|
123
|
+
} else {
|
|
124
|
+
// Simple key
|
|
125
|
+
pluginContext.addKey({ key, ns, defaultValue: defaultValue ?? key })
|
|
126
|
+
}
|
|
99
127
|
}
|
|
100
128
|
}
|
|
101
129
|
}
|
|
@@ -114,8 +142,24 @@ function generatePluralKeys (
|
|
|
114
142
|
): void {
|
|
115
143
|
try {
|
|
116
144
|
const type = isOrdinal ? 'ordinal' : 'cardinal'
|
|
117
|
-
|
|
118
|
-
|
|
145
|
+
|
|
146
|
+
// Generate plural forms for ALL target languages to ensure we have all necessary keys
|
|
147
|
+
const allPluralCategories = new Set<string>()
|
|
148
|
+
|
|
149
|
+
for (const locale of config.locales) {
|
|
150
|
+
try {
|
|
151
|
+
const pluralRules = new Intl.PluralRules(locale, { type })
|
|
152
|
+
const categories = pluralRules.resolvedOptions().pluralCategories
|
|
153
|
+
categories.forEach(cat => allPluralCategories.add(cat))
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// If a locale is invalid, fall back to English rules
|
|
156
|
+
const englishRules = new Intl.PluralRules('en', { type })
|
|
157
|
+
const categories = englishRules.resolvedOptions().pluralCategories
|
|
158
|
+
categories.forEach(cat => allPluralCategories.add(cat))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const pluralCategories = Array.from(allPluralCategories).sort()
|
|
119
163
|
const pluralSeparator = config.extract.pluralSeparator ?? '_'
|
|
120
164
|
|
|
121
165
|
// Generate keys for each plural category
|
|
@@ -152,8 +196,24 @@ function generateContextPluralKeys (
|
|
|
152
196
|
): void {
|
|
153
197
|
try {
|
|
154
198
|
const type = isOrdinal ? 'ordinal' : 'cardinal'
|
|
155
|
-
|
|
156
|
-
|
|
199
|
+
|
|
200
|
+
// Generate plural forms for ALL target languages to ensure we have all necessary keys
|
|
201
|
+
const allPluralCategories = new Set<string>()
|
|
202
|
+
|
|
203
|
+
for (const locale of config.locales) {
|
|
204
|
+
try {
|
|
205
|
+
const pluralRules = new Intl.PluralRules(locale, { type })
|
|
206
|
+
const categories = pluralRules.resolvedOptions().pluralCategories
|
|
207
|
+
categories.forEach(cat => allPluralCategories.add(cat))
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// If a locale is invalid, fall back to English rules
|
|
210
|
+
const englishRules = new Intl.PluralRules(config.extract.primaryLanguage || 'en', { type })
|
|
211
|
+
const categories = englishRules.resolvedOptions().pluralCategories
|
|
212
|
+
categories.forEach(cat => allPluralCategories.add(cat))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const pluralCategories = Array.from(allPluralCategories).sort()
|
|
157
217
|
const pluralSeparator = config.extract.pluralSeparator ?? '_'
|
|
158
218
|
|
|
159
219
|
// Generate keys for each context + plural combination
|