i18next-cli 1.39.12 ā 1.40.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/dist/cjs/cli.js +1 -1
- package/dist/cjs/rename-key.js +186 -50
- package/dist/esm/cli.js +1 -1
- package/dist/esm/rename-key.js +186 -50
- package/package.json +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ const program = new commander.Command();
|
|
|
28
28
|
program
|
|
29
29
|
.name('i18next-cli')
|
|
30
30
|
.description('A unified, high-performance i18next CLI.')
|
|
31
|
-
.version('1.
|
|
31
|
+
.version('1.40.0'); // This string is replaced with the actual version at build time by rollup
|
|
32
32
|
// new: global config override option
|
|
33
33
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
34
34
|
program
|
package/dist/cjs/rename-key.js
CHANGED
|
@@ -78,9 +78,9 @@ async function runRenameKey(config, oldKey, newKey, options = {}, logger$1 = new
|
|
|
78
78
|
const namespaceKeyMap = await buildNamespaceKeyMap(config);
|
|
79
79
|
logger$1.info(`š Scanning for usages of "${oldKey}"...`);
|
|
80
80
|
// Find and update source files
|
|
81
|
-
const sourceResults = await updateSourceFiles(oldParts, newParts, config, dryRun, logger$1
|
|
81
|
+
const sourceResults = await updateSourceFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger$1);
|
|
82
82
|
// Update translation files
|
|
83
|
-
const translationResults = await updateTranslationFiles(oldParts, newParts, config, dryRun, logger$1);
|
|
83
|
+
const translationResults = await updateTranslationFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger$1);
|
|
84
84
|
const totalChanges = sourceResults.reduce((sum, r) => sum + r.changes, 0);
|
|
85
85
|
if (!dryRun && totalChanges > 0) {
|
|
86
86
|
logger$1.info('\n⨠Successfully renamed key!');
|
|
@@ -229,7 +229,7 @@ async function buildNamespaceKeyMap(config) {
|
|
|
229
229
|
}
|
|
230
230
|
return map;
|
|
231
231
|
}
|
|
232
|
-
async function updateSourceFiles(oldParts, newParts, config, dryRun,
|
|
232
|
+
async function updateSourceFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger) {
|
|
233
233
|
const defaultIgnore = ['node_modules/**'];
|
|
234
234
|
const userIgnore = Array.isArray(config.extract.ignore)
|
|
235
235
|
? config.extract.ignore
|
|
@@ -283,7 +283,26 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
|
|
|
283
283
|
if (!ns)
|
|
284
284
|
return false;
|
|
285
285
|
const set = namespaceKeyMap.get(ns);
|
|
286
|
-
|
|
286
|
+
if (!set)
|
|
287
|
+
return false;
|
|
288
|
+
// exact key match
|
|
289
|
+
if (set.has(oldParts.key))
|
|
290
|
+
return true;
|
|
291
|
+
// nested keys using keySeparator, e.g. "key.one", "key.other"
|
|
292
|
+
const keySeparator = config.extract.keySeparator ?? '.';
|
|
293
|
+
const nestedPrefix = `${oldParts.key}${String(keySeparator)}`;
|
|
294
|
+
for (const s of set) {
|
|
295
|
+
if (s.startsWith(nestedPrefix))
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
// flat plural keys like "key_one", "key_other"
|
|
299
|
+
const pluralSuffixes = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
300
|
+
const flatPluralRegex = new RegExp(`^${escapeRegex(oldParts.key)}_(${pluralSuffixes.join('|')})$`);
|
|
301
|
+
for (const s of set) {
|
|
302
|
+
if (flatPluralRegex.test(s))
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
287
306
|
};
|
|
288
307
|
for (const fnPattern of configuredFunctions) {
|
|
289
308
|
const prefix = fnPrefixToRegex(fnPattern);
|
|
@@ -505,60 +524,177 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
|
|
|
505
524
|
function escapeRegex(str) {
|
|
506
525
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
507
526
|
}
|
|
508
|
-
async function updateTranslationFiles(oldParts, newParts, config, dryRun, logger) {
|
|
527
|
+
async function updateTranslationFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger) {
|
|
509
528
|
const results = [];
|
|
510
529
|
const keySeparator = config.extract.keySeparator ?? '.';
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
530
|
+
// plural suffixes commonly used by i18next (for flat underscore style: key_one)
|
|
531
|
+
const pluralSuffixes = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
532
|
+
// Helper: determine whether a flattened key-set indicates presence of `baseKey`.
|
|
533
|
+
const namespaceHasKey = (set, baseKey) => {
|
|
534
|
+
if (!set)
|
|
535
|
+
return false;
|
|
536
|
+
// exact key (for scalar or object)
|
|
537
|
+
if (set.has(baseKey))
|
|
538
|
+
return true;
|
|
539
|
+
// nested keys using keySeparator, e.g. "key.one", "key.other"
|
|
540
|
+
const nestedPrefix = `${baseKey}${String(keySeparator)}`;
|
|
541
|
+
for (const s of set) {
|
|
542
|
+
if (s.startsWith(nestedPrefix))
|
|
543
|
+
return true;
|
|
520
544
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
continue;
|
|
527
|
-
if (oldParts.namespace === newParts.namespace) {
|
|
528
|
-
// Rename within the same namespace
|
|
529
|
-
deleteNestedValue(oldTranslations, oldParts.key, keySeparator);
|
|
530
|
-
nestedObject.setNestedValue(oldTranslations, newParts.key, oldValue, keySeparator);
|
|
531
|
-
if (!dryRun) {
|
|
532
|
-
const content = fileUtils.serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
533
|
-
await promises.writeFile(oldFullPath, content, 'utf-8');
|
|
534
|
-
}
|
|
535
|
-
results.push({ path: oldFullPath, updated: true });
|
|
536
|
-
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
545
|
+
// flat plural keys like "key_one", "key_other"
|
|
546
|
+
const flatPluralRegex = new RegExp(`^${escapeRegex(baseKey)}_(${pluralSuffixes.join('|')})$`);
|
|
547
|
+
for (const s of set) {
|
|
548
|
+
if (flatPluralRegex.test(s))
|
|
549
|
+
return true;
|
|
537
550
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
551
|
+
return false;
|
|
552
|
+
};
|
|
553
|
+
// Decide candidate namespaces to inspect:
|
|
554
|
+
// - If the old key was explicitly namespaced in the CLI (oldParts.explicitNamespace),
|
|
555
|
+
// we only inspect that namespace.
|
|
556
|
+
// - Otherwise, inspect every namespace that appears to contain the key (from namespaceKeyMap).
|
|
557
|
+
const candidateNamespaces = [];
|
|
558
|
+
if (oldParts.explicitNamespace && oldParts.namespace) {
|
|
559
|
+
candidateNamespaces.push(oldParts.namespace);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
for (const [ns, set] of namespaceKeyMap.entries()) {
|
|
563
|
+
if (namespaceHasKey(set, oldParts.key))
|
|
564
|
+
candidateNamespaces.push(ns);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// if nothing found, nothing to do
|
|
568
|
+
if (candidateNamespaces.length === 0) {
|
|
569
|
+
return results;
|
|
570
|
+
}
|
|
571
|
+
// Iterate each locale and each candidate source namespace found
|
|
572
|
+
for (const locale of config.locales) {
|
|
573
|
+
for (const ns of candidateNamespaces) {
|
|
574
|
+
const oldOutputPath = fileUtils.getOutputPath(config.extract.output, locale, ns);
|
|
575
|
+
const oldFullPath = node_path.resolve(process.cwd(), oldOutputPath);
|
|
576
|
+
// When explicitly targeting a namespace in the CLI, always use that target.
|
|
577
|
+
// When not explicit, keep keys in their current namespace (don't move them).
|
|
578
|
+
const targetNamespace = (oldParts.explicitNamespace || newParts.explicitNamespace)
|
|
579
|
+
? newParts.namespace
|
|
580
|
+
: ns;
|
|
581
|
+
const newOutputPath = fileUtils.getOutputPath(config.extract.output, locale, targetNamespace);
|
|
582
|
+
const newFullPath = node_path.resolve(process.cwd(), newOutputPath);
|
|
583
|
+
let oldTranslations;
|
|
584
|
+
let newTranslations;
|
|
549
585
|
try {
|
|
550
|
-
|
|
586
|
+
oldTranslations = await fileUtils.loadTranslationFile(oldFullPath);
|
|
551
587
|
}
|
|
552
588
|
catch { }
|
|
553
|
-
if (!
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
589
|
+
if (!oldTranslations)
|
|
590
|
+
continue;
|
|
591
|
+
// 1) nested/exact path value (object or scalar) at the nested path
|
|
592
|
+
const oldValue = nestedObject.getNestedValue(oldTranslations, oldParts.key, keySeparator);
|
|
593
|
+
// 2) flat plural matches like `key_one`, `key_other`
|
|
594
|
+
const flatPluralMatches = [];
|
|
595
|
+
if (oldTranslations && typeof oldTranslations === 'object') {
|
|
596
|
+
const re = new RegExp(`^${escapeRegex(oldParts.key)}_(${pluralSuffixes.join('|')})$`);
|
|
597
|
+
for (const k of Object.keys(oldTranslations)) {
|
|
598
|
+
const m = k.match(re);
|
|
599
|
+
if (m)
|
|
600
|
+
flatPluralMatches.push({ flatKey: k, suffix: m[1], value: oldTranslations[k] });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// nothing found for this file/namespace
|
|
604
|
+
if (oldValue === undefined && flatPluralMatches.length === 0)
|
|
605
|
+
continue;
|
|
606
|
+
//
|
|
607
|
+
// Handle flat plurals first (top-level underscore keys)
|
|
608
|
+
//
|
|
609
|
+
if (flatPluralMatches.length > 0) {
|
|
610
|
+
if (ns === targetNamespace) {
|
|
611
|
+
// rename in-place within same file
|
|
612
|
+
for (const m of flatPluralMatches) {
|
|
613
|
+
delete oldTranslations[m.flatKey];
|
|
614
|
+
const newFlatKey = `${newParts.key}_${m.suffix}`;
|
|
615
|
+
oldTranslations[newFlatKey] = m.value;
|
|
616
|
+
}
|
|
617
|
+
if (!dryRun) {
|
|
618
|
+
const content = fileUtils.serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
619
|
+
await promises.writeFile(oldFullPath, content, 'utf-8');
|
|
620
|
+
}
|
|
621
|
+
results.push({ path: oldFullPath, updated: true });
|
|
622
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// move them to the new namespace file
|
|
626
|
+
for (const m of flatPluralMatches)
|
|
627
|
+
delete oldTranslations[m.flatKey];
|
|
628
|
+
if (!dryRun) {
|
|
629
|
+
const contentOld = fileUtils.serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
630
|
+
await promises.writeFile(oldFullPath, contentOld, 'utf-8');
|
|
631
|
+
}
|
|
632
|
+
results.push({ path: oldFullPath, updated: true });
|
|
633
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
634
|
+
try {
|
|
635
|
+
newTranslations = await fileUtils.loadTranslationFile(newFullPath);
|
|
636
|
+
}
|
|
637
|
+
catch { }
|
|
638
|
+
if (!newTranslations)
|
|
639
|
+
newTranslations = {};
|
|
640
|
+
for (const m of flatPluralMatches) {
|
|
641
|
+
const newFlatKey = `${newParts.key}_${m.suffix}`;
|
|
642
|
+
newTranslations[newFlatKey] = m.value;
|
|
643
|
+
}
|
|
644
|
+
if (!dryRun) {
|
|
645
|
+
const contentNew = fileUtils.serializeTranslationFile(newTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
646
|
+
await promises.writeFile(newFullPath, contentNew, 'utf-8');
|
|
647
|
+
}
|
|
648
|
+
results.push({ path: newFullPath, updated: true });
|
|
649
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${newFullPath}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
//
|
|
653
|
+
// Handle nested/exact key
|
|
654
|
+
//
|
|
655
|
+
if (oldValue !== undefined) {
|
|
656
|
+
if (ns === targetNamespace) {
|
|
657
|
+
// rename within same file (nested)
|
|
658
|
+
deleteNestedValue(oldTranslations, oldParts.key, keySeparator);
|
|
659
|
+
nestedObject.setNestedValue(oldTranslations, newParts.key, oldValue, keySeparator);
|
|
660
|
+
if (!dryRun) {
|
|
661
|
+
const content = fileUtils.serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
662
|
+
await promises.writeFile(oldFullPath, content, 'utf-8');
|
|
663
|
+
}
|
|
664
|
+
if (!results.find(r => r.path === oldFullPath)) {
|
|
665
|
+
results.push({ path: oldFullPath, updated: true });
|
|
666
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
// move nested value across namespaces
|
|
671
|
+
const updatedOld = { ...oldTranslations };
|
|
672
|
+
deleteNestedValue(updatedOld, oldParts.key, keySeparator);
|
|
673
|
+
if (!dryRun) {
|
|
674
|
+
const contentOld = fileUtils.serializeTranslationFile(updatedOld, config.extract.outputFormat, config.extract.indentation);
|
|
675
|
+
await promises.writeFile(oldFullPath, contentOld, 'utf-8');
|
|
676
|
+
}
|
|
677
|
+
if (!results.find(r => r.path === oldFullPath)) {
|
|
678
|
+
results.push({ path: oldFullPath, updated: true });
|
|
679
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
newTranslations = await fileUtils.loadTranslationFile(newFullPath);
|
|
683
|
+
}
|
|
684
|
+
catch { }
|
|
685
|
+
if (!newTranslations)
|
|
686
|
+
newTranslations = {};
|
|
687
|
+
nestedObject.setNestedValue(newTranslations, newParts.key, oldValue, keySeparator);
|
|
688
|
+
if (!dryRun) {
|
|
689
|
+
const contentNew = fileUtils.serializeTranslationFile(newTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
690
|
+
await promises.writeFile(newFullPath, contentNew, 'utf-8');
|
|
691
|
+
}
|
|
692
|
+
if (!results.find(r => r.path === newFullPath)) {
|
|
693
|
+
results.push({ path: newFullPath, updated: true });
|
|
694
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${newFullPath}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
559
697
|
}
|
|
560
|
-
results.push({ path: newFullPath, updated: true });
|
|
561
|
-
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${newFullPath}`);
|
|
562
698
|
}
|
|
563
699
|
}
|
|
564
700
|
if (results.length > 0) {
|
package/dist/esm/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ const program = new Command();
|
|
|
26
26
|
program
|
|
27
27
|
.name('i18next-cli')
|
|
28
28
|
.description('A unified, high-performance i18next CLI.')
|
|
29
|
-
.version('1.
|
|
29
|
+
.version('1.40.0'); // This string is replaced with the actual version at build time by rollup
|
|
30
30
|
// new: global config override option
|
|
31
31
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
32
32
|
program
|
package/dist/esm/rename-key.js
CHANGED
|
@@ -76,9 +76,9 @@ async function runRenameKey(config, oldKey, newKey, options = {}, logger = new C
|
|
|
76
76
|
const namespaceKeyMap = await buildNamespaceKeyMap(config);
|
|
77
77
|
logger.info(`š Scanning for usages of "${oldKey}"...`);
|
|
78
78
|
// Find and update source files
|
|
79
|
-
const sourceResults = await updateSourceFiles(oldParts, newParts, config, dryRun,
|
|
79
|
+
const sourceResults = await updateSourceFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger);
|
|
80
80
|
// Update translation files
|
|
81
|
-
const translationResults = await updateTranslationFiles(oldParts, newParts, config, dryRun, logger);
|
|
81
|
+
const translationResults = await updateTranslationFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger);
|
|
82
82
|
const totalChanges = sourceResults.reduce((sum, r) => sum + r.changes, 0);
|
|
83
83
|
if (!dryRun && totalChanges > 0) {
|
|
84
84
|
logger.info('\n⨠Successfully renamed key!');
|
|
@@ -227,7 +227,7 @@ async function buildNamespaceKeyMap(config) {
|
|
|
227
227
|
}
|
|
228
228
|
return map;
|
|
229
229
|
}
|
|
230
|
-
async function updateSourceFiles(oldParts, newParts, config, dryRun,
|
|
230
|
+
async function updateSourceFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger) {
|
|
231
231
|
const defaultIgnore = ['node_modules/**'];
|
|
232
232
|
const userIgnore = Array.isArray(config.extract.ignore)
|
|
233
233
|
? config.extract.ignore
|
|
@@ -281,7 +281,26 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
|
|
|
281
281
|
if (!ns)
|
|
282
282
|
return false;
|
|
283
283
|
const set = namespaceKeyMap.get(ns);
|
|
284
|
-
|
|
284
|
+
if (!set)
|
|
285
|
+
return false;
|
|
286
|
+
// exact key match
|
|
287
|
+
if (set.has(oldParts.key))
|
|
288
|
+
return true;
|
|
289
|
+
// nested keys using keySeparator, e.g. "key.one", "key.other"
|
|
290
|
+
const keySeparator = config.extract.keySeparator ?? '.';
|
|
291
|
+
const nestedPrefix = `${oldParts.key}${String(keySeparator)}`;
|
|
292
|
+
for (const s of set) {
|
|
293
|
+
if (s.startsWith(nestedPrefix))
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
// flat plural keys like "key_one", "key_other"
|
|
297
|
+
const pluralSuffixes = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
298
|
+
const flatPluralRegex = new RegExp(`^${escapeRegex(oldParts.key)}_(${pluralSuffixes.join('|')})$`);
|
|
299
|
+
for (const s of set) {
|
|
300
|
+
if (flatPluralRegex.test(s))
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
285
304
|
};
|
|
286
305
|
for (const fnPattern of configuredFunctions) {
|
|
287
306
|
const prefix = fnPrefixToRegex(fnPattern);
|
|
@@ -503,60 +522,177 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
|
|
|
503
522
|
function escapeRegex(str) {
|
|
504
523
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
505
524
|
}
|
|
506
|
-
async function updateTranslationFiles(oldParts, newParts, config, dryRun, logger) {
|
|
525
|
+
async function updateTranslationFiles(oldParts, newParts, config, dryRun, namespaceKeyMap, logger) {
|
|
507
526
|
const results = [];
|
|
508
527
|
const keySeparator = config.extract.keySeparator ?? '.';
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
528
|
+
// plural suffixes commonly used by i18next (for flat underscore style: key_one)
|
|
529
|
+
const pluralSuffixes = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
530
|
+
// Helper: determine whether a flattened key-set indicates presence of `baseKey`.
|
|
531
|
+
const namespaceHasKey = (set, baseKey) => {
|
|
532
|
+
if (!set)
|
|
533
|
+
return false;
|
|
534
|
+
// exact key (for scalar or object)
|
|
535
|
+
if (set.has(baseKey))
|
|
536
|
+
return true;
|
|
537
|
+
// nested keys using keySeparator, e.g. "key.one", "key.other"
|
|
538
|
+
const nestedPrefix = `${baseKey}${String(keySeparator)}`;
|
|
539
|
+
for (const s of set) {
|
|
540
|
+
if (s.startsWith(nestedPrefix))
|
|
541
|
+
return true;
|
|
518
542
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
continue;
|
|
525
|
-
if (oldParts.namespace === newParts.namespace) {
|
|
526
|
-
// Rename within the same namespace
|
|
527
|
-
deleteNestedValue(oldTranslations, oldParts.key, keySeparator);
|
|
528
|
-
setNestedValue(oldTranslations, newParts.key, oldValue, keySeparator);
|
|
529
|
-
if (!dryRun) {
|
|
530
|
-
const content = serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
531
|
-
await writeFile(oldFullPath, content, 'utf-8');
|
|
532
|
-
}
|
|
533
|
-
results.push({ path: oldFullPath, updated: true });
|
|
534
|
-
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
543
|
+
// flat plural keys like "key_one", "key_other"
|
|
544
|
+
const flatPluralRegex = new RegExp(`^${escapeRegex(baseKey)}_(${pluralSuffixes.join('|')})$`);
|
|
545
|
+
for (const s of set) {
|
|
546
|
+
if (flatPluralRegex.test(s))
|
|
547
|
+
return true;
|
|
535
548
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
549
|
+
return false;
|
|
550
|
+
};
|
|
551
|
+
// Decide candidate namespaces to inspect:
|
|
552
|
+
// - If the old key was explicitly namespaced in the CLI (oldParts.explicitNamespace),
|
|
553
|
+
// we only inspect that namespace.
|
|
554
|
+
// - Otherwise, inspect every namespace that appears to contain the key (from namespaceKeyMap).
|
|
555
|
+
const candidateNamespaces = [];
|
|
556
|
+
if (oldParts.explicitNamespace && oldParts.namespace) {
|
|
557
|
+
candidateNamespaces.push(oldParts.namespace);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
for (const [ns, set] of namespaceKeyMap.entries()) {
|
|
561
|
+
if (namespaceHasKey(set, oldParts.key))
|
|
562
|
+
candidateNamespaces.push(ns);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// if nothing found, nothing to do
|
|
566
|
+
if (candidateNamespaces.length === 0) {
|
|
567
|
+
return results;
|
|
568
|
+
}
|
|
569
|
+
// Iterate each locale and each candidate source namespace found
|
|
570
|
+
for (const locale of config.locales) {
|
|
571
|
+
for (const ns of candidateNamespaces) {
|
|
572
|
+
const oldOutputPath = getOutputPath(config.extract.output, locale, ns);
|
|
573
|
+
const oldFullPath = resolve(process.cwd(), oldOutputPath);
|
|
574
|
+
// When explicitly targeting a namespace in the CLI, always use that target.
|
|
575
|
+
// When not explicit, keep keys in their current namespace (don't move them).
|
|
576
|
+
const targetNamespace = (oldParts.explicitNamespace || newParts.explicitNamespace)
|
|
577
|
+
? newParts.namespace
|
|
578
|
+
: ns;
|
|
579
|
+
const newOutputPath = getOutputPath(config.extract.output, locale, targetNamespace);
|
|
580
|
+
const newFullPath = resolve(process.cwd(), newOutputPath);
|
|
581
|
+
let oldTranslations;
|
|
582
|
+
let newTranslations;
|
|
547
583
|
try {
|
|
548
|
-
|
|
584
|
+
oldTranslations = await loadTranslationFile(oldFullPath);
|
|
549
585
|
}
|
|
550
586
|
catch { }
|
|
551
|
-
if (!
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
587
|
+
if (!oldTranslations)
|
|
588
|
+
continue;
|
|
589
|
+
// 1) nested/exact path value (object or scalar) at the nested path
|
|
590
|
+
const oldValue = getNestedValue(oldTranslations, oldParts.key, keySeparator);
|
|
591
|
+
// 2) flat plural matches like `key_one`, `key_other`
|
|
592
|
+
const flatPluralMatches = [];
|
|
593
|
+
if (oldTranslations && typeof oldTranslations === 'object') {
|
|
594
|
+
const re = new RegExp(`^${escapeRegex(oldParts.key)}_(${pluralSuffixes.join('|')})$`);
|
|
595
|
+
for (const k of Object.keys(oldTranslations)) {
|
|
596
|
+
const m = k.match(re);
|
|
597
|
+
if (m)
|
|
598
|
+
flatPluralMatches.push({ flatKey: k, suffix: m[1], value: oldTranslations[k] });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// nothing found for this file/namespace
|
|
602
|
+
if (oldValue === undefined && flatPluralMatches.length === 0)
|
|
603
|
+
continue;
|
|
604
|
+
//
|
|
605
|
+
// Handle flat plurals first (top-level underscore keys)
|
|
606
|
+
//
|
|
607
|
+
if (flatPluralMatches.length > 0) {
|
|
608
|
+
if (ns === targetNamespace) {
|
|
609
|
+
// rename in-place within same file
|
|
610
|
+
for (const m of flatPluralMatches) {
|
|
611
|
+
delete oldTranslations[m.flatKey];
|
|
612
|
+
const newFlatKey = `${newParts.key}_${m.suffix}`;
|
|
613
|
+
oldTranslations[newFlatKey] = m.value;
|
|
614
|
+
}
|
|
615
|
+
if (!dryRun) {
|
|
616
|
+
const content = serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
617
|
+
await writeFile(oldFullPath, content, 'utf-8');
|
|
618
|
+
}
|
|
619
|
+
results.push({ path: oldFullPath, updated: true });
|
|
620
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
// move them to the new namespace file
|
|
624
|
+
for (const m of flatPluralMatches)
|
|
625
|
+
delete oldTranslations[m.flatKey];
|
|
626
|
+
if (!dryRun) {
|
|
627
|
+
const contentOld = serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
628
|
+
await writeFile(oldFullPath, contentOld, 'utf-8');
|
|
629
|
+
}
|
|
630
|
+
results.push({ path: oldFullPath, updated: true });
|
|
631
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
632
|
+
try {
|
|
633
|
+
newTranslations = await loadTranslationFile(newFullPath);
|
|
634
|
+
}
|
|
635
|
+
catch { }
|
|
636
|
+
if (!newTranslations)
|
|
637
|
+
newTranslations = {};
|
|
638
|
+
for (const m of flatPluralMatches) {
|
|
639
|
+
const newFlatKey = `${newParts.key}_${m.suffix}`;
|
|
640
|
+
newTranslations[newFlatKey] = m.value;
|
|
641
|
+
}
|
|
642
|
+
if (!dryRun) {
|
|
643
|
+
const contentNew = serializeTranslationFile(newTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
644
|
+
await writeFile(newFullPath, contentNew, 'utf-8');
|
|
645
|
+
}
|
|
646
|
+
results.push({ path: newFullPath, updated: true });
|
|
647
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${newFullPath}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
//
|
|
651
|
+
// Handle nested/exact key
|
|
652
|
+
//
|
|
653
|
+
if (oldValue !== undefined) {
|
|
654
|
+
if (ns === targetNamespace) {
|
|
655
|
+
// rename within same file (nested)
|
|
656
|
+
deleteNestedValue(oldTranslations, oldParts.key, keySeparator);
|
|
657
|
+
setNestedValue(oldTranslations, newParts.key, oldValue, keySeparator);
|
|
658
|
+
if (!dryRun) {
|
|
659
|
+
const content = serializeTranslationFile(oldTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
660
|
+
await writeFile(oldFullPath, content, 'utf-8');
|
|
661
|
+
}
|
|
662
|
+
if (!results.find(r => r.path === oldFullPath)) {
|
|
663
|
+
results.push({ path: oldFullPath, updated: true });
|
|
664
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
// move nested value across namespaces
|
|
669
|
+
const updatedOld = { ...oldTranslations };
|
|
670
|
+
deleteNestedValue(updatedOld, oldParts.key, keySeparator);
|
|
671
|
+
if (!dryRun) {
|
|
672
|
+
const contentOld = serializeTranslationFile(updatedOld, config.extract.outputFormat, config.extract.indentation);
|
|
673
|
+
await writeFile(oldFullPath, contentOld, 'utf-8');
|
|
674
|
+
}
|
|
675
|
+
if (!results.find(r => r.path === oldFullPath)) {
|
|
676
|
+
results.push({ path: oldFullPath, updated: true });
|
|
677
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${oldFullPath}`);
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
newTranslations = await loadTranslationFile(newFullPath);
|
|
681
|
+
}
|
|
682
|
+
catch { }
|
|
683
|
+
if (!newTranslations)
|
|
684
|
+
newTranslations = {};
|
|
685
|
+
setNestedValue(newTranslations, newParts.key, oldValue, keySeparator);
|
|
686
|
+
if (!dryRun) {
|
|
687
|
+
const contentNew = serializeTranslationFile(newTranslations, config.extract.outputFormat, config.extract.indentation);
|
|
688
|
+
await writeFile(newFullPath, contentNew, 'utf-8');
|
|
689
|
+
}
|
|
690
|
+
if (!results.find(r => r.path === newFullPath)) {
|
|
691
|
+
results.push({ path: newFullPath, updated: true });
|
|
692
|
+
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${newFullPath}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
557
695
|
}
|
|
558
|
-
results.push({ path: newFullPath, updated: true });
|
|
559
|
-
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${newFullPath}`);
|
|
560
696
|
}
|
|
561
697
|
}
|
|
562
698
|
if (results.length > 0) {
|