poly-lexis 0.9.3 → 0.11.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/README.md +33 -0
- package/dist/cli/translations.js +382 -108
- package/dist/cli/translations.js.map +1 -1
- package/dist/index.d.ts +120 -4
- package/dist/index.js +357 -93
- package/dist/index.js.map +1 -1
- package/dist/scripts/verify-translations.js +147 -37
- package/dist/scripts/verify-translations.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/translations.js
CHANGED
|
@@ -529,9 +529,100 @@ var init_language_fallback = __esm({
|
|
|
529
529
|
}
|
|
530
530
|
});
|
|
531
531
|
|
|
532
|
-
// src/translations/utils/
|
|
532
|
+
// src/translations/utils/metadata.ts
|
|
533
|
+
import { createHash } from "crypto";
|
|
533
534
|
import * as fs from "fs";
|
|
534
535
|
import * as path2 from "path";
|
|
536
|
+
function emptyMetadata() {
|
|
537
|
+
return { version: CURRENT_VERSION, languages: {} };
|
|
538
|
+
}
|
|
539
|
+
function hashSourceValue(value) {
|
|
540
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
541
|
+
}
|
|
542
|
+
function getMetadataPath(translationsPath) {
|
|
543
|
+
return path2.join(translationsPath, METADATA_FILE_NAME);
|
|
544
|
+
}
|
|
545
|
+
function metadataExists(translationsPath) {
|
|
546
|
+
return fs.existsSync(getMetadataPath(translationsPath));
|
|
547
|
+
}
|
|
548
|
+
function readMetadata(translationsPath) {
|
|
549
|
+
const metadataPath = getMetadataPath(translationsPath);
|
|
550
|
+
if (!fs.existsSync(metadataPath)) {
|
|
551
|
+
return emptyMetadata();
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
|
555
|
+
return {
|
|
556
|
+
version: parsed.version ?? CURRENT_VERSION,
|
|
557
|
+
languages: parsed.languages ?? {}
|
|
558
|
+
};
|
|
559
|
+
} catch {
|
|
560
|
+
return emptyMetadata();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function writeMetadata(translationsPath, metadata) {
|
|
564
|
+
if (!fs.existsSync(translationsPath)) {
|
|
565
|
+
fs.mkdirSync(translationsPath, { recursive: true });
|
|
566
|
+
}
|
|
567
|
+
const metadataPath = getMetadataPath(translationsPath);
|
|
568
|
+
fs.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}
|
|
569
|
+
`, "utf-8");
|
|
570
|
+
}
|
|
571
|
+
function setSourceHash(metadata, language, namespace, key, sourceValue) {
|
|
572
|
+
if (!metadata.languages[language]) {
|
|
573
|
+
metadata.languages[language] = {};
|
|
574
|
+
}
|
|
575
|
+
if (!metadata.languages[language][namespace]) {
|
|
576
|
+
metadata.languages[language][namespace] = {};
|
|
577
|
+
}
|
|
578
|
+
metadata.languages[language][namespace][key] = hashSourceValue(sourceValue);
|
|
579
|
+
return metadata;
|
|
580
|
+
}
|
|
581
|
+
function getSourceHash(metadata, language, namespace, key) {
|
|
582
|
+
return metadata.languages[language]?.[namespace]?.[key];
|
|
583
|
+
}
|
|
584
|
+
function pruneMetadata(metadata, sourceKeysByNamespace) {
|
|
585
|
+
let changed = false;
|
|
586
|
+
for (const language of Object.keys(metadata.languages)) {
|
|
587
|
+
const namespaces = metadata.languages[language];
|
|
588
|
+
for (const namespace of Object.keys(namespaces)) {
|
|
589
|
+
const sourceKeys = sourceKeysByNamespace[namespace];
|
|
590
|
+
if (!sourceKeys) {
|
|
591
|
+
delete namespaces[namespace];
|
|
592
|
+
changed = true;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
for (const key of Object.keys(namespaces[namespace])) {
|
|
596
|
+
if (!sourceKeys.has(key)) {
|
|
597
|
+
delete namespaces[namespace][key];
|
|
598
|
+
changed = true;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (Object.keys(namespaces[namespace]).length === 0) {
|
|
602
|
+
delete namespaces[namespace];
|
|
603
|
+
changed = true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (Object.keys(namespaces).length === 0) {
|
|
607
|
+
delete metadata.languages[language];
|
|
608
|
+
changed = true;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return changed;
|
|
612
|
+
}
|
|
613
|
+
var METADATA_FILE_NAME, CURRENT_VERSION;
|
|
614
|
+
var init_metadata = __esm({
|
|
615
|
+
"src/translations/utils/metadata.ts"() {
|
|
616
|
+
"use strict";
|
|
617
|
+
init_esm_shims();
|
|
618
|
+
METADATA_FILE_NAME = ".translations-meta.json";
|
|
619
|
+
CURRENT_VERSION = 1;
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// src/translations/utils/utils.ts
|
|
624
|
+
import * as fs2 from "fs";
|
|
625
|
+
import * as path3 from "path";
|
|
535
626
|
function flattenObject(obj, prefix = "") {
|
|
536
627
|
const result = {};
|
|
537
628
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -548,16 +639,16 @@ function isNestedObject(obj) {
|
|
|
548
639
|
return Object.values(obj).some((value) => typeof value === "object" && value !== null);
|
|
549
640
|
}
|
|
550
641
|
function readTranslations(translationsPath, language) {
|
|
551
|
-
const langPath =
|
|
552
|
-
if (!
|
|
642
|
+
const langPath = path3.join(translationsPath, language);
|
|
643
|
+
if (!fs2.existsSync(langPath)) {
|
|
553
644
|
return {};
|
|
554
645
|
}
|
|
555
|
-
const files =
|
|
646
|
+
const files = fs2.readdirSync(langPath).filter((f) => f.endsWith(".json"));
|
|
556
647
|
const translations = {};
|
|
557
648
|
for (const file of files) {
|
|
558
|
-
const namespace =
|
|
559
|
-
const filePath =
|
|
560
|
-
const content =
|
|
649
|
+
const namespace = path3.basename(file, ".json");
|
|
650
|
+
const filePath = path3.join(langPath, file);
|
|
651
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
561
652
|
const parsed = JSON.parse(content);
|
|
562
653
|
if (isNestedObject(parsed)) {
|
|
563
654
|
translations[namespace] = flattenObject(parsed);
|
|
@@ -568,26 +659,26 @@ function readTranslations(translationsPath, language) {
|
|
|
568
659
|
return translations;
|
|
569
660
|
}
|
|
570
661
|
function writeTranslation(translationsPath, language, namespace, translations) {
|
|
571
|
-
const langPath =
|
|
572
|
-
if (!
|
|
573
|
-
|
|
662
|
+
const langPath = path3.join(translationsPath, language);
|
|
663
|
+
if (!fs2.existsSync(langPath)) {
|
|
664
|
+
fs2.mkdirSync(langPath, { recursive: true });
|
|
574
665
|
}
|
|
575
|
-
const filePath =
|
|
576
|
-
|
|
666
|
+
const filePath = path3.join(langPath, `${namespace}.json`);
|
|
667
|
+
fs2.writeFileSync(filePath, `${JSON.stringify(translations, null, 2)}
|
|
577
668
|
`, "utf-8");
|
|
578
669
|
}
|
|
579
670
|
function getAvailableLanguages(translationsPath) {
|
|
580
|
-
if (!
|
|
671
|
+
if (!fs2.existsSync(translationsPath)) {
|
|
581
672
|
return [];
|
|
582
673
|
}
|
|
583
|
-
return
|
|
674
|
+
return fs2.readdirSync(translationsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
584
675
|
}
|
|
585
676
|
function getNamespaces(translationsPath, language) {
|
|
586
|
-
const langPath =
|
|
587
|
-
if (!
|
|
677
|
+
const langPath = path3.join(translationsPath, language);
|
|
678
|
+
if (!fs2.existsSync(langPath)) {
|
|
588
679
|
return [];
|
|
589
680
|
}
|
|
590
|
-
return
|
|
681
|
+
return fs2.readdirSync(langPath).filter((f) => f.endsWith(".json")).map((f) => path3.basename(f, ".json"));
|
|
591
682
|
}
|
|
592
683
|
function sortKeys(obj) {
|
|
593
684
|
const sorted = {};
|
|
@@ -598,13 +689,13 @@ function sortKeys(obj) {
|
|
|
598
689
|
return sorted;
|
|
599
690
|
}
|
|
600
691
|
function ensureTranslationsStructure(translationsPath, languages) {
|
|
601
|
-
if (!
|
|
602
|
-
|
|
692
|
+
if (!fs2.existsSync(translationsPath)) {
|
|
693
|
+
fs2.mkdirSync(translationsPath, { recursive: true });
|
|
603
694
|
}
|
|
604
695
|
for (const lang of languages) {
|
|
605
|
-
const langPath =
|
|
606
|
-
if (!
|
|
607
|
-
|
|
696
|
+
const langPath = path3.join(translationsPath, lang);
|
|
697
|
+
if (!fs2.existsSync(langPath)) {
|
|
698
|
+
fs2.mkdirSync(langPath, { recursive: true });
|
|
608
699
|
}
|
|
609
700
|
}
|
|
610
701
|
}
|
|
@@ -635,8 +726,8 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
|
|
|
635
726
|
const targetNamespaces = getNamespaces(translationsPath, language);
|
|
636
727
|
for (const namespace of targetNamespaces) {
|
|
637
728
|
if (!sourceNamespaces.includes(namespace)) {
|
|
638
|
-
const filePath =
|
|
639
|
-
|
|
729
|
+
const filePath = path3.join(translationsPath, language, `${namespace}.json`);
|
|
730
|
+
fs2.unlinkSync(filePath);
|
|
640
731
|
result.removedNamespaces.push({
|
|
641
732
|
language,
|
|
642
733
|
namespace,
|
|
@@ -645,9 +736,9 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
|
|
|
645
736
|
}
|
|
646
737
|
}
|
|
647
738
|
for (const namespace of sourceNamespaces) {
|
|
648
|
-
const filePath =
|
|
739
|
+
const filePath = path3.join(translationsPath, language, `${namespace}.json`);
|
|
649
740
|
const sourceFile = sourceTranslations[namespace] || {};
|
|
650
|
-
if (
|
|
741
|
+
if (fs2.existsSync(filePath)) {
|
|
651
742
|
const targetFile = targetTranslations[namespace] || {};
|
|
652
743
|
let hasOrphanedKeys = false;
|
|
653
744
|
const cleanedFile = {};
|
|
@@ -687,12 +778,23 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
|
|
|
687
778
|
});
|
|
688
779
|
}
|
|
689
780
|
}
|
|
781
|
+
if (metadataExists(translationsPath)) {
|
|
782
|
+
const sourceKeysByNamespace = {};
|
|
783
|
+
for (const namespace of sourceNamespaces) {
|
|
784
|
+
sourceKeysByNamespace[namespace] = new Set(Object.keys(sourceTranslations[namespace] || {}));
|
|
785
|
+
}
|
|
786
|
+
const metadata = readMetadata(translationsPath);
|
|
787
|
+
if (pruneMetadata(metadata, sourceKeysByNamespace)) {
|
|
788
|
+
writeMetadata(translationsPath, metadata);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
690
791
|
return result;
|
|
691
792
|
}
|
|
692
793
|
var init_utils = __esm({
|
|
693
794
|
"src/translations/utils/utils.ts"() {
|
|
694
795
|
"use strict";
|
|
695
796
|
init_esm_shims();
|
|
797
|
+
init_metadata();
|
|
696
798
|
}
|
|
697
799
|
});
|
|
698
800
|
|
|
@@ -724,13 +826,13 @@ __export(init_exports, {
|
|
|
724
826
|
initTranslations: () => initTranslations,
|
|
725
827
|
loadConfig: () => loadConfig
|
|
726
828
|
});
|
|
727
|
-
import * as
|
|
728
|
-
import * as
|
|
829
|
+
import * as fs3 from "fs";
|
|
830
|
+
import * as path4 from "path";
|
|
729
831
|
function detectExistingTranslations(projectRoot) {
|
|
730
832
|
const possiblePaths = ["public/static/locales", "public/locales", "src/locales", "locales", "i18n", "translations"];
|
|
731
833
|
for (const possiblePath of possiblePaths) {
|
|
732
|
-
const fullPath =
|
|
733
|
-
if (
|
|
834
|
+
const fullPath = path4.join(projectRoot, possiblePath);
|
|
835
|
+
if (fs3.existsSync(fullPath)) {
|
|
734
836
|
const languages = getAvailableLanguages(fullPath);
|
|
735
837
|
if (languages.length > 0) {
|
|
736
838
|
return { path: possiblePath, languages };
|
|
@@ -760,7 +862,7 @@ function initTranslations(projectRoot, config = {}) {
|
|
|
760
862
|
languages: validLanguages.length > 0 ? validLanguages : finalConfig.languages
|
|
761
863
|
};
|
|
762
864
|
}
|
|
763
|
-
const translationsPath =
|
|
865
|
+
const translationsPath = path4.join(projectRoot, finalConfig.translationsPath);
|
|
764
866
|
const languages = finalConfig.languages.length > 0 ? finalConfig.languages : [...DEFAULT_LANGUAGES];
|
|
765
867
|
console.log(`Project root: ${projectRoot}`);
|
|
766
868
|
console.log(`Translations path: ${translationsPath}`);
|
|
@@ -772,9 +874,9 @@ function initTranslations(projectRoot, config = {}) {
|
|
|
772
874
|
}
|
|
773
875
|
ensureTranslationsStructure(translationsPath, languages);
|
|
774
876
|
const sourceLanguage = finalConfig.sourceLanguage;
|
|
775
|
-
const sourcePath =
|
|
776
|
-
const commonPath =
|
|
777
|
-
if (!
|
|
877
|
+
const sourcePath = path4.join(translationsPath, sourceLanguage);
|
|
878
|
+
const commonPath = path4.join(sourcePath, "common.json");
|
|
879
|
+
if (!fs3.existsSync(commonPath)) {
|
|
778
880
|
const sampleTranslations = {
|
|
779
881
|
LOADING: "Loading",
|
|
780
882
|
SAVE: "Save",
|
|
@@ -783,7 +885,7 @@ function initTranslations(projectRoot, config = {}) {
|
|
|
783
885
|
ERROR: "Error",
|
|
784
886
|
SUCCESS: "Success"
|
|
785
887
|
};
|
|
786
|
-
|
|
888
|
+
fs3.writeFileSync(commonPath, `${JSON.stringify(sampleTranslations, null, 2)}
|
|
787
889
|
`, "utf-8");
|
|
788
890
|
console.log(`Created sample file: ${commonPath}`);
|
|
789
891
|
}
|
|
@@ -797,8 +899,8 @@ function initTranslations(projectRoot, config = {}) {
|
|
|
797
899
|
console.log(` ${lang}: ${langFiles.map((f) => f.namespace).join(", ")}`);
|
|
798
900
|
}
|
|
799
901
|
}
|
|
800
|
-
const configPath =
|
|
801
|
-
if (!
|
|
902
|
+
const configPath = path4.join(projectRoot, ".translationsrc.json");
|
|
903
|
+
if (!fs3.existsSync(configPath)) {
|
|
802
904
|
const configContent = {
|
|
803
905
|
$schema: "./node_modules/poly-lexis/dist/translations/core/translations-config.schema.json",
|
|
804
906
|
translationsPath: finalConfig.translationsPath,
|
|
@@ -807,7 +909,7 @@ function initTranslations(projectRoot, config = {}) {
|
|
|
807
909
|
typesOutputPath: finalConfig.typesOutputPath,
|
|
808
910
|
provider: finalConfig.provider
|
|
809
911
|
};
|
|
810
|
-
|
|
912
|
+
fs3.writeFileSync(configPath, `${JSON.stringify(configContent, null, 2)}
|
|
811
913
|
`, "utf-8");
|
|
812
914
|
console.log(`Created config file: ${configPath}`);
|
|
813
915
|
}
|
|
@@ -816,8 +918,8 @@ function initTranslations(projectRoot, config = {}) {
|
|
|
816
918
|
console.log("=====");
|
|
817
919
|
}
|
|
818
920
|
function loadConfig(projectRoot) {
|
|
819
|
-
const configPath =
|
|
820
|
-
if (!
|
|
921
|
+
const configPath = path4.join(projectRoot, ".translationsrc.json");
|
|
922
|
+
if (!fs3.existsSync(configPath)) {
|
|
821
923
|
const existing = detectExistingTranslations(projectRoot);
|
|
822
924
|
if (existing.path && existing.languages.length > 0) {
|
|
823
925
|
console.log(`\u2139\uFE0F No config found, but detected translations at ${existing.path}`);
|
|
@@ -829,7 +931,7 @@ function loadConfig(projectRoot) {
|
|
|
829
931
|
}
|
|
830
932
|
return DEFAULT_CONFIG;
|
|
831
933
|
}
|
|
832
|
-
const configContent =
|
|
934
|
+
const configContent = fs3.readFileSync(configPath, "utf-8");
|
|
833
935
|
const config = JSON.parse(configContent);
|
|
834
936
|
if (config.languages) {
|
|
835
937
|
const validation = validateLanguages(config.languages);
|
|
@@ -857,8 +959,8 @@ __export(generate_types_exports, {
|
|
|
857
959
|
generateTranslationTypes: () => generateTranslationTypes
|
|
858
960
|
});
|
|
859
961
|
import { execSync } from "child_process";
|
|
860
|
-
import * as
|
|
861
|
-
import * as
|
|
962
|
+
import * as fs4 from "fs";
|
|
963
|
+
import * as path5 from "path";
|
|
862
964
|
function extractPluralBaseKeys(keys) {
|
|
863
965
|
const keySet = new Set(keys);
|
|
864
966
|
const baseKeys = /* @__PURE__ */ new Set();
|
|
@@ -881,11 +983,11 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
|
|
|
881
983
|
console.log("Generating i18n types");
|
|
882
984
|
console.log("=====");
|
|
883
985
|
const config = loadConfig(projectRoot);
|
|
884
|
-
const translationsPath =
|
|
986
|
+
const translationsPath = path5.join(projectRoot, config.translationsPath);
|
|
885
987
|
const sourceLanguage = config.sourceLanguage;
|
|
886
|
-
const outputFilePath =
|
|
887
|
-
const dirPath =
|
|
888
|
-
if (!
|
|
988
|
+
const outputFilePath = path5.join(projectRoot, config.typesOutputPath);
|
|
989
|
+
const dirPath = path5.join(translationsPath, sourceLanguage);
|
|
990
|
+
if (!fs4.existsSync(dirPath)) {
|
|
889
991
|
throw new Error(`Source language directory not found: ${dirPath}`);
|
|
890
992
|
}
|
|
891
993
|
const namespaces = getNamespaces(translationsPath, sourceLanguage);
|
|
@@ -900,12 +1002,12 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
|
|
|
900
1002
|
}
|
|
901
1003
|
const pluralBaseKeys = extractPluralBaseKeys(allKeys);
|
|
902
1004
|
allKeys = allKeys.concat(pluralBaseKeys);
|
|
903
|
-
const outputDir =
|
|
904
|
-
if (!
|
|
905
|
-
|
|
1005
|
+
const outputDir = path5.dirname(outputFilePath);
|
|
1006
|
+
if (!fs4.existsSync(outputDir)) {
|
|
1007
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
906
1008
|
}
|
|
907
1009
|
const typeString = typeTemplate(allKeys, namespaces, config.languages);
|
|
908
|
-
|
|
1010
|
+
fs4.writeFileSync(outputFilePath, typeString, "utf8");
|
|
909
1011
|
console.log(`Generated types with ${allKeys.length} keys and ${namespaces.length} namespaces`);
|
|
910
1012
|
console.log(`Output: ${outputFilePath}`);
|
|
911
1013
|
try {
|
|
@@ -946,8 +1048,8 @@ __export(find_unused_exports, {
|
|
|
946
1048
|
printUnusedKeysResult: () => printUnusedKeysResult
|
|
947
1049
|
});
|
|
948
1050
|
import { execSync as execSync2, spawnSync } from "child_process";
|
|
949
|
-
import * as
|
|
950
|
-
import * as
|
|
1051
|
+
import * as fs7 from "fs";
|
|
1052
|
+
import * as path11 from "path";
|
|
951
1053
|
function isRipgrepAvailable() {
|
|
952
1054
|
try {
|
|
953
1055
|
execSync2("rg --version", { stdio: "ignore" });
|
|
@@ -1008,15 +1110,15 @@ function findFiles(rootDir, searchPaths, extensions) {
|
|
|
1008
1110
|
]);
|
|
1009
1111
|
function scanDirectory(dir) {
|
|
1010
1112
|
try {
|
|
1011
|
-
const entries =
|
|
1113
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
1012
1114
|
for (const entry of entries) {
|
|
1013
|
-
const fullPath =
|
|
1115
|
+
const fullPath = path11.join(dir, entry.name);
|
|
1014
1116
|
if (entry.isDirectory()) {
|
|
1015
1117
|
if (!excludeDirs.has(entry.name)) {
|
|
1016
1118
|
scanDirectory(fullPath);
|
|
1017
1119
|
}
|
|
1018
1120
|
} else if (entry.isFile()) {
|
|
1019
|
-
const ext =
|
|
1121
|
+
const ext = path11.extname(entry.name);
|
|
1020
1122
|
if (extensionSet.has(ext)) {
|
|
1021
1123
|
files.push(fullPath);
|
|
1022
1124
|
}
|
|
@@ -1026,8 +1128,8 @@ function findFiles(rootDir, searchPaths, extensions) {
|
|
|
1026
1128
|
}
|
|
1027
1129
|
}
|
|
1028
1130
|
for (const searchPath of searchPaths) {
|
|
1029
|
-
const fullSearchPath =
|
|
1030
|
-
if (
|
|
1131
|
+
const fullSearchPath = path11.join(rootDir, searchPath);
|
|
1132
|
+
if (fs7.existsSync(fullSearchPath)) {
|
|
1031
1133
|
scanDirectory(fullSearchPath);
|
|
1032
1134
|
}
|
|
1033
1135
|
}
|
|
@@ -1035,7 +1137,7 @@ function findFiles(rootDir, searchPaths, extensions) {
|
|
|
1035
1137
|
}
|
|
1036
1138
|
function checkKeyInFile(key, filePath) {
|
|
1037
1139
|
try {
|
|
1038
|
-
const content =
|
|
1140
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
1039
1141
|
const exactPatterns = [new RegExp(`['"\`]${key}['"\`]`, "g"), new RegExp(`\\b${key}\\b`, "g")];
|
|
1040
1142
|
for (const pattern of exactPatterns) {
|
|
1041
1143
|
if (pattern.test(content)) {
|
|
@@ -1058,7 +1160,7 @@ function checkPartialMatches(key, files) {
|
|
|
1058
1160
|
const foundParts = /* @__PURE__ */ new Set();
|
|
1059
1161
|
for (const file of filesToCheck) {
|
|
1060
1162
|
try {
|
|
1061
|
-
const content =
|
|
1163
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
1062
1164
|
for (const part of significantParts) {
|
|
1063
1165
|
if (foundParts.has(part)) continue;
|
|
1064
1166
|
const partPattern = new RegExp(`\\b${part}\\b`, "gi");
|
|
@@ -1078,7 +1180,7 @@ function checkPartialMatches(key, files) {
|
|
|
1078
1180
|
}
|
|
1079
1181
|
function findUnusedKeys(projectRoot = process.cwd()) {
|
|
1080
1182
|
const config = loadConfig(projectRoot);
|
|
1081
|
-
const translationsPath =
|
|
1183
|
+
const translationsPath = path11.join(projectRoot, config.translationsPath);
|
|
1082
1184
|
const sourceLanguage = config.sourceLanguage;
|
|
1083
1185
|
const searchPaths = config.searchPaths || ["src", "app", "pages", "components"];
|
|
1084
1186
|
const searchExtensions = config.searchExtensions || [".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"];
|
|
@@ -1227,10 +1329,10 @@ __export(find_duplicates_exports, {
|
|
|
1227
1329
|
findDuplicates: () => findDuplicates,
|
|
1228
1330
|
printDuplicateKeysResult: () => printDuplicateKeysResult
|
|
1229
1331
|
});
|
|
1230
|
-
import * as
|
|
1332
|
+
import * as path12 from "path";
|
|
1231
1333
|
function findDuplicates(projectRoot = process.cwd()) {
|
|
1232
1334
|
const config = loadConfig(projectRoot);
|
|
1233
|
-
const translationsPath =
|
|
1335
|
+
const translationsPath = path12.join(projectRoot, config.translationsPath);
|
|
1234
1336
|
const sourceLanguage = config.sourceLanguage;
|
|
1235
1337
|
const namespaces = getNamespaces(translationsPath, sourceLanguage);
|
|
1236
1338
|
const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
|
|
@@ -1310,14 +1412,14 @@ var init_find_duplicates = __esm({
|
|
|
1310
1412
|
// src/cli/translations.ts
|
|
1311
1413
|
init_esm_shims();
|
|
1312
1414
|
import "dotenv/config";
|
|
1313
|
-
import * as
|
|
1314
|
-
import * as
|
|
1415
|
+
import * as fs8 from "fs";
|
|
1416
|
+
import * as path13 from "path";
|
|
1315
1417
|
import { parseArgs } from "util";
|
|
1316
1418
|
import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
|
|
1317
1419
|
|
|
1318
1420
|
// src/translations/cli/add-key.ts
|
|
1319
1421
|
init_esm_shims();
|
|
1320
|
-
import * as
|
|
1422
|
+
import * as path6 from "path";
|
|
1321
1423
|
|
|
1322
1424
|
// src/translations/utils/deepl-translate-provider.ts
|
|
1323
1425
|
init_esm_shims();
|
|
@@ -1525,6 +1627,9 @@ var GoogleTranslateProvider = class {
|
|
|
1525
1627
|
}
|
|
1526
1628
|
};
|
|
1527
1629
|
|
|
1630
|
+
// src/translations/cli/add-key.ts
|
|
1631
|
+
init_metadata();
|
|
1632
|
+
|
|
1528
1633
|
// src/translations/utils/translator.ts
|
|
1529
1634
|
init_esm_shims();
|
|
1530
1635
|
var defaultProvider = new GoogleTranslateProvider();
|
|
@@ -1553,7 +1658,7 @@ init_generate_types();
|
|
|
1553
1658
|
init_init();
|
|
1554
1659
|
async function addTranslationKey(projectRoot, options) {
|
|
1555
1660
|
const config = loadConfig(projectRoot);
|
|
1556
|
-
const translationsPath =
|
|
1661
|
+
const translationsPath = path6.join(projectRoot, config.translationsPath);
|
|
1557
1662
|
const { namespace, key, value, autoTranslate = false, apiKey } = options;
|
|
1558
1663
|
const currentProvider = getTranslationProvider();
|
|
1559
1664
|
const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
|
|
@@ -1587,6 +1692,7 @@ async function addTranslationKey(projectRoot, options) {
|
|
|
1587
1692
|
const otherLanguages = config.languages.filter((lang) => lang !== sourceLang);
|
|
1588
1693
|
if (autoTranslate && apiKey) {
|
|
1589
1694
|
console.log("\nAuto-translating to other languages...");
|
|
1695
|
+
const metadata = readMetadata(translationsPath);
|
|
1590
1696
|
for (const lang of otherLanguages) {
|
|
1591
1697
|
try {
|
|
1592
1698
|
const targetTranslations = readTranslations(translationsPath, lang);
|
|
@@ -1605,6 +1711,7 @@ async function addTranslationKey(projectRoot, options) {
|
|
|
1605
1711
|
targetTranslations[namespace][key] = translated;
|
|
1606
1712
|
const sorted = sortKeys(targetTranslations[namespace]);
|
|
1607
1713
|
writeTranslation(translationsPath, lang, namespace, sorted);
|
|
1714
|
+
setSourceHash(metadata, lang, namespace, key, value);
|
|
1608
1715
|
console.log(` \u2713 ${lang}: "${translated}"`);
|
|
1609
1716
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1610
1717
|
} else {
|
|
@@ -1614,6 +1721,7 @@ async function addTranslationKey(projectRoot, options) {
|
|
|
1614
1721
|
console.error(` \u2717 ${lang}: Translation failed - ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1615
1722
|
}
|
|
1616
1723
|
}
|
|
1724
|
+
writeMetadata(translationsPath, metadata);
|
|
1617
1725
|
} else {
|
|
1618
1726
|
console.log("\nAdding empty values to other languages...");
|
|
1619
1727
|
for (const lang of otherLanguages) {
|
|
@@ -1653,13 +1761,13 @@ init_esm_shims();
|
|
|
1653
1761
|
init_schema();
|
|
1654
1762
|
init_types();
|
|
1655
1763
|
init_init();
|
|
1656
|
-
import * as
|
|
1657
|
-
import * as
|
|
1764
|
+
import * as fs5 from "fs";
|
|
1765
|
+
import * as path7 from "path";
|
|
1658
1766
|
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
1659
1767
|
async function initTranslationsInteractive(projectRoot = process.cwd()) {
|
|
1660
1768
|
console.log("\n\u{1F30D} Translation System Setup\n");
|
|
1661
|
-
const configPath =
|
|
1662
|
-
const alreadyExists =
|
|
1769
|
+
const configPath = path7.join(projectRoot, ".translationsrc.json");
|
|
1770
|
+
const alreadyExists = fs5.existsSync(configPath);
|
|
1663
1771
|
if (alreadyExists) {
|
|
1664
1772
|
console.log("\u26A0\uFE0F Configuration file already exists at .translationsrc.json\n");
|
|
1665
1773
|
const shouldOverwrite = await confirm({
|
|
@@ -1828,29 +1936,33 @@ function getLanguageName(code) {
|
|
|
1828
1936
|
// src/translations/cli/manage.ts
|
|
1829
1937
|
init_esm_shims();
|
|
1830
1938
|
init_utils();
|
|
1831
|
-
import * as
|
|
1832
|
-
import * as
|
|
1939
|
+
import * as fs6 from "fs";
|
|
1940
|
+
import * as path10 from "path";
|
|
1833
1941
|
|
|
1834
1942
|
// src/translations/cli/auto-fill.ts
|
|
1835
1943
|
init_esm_shims();
|
|
1836
|
-
import * as
|
|
1944
|
+
import * as path9 from "path";
|
|
1945
|
+
init_metadata();
|
|
1837
1946
|
init_utils();
|
|
1838
1947
|
init_init();
|
|
1839
1948
|
|
|
1840
1949
|
// src/translations/cli/validate.ts
|
|
1841
1950
|
init_esm_shims();
|
|
1951
|
+
init_metadata();
|
|
1842
1952
|
init_utils();
|
|
1843
1953
|
init_init();
|
|
1844
|
-
import * as
|
|
1954
|
+
import * as path8 from "path";
|
|
1845
1955
|
function validateTranslations(projectRoot = process.cwd()) {
|
|
1846
1956
|
const config = loadConfig(projectRoot);
|
|
1847
|
-
const translationsPath =
|
|
1957
|
+
const translationsPath = path8.join(projectRoot, config.translationsPath);
|
|
1848
1958
|
const sourceLanguage = config.sourceLanguage;
|
|
1849
1959
|
const missing = [];
|
|
1850
1960
|
const empty = [];
|
|
1851
1961
|
const orphaned = [];
|
|
1962
|
+
const stale = [];
|
|
1852
1963
|
const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
|
|
1853
1964
|
const sourceNamespaces = getNamespaces(translationsPath, sourceLanguage);
|
|
1965
|
+
const metadata = readMetadata(translationsPath);
|
|
1854
1966
|
const languages = config.languages.filter((lang) => lang !== sourceLanguage);
|
|
1855
1967
|
const syncResult = syncTranslationStructure(translationsPath, config.languages, sourceLanguage);
|
|
1856
1968
|
if (syncResult.createdFiles.length > 0) {
|
|
@@ -1887,6 +1999,17 @@ function validateTranslations(projectRoot = process.cwd()) {
|
|
|
1887
1999
|
language,
|
|
1888
2000
|
sourceValue
|
|
1889
2001
|
});
|
|
2002
|
+
} else if (typeof targetValue === "string") {
|
|
2003
|
+
const recordedHash = getSourceHash(metadata, language, namespace, key);
|
|
2004
|
+
if (recordedHash !== void 0 && recordedHash !== hashSourceValue(sourceValue)) {
|
|
2005
|
+
stale.push({
|
|
2006
|
+
namespace,
|
|
2007
|
+
key,
|
|
2008
|
+
language,
|
|
2009
|
+
sourceValue,
|
|
2010
|
+
currentValue: targetValue
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
1890
2013
|
}
|
|
1891
2014
|
}
|
|
1892
2015
|
for (const [key, targetValue] of Object.entries(targetKeys)) {
|
|
@@ -1902,7 +2025,7 @@ function validateTranslations(projectRoot = process.cwd()) {
|
|
|
1902
2025
|
}
|
|
1903
2026
|
}
|
|
1904
2027
|
const valid = !missing.length && !empty.length && !orphaned.length;
|
|
1905
|
-
if (valid) {
|
|
2028
|
+
if (valid && !stale.length) {
|
|
1906
2029
|
console.log("\u2713 All translations are valid!");
|
|
1907
2030
|
} else {
|
|
1908
2031
|
if (missing.length > 0) {
|
|
@@ -1935,18 +2058,73 @@ function validateTranslations(projectRoot = process.cwd()) {
|
|
|
1935
2058
|
console.log(` ... and ${orphaned.length - 10} more`);
|
|
1936
2059
|
}
|
|
1937
2060
|
}
|
|
2061
|
+
if (stale.length > 0) {
|
|
2062
|
+
console.log(
|
|
2063
|
+
`
|
|
2064
|
+
\u26A0 Found ${stale.length} potentially outdated translations (source value changed since translation):`
|
|
2065
|
+
);
|
|
2066
|
+
for (const item of stale.slice(0, 10)) {
|
|
2067
|
+
console.log(` ${item.language}/${item.namespace}.json -> ${item.key}`);
|
|
2068
|
+
}
|
|
2069
|
+
if (stale.length > 10) {
|
|
2070
|
+
console.log(` ... and ${stale.length - 10} more`);
|
|
2071
|
+
}
|
|
2072
|
+
console.log(" Run auto-fill with --retranslate-changed to update them.");
|
|
2073
|
+
}
|
|
1938
2074
|
}
|
|
1939
2075
|
console.log("=====");
|
|
1940
|
-
return { valid, missing, empty, orphaned };
|
|
2076
|
+
return { valid, missing, empty, orphaned, stale };
|
|
1941
2077
|
}
|
|
1942
|
-
function getMissingForLanguage(projectRoot, language) {
|
|
2078
|
+
function getMissingForLanguage(projectRoot, language, options = {}) {
|
|
1943
2079
|
const result = validateTranslations(projectRoot);
|
|
1944
2080
|
const items = [
|
|
1945
2081
|
...result.missing.filter((m) => m.language === language).map((m) => ({ ...m, type: "missing" })),
|
|
1946
2082
|
...result.empty.filter((e) => e.language === language).map((e) => ({ ...e, type: "empty" }))
|
|
1947
2083
|
];
|
|
2084
|
+
if (options.includeStale) {
|
|
2085
|
+
items.push(
|
|
2086
|
+
...result.stale.filter((s) => s.language === language).map((s) => ({
|
|
2087
|
+
namespace: s.namespace,
|
|
2088
|
+
key: s.key,
|
|
2089
|
+
language: s.language,
|
|
2090
|
+
sourceValue: s.sourceValue,
|
|
2091
|
+
type: "stale"
|
|
2092
|
+
}))
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
1948
2095
|
return items;
|
|
1949
2096
|
}
|
|
2097
|
+
function ensureBaselineMetadata(projectRoot = process.cwd()) {
|
|
2098
|
+
const config = loadConfig(projectRoot);
|
|
2099
|
+
const translationsPath = path8.join(projectRoot, config.translationsPath);
|
|
2100
|
+
const sourceLanguage = config.sourceLanguage;
|
|
2101
|
+
const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
|
|
2102
|
+
const sourceNamespaces = getNamespaces(translationsPath, sourceLanguage);
|
|
2103
|
+
const languages = config.languages.filter((lang) => lang !== sourceLanguage);
|
|
2104
|
+
const existedBefore = metadataExists(translationsPath);
|
|
2105
|
+
const metadata = readMetadata(translationsPath);
|
|
2106
|
+
let recorded = 0;
|
|
2107
|
+
for (const language of languages) {
|
|
2108
|
+
const targetTranslations = readTranslations(translationsPath, language);
|
|
2109
|
+
for (const namespace of sourceNamespaces) {
|
|
2110
|
+
const sourceKeys = sourceTranslations[namespace] || {};
|
|
2111
|
+
const targetKeys = targetTranslations[namespace] || {};
|
|
2112
|
+
for (const [key, sourceValue] of Object.entries(sourceKeys)) {
|
|
2113
|
+
const targetValue = targetKeys[key];
|
|
2114
|
+
const alreadyTranslated = typeof targetValue === "string" && targetValue.trim() !== "";
|
|
2115
|
+
const alreadyTracked = getSourceHash(metadata, language, namespace, key) !== void 0;
|
|
2116
|
+
if (alreadyTranslated && !alreadyTracked) {
|
|
2117
|
+
setSourceHash(metadata, language, namespace, key, sourceValue);
|
|
2118
|
+
recorded++;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (recorded > 0) {
|
|
2124
|
+
writeMetadata(translationsPath, metadata);
|
|
2125
|
+
}
|
|
2126
|
+
return { recorded, created: !existedBefore && recorded > 0 };
|
|
2127
|
+
}
|
|
1950
2128
|
|
|
1951
2129
|
// src/translations/cli/auto-fill.ts
|
|
1952
2130
|
async function processConcurrently(items, concurrency, processor) {
|
|
@@ -1969,10 +2147,31 @@ async function processConcurrently(items, concurrency, processor) {
|
|
|
1969
2147
|
await Promise.all(executing);
|
|
1970
2148
|
return results;
|
|
1971
2149
|
}
|
|
2150
|
+
function getFillItemsForLanguage(projectRoot, translationsPath, sourceLanguage, language, options) {
|
|
2151
|
+
if (options.force) {
|
|
2152
|
+
const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
|
|
2153
|
+
const items = [];
|
|
2154
|
+
for (const [namespace, keys] of Object.entries(sourceTranslations)) {
|
|
2155
|
+
for (const [key, sourceValue] of Object.entries(keys)) {
|
|
2156
|
+
items.push({ namespace, key, language, sourceValue, type: "forced" });
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return items;
|
|
2160
|
+
}
|
|
2161
|
+
return getMissingForLanguage(projectRoot, language, { includeStale: options.retranslateChanged });
|
|
2162
|
+
}
|
|
1972
2163
|
async function autoFillTranslations(projectRoot = process.cwd(), options = {}) {
|
|
1973
2164
|
const config = loadConfig(projectRoot);
|
|
1974
|
-
const translationsPath =
|
|
1975
|
-
const {
|
|
2165
|
+
const translationsPath = path9.join(projectRoot, config.translationsPath);
|
|
2166
|
+
const {
|
|
2167
|
+
apiKey,
|
|
2168
|
+
limit = Infinity,
|
|
2169
|
+
delayMs = 50,
|
|
2170
|
+
dryRun = false,
|
|
2171
|
+
concurrency = 5,
|
|
2172
|
+
retranslateChanged = false,
|
|
2173
|
+
force = false
|
|
2174
|
+
} = options;
|
|
1976
2175
|
const currentProvider = getTranslationProvider();
|
|
1977
2176
|
const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
|
|
1978
2177
|
if (isDefaultGoogleProvider) {
|
|
@@ -1995,14 +2194,17 @@ async function autoFillTranslations(projectRoot = process.cwd(), options = {}) {
|
|
|
1995
2194
|
console.log(`Created ${syncResult.createdFiles.length} namespace files
|
|
1996
2195
|
`);
|
|
1997
2196
|
}
|
|
2197
|
+
const mode = force ? "all keys (--force)" : retranslateChanged ? "missing, empty & changed" : "missing & empty";
|
|
1998
2198
|
console.log("=====");
|
|
1999
2199
|
console.log("Auto-filling translations");
|
|
2000
2200
|
console.log("=====");
|
|
2001
2201
|
console.log(`Languages: ${languagesToProcess.join(", ")}`);
|
|
2202
|
+
console.log(`Mode: ${mode}`);
|
|
2002
2203
|
console.log(`Limit: ${limit === Infinity ? "unlimited" : limit}`);
|
|
2003
2204
|
console.log(`Concurrency: ${concurrency}`);
|
|
2004
2205
|
console.log(`Dry run: ${dryRun}`);
|
|
2005
2206
|
console.log("=====");
|
|
2207
|
+
const metadata = readMetadata(translationsPath);
|
|
2006
2208
|
let totalProcessed = 0;
|
|
2007
2209
|
let totalTranslated = 0;
|
|
2008
2210
|
for (const language of languagesToProcess) {
|
|
@@ -2013,9 +2215,12 @@ Reached limit of ${limit} translations`);
|
|
|
2013
2215
|
}
|
|
2014
2216
|
console.log(`
|
|
2015
2217
|
Processing language: ${language}`);
|
|
2016
|
-
const missing =
|
|
2218
|
+
const missing = getFillItemsForLanguage(projectRoot, translationsPath, config.sourceLanguage, language, {
|
|
2219
|
+
retranslateChanged,
|
|
2220
|
+
force
|
|
2221
|
+
});
|
|
2017
2222
|
if (!missing.length) {
|
|
2018
|
-
console.log("
|
|
2223
|
+
console.log(" Nothing to translate");
|
|
2019
2224
|
continue;
|
|
2020
2225
|
}
|
|
2021
2226
|
console.log(` Found ${missing.length} translations to fill`);
|
|
@@ -2044,6 +2249,7 @@ Processing language: ${language}`);
|
|
|
2044
2249
|
translations[item.namespace][item.key] = translated;
|
|
2045
2250
|
const sorted = sortKeys(translations[item.namespace]);
|
|
2046
2251
|
writeTranslation(translationsPath, language, item.namespace, sorted);
|
|
2252
|
+
setSourceHash(metadata, language, item.namespace, item.key, item.sourceValue);
|
|
2047
2253
|
console.log(" \u2713 Saved");
|
|
2048
2254
|
} else {
|
|
2049
2255
|
console.log(" \u2713 Dry run - not saved");
|
|
@@ -2059,6 +2265,9 @@ Processing language: ${language}`);
|
|
|
2059
2265
|
});
|
|
2060
2266
|
totalProcessed += itemsToProcess.length;
|
|
2061
2267
|
totalTranslated += results.filter((r) => r.success).length;
|
|
2268
|
+
if (!dryRun) {
|
|
2269
|
+
writeMetadata(translationsPath, metadata);
|
|
2270
|
+
}
|
|
2062
2271
|
}
|
|
2063
2272
|
console.log("\n=====");
|
|
2064
2273
|
console.log(`Total processed: ${totalProcessed}`);
|
|
@@ -2073,12 +2282,22 @@ Processing language: ${language}`);
|
|
|
2073
2282
|
init_generate_types();
|
|
2074
2283
|
init_init();
|
|
2075
2284
|
async function manageTranslations(projectRoot = process.cwd(), options = {}) {
|
|
2076
|
-
const {
|
|
2285
|
+
const {
|
|
2286
|
+
autoFill = false,
|
|
2287
|
+
apiKey,
|
|
2288
|
+
limit,
|
|
2289
|
+
concurrency = 5,
|
|
2290
|
+
language,
|
|
2291
|
+
skipTypes = false,
|
|
2292
|
+
dryRun = false,
|
|
2293
|
+
retranslateChanged = false,
|
|
2294
|
+
force = false
|
|
2295
|
+
} = options;
|
|
2077
2296
|
console.log("=====");
|
|
2078
2297
|
console.log("Translation Management");
|
|
2079
2298
|
console.log("=====");
|
|
2080
|
-
const configPath =
|
|
2081
|
-
const isInitialized =
|
|
2299
|
+
const configPath = path10.join(projectRoot, ".translationsrc.json");
|
|
2300
|
+
const isInitialized = fs6.existsSync(configPath);
|
|
2082
2301
|
if (!isInitialized) {
|
|
2083
2302
|
console.log("\u{1F4C1} No translation configuration found. Initializing...\n");
|
|
2084
2303
|
initTranslations(projectRoot);
|
|
@@ -2087,9 +2306,9 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
|
|
|
2087
2306
|
console.log("\u2713 Translation structure initialized\n");
|
|
2088
2307
|
}
|
|
2089
2308
|
const config = loadConfig(projectRoot);
|
|
2090
|
-
const translationsPath =
|
|
2091
|
-
const sourceLangPath =
|
|
2092
|
-
if (!
|
|
2309
|
+
const translationsPath = path10.join(projectRoot, config.translationsPath);
|
|
2310
|
+
const sourceLangPath = path10.join(translationsPath, config.sourceLanguage);
|
|
2311
|
+
if (!fs6.existsSync(sourceLangPath)) {
|
|
2093
2312
|
console.log(`\u26A0\uFE0F Source language directory not found: ${sourceLangPath}`);
|
|
2094
2313
|
console.log("Please add translation files to the source language directory.\n");
|
|
2095
2314
|
return false;
|
|
@@ -2111,11 +2330,25 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
|
|
|
2111
2330
|
if (syncResult.createdFiles.length === 0 && syncResult.cleanedKeys.length === 0 && syncResult.removedNamespaces.length === 0) {
|
|
2112
2331
|
console.log("\u2713 Translation structure is already synchronized\n");
|
|
2113
2332
|
}
|
|
2333
|
+
if (!dryRun) {
|
|
2334
|
+
const baseline = ensureBaselineMetadata(projectRoot);
|
|
2335
|
+
if (baseline.recorded > 0) {
|
|
2336
|
+
const verb = baseline.created ? "Created change-tracking baseline for" : "Added change tracking for";
|
|
2337
|
+
console.log(`\u{1F4CC} ${verb} ${baseline.recorded} existing translations
|
|
2338
|
+
`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2114
2341
|
console.log("\u{1F50D} Validating translations...\n");
|
|
2115
2342
|
const validationResult = validateTranslations(projectRoot);
|
|
2116
|
-
|
|
2343
|
+
const hasWorkToFill = !validationResult.valid || force || retranslateChanged && validationResult.stale.length > 0;
|
|
2344
|
+
if (validationResult.valid && !validationResult.stale.length) {
|
|
2117
2345
|
console.log("\n\u2705 All translations are complete!\n");
|
|
2118
|
-
} else {
|
|
2346
|
+
} else if (validationResult.valid) {
|
|
2347
|
+
console.log(`
|
|
2348
|
+
\u26A0\uFE0F ${validationResult.stale.length} translations may be outdated.
|
|
2349
|
+
`);
|
|
2350
|
+
}
|
|
2351
|
+
if (hasWorkToFill) {
|
|
2119
2352
|
const totalMissing = validationResult.missing.length + validationResult.empty.length + validationResult.orphaned.length;
|
|
2120
2353
|
if (autoFill) {
|
|
2121
2354
|
if (!apiKey) {
|
|
@@ -2125,29 +2358,38 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
|
|
|
2125
2358
|
console.log(`Set ${envVarName} or pass --api-key to enable auto-fill.
|
|
2126
2359
|
`);
|
|
2127
2360
|
} else {
|
|
2128
|
-
|
|
2129
|
-
\u{1F916}
|
|
2361
|
+
if (force) {
|
|
2362
|
+
console.log("\n\u{1F916} Force re-translating all keys...\n");
|
|
2363
|
+
} else {
|
|
2364
|
+
const staleCount = retranslateChanged ? validationResult.stale.length : 0;
|
|
2365
|
+
console.log(`
|
|
2366
|
+
\u{1F916} Auto-filling ${totalMissing} missing and ${staleCount} changed translations...
|
|
2130
2367
|
`);
|
|
2368
|
+
}
|
|
2131
2369
|
await autoFillTranslations(projectRoot, {
|
|
2132
2370
|
apiKey,
|
|
2133
2371
|
limit,
|
|
2134
2372
|
concurrency,
|
|
2135
2373
|
language,
|
|
2136
2374
|
dryRun,
|
|
2137
|
-
delayMs: 50
|
|
2375
|
+
delayMs: 50,
|
|
2376
|
+
retranslateChanged,
|
|
2377
|
+
force
|
|
2138
2378
|
});
|
|
2139
2379
|
if (!dryRun) {
|
|
2140
2380
|
console.log("\n\u{1F50D} Re-validating after auto-fill...\n");
|
|
2141
2381
|
const revalidation = validateTranslations(projectRoot);
|
|
2142
|
-
if (revalidation.valid) {
|
|
2382
|
+
if (revalidation.valid && !revalidation.stale.length) {
|
|
2143
2383
|
console.log("\n\u2705 All translations are now complete!\n");
|
|
2144
2384
|
}
|
|
2145
2385
|
}
|
|
2146
2386
|
}
|
|
2147
|
-
} else {
|
|
2387
|
+
} else if (!validationResult.valid) {
|
|
2148
2388
|
console.log(`
|
|
2149
2389
|
\u{1F4A1} Tip: Run with --auto-fill to automatically translate missing keys.
|
|
2150
2390
|
`);
|
|
2391
|
+
} else if (validationResult.stale.length > 0) {
|
|
2392
|
+
console.log("\n\u{1F4A1} Tip: Run with --auto-fill --retranslate-changed to update outdated translations.\n");
|
|
2151
2393
|
}
|
|
2152
2394
|
}
|
|
2153
2395
|
if (!skipTypes && !dryRun) {
|
|
@@ -2173,9 +2415,17 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
|
|
|
2173
2415
|
if (validationResult.orphaned.length > 0) {
|
|
2174
2416
|
console.log(`\u26A0\uFE0F ${validationResult.orphaned.length} orphaned translations (will be auto-removed on next sync)`);
|
|
2175
2417
|
}
|
|
2418
|
+
if (validationResult.stale.length > 0) {
|
|
2419
|
+
console.log(`\u26A0\uFE0F ${validationResult.stale.length} potentially outdated translations`);
|
|
2420
|
+
}
|
|
2176
2421
|
console.log("\nNext steps:");
|
|
2177
2422
|
console.log(" 1. Add missing translations manually, or");
|
|
2178
2423
|
console.log(" 2. Run with --auto-fill to translate automatically");
|
|
2424
|
+
} else if (validationResult.valid && validationResult.stale.length > 0 && !autoFill) {
|
|
2425
|
+
console.log(`
|
|
2426
|
+
\u26A0\uFE0F ${validationResult.stale.length} potentially outdated translations`);
|
|
2427
|
+
console.log("\nNext steps:");
|
|
2428
|
+
console.log(" Run with --auto-fill --retranslate-changed to update them");
|
|
2179
2429
|
} else if (validationResult.valid) {
|
|
2180
2430
|
console.log("\n\u2705 All systems ready!");
|
|
2181
2431
|
}
|
|
@@ -2219,6 +2469,14 @@ var { values, positionals } = parseArgs({
|
|
|
2219
2469
|
short: "d",
|
|
2220
2470
|
default: false
|
|
2221
2471
|
},
|
|
2472
|
+
"retranslate-changed": {
|
|
2473
|
+
type: "boolean",
|
|
2474
|
+
default: false
|
|
2475
|
+
},
|
|
2476
|
+
force: {
|
|
2477
|
+
type: "boolean",
|
|
2478
|
+
default: false
|
|
2479
|
+
},
|
|
2222
2480
|
namespace: {
|
|
2223
2481
|
type: "string",
|
|
2224
2482
|
short: "n"
|
|
@@ -2257,6 +2515,8 @@ Options (Smart Mode):
|
|
|
2257
2515
|
-l, --language <lang> Process only this language
|
|
2258
2516
|
--limit <number> Max translations to process (default: unlimited)
|
|
2259
2517
|
--concurrency <number> Number of concurrent translation requests (default: 5)
|
|
2518
|
+
--retranslate-changed Re-translate keys whose source value has changed since last run
|
|
2519
|
+
--force Re-translate every key, even ones already translated
|
|
2260
2520
|
--skip-types Skip TypeScript type generation
|
|
2261
2521
|
-d, --dry-run Preview changes without saving
|
|
2262
2522
|
-h, --help Show this help
|
|
@@ -2287,6 +2547,12 @@ Examples:
|
|
|
2287
2547
|
# Preview what would be translated (dry-run)
|
|
2288
2548
|
translations --auto-fill --dry-run
|
|
2289
2549
|
|
|
2550
|
+
# Re-translate keys whose source string changed since last run
|
|
2551
|
+
translations --auto-fill --retranslate-changed
|
|
2552
|
+
|
|
2553
|
+
# Re-translate everything from scratch
|
|
2554
|
+
translations --auto-fill --force
|
|
2555
|
+
|
|
2290
2556
|
# Add a new translation key (interactive mode)
|
|
2291
2557
|
translations add
|
|
2292
2558
|
|
|
@@ -2341,8 +2607,8 @@ if (command === "find-unused") {
|
|
|
2341
2607
|
(async () => {
|
|
2342
2608
|
try {
|
|
2343
2609
|
console.log("\n\u2728 Add a new translation key\n");
|
|
2344
|
-
const configPath =
|
|
2345
|
-
const isInitialized =
|
|
2610
|
+
const configPath = path13.join(process.cwd(), ".translationsrc.json");
|
|
2611
|
+
const isInitialized = fs8.existsSync(configPath);
|
|
2346
2612
|
if (!isInitialized) {
|
|
2347
2613
|
console.log("\u26A0\uFE0F Translation structure not initialized.");
|
|
2348
2614
|
const shouldInit = await confirm2({
|
|
@@ -2358,7 +2624,7 @@ if (command === "find-unused") {
|
|
|
2358
2624
|
}
|
|
2359
2625
|
}
|
|
2360
2626
|
const config = loadConfig(process.cwd());
|
|
2361
|
-
const translationsPath =
|
|
2627
|
+
const translationsPath = path13.join(process.cwd(), config.translationsPath);
|
|
2362
2628
|
const existingNamespaces = getNamespaces(translationsPath, config.sourceLanguage);
|
|
2363
2629
|
let namespace;
|
|
2364
2630
|
if (existingNamespaces.length > 0) {
|
|
@@ -2479,23 +2745,26 @@ if (command === "find-unused") {
|
|
|
2479
2745
|
console.log("Use --help for usage information");
|
|
2480
2746
|
process.exit(1);
|
|
2481
2747
|
} else {
|
|
2482
|
-
const hasFlags = values["auto-fill"] || values.language || values["skip-types"] || values["dry-run"];
|
|
2748
|
+
const hasFlags = values["auto-fill"] || values.language || values["skip-types"] || values["dry-run"] || values["retranslate-changed"] || values.force;
|
|
2483
2749
|
if (hasFlags) {
|
|
2484
|
-
const configPath =
|
|
2485
|
-
const config =
|
|
2750
|
+
const configPath = path13.join(process.cwd(), ".translationsrc.json");
|
|
2751
|
+
const config = fs8.existsSync(configPath) ? loadConfig(process.cwd()) : { provider: "deepl" };
|
|
2486
2752
|
const provider = config.provider || "deepl";
|
|
2487
2753
|
const envVarName = provider === "google" ? "GOOGLE_TRANSLATE_API_KEY" : "DEEPL_API_KEY";
|
|
2488
2754
|
const apiKey = values["api-key"] || process.env[envVarName];
|
|
2489
2755
|
const limit = values.limit ? Number.parseInt(values.limit, 10) : void 0;
|
|
2490
2756
|
const concurrency = Number.parseInt(values.concurrency || "5", 10);
|
|
2757
|
+
const autoFill = values["auto-fill"] || values["retranslate-changed"] || values.force;
|
|
2491
2758
|
manageTranslations(process.cwd(), {
|
|
2492
|
-
autoFill
|
|
2759
|
+
autoFill,
|
|
2493
2760
|
apiKey,
|
|
2494
2761
|
limit,
|
|
2495
2762
|
concurrency,
|
|
2496
2763
|
language: values.language,
|
|
2497
2764
|
skipTypes: values["skip-types"],
|
|
2498
|
-
dryRun: values["dry-run"]
|
|
2765
|
+
dryRun: values["dry-run"],
|
|
2766
|
+
retranslateChanged: values["retranslate-changed"],
|
|
2767
|
+
force: values.force
|
|
2499
2768
|
}).then((isValid) => {
|
|
2500
2769
|
if (!isValid) {
|
|
2501
2770
|
process.exit(1);
|
|
@@ -2507,8 +2776,8 @@ if (command === "find-unused") {
|
|
|
2507
2776
|
} else {
|
|
2508
2777
|
(async () => {
|
|
2509
2778
|
try {
|
|
2510
|
-
const configPath =
|
|
2511
|
-
const isInitialized =
|
|
2779
|
+
const configPath = path13.join(process.cwd(), ".translationsrc.json");
|
|
2780
|
+
const isInitialized = fs8.existsSync(configPath);
|
|
2512
2781
|
console.log("\n\u{1F30D} Translation Management\n");
|
|
2513
2782
|
const action = await select2({
|
|
2514
2783
|
message: "What would you like to do?",
|
|
@@ -2572,7 +2841,7 @@ if (command === "find-unused") {
|
|
|
2572
2841
|
}
|
|
2573
2842
|
}
|
|
2574
2843
|
const config = loadConfig(process.cwd());
|
|
2575
|
-
const translationsPath =
|
|
2844
|
+
const translationsPath = path13.join(process.cwd(), config.translationsPath);
|
|
2576
2845
|
const existingNamespaces = getNamespaces(translationsPath, config.sourceLanguage);
|
|
2577
2846
|
let namespace;
|
|
2578
2847
|
if (existingNamespaces.length > 0) {
|
|
@@ -2673,9 +2942,14 @@ if (command === "find-unused") {
|
|
|
2673
2942
|
default: true
|
|
2674
2943
|
});
|
|
2675
2944
|
if (shouldContinue) {
|
|
2945
|
+
const retranslateChanged = await confirm2({
|
|
2946
|
+
message: "Also re-translate keys whose source value has changed?",
|
|
2947
|
+
default: false
|
|
2948
|
+
});
|
|
2676
2949
|
await manageTranslations(process.cwd(), {
|
|
2677
2950
|
autoFill: true,
|
|
2678
|
-
apiKey
|
|
2951
|
+
apiKey,
|
|
2952
|
+
retranslateChanged
|
|
2679
2953
|
});
|
|
2680
2954
|
}
|
|
2681
2955
|
} else if (action === "find-unused") {
|