i18next-cli 1.51.9 → 1.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/cjs/cli.js +3 -1
- package/dist/cjs/extractor/core/extractor.js +1 -0
- package/dist/cjs/extractor/core/translation-manager.js +41 -16
- package/dist/esm/cli.js +3 -1
- package/dist/esm/extractor/core/extractor.js +1 -0
- package/dist/esm/extractor/core/translation-manager.js +41 -16
- package/package.json +1 -1
- package/types/cli.d.ts.map +1 -1
- package/types/extractor/core/extractor.d.ts +1 -0
- package/types/extractor/core/extractor.d.ts.map +1 -1
- package/types/extractor/core/translation-manager.d.ts +2 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,7 @@ npx i18next-cli extract [options]
|
|
|
119
119
|
- `--dry-run`: Does not change any files - useful in combination with `--ci` (for CI/CD)
|
|
120
120
|
- `--sync-primary`: Sync primary language values with default values from code
|
|
121
121
|
- `--sync-all`: Sync primary language values with default values from code AND clear synced keys in all other locales (implies `--sync-primary`)
|
|
122
|
+
- `--trust-derived`: When used with `--sync-primary` or `--sync-all`, also trust defaults inferred from keys such as `t('Hello')` or `keyPrefix`-derived values. This keeps the default sync behavior strict unless you opt in.
|
|
122
123
|
- `--quiet`: Suppress spinner and non-essential output (for CI or scripting)
|
|
123
124
|
|
|
124
125
|
### Spinner and Logger Output Control
|
|
@@ -165,6 +166,9 @@ npx i18next-cli extract --sync-primary
|
|
|
165
166
|
# Sync primary and clear synced keys in all other locales
|
|
166
167
|
npx i18next-cli extract --sync-all
|
|
167
168
|
|
|
169
|
+
# Sync using explicit defaults plus inferred key-derived defaults
|
|
170
|
+
npx i18next-cli extract --sync-all --trust-derived
|
|
171
|
+
|
|
168
172
|
# Combine options for optimal development workflow
|
|
169
173
|
npx i18next-cli extract --sync-primary --watch
|
|
170
174
|
```
|
package/dist/cjs/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ const program = new commander.Command();
|
|
|
31
31
|
program
|
|
32
32
|
.name('i18next-cli')
|
|
33
33
|
.description('A unified, high-performance i18next CLI.')
|
|
34
|
-
.version('1.
|
|
34
|
+
.version('1.52.1'); // This string is replaced with the actual version at build time by rollup
|
|
35
35
|
// new: global config override option
|
|
36
36
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
37
37
|
program
|
|
@@ -42,6 +42,7 @@ program
|
|
|
42
42
|
.option('--dry-run', 'Run the extractor without writing any files to disk.')
|
|
43
43
|
.option('--sync-primary', 'Sync primary language values with default values from code.')
|
|
44
44
|
.option('--sync-all', 'Sync primary language values with default values from code AND clear synced keys in all other locales.')
|
|
45
|
+
.option('--trust-derived', 'When used with --sync-primary or --sync-all, also trust defaults inferred from keys (including keyPrefix-derived values).')
|
|
45
46
|
.option('-q, --quiet', 'Suppress spinner and output')
|
|
46
47
|
.action(async (options) => {
|
|
47
48
|
try {
|
|
@@ -55,6 +56,7 @@ program
|
|
|
55
56
|
isDryRun: !!options.dryRun,
|
|
56
57
|
syncPrimaryWithDefaults: syncPrimary,
|
|
57
58
|
syncAll: !!options.syncAll,
|
|
59
|
+
trustDerivedDefaults: !!options.trustDerived,
|
|
58
60
|
quiet: !!options.quiet
|
|
59
61
|
});
|
|
60
62
|
if (options.ci && !anyFileUpdated) {
|
|
@@ -59,6 +59,7 @@ async function runExtractor(config, options = {}) {
|
|
|
59
59
|
const results = await translationManager.getTranslations(allKeys, objectKeys, config, {
|
|
60
60
|
syncPrimaryWithDefaults: options.syncPrimaryWithDefaults,
|
|
61
61
|
syncAll: options.syncAll,
|
|
62
|
+
trustDerivedDefaults: options.trustDerivedDefaults,
|
|
62
63
|
logger: options.logger
|
|
63
64
|
});
|
|
64
65
|
let anyFileUpdated = false;
|
|
@@ -210,8 +210,9 @@ function sortObject(obj, config, customSort) {
|
|
|
210
210
|
* A helper function to build a new translation object for a single namespace.
|
|
211
211
|
* This centralizes the core logic of merging keys.
|
|
212
212
|
*/
|
|
213
|
-
function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, namespace, preservePatterns = [], objectKeys = new Set(), syncPrimaryWithDefaults = false, syncAll = false, logger$1 = new logger.ConsoleLogger()) {
|
|
214
|
-
const { keySeparator = '.', sort = true, removeUnusedKeys = true,
|
|
213
|
+
function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, namespace, preservePatterns = [], objectKeys = new Set(), syncPrimaryWithDefaults = false, syncAll = false, trustDerivedDefaults = false, primaryExistingTranslations = {}, logger$1 = new logger.ConsoleLogger()) {
|
|
214
|
+
const { keySeparator = '.', sort = true, removeUnusedKeys = true, defaultValue: emptyDefaultValue = '', pluralSeparator = '_', contextSeparator = '_', preserveContextVariants = false, } = config.extract;
|
|
215
|
+
const primaryLanguage = config.extract.primaryLanguage || config.locales[0] || 'en';
|
|
215
216
|
const nsSep = typeof config.extract.nsSeparator === 'string' ? config.extract.nsSeparator : ':';
|
|
216
217
|
// Keep the raw configured defaultValue so we can distinguish:
|
|
217
218
|
// - "not provided" (undefined) vs
|
|
@@ -621,17 +622,24 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
621
622
|
}
|
|
622
623
|
}
|
|
623
624
|
const existingValue = nestedObject.getNestedValue(existingTranslations, key, separator);
|
|
625
|
+
const primaryExistingValue = locale === primaryLanguage
|
|
626
|
+
? existingValue
|
|
627
|
+
: nestedObject.getNestedValue(primaryExistingTranslations, key, separator);
|
|
624
628
|
// When keySeparator === false we are working with flat keys (no nesting).
|
|
625
629
|
// Avoid concatenating false into strings (``${key}${false}`` => "keyfalse") which breaks the startsWith check.
|
|
626
630
|
// For flat keys there cannot be nested children, so treat them as leaves.
|
|
627
631
|
const isLeafInNewKeys = keySeparator === false
|
|
628
632
|
? true
|
|
629
633
|
: !filteredKeys.some(otherKey => otherKey.key !== key && otherKey.key.startsWith(`${key}${keySeparator}`));
|
|
634
|
+
const isDerivedDefault = isDerivedFromKey(key, defaultValue$1, explicitDefault);
|
|
630
635
|
// Determine if we should preserve an existing object
|
|
631
636
|
const shouldPreserveObject = typeof existingValue === 'object' && existingValue !== null && (objectKeys.has(key) || // Explicit returnObjects
|
|
632
637
|
!defaultValue$1 || defaultValue$1 === key // No explicit default or default equals key
|
|
633
638
|
);
|
|
634
639
|
const isStaleObject = typeof existingValue === 'object' && existingValue !== null && isLeafInNewKeys && !objectKeys.has(key) && !shouldPreserveObject;
|
|
640
|
+
const primaryShouldPreserveObject = typeof primaryExistingValue === 'object' && primaryExistingValue !== null && (objectKeys.has(key) ||
|
|
641
|
+
!defaultValue$1 || defaultValue$1 === key);
|
|
642
|
+
const primaryIsStaleObject = typeof primaryExistingValue === 'object' && primaryExistingValue !== null && isLeafInNewKeys && !objectKeys.has(key) && !primaryShouldPreserveObject;
|
|
635
643
|
// Special handling for existing objects that should be preserved
|
|
636
644
|
if (shouldPreserveObject) {
|
|
637
645
|
nestedObject.setNestedValue(newTranslations, key, existingValue, separator);
|
|
@@ -641,18 +649,15 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
641
649
|
if (existingValue === undefined || isStaleObject) {
|
|
642
650
|
if (locale === primaryLanguage) {
|
|
643
651
|
if (syncPrimaryWithDefaults) {
|
|
644
|
-
// use the unified "derived" detector (includes keyPrefix suffixes).
|
|
645
|
-
const isDerivedDefault = isDerivedFromKey(key, defaultValue$1, explicitDefault);
|
|
646
652
|
valueToSet =
|
|
647
|
-
(defaultValue$1 && !isDerivedDefault)
|
|
653
|
+
(defaultValue$1 && (!isDerivedDefault || trustDerivedDefaults))
|
|
648
654
|
? defaultValue$1
|
|
649
655
|
: defaultValue.resolveDefaultValue(emptyDefaultValue, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
|
|
650
656
|
}
|
|
651
657
|
else {
|
|
652
658
|
// If there's no real code-provided default (defaultValue is derived fallback),
|
|
653
659
|
// use the configured extract.defaultValue for PRIMARY language too.
|
|
654
|
-
|
|
655
|
-
if (derived && configuredDefaultValue !== undefined) {
|
|
660
|
+
if (isDerivedDefault && configuredDefaultValue !== undefined) {
|
|
656
661
|
valueToSet = defaultValue.resolveDefaultValue(configuredDefaultValue, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
|
|
657
662
|
}
|
|
658
663
|
else {
|
|
@@ -668,15 +673,13 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
668
673
|
else {
|
|
669
674
|
// Existing value exists - decide whether to preserve, sync primary, or clear other locales when requested
|
|
670
675
|
if (locale === primaryLanguage && syncPrimaryWithDefaults) {
|
|
671
|
-
// Reuse the same derived-default detection as the initial write path so reruns stay idempotent.
|
|
672
|
-
const isDerivedDefault = isDerivedFromKey(key, defaultValue$1, explicitDefault);
|
|
673
676
|
// If this key looks like a plural/context variant and the default
|
|
674
677
|
// wasn't explicitly provided in source code, preserve the existing value.
|
|
675
678
|
const isVariantKey = key.includes(pluralSeparator) || key.includes(contextSeparator);
|
|
676
679
|
if (isVariantKey && !explicitDefault) {
|
|
677
680
|
valueToSet = existingValue;
|
|
678
681
|
}
|
|
679
|
-
else if (defaultValue$1 && !isDerivedDefault) {
|
|
682
|
+
else if (defaultValue$1 && (!isDerivedDefault || trustDerivedDefaults)) {
|
|
680
683
|
valueToSet = defaultValue.resolveDefaultValue(defaultValue$1, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
|
|
681
684
|
}
|
|
682
685
|
else {
|
|
@@ -685,7 +688,19 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
685
688
|
}
|
|
686
689
|
else {
|
|
687
690
|
// Non-primary locale behavior
|
|
688
|
-
|
|
691
|
+
const isVariantKey = key.includes(pluralSeparator) || key.includes(contextSeparator);
|
|
692
|
+
const syncDerivedDefault = Boolean(syncAll &&
|
|
693
|
+
locale !== primaryLanguage &&
|
|
694
|
+
syncPrimaryWithDefaults &&
|
|
695
|
+
trustDerivedDefaults &&
|
|
696
|
+
defaultValue$1 &&
|
|
697
|
+
isDerivedDefault &&
|
|
698
|
+
!primaryShouldPreserveObject &&
|
|
699
|
+
(primaryExistingValue === undefined ||
|
|
700
|
+
primaryIsStaleObject ||
|
|
701
|
+
((!isVariantKey || explicitDefault) &&
|
|
702
|
+
primaryExistingValue !== defaultValue.resolveDefaultValue(defaultValue$1, key, namespace || config?.extract?.defaultNS || 'translation', primaryLanguage, defaultValue$1))));
|
|
703
|
+
if (syncAll && locale !== primaryLanguage && (explicitDefault || syncDerivedDefault)) {
|
|
689
704
|
// When syncAll is requested, clear (reset) any existing translations for keys
|
|
690
705
|
// that had explicit defaults in code so the primary default can be propagated
|
|
691
706
|
// while secondary locales get a blank/placeholder value.
|
|
@@ -838,9 +853,10 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
838
853
|
* // Results contain update status and new/existing translations for each locale.
|
|
839
854
|
* ```
|
|
840
855
|
*/
|
|
841
|
-
async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaults = false, syncAll = false, logger: logger$1 = new logger.ConsoleLogger() } = {}) {
|
|
856
|
+
async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaults = false, syncAll = false, trustDerivedDefaults = false, logger: logger$1 = new logger.ConsoleLogger() } = {}) {
|
|
842
857
|
config.extract.primaryLanguage ||= config.locales[0] || 'en';
|
|
843
|
-
config.extract.
|
|
858
|
+
const primaryLanguage = config.extract.primaryLanguage || config.locales[0] || 'en';
|
|
859
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l) => l !== primaryLanguage);
|
|
844
860
|
const patternsToPreserve = [...(config.extract.preservePatterns || [])];
|
|
845
861
|
const indentation = config.extract.indentation ?? 2;
|
|
846
862
|
for (const key of objectKeys) {
|
|
@@ -884,6 +900,10 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
884
900
|
const outputPath = fileUtils.getOutputPath(config.extract.output, locale);
|
|
885
901
|
const fullPath = node_path.resolve(process.cwd(), outputPath);
|
|
886
902
|
const existingMergedFile = await fileUtils.loadTranslationFile(fullPath) || {};
|
|
903
|
+
const primaryMergedPath = node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage));
|
|
904
|
+
const primaryMergedFile = locale === primaryLanguage
|
|
905
|
+
? existingMergedFile
|
|
906
|
+
: (await fileUtils.loadTranslationFile(primaryMergedPath) || {});
|
|
887
907
|
// Determine whether the existing merged file already uses namespace objects
|
|
888
908
|
// or is a flat mapping of translation keys -> values.
|
|
889
909
|
// If it's flat (values are primitives), we must NOT treat each translation key as a namespace.
|
|
@@ -919,12 +939,13 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
919
939
|
const nsKeys = keysByNS.get(nsKey) || [];
|
|
920
940
|
if (isTopLevel(nsKey)) {
|
|
921
941
|
// keys without namespace -> merged into top-level of the merged file
|
|
922
|
-
const built = buildNewTranslationsForNs(nsKeys, existingMergedFile, config, locale, undefined, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, logger$1);
|
|
942
|
+
const built = buildNewTranslationsForNs(nsKeys, existingMergedFile, config, locale, undefined, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, trustDerivedDefaults, primaryMergedFile, logger$1);
|
|
923
943
|
Object.assign(newMergedTranslations, built);
|
|
924
944
|
}
|
|
925
945
|
else {
|
|
926
946
|
const existingTranslations = existingMergedFile[nsKey] || {};
|
|
927
|
-
|
|
947
|
+
const primaryExistingTranslations = primaryMergedFile[nsKey] || {};
|
|
948
|
+
newMergedTranslations[nsKey] = buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, nsKey, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, trustDerivedDefaults, primaryExistingTranslations, logger$1);
|
|
928
949
|
}
|
|
929
950
|
}
|
|
930
951
|
// Preserve ignored namespaces as-is from the existing merged file
|
|
@@ -962,7 +983,11 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
962
983
|
const outputPath = fileUtils.getOutputPath(config.extract.output, locale, ns);
|
|
963
984
|
const fullPath = node_path.resolve(process.cwd(), outputPath);
|
|
964
985
|
const existingTranslations = await fileUtils.loadTranslationFile(fullPath) || {};
|
|
965
|
-
const
|
|
986
|
+
const primaryOutputPath = node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, ns));
|
|
987
|
+
const primaryExistingTranslations = locale === primaryLanguage
|
|
988
|
+
? existingTranslations
|
|
989
|
+
: (await fileUtils.loadTranslationFile(primaryOutputPath) || {});
|
|
990
|
+
const newTranslations = buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, ns, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, trustDerivedDefaults, primaryExistingTranslations, logger$1);
|
|
966
991
|
const oldContent = JSON.stringify(existingTranslations, null, indentation);
|
|
967
992
|
const newContent = JSON.stringify(newTranslations, null, indentation);
|
|
968
993
|
// Push one result per namespace file
|
package/dist/esm/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ const program = new Command();
|
|
|
29
29
|
program
|
|
30
30
|
.name('i18next-cli')
|
|
31
31
|
.description('A unified, high-performance i18next CLI.')
|
|
32
|
-
.version('1.
|
|
32
|
+
.version('1.52.1'); // This string is replaced with the actual version at build time by rollup
|
|
33
33
|
// new: global config override option
|
|
34
34
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
35
35
|
program
|
|
@@ -40,6 +40,7 @@ program
|
|
|
40
40
|
.option('--dry-run', 'Run the extractor without writing any files to disk.')
|
|
41
41
|
.option('--sync-primary', 'Sync primary language values with default values from code.')
|
|
42
42
|
.option('--sync-all', 'Sync primary language values with default values from code AND clear synced keys in all other locales.')
|
|
43
|
+
.option('--trust-derived', 'When used with --sync-primary or --sync-all, also trust defaults inferred from keys (including keyPrefix-derived values).')
|
|
43
44
|
.option('-q, --quiet', 'Suppress spinner and output')
|
|
44
45
|
.action(async (options) => {
|
|
45
46
|
try {
|
|
@@ -53,6 +54,7 @@ program
|
|
|
53
54
|
isDryRun: !!options.dryRun,
|
|
54
55
|
syncPrimaryWithDefaults: syncPrimary,
|
|
55
56
|
syncAll: !!options.syncAll,
|
|
57
|
+
trustDerivedDefaults: !!options.trustDerived,
|
|
56
58
|
quiet: !!options.quiet
|
|
57
59
|
});
|
|
58
60
|
if (options.ci && !anyFileUpdated) {
|
|
@@ -57,6 +57,7 @@ async function runExtractor(config, options = {}) {
|
|
|
57
57
|
const results = await getTranslations(allKeys, objectKeys, config, {
|
|
58
58
|
syncPrimaryWithDefaults: options.syncPrimaryWithDefaults,
|
|
59
59
|
syncAll: options.syncAll,
|
|
60
|
+
trustDerivedDefaults: options.trustDerivedDefaults,
|
|
60
61
|
logger: options.logger
|
|
61
62
|
});
|
|
62
63
|
let anyFileUpdated = false;
|
|
@@ -208,8 +208,9 @@ function sortObject(obj, config, customSort) {
|
|
|
208
208
|
* A helper function to build a new translation object for a single namespace.
|
|
209
209
|
* This centralizes the core logic of merging keys.
|
|
210
210
|
*/
|
|
211
|
-
function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, namespace, preservePatterns = [], objectKeys = new Set(), syncPrimaryWithDefaults = false, syncAll = false, logger = new ConsoleLogger()) {
|
|
212
|
-
const { keySeparator = '.', sort = true, removeUnusedKeys = true,
|
|
211
|
+
function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, namespace, preservePatterns = [], objectKeys = new Set(), syncPrimaryWithDefaults = false, syncAll = false, trustDerivedDefaults = false, primaryExistingTranslations = {}, logger = new ConsoleLogger()) {
|
|
212
|
+
const { keySeparator = '.', sort = true, removeUnusedKeys = true, defaultValue: emptyDefaultValue = '', pluralSeparator = '_', contextSeparator = '_', preserveContextVariants = false, } = config.extract;
|
|
213
|
+
const primaryLanguage = config.extract.primaryLanguage || config.locales[0] || 'en';
|
|
213
214
|
const nsSep = typeof config.extract.nsSeparator === 'string' ? config.extract.nsSeparator : ':';
|
|
214
215
|
// Keep the raw configured defaultValue so we can distinguish:
|
|
215
216
|
// - "not provided" (undefined) vs
|
|
@@ -619,17 +620,24 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
619
620
|
}
|
|
620
621
|
}
|
|
621
622
|
const existingValue = getNestedValue(existingTranslations, key, separator);
|
|
623
|
+
const primaryExistingValue = locale === primaryLanguage
|
|
624
|
+
? existingValue
|
|
625
|
+
: getNestedValue(primaryExistingTranslations, key, separator);
|
|
622
626
|
// When keySeparator === false we are working with flat keys (no nesting).
|
|
623
627
|
// Avoid concatenating false into strings (``${key}${false}`` => "keyfalse") which breaks the startsWith check.
|
|
624
628
|
// For flat keys there cannot be nested children, so treat them as leaves.
|
|
625
629
|
const isLeafInNewKeys = keySeparator === false
|
|
626
630
|
? true
|
|
627
631
|
: !filteredKeys.some(otherKey => otherKey.key !== key && otherKey.key.startsWith(`${key}${keySeparator}`));
|
|
632
|
+
const isDerivedDefault = isDerivedFromKey(key, defaultValue, explicitDefault);
|
|
628
633
|
// Determine if we should preserve an existing object
|
|
629
634
|
const shouldPreserveObject = typeof existingValue === 'object' && existingValue !== null && (objectKeys.has(key) || // Explicit returnObjects
|
|
630
635
|
!defaultValue || defaultValue === key // No explicit default or default equals key
|
|
631
636
|
);
|
|
632
637
|
const isStaleObject = typeof existingValue === 'object' && existingValue !== null && isLeafInNewKeys && !objectKeys.has(key) && !shouldPreserveObject;
|
|
638
|
+
const primaryShouldPreserveObject = typeof primaryExistingValue === 'object' && primaryExistingValue !== null && (objectKeys.has(key) ||
|
|
639
|
+
!defaultValue || defaultValue === key);
|
|
640
|
+
const primaryIsStaleObject = typeof primaryExistingValue === 'object' && primaryExistingValue !== null && isLeafInNewKeys && !objectKeys.has(key) && !primaryShouldPreserveObject;
|
|
633
641
|
// Special handling for existing objects that should be preserved
|
|
634
642
|
if (shouldPreserveObject) {
|
|
635
643
|
setNestedValue(newTranslations, key, existingValue, separator);
|
|
@@ -639,18 +647,15 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
639
647
|
if (existingValue === undefined || isStaleObject) {
|
|
640
648
|
if (locale === primaryLanguage) {
|
|
641
649
|
if (syncPrimaryWithDefaults) {
|
|
642
|
-
// use the unified "derived" detector (includes keyPrefix suffixes).
|
|
643
|
-
const isDerivedDefault = isDerivedFromKey(key, defaultValue, explicitDefault);
|
|
644
650
|
valueToSet =
|
|
645
|
-
(defaultValue && !isDerivedDefault)
|
|
651
|
+
(defaultValue && (!isDerivedDefault || trustDerivedDefaults))
|
|
646
652
|
? defaultValue
|
|
647
653
|
: resolveDefaultValue(emptyDefaultValue, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
|
|
648
654
|
}
|
|
649
655
|
else {
|
|
650
656
|
// If there's no real code-provided default (defaultValue is derived fallback),
|
|
651
657
|
// use the configured extract.defaultValue for PRIMARY language too.
|
|
652
|
-
|
|
653
|
-
if (derived && configuredDefaultValue !== undefined) {
|
|
658
|
+
if (isDerivedDefault && configuredDefaultValue !== undefined) {
|
|
654
659
|
valueToSet = resolveDefaultValue(configuredDefaultValue, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
|
|
655
660
|
}
|
|
656
661
|
else {
|
|
@@ -666,15 +671,13 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
666
671
|
else {
|
|
667
672
|
// Existing value exists - decide whether to preserve, sync primary, or clear other locales when requested
|
|
668
673
|
if (locale === primaryLanguage && syncPrimaryWithDefaults) {
|
|
669
|
-
// Reuse the same derived-default detection as the initial write path so reruns stay idempotent.
|
|
670
|
-
const isDerivedDefault = isDerivedFromKey(key, defaultValue, explicitDefault);
|
|
671
674
|
// If this key looks like a plural/context variant and the default
|
|
672
675
|
// wasn't explicitly provided in source code, preserve the existing value.
|
|
673
676
|
const isVariantKey = key.includes(pluralSeparator) || key.includes(contextSeparator);
|
|
674
677
|
if (isVariantKey && !explicitDefault) {
|
|
675
678
|
valueToSet = existingValue;
|
|
676
679
|
}
|
|
677
|
-
else if (defaultValue && !isDerivedDefault) {
|
|
680
|
+
else if (defaultValue && (!isDerivedDefault || trustDerivedDefaults)) {
|
|
678
681
|
valueToSet = resolveDefaultValue(defaultValue, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
|
|
679
682
|
}
|
|
680
683
|
else {
|
|
@@ -683,7 +686,19 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
683
686
|
}
|
|
684
687
|
else {
|
|
685
688
|
// Non-primary locale behavior
|
|
686
|
-
|
|
689
|
+
const isVariantKey = key.includes(pluralSeparator) || key.includes(contextSeparator);
|
|
690
|
+
const syncDerivedDefault = Boolean(syncAll &&
|
|
691
|
+
locale !== primaryLanguage &&
|
|
692
|
+
syncPrimaryWithDefaults &&
|
|
693
|
+
trustDerivedDefaults &&
|
|
694
|
+
defaultValue &&
|
|
695
|
+
isDerivedDefault &&
|
|
696
|
+
!primaryShouldPreserveObject &&
|
|
697
|
+
(primaryExistingValue === undefined ||
|
|
698
|
+
primaryIsStaleObject ||
|
|
699
|
+
((!isVariantKey || explicitDefault) &&
|
|
700
|
+
primaryExistingValue !== resolveDefaultValue(defaultValue, key, namespace || config?.extract?.defaultNS || 'translation', primaryLanguage, defaultValue))));
|
|
701
|
+
if (syncAll && locale !== primaryLanguage && (explicitDefault || syncDerivedDefault)) {
|
|
687
702
|
// When syncAll is requested, clear (reset) any existing translations for keys
|
|
688
703
|
// that had explicit defaults in code so the primary default can be propagated
|
|
689
704
|
// while secondary locales get a blank/placeholder value.
|
|
@@ -836,9 +851,10 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
836
851
|
* // Results contain update status and new/existing translations for each locale.
|
|
837
852
|
* ```
|
|
838
853
|
*/
|
|
839
|
-
async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaults = false, syncAll = false, logger = new ConsoleLogger() } = {}) {
|
|
854
|
+
async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaults = false, syncAll = false, trustDerivedDefaults = false, logger = new ConsoleLogger() } = {}) {
|
|
840
855
|
config.extract.primaryLanguage ||= config.locales[0] || 'en';
|
|
841
|
-
config.extract.
|
|
856
|
+
const primaryLanguage = config.extract.primaryLanguage || config.locales[0] || 'en';
|
|
857
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l) => l !== primaryLanguage);
|
|
842
858
|
const patternsToPreserve = [...(config.extract.preservePatterns || [])];
|
|
843
859
|
const indentation = config.extract.indentation ?? 2;
|
|
844
860
|
for (const key of objectKeys) {
|
|
@@ -882,6 +898,10 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
882
898
|
const outputPath = getOutputPath(config.extract.output, locale);
|
|
883
899
|
const fullPath = resolve(process.cwd(), outputPath);
|
|
884
900
|
const existingMergedFile = await loadTranslationFile(fullPath) || {};
|
|
901
|
+
const primaryMergedPath = resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage));
|
|
902
|
+
const primaryMergedFile = locale === primaryLanguage
|
|
903
|
+
? existingMergedFile
|
|
904
|
+
: (await loadTranslationFile(primaryMergedPath) || {});
|
|
885
905
|
// Determine whether the existing merged file already uses namespace objects
|
|
886
906
|
// or is a flat mapping of translation keys -> values.
|
|
887
907
|
// If it's flat (values are primitives), we must NOT treat each translation key as a namespace.
|
|
@@ -917,12 +937,13 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
917
937
|
const nsKeys = keysByNS.get(nsKey) || [];
|
|
918
938
|
if (isTopLevel(nsKey)) {
|
|
919
939
|
// keys without namespace -> merged into top-level of the merged file
|
|
920
|
-
const built = buildNewTranslationsForNs(nsKeys, existingMergedFile, config, locale, undefined, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, logger);
|
|
940
|
+
const built = buildNewTranslationsForNs(nsKeys, existingMergedFile, config, locale, undefined, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, trustDerivedDefaults, primaryMergedFile, logger);
|
|
921
941
|
Object.assign(newMergedTranslations, built);
|
|
922
942
|
}
|
|
923
943
|
else {
|
|
924
944
|
const existingTranslations = existingMergedFile[nsKey] || {};
|
|
925
|
-
|
|
945
|
+
const primaryExistingTranslations = primaryMergedFile[nsKey] || {};
|
|
946
|
+
newMergedTranslations[nsKey] = buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, nsKey, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, trustDerivedDefaults, primaryExistingTranslations, logger);
|
|
926
947
|
}
|
|
927
948
|
}
|
|
928
949
|
// Preserve ignored namespaces as-is from the existing merged file
|
|
@@ -960,7 +981,11 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
960
981
|
const outputPath = getOutputPath(config.extract.output, locale, ns);
|
|
961
982
|
const fullPath = resolve(process.cwd(), outputPath);
|
|
962
983
|
const existingTranslations = await loadTranslationFile(fullPath) || {};
|
|
963
|
-
const
|
|
984
|
+
const primaryOutputPath = resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, ns));
|
|
985
|
+
const primaryExistingTranslations = locale === primaryLanguage
|
|
986
|
+
? existingTranslations
|
|
987
|
+
: (await loadTranslationFile(primaryOutputPath) || {});
|
|
988
|
+
const newTranslations = buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, ns, preservePatterns, objectKeys, syncPrimaryWithDefaults, syncAll, trustDerivedDefaults, primaryExistingTranslations, logger);
|
|
964
989
|
const oldContent = JSON.stringify(existingTranslations, null, indentation);
|
|
965
990
|
const newContent = JSON.stringify(newTranslations, null, indentation);
|
|
966
991
|
// Push one result per namespace file
|
package/package.json
CHANGED
package/types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAmBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAmBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;AA6U7B,OAAO,EAAE,OAAO,EAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAO5G,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAK/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAO5G,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAK/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CA8E1D;AAwBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAoJf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CA6Df;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAO1K"}
|
|
@@ -28,9 +28,10 @@ import { TranslationResult, ExtractedKey, I18nextToolkitConfig, Logger } from '.
|
|
|
28
28
|
* // Results contain update status and new/existing translations for each locale.
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
export declare function getTranslations(keys: Map<string, ExtractedKey>, objectKeys: Set<string>, config: I18nextToolkitConfig, { syncPrimaryWithDefaults, syncAll, logger }?: {
|
|
31
|
+
export declare function getTranslations(keys: Map<string, ExtractedKey>, objectKeys: Set<string>, config: I18nextToolkitConfig, { syncPrimaryWithDefaults, syncAll, trustDerivedDefaults, logger }?: {
|
|
32
32
|
syncPrimaryWithDefaults?: boolean;
|
|
33
33
|
syncAll?: boolean;
|
|
34
|
+
trustDerivedDefaults?: boolean;
|
|
34
35
|
logger?: Logger;
|
|
35
36
|
}): Promise<TranslationResult[]>;
|
|
36
37
|
//# sourceMappingURL=translation-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAo8B9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EACf,oBAA4B,EAC5B,MAA4B,EAC7B,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAiK9B"}
|