poly-lexis 0.9.2 → 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/dist/index.js CHANGED
@@ -431,6 +431,14 @@ var init_schema = __esm({
431
431
  type: "string"
432
432
  },
433
433
  default: [".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"]
434
+ },
435
+ protectedTerms: {
436
+ type: "array",
437
+ description: "List of terms that should never be translated (e.g. brand names, technical terms)",
438
+ items: {
439
+ type: "string"
440
+ },
441
+ default: []
434
442
  }
435
443
  },
436
444
  required: ["translationsPath", "languages", "sourceLanguage"],
@@ -623,9 +631,99 @@ var init_language_fallback = __esm({
623
631
  }
624
632
  });
625
633
 
626
- // src/translations/utils/utils.ts
634
+ // src/translations/utils/metadata.ts
635
+ import { createHash } from "crypto";
627
636
  import * as fs from "fs";
628
637
  import * as path from "path";
638
+ function emptyMetadata() {
639
+ return { version: CURRENT_VERSION, languages: {} };
640
+ }
641
+ function hashSourceValue(value) {
642
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
643
+ }
644
+ function getMetadataPath(translationsPath) {
645
+ return path.join(translationsPath, METADATA_FILE_NAME);
646
+ }
647
+ function metadataExists(translationsPath) {
648
+ return fs.existsSync(getMetadataPath(translationsPath));
649
+ }
650
+ function readMetadata(translationsPath) {
651
+ const metadataPath = getMetadataPath(translationsPath);
652
+ if (!fs.existsSync(metadataPath)) {
653
+ return emptyMetadata();
654
+ }
655
+ try {
656
+ const parsed = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
657
+ return {
658
+ version: parsed.version ?? CURRENT_VERSION,
659
+ languages: parsed.languages ?? {}
660
+ };
661
+ } catch {
662
+ return emptyMetadata();
663
+ }
664
+ }
665
+ function writeMetadata(translationsPath, metadata) {
666
+ if (!fs.existsSync(translationsPath)) {
667
+ fs.mkdirSync(translationsPath, { recursive: true });
668
+ }
669
+ const metadataPath = getMetadataPath(translationsPath);
670
+ fs.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}
671
+ `, "utf-8");
672
+ }
673
+ function setSourceHash(metadata, language, namespace, key, sourceValue) {
674
+ if (!metadata.languages[language]) {
675
+ metadata.languages[language] = {};
676
+ }
677
+ if (!metadata.languages[language][namespace]) {
678
+ metadata.languages[language][namespace] = {};
679
+ }
680
+ metadata.languages[language][namespace][key] = hashSourceValue(sourceValue);
681
+ return metadata;
682
+ }
683
+ function getSourceHash(metadata, language, namespace, key) {
684
+ return metadata.languages[language]?.[namespace]?.[key];
685
+ }
686
+ function pruneMetadata(metadata, sourceKeysByNamespace) {
687
+ let changed = false;
688
+ for (const language of Object.keys(metadata.languages)) {
689
+ const namespaces = metadata.languages[language];
690
+ for (const namespace of Object.keys(namespaces)) {
691
+ const sourceKeys = sourceKeysByNamespace[namespace];
692
+ if (!sourceKeys) {
693
+ delete namespaces[namespace];
694
+ changed = true;
695
+ continue;
696
+ }
697
+ for (const key of Object.keys(namespaces[namespace])) {
698
+ if (!sourceKeys.has(key)) {
699
+ delete namespaces[namespace][key];
700
+ changed = true;
701
+ }
702
+ }
703
+ if (Object.keys(namespaces[namespace]).length === 0) {
704
+ delete namespaces[namespace];
705
+ changed = true;
706
+ }
707
+ }
708
+ if (Object.keys(namespaces).length === 0) {
709
+ delete metadata.languages[language];
710
+ changed = true;
711
+ }
712
+ }
713
+ return changed;
714
+ }
715
+ var METADATA_FILE_NAME, CURRENT_VERSION;
716
+ var init_metadata = __esm({
717
+ "src/translations/utils/metadata.ts"() {
718
+ "use strict";
719
+ METADATA_FILE_NAME = ".translations-meta.json";
720
+ CURRENT_VERSION = 1;
721
+ }
722
+ });
723
+
724
+ // src/translations/utils/utils.ts
725
+ import * as fs2 from "fs";
726
+ import * as path2 from "path";
629
727
  function flattenObject(obj, prefix = "") {
630
728
  const result = {};
631
729
  for (const [key, value] of Object.entries(obj)) {
@@ -658,16 +756,16 @@ function isNestedObject(obj) {
658
756
  return Object.values(obj).some((value) => typeof value === "object" && value !== null);
659
757
  }
660
758
  function readTranslations(translationsPath, language) {
661
- const langPath = path.join(translationsPath, language);
662
- if (!fs.existsSync(langPath)) {
759
+ const langPath = path2.join(translationsPath, language);
760
+ if (!fs2.existsSync(langPath)) {
663
761
  return {};
664
762
  }
665
- const files = fs.readdirSync(langPath).filter((f) => f.endsWith(".json"));
763
+ const files = fs2.readdirSync(langPath).filter((f) => f.endsWith(".json"));
666
764
  const translations = {};
667
765
  for (const file of files) {
668
- const namespace = path.basename(file, ".json");
669
- const filePath = path.join(langPath, file);
670
- const content = fs.readFileSync(filePath, "utf-8");
766
+ const namespace = path2.basename(file, ".json");
767
+ const filePath = path2.join(langPath, file);
768
+ const content = fs2.readFileSync(filePath, "utf-8");
671
769
  const parsed = JSON.parse(content);
672
770
  if (isNestedObject(parsed)) {
673
771
  translations[namespace] = flattenObject(parsed);
@@ -678,26 +776,26 @@ function readTranslations(translationsPath, language) {
678
776
  return translations;
679
777
  }
680
778
  function writeTranslation(translationsPath, language, namespace, translations) {
681
- const langPath = path.join(translationsPath, language);
682
- if (!fs.existsSync(langPath)) {
683
- fs.mkdirSync(langPath, { recursive: true });
779
+ const langPath = path2.join(translationsPath, language);
780
+ if (!fs2.existsSync(langPath)) {
781
+ fs2.mkdirSync(langPath, { recursive: true });
684
782
  }
685
- const filePath = path.join(langPath, `${namespace}.json`);
686
- fs.writeFileSync(filePath, `${JSON.stringify(translations, null, 2)}
783
+ const filePath = path2.join(langPath, `${namespace}.json`);
784
+ fs2.writeFileSync(filePath, `${JSON.stringify(translations, null, 2)}
687
785
  `, "utf-8");
688
786
  }
689
787
  function getAvailableLanguages(translationsPath) {
690
- if (!fs.existsSync(translationsPath)) {
788
+ if (!fs2.existsSync(translationsPath)) {
691
789
  return [];
692
790
  }
693
- return fs.readdirSync(translationsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
791
+ return fs2.readdirSync(translationsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
694
792
  }
695
793
  function getNamespaces(translationsPath, language) {
696
- const langPath = path.join(translationsPath, language);
697
- if (!fs.existsSync(langPath)) {
794
+ const langPath = path2.join(translationsPath, language);
795
+ if (!fs2.existsSync(langPath)) {
698
796
  return [];
699
797
  }
700
- return fs.readdirSync(langPath).filter((f) => f.endsWith(".json")).map((f) => path.basename(f, ".json"));
798
+ return fs2.readdirSync(langPath).filter((f) => f.endsWith(".json")).map((f) => path2.basename(f, ".json"));
701
799
  }
702
800
  function hasInterpolation(text) {
703
801
  return /\{\{[^}]+\}\}/g.test(text);
@@ -724,13 +822,13 @@ function sortKeys(obj) {
724
822
  return sorted;
725
823
  }
726
824
  function ensureTranslationsStructure(translationsPath, languages) {
727
- if (!fs.existsSync(translationsPath)) {
728
- fs.mkdirSync(translationsPath, { recursive: true });
825
+ if (!fs2.existsSync(translationsPath)) {
826
+ fs2.mkdirSync(translationsPath, { recursive: true });
729
827
  }
730
828
  for (const lang of languages) {
731
- const langPath = path.join(translationsPath, lang);
732
- if (!fs.existsSync(langPath)) {
733
- fs.mkdirSync(langPath, { recursive: true });
829
+ const langPath = path2.join(translationsPath, lang);
830
+ if (!fs2.existsSync(langPath)) {
831
+ fs2.mkdirSync(langPath, { recursive: true });
734
832
  }
735
833
  }
736
834
  }
@@ -761,8 +859,8 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
761
859
  const targetNamespaces = getNamespaces(translationsPath, language);
762
860
  for (const namespace of targetNamespaces) {
763
861
  if (!sourceNamespaces.includes(namespace)) {
764
- const filePath = path.join(translationsPath, language, `${namespace}.json`);
765
- fs.unlinkSync(filePath);
862
+ const filePath = path2.join(translationsPath, language, `${namespace}.json`);
863
+ fs2.unlinkSync(filePath);
766
864
  result.removedNamespaces.push({
767
865
  language,
768
866
  namespace,
@@ -771,9 +869,9 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
771
869
  }
772
870
  }
773
871
  for (const namespace of sourceNamespaces) {
774
- const filePath = path.join(translationsPath, language, `${namespace}.json`);
872
+ const filePath = path2.join(translationsPath, language, `${namespace}.json`);
775
873
  const sourceFile = sourceTranslations[namespace] || {};
776
- if (fs.existsSync(filePath)) {
874
+ if (fs2.existsSync(filePath)) {
777
875
  const targetFile = targetTranslations[namespace] || {};
778
876
  let hasOrphanedKeys = false;
779
877
  const cleanedFile = {};
@@ -813,11 +911,22 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
813
911
  });
814
912
  }
815
913
  }
914
+ if (metadataExists(translationsPath)) {
915
+ const sourceKeysByNamespace = {};
916
+ for (const namespace of sourceNamespaces) {
917
+ sourceKeysByNamespace[namespace] = new Set(Object.keys(sourceTranslations[namespace] || {}));
918
+ }
919
+ const metadata = readMetadata(translationsPath);
920
+ if (pruneMetadata(metadata, sourceKeysByNamespace)) {
921
+ writeMetadata(translationsPath, metadata);
922
+ }
923
+ }
816
924
  return result;
817
925
  }
818
926
  var init_utils = __esm({
819
927
  "src/translations/utils/utils.ts"() {
820
928
  "use strict";
929
+ init_metadata();
821
930
  }
822
931
  });
823
932
 
@@ -848,13 +957,13 @@ __export(init_exports, {
848
957
  initTranslations: () => initTranslations,
849
958
  loadConfig: () => loadConfig
850
959
  });
851
- import * as fs2 from "fs";
852
- import * as path2 from "path";
960
+ import * as fs3 from "fs";
961
+ import * as path3 from "path";
853
962
  function detectExistingTranslations(projectRoot) {
854
963
  const possiblePaths = ["public/static/locales", "public/locales", "src/locales", "locales", "i18n", "translations"];
855
964
  for (const possiblePath of possiblePaths) {
856
- const fullPath = path2.join(projectRoot, possiblePath);
857
- if (fs2.existsSync(fullPath)) {
965
+ const fullPath = path3.join(projectRoot, possiblePath);
966
+ if (fs3.existsSync(fullPath)) {
858
967
  const languages = getAvailableLanguages(fullPath);
859
968
  if (languages.length > 0) {
860
969
  return { path: possiblePath, languages };
@@ -884,7 +993,7 @@ function initTranslations(projectRoot, config = {}) {
884
993
  languages: validLanguages.length > 0 ? validLanguages : finalConfig.languages
885
994
  };
886
995
  }
887
- const translationsPath = path2.join(projectRoot, finalConfig.translationsPath);
996
+ const translationsPath = path3.join(projectRoot, finalConfig.translationsPath);
888
997
  const languages = finalConfig.languages.length > 0 ? finalConfig.languages : [...DEFAULT_LANGUAGES];
889
998
  console.log(`Project root: ${projectRoot}`);
890
999
  console.log(`Translations path: ${translationsPath}`);
@@ -896,9 +1005,9 @@ function initTranslations(projectRoot, config = {}) {
896
1005
  }
897
1006
  ensureTranslationsStructure(translationsPath, languages);
898
1007
  const sourceLanguage = finalConfig.sourceLanguage;
899
- const sourcePath = path2.join(translationsPath, sourceLanguage);
900
- const commonPath = path2.join(sourcePath, "common.json");
901
- if (!fs2.existsSync(commonPath)) {
1008
+ const sourcePath = path3.join(translationsPath, sourceLanguage);
1009
+ const commonPath = path3.join(sourcePath, "common.json");
1010
+ if (!fs3.existsSync(commonPath)) {
902
1011
  const sampleTranslations = {
903
1012
  LOADING: "Loading",
904
1013
  SAVE: "Save",
@@ -907,7 +1016,7 @@ function initTranslations(projectRoot, config = {}) {
907
1016
  ERROR: "Error",
908
1017
  SUCCESS: "Success"
909
1018
  };
910
- fs2.writeFileSync(commonPath, `${JSON.stringify(sampleTranslations, null, 2)}
1019
+ fs3.writeFileSync(commonPath, `${JSON.stringify(sampleTranslations, null, 2)}
911
1020
  `, "utf-8");
912
1021
  console.log(`Created sample file: ${commonPath}`);
913
1022
  }
@@ -921,8 +1030,8 @@ function initTranslations(projectRoot, config = {}) {
921
1030
  console.log(` ${lang}: ${langFiles.map((f) => f.namespace).join(", ")}`);
922
1031
  }
923
1032
  }
924
- const configPath = path2.join(projectRoot, ".translationsrc.json");
925
- if (!fs2.existsSync(configPath)) {
1033
+ const configPath = path3.join(projectRoot, ".translationsrc.json");
1034
+ if (!fs3.existsSync(configPath)) {
926
1035
  const configContent = {
927
1036
  $schema: "./node_modules/poly-lexis/dist/translations/core/translations-config.schema.json",
928
1037
  translationsPath: finalConfig.translationsPath,
@@ -931,7 +1040,7 @@ function initTranslations(projectRoot, config = {}) {
931
1040
  typesOutputPath: finalConfig.typesOutputPath,
932
1041
  provider: finalConfig.provider
933
1042
  };
934
- fs2.writeFileSync(configPath, `${JSON.stringify(configContent, null, 2)}
1043
+ fs3.writeFileSync(configPath, `${JSON.stringify(configContent, null, 2)}
935
1044
  `, "utf-8");
936
1045
  console.log(`Created config file: ${configPath}`);
937
1046
  }
@@ -940,8 +1049,8 @@ function initTranslations(projectRoot, config = {}) {
940
1049
  console.log("=====");
941
1050
  }
942
1051
  function loadConfig(projectRoot) {
943
- const configPath = path2.join(projectRoot, ".translationsrc.json");
944
- if (!fs2.existsSync(configPath)) {
1052
+ const configPath = path3.join(projectRoot, ".translationsrc.json");
1053
+ if (!fs3.existsSync(configPath)) {
945
1054
  const existing = detectExistingTranslations(projectRoot);
946
1055
  if (existing.path && existing.languages.length > 0) {
947
1056
  console.log(`\u2139\uFE0F No config found, but detected translations at ${existing.path}`);
@@ -953,7 +1062,7 @@ function loadConfig(projectRoot) {
953
1062
  }
954
1063
  return DEFAULT_CONFIG;
955
1064
  }
956
- const configContent = fs2.readFileSync(configPath, "utf-8");
1065
+ const configContent = fs3.readFileSync(configPath, "utf-8");
957
1066
  const config = JSON.parse(configContent);
958
1067
  if (config.languages) {
959
1068
  const validation = validateLanguages(config.languages);
@@ -974,7 +1083,7 @@ var init_init = __esm({
974
1083
  });
975
1084
 
976
1085
  // src/translations/cli/add-key.ts
977
- import * as path4 from "path";
1086
+ import * as path5 from "path";
978
1087
 
979
1088
  // src/translations/utils/deepl-translate-provider.ts
980
1089
  init_language_fallback();
@@ -1180,6 +1289,9 @@ var GoogleTranslateProvider = class {
1180
1289
  }
1181
1290
  };
1182
1291
 
1292
+ // src/translations/cli/add-key.ts
1293
+ init_metadata();
1294
+
1183
1295
  // src/translations/utils/translator.ts
1184
1296
  var defaultProvider = new GoogleTranslateProvider();
1185
1297
  var customProvider = null;
@@ -1215,8 +1327,8 @@ init_utils();
1215
1327
  init_utils();
1216
1328
  init_init();
1217
1329
  import { execSync } from "child_process";
1218
- import * as fs3 from "fs";
1219
- import * as path3 from "path";
1330
+ import * as fs4 from "fs";
1331
+ import * as path4 from "path";
1220
1332
  var PLURAL_SUFFIXES = ["_zero", "_one", "_two", "_few", "_many", "_other"];
1221
1333
  function extractPluralBaseKeys(keys) {
1222
1334
  const keySet = new Set(keys);
@@ -1249,11 +1361,11 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
1249
1361
  console.log("Generating i18n types");
1250
1362
  console.log("=====");
1251
1363
  const config = loadConfig(projectRoot);
1252
- const translationsPath = path3.join(projectRoot, config.translationsPath);
1364
+ const translationsPath = path4.join(projectRoot, config.translationsPath);
1253
1365
  const sourceLanguage = config.sourceLanguage;
1254
- const outputFilePath = path3.join(projectRoot, config.typesOutputPath);
1255
- const dirPath = path3.join(translationsPath, sourceLanguage);
1256
- if (!fs3.existsSync(dirPath)) {
1366
+ const outputFilePath = path4.join(projectRoot, config.typesOutputPath);
1367
+ const dirPath = path4.join(translationsPath, sourceLanguage);
1368
+ if (!fs4.existsSync(dirPath)) {
1257
1369
  throw new Error(`Source language directory not found: ${dirPath}`);
1258
1370
  }
1259
1371
  const namespaces = getNamespaces(translationsPath, sourceLanguage);
@@ -1268,12 +1380,12 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
1268
1380
  }
1269
1381
  const pluralBaseKeys = extractPluralBaseKeys(allKeys);
1270
1382
  allKeys = allKeys.concat(pluralBaseKeys);
1271
- const outputDir = path3.dirname(outputFilePath);
1272
- if (!fs3.existsSync(outputDir)) {
1273
- fs3.mkdirSync(outputDir, { recursive: true });
1383
+ const outputDir = path4.dirname(outputFilePath);
1384
+ if (!fs4.existsSync(outputDir)) {
1385
+ fs4.mkdirSync(outputDir, { recursive: true });
1274
1386
  }
1275
1387
  const typeString = typeTemplate(allKeys, namespaces, config.languages);
1276
- fs3.writeFileSync(outputFilePath, typeString, "utf8");
1388
+ fs4.writeFileSync(outputFilePath, typeString, "utf8");
1277
1389
  console.log(`Generated types with ${allKeys.length} keys and ${namespaces.length} namespaces`);
1278
1390
  console.log(`Output: ${outputFilePath}`);
1279
1391
  try {
@@ -1292,7 +1404,7 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
1292
1404
  init_init();
1293
1405
  async function addTranslationKey(projectRoot, options) {
1294
1406
  const config = loadConfig(projectRoot);
1295
- const translationsPath = path4.join(projectRoot, config.translationsPath);
1407
+ const translationsPath = path5.join(projectRoot, config.translationsPath);
1296
1408
  const { namespace, key, value, autoTranslate = false, apiKey } = options;
1297
1409
  const currentProvider = getTranslationProvider();
1298
1410
  const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
@@ -1326,6 +1438,7 @@ async function addTranslationKey(projectRoot, options) {
1326
1438
  const otherLanguages = config.languages.filter((lang) => lang !== sourceLang);
1327
1439
  if (autoTranslate && apiKey) {
1328
1440
  console.log("\nAuto-translating to other languages...");
1441
+ const metadata = readMetadata(translationsPath);
1329
1442
  for (const lang of otherLanguages) {
1330
1443
  try {
1331
1444
  const targetTranslations = readTranslations(translationsPath, lang);
@@ -1344,6 +1457,7 @@ async function addTranslationKey(projectRoot, options) {
1344
1457
  targetTranslations[namespace][key] = translated;
1345
1458
  const sorted = sortKeys(targetTranslations[namespace]);
1346
1459
  writeTranslation(translationsPath, lang, namespace, sorted);
1460
+ setSourceHash(metadata, lang, namespace, key, value);
1347
1461
  console.log(` \u2713 ${lang}: "${translated}"`);
1348
1462
  await new Promise((resolve) => setTimeout(resolve, 100));
1349
1463
  } else {
@@ -1353,6 +1467,7 @@ async function addTranslationKey(projectRoot, options) {
1353
1467
  console.error(` \u2717 ${lang}: Translation failed - ${error instanceof Error ? error.message : "Unknown error"}`);
1354
1468
  }
1355
1469
  }
1470
+ writeMetadata(translationsPath, metadata);
1356
1471
  } else {
1357
1472
  console.log("\nAdding empty values to other languages...");
1358
1473
  for (const lang of otherLanguages) {
@@ -1397,23 +1512,27 @@ async function addTranslationKeys(projectRoot, entries, autoTranslate = false, a
1397
1512
  }
1398
1513
 
1399
1514
  // src/translations/cli/auto-fill.ts
1400
- import * as path6 from "path";
1515
+ import * as path7 from "path";
1516
+ init_metadata();
1401
1517
  init_utils();
1402
1518
  init_init();
1403
1519
 
1404
1520
  // src/translations/cli/validate.ts
1521
+ init_metadata();
1405
1522
  init_utils();
1406
1523
  init_init();
1407
- import * as path5 from "path";
1524
+ import * as path6 from "path";
1408
1525
  function validateTranslations(projectRoot = process.cwd()) {
1409
1526
  const config = loadConfig(projectRoot);
1410
- const translationsPath = path5.join(projectRoot, config.translationsPath);
1527
+ const translationsPath = path6.join(projectRoot, config.translationsPath);
1411
1528
  const sourceLanguage = config.sourceLanguage;
1412
1529
  const missing = [];
1413
1530
  const empty = [];
1414
1531
  const orphaned = [];
1532
+ const stale = [];
1415
1533
  const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
1416
1534
  const sourceNamespaces = getNamespaces(translationsPath, sourceLanguage);
1535
+ const metadata = readMetadata(translationsPath);
1417
1536
  const languages = config.languages.filter((lang) => lang !== sourceLanguage);
1418
1537
  const syncResult = syncTranslationStructure(translationsPath, config.languages, sourceLanguage);
1419
1538
  if (syncResult.createdFiles.length > 0) {
@@ -1450,6 +1569,17 @@ function validateTranslations(projectRoot = process.cwd()) {
1450
1569
  language,
1451
1570
  sourceValue
1452
1571
  });
1572
+ } else if (typeof targetValue === "string") {
1573
+ const recordedHash = getSourceHash(metadata, language, namespace, key);
1574
+ if (recordedHash !== void 0 && recordedHash !== hashSourceValue(sourceValue)) {
1575
+ stale.push({
1576
+ namespace,
1577
+ key,
1578
+ language,
1579
+ sourceValue,
1580
+ currentValue: targetValue
1581
+ });
1582
+ }
1453
1583
  }
1454
1584
  }
1455
1585
  for (const [key, targetValue] of Object.entries(targetKeys)) {
@@ -1465,7 +1595,7 @@ function validateTranslations(projectRoot = process.cwd()) {
1465
1595
  }
1466
1596
  }
1467
1597
  const valid = !missing.length && !empty.length && !orphaned.length;
1468
- if (valid) {
1598
+ if (valid && !stale.length) {
1469
1599
  console.log("\u2713 All translations are valid!");
1470
1600
  } else {
1471
1601
  if (missing.length > 0) {
@@ -1498,18 +1628,73 @@ function validateTranslations(projectRoot = process.cwd()) {
1498
1628
  console.log(` ... and ${orphaned.length - 10} more`);
1499
1629
  }
1500
1630
  }
1631
+ if (stale.length > 0) {
1632
+ console.log(
1633
+ `
1634
+ \u26A0 Found ${stale.length} potentially outdated translations (source value changed since translation):`
1635
+ );
1636
+ for (const item of stale.slice(0, 10)) {
1637
+ console.log(` ${item.language}/${item.namespace}.json -> ${item.key}`);
1638
+ }
1639
+ if (stale.length > 10) {
1640
+ console.log(` ... and ${stale.length - 10} more`);
1641
+ }
1642
+ console.log(" Run auto-fill with --retranslate-changed to update them.");
1643
+ }
1501
1644
  }
1502
1645
  console.log("=====");
1503
- return { valid, missing, empty, orphaned };
1646
+ return { valid, missing, empty, orphaned, stale };
1504
1647
  }
1505
- function getMissingForLanguage(projectRoot, language) {
1648
+ function getMissingForLanguage(projectRoot, language, options = {}) {
1506
1649
  const result = validateTranslations(projectRoot);
1507
1650
  const items = [
1508
1651
  ...result.missing.filter((m) => m.language === language).map((m) => ({ ...m, type: "missing" })),
1509
1652
  ...result.empty.filter((e) => e.language === language).map((e) => ({ ...e, type: "empty" }))
1510
1653
  ];
1654
+ if (options.includeStale) {
1655
+ items.push(
1656
+ ...result.stale.filter((s) => s.language === language).map((s) => ({
1657
+ namespace: s.namespace,
1658
+ key: s.key,
1659
+ language: s.language,
1660
+ sourceValue: s.sourceValue,
1661
+ type: "stale"
1662
+ }))
1663
+ );
1664
+ }
1511
1665
  return items;
1512
1666
  }
1667
+ function ensureBaselineMetadata(projectRoot = process.cwd()) {
1668
+ const config = loadConfig(projectRoot);
1669
+ const translationsPath = path6.join(projectRoot, config.translationsPath);
1670
+ const sourceLanguage = config.sourceLanguage;
1671
+ const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
1672
+ const sourceNamespaces = getNamespaces(translationsPath, sourceLanguage);
1673
+ const languages = config.languages.filter((lang) => lang !== sourceLanguage);
1674
+ const existedBefore = metadataExists(translationsPath);
1675
+ const metadata = readMetadata(translationsPath);
1676
+ let recorded = 0;
1677
+ for (const language of languages) {
1678
+ const targetTranslations = readTranslations(translationsPath, language);
1679
+ for (const namespace of sourceNamespaces) {
1680
+ const sourceKeys = sourceTranslations[namespace] || {};
1681
+ const targetKeys = targetTranslations[namespace] || {};
1682
+ for (const [key, sourceValue] of Object.entries(sourceKeys)) {
1683
+ const targetValue = targetKeys[key];
1684
+ const alreadyTranslated = typeof targetValue === "string" && targetValue.trim() !== "";
1685
+ const alreadyTracked = getSourceHash(metadata, language, namespace, key) !== void 0;
1686
+ if (alreadyTranslated && !alreadyTracked) {
1687
+ setSourceHash(metadata, language, namespace, key, sourceValue);
1688
+ recorded++;
1689
+ }
1690
+ }
1691
+ }
1692
+ }
1693
+ if (recorded > 0) {
1694
+ writeMetadata(translationsPath, metadata);
1695
+ }
1696
+ return { recorded, created: !existedBefore && recorded > 0 };
1697
+ }
1513
1698
 
1514
1699
  // src/translations/cli/auto-fill.ts
1515
1700
  async function processConcurrently(items, concurrency, processor) {
@@ -1532,10 +1717,31 @@ async function processConcurrently(items, concurrency, processor) {
1532
1717
  await Promise.all(executing);
1533
1718
  return results;
1534
1719
  }
1720
+ function getFillItemsForLanguage(projectRoot, translationsPath, sourceLanguage, language, options) {
1721
+ if (options.force) {
1722
+ const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
1723
+ const items = [];
1724
+ for (const [namespace, keys] of Object.entries(sourceTranslations)) {
1725
+ for (const [key, sourceValue] of Object.entries(keys)) {
1726
+ items.push({ namespace, key, language, sourceValue, type: "forced" });
1727
+ }
1728
+ }
1729
+ return items;
1730
+ }
1731
+ return getMissingForLanguage(projectRoot, language, { includeStale: options.retranslateChanged });
1732
+ }
1535
1733
  async function autoFillTranslations(projectRoot = process.cwd(), options = {}) {
1536
1734
  const config = loadConfig(projectRoot);
1537
- const translationsPath = path6.join(projectRoot, config.translationsPath);
1538
- const { apiKey, limit = Infinity, delayMs = 50, dryRun = false, concurrency = 5 } = options;
1735
+ const translationsPath = path7.join(projectRoot, config.translationsPath);
1736
+ const {
1737
+ apiKey,
1738
+ limit = Infinity,
1739
+ delayMs = 50,
1740
+ dryRun = false,
1741
+ concurrency = 5,
1742
+ retranslateChanged = false,
1743
+ force = false
1744
+ } = options;
1539
1745
  const currentProvider = getTranslationProvider();
1540
1746
  const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
1541
1747
  if (isDefaultGoogleProvider) {
@@ -1558,14 +1764,17 @@ async function autoFillTranslations(projectRoot = process.cwd(), options = {}) {
1558
1764
  console.log(`Created ${syncResult.createdFiles.length} namespace files
1559
1765
  `);
1560
1766
  }
1767
+ const mode = force ? "all keys (--force)" : retranslateChanged ? "missing, empty & changed" : "missing & empty";
1561
1768
  console.log("=====");
1562
1769
  console.log("Auto-filling translations");
1563
1770
  console.log("=====");
1564
1771
  console.log(`Languages: ${languagesToProcess.join(", ")}`);
1772
+ console.log(`Mode: ${mode}`);
1565
1773
  console.log(`Limit: ${limit === Infinity ? "unlimited" : limit}`);
1566
1774
  console.log(`Concurrency: ${concurrency}`);
1567
1775
  console.log(`Dry run: ${dryRun}`);
1568
1776
  console.log("=====");
1777
+ const metadata = readMetadata(translationsPath);
1569
1778
  let totalProcessed = 0;
1570
1779
  let totalTranslated = 0;
1571
1780
  for (const language of languagesToProcess) {
@@ -1576,9 +1785,12 @@ Reached limit of ${limit} translations`);
1576
1785
  }
1577
1786
  console.log(`
1578
1787
  Processing language: ${language}`);
1579
- const missing = getMissingForLanguage(projectRoot, language);
1788
+ const missing = getFillItemsForLanguage(projectRoot, translationsPath, config.sourceLanguage, language, {
1789
+ retranslateChanged,
1790
+ force
1791
+ });
1580
1792
  if (!missing.length) {
1581
- console.log(" No missing or empty translations");
1793
+ console.log(" Nothing to translate");
1582
1794
  continue;
1583
1795
  }
1584
1796
  console.log(` Found ${missing.length} translations to fill`);
@@ -1607,6 +1819,7 @@ Processing language: ${language}`);
1607
1819
  translations[item.namespace][item.key] = translated;
1608
1820
  const sorted = sortKeys(translations[item.namespace]);
1609
1821
  writeTranslation(translationsPath, language, item.namespace, sorted);
1822
+ setSourceHash(metadata, language, item.namespace, item.key, item.sourceValue);
1610
1823
  console.log(" \u2713 Saved");
1611
1824
  } else {
1612
1825
  console.log(" \u2713 Dry run - not saved");
@@ -1622,6 +1835,9 @@ Processing language: ${language}`);
1622
1835
  });
1623
1836
  totalProcessed += itemsToProcess.length;
1624
1837
  totalTranslated += results.filter((r) => r.success).length;
1838
+ if (!dryRun) {
1839
+ writeMetadata(translationsPath, metadata);
1840
+ }
1625
1841
  }
1626
1842
  console.log("\n=====");
1627
1843
  console.log(`Total processed: ${totalProcessed}`);
@@ -1633,7 +1849,7 @@ Processing language: ${language}`);
1633
1849
  }
1634
1850
  async function fillNamespace(projectRoot, language, namespace, apiKey) {
1635
1851
  const config = loadConfig(projectRoot);
1636
- const translationsPath = path6.join(projectRoot, config.translationsPath);
1852
+ const translationsPath = path7.join(projectRoot, config.translationsPath);
1637
1853
  const currentProvider = getTranslationProvider();
1638
1854
  const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
1639
1855
  if (isDefaultGoogleProvider) {
@@ -1649,6 +1865,7 @@ async function fillNamespace(projectRoot, language, namespace, apiKey) {
1649
1865
  const targetTranslations = readTranslations(translationsPath, language);
1650
1866
  const sourceKeys = sourceTranslations[namespace] || {};
1651
1867
  const targetKeys = targetTranslations[namespace] || {};
1868
+ const metadata = readMetadata(translationsPath);
1652
1869
  let count = 0;
1653
1870
  for (const [key, sourceValue] of Object.entries(sourceKeys)) {
1654
1871
  const targetValue = targetKeys[key];
@@ -1665,12 +1882,14 @@ async function fillNamespace(projectRoot, language, namespace, apiKey) {
1665
1882
  config.protectedTerms ?? []
1666
1883
  );
1667
1884
  targetKeys[key] = translated;
1885
+ setSourceHash(metadata, language, namespace, key, sourceValue);
1668
1886
  count++;
1669
1887
  await new Promise((resolve) => setTimeout(resolve, 100));
1670
1888
  }
1671
1889
  if (count > 0) {
1672
1890
  const sorted = sortKeys(targetKeys);
1673
1891
  writeTranslation(translationsPath, language, namespace, sorted);
1892
+ writeMetadata(translationsPath, metadata);
1674
1893
  console.log(`\u2713 Filled ${count} translations`);
1675
1894
  } else {
1676
1895
  console.log("No translations to fill");
@@ -1681,8 +1900,8 @@ async function fillNamespace(projectRoot, language, namespace, apiKey) {
1681
1900
  init_utils();
1682
1901
  init_init();
1683
1902
  import { execSync as execSync2, spawnSync } from "child_process";
1684
- import * as fs4 from "fs";
1685
- import * as path7 from "path";
1903
+ import * as fs5 from "fs";
1904
+ import * as path8 from "path";
1686
1905
  function isRipgrepAvailable() {
1687
1906
  try {
1688
1907
  execSync2("rg --version", { stdio: "ignore" });
@@ -1743,15 +1962,15 @@ function findFiles(rootDir, searchPaths, extensions) {
1743
1962
  ]);
1744
1963
  function scanDirectory(dir) {
1745
1964
  try {
1746
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
1965
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1747
1966
  for (const entry of entries) {
1748
- const fullPath = path7.join(dir, entry.name);
1967
+ const fullPath = path8.join(dir, entry.name);
1749
1968
  if (entry.isDirectory()) {
1750
1969
  if (!excludeDirs.has(entry.name)) {
1751
1970
  scanDirectory(fullPath);
1752
1971
  }
1753
1972
  } else if (entry.isFile()) {
1754
- const ext = path7.extname(entry.name);
1973
+ const ext = path8.extname(entry.name);
1755
1974
  if (extensionSet.has(ext)) {
1756
1975
  files.push(fullPath);
1757
1976
  }
@@ -1761,8 +1980,8 @@ function findFiles(rootDir, searchPaths, extensions) {
1761
1980
  }
1762
1981
  }
1763
1982
  for (const searchPath of searchPaths) {
1764
- const fullSearchPath = path7.join(rootDir, searchPath);
1765
- if (fs4.existsSync(fullSearchPath)) {
1983
+ const fullSearchPath = path8.join(rootDir, searchPath);
1984
+ if (fs5.existsSync(fullSearchPath)) {
1766
1985
  scanDirectory(fullSearchPath);
1767
1986
  }
1768
1987
  }
@@ -1770,7 +1989,7 @@ function findFiles(rootDir, searchPaths, extensions) {
1770
1989
  }
1771
1990
  function checkKeyInFile(key, filePath) {
1772
1991
  try {
1773
- const content = fs4.readFileSync(filePath, "utf-8");
1992
+ const content = fs5.readFileSync(filePath, "utf-8");
1774
1993
  const exactPatterns = [new RegExp(`['"\`]${key}['"\`]`, "g"), new RegExp(`\\b${key}\\b`, "g")];
1775
1994
  for (const pattern of exactPatterns) {
1776
1995
  if (pattern.test(content)) {
@@ -1793,7 +2012,7 @@ function checkPartialMatches(key, files) {
1793
2012
  const foundParts = /* @__PURE__ */ new Set();
1794
2013
  for (const file of filesToCheck) {
1795
2014
  try {
1796
- const content = fs4.readFileSync(file, "utf-8");
2015
+ const content = fs5.readFileSync(file, "utf-8");
1797
2016
  for (const part of significantParts) {
1798
2017
  if (foundParts.has(part)) continue;
1799
2018
  const partPattern = new RegExp(`\\b${part}\\b`, "gi");
@@ -1813,7 +2032,7 @@ function checkPartialMatches(key, files) {
1813
2032
  }
1814
2033
  function findUnusedKeys(projectRoot = process.cwd()) {
1815
2034
  const config = loadConfig(projectRoot);
1816
- const translationsPath = path7.join(projectRoot, config.translationsPath);
2035
+ const translationsPath = path8.join(projectRoot, config.translationsPath);
1817
2036
  const sourceLanguage = config.sourceLanguage;
1818
2037
  const searchPaths = config.searchPaths || ["src", "app", "pages", "components"];
1819
2038
  const searchExtensions = config.searchExtensions || [".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"];
@@ -1955,13 +2174,13 @@ init_init();
1955
2174
  init_schema();
1956
2175
  init_types();
1957
2176
  init_init();
1958
- import * as fs5 from "fs";
1959
- import * as path8 from "path";
2177
+ import * as fs6 from "fs";
2178
+ import * as path9 from "path";
1960
2179
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
1961
2180
  async function initTranslationsInteractive(projectRoot = process.cwd()) {
1962
2181
  console.log("\n\u{1F30D} Translation System Setup\n");
1963
- const configPath = path8.join(projectRoot, ".translationsrc.json");
1964
- const alreadyExists = fs5.existsSync(configPath);
2182
+ const configPath = path9.join(projectRoot, ".translationsrc.json");
2183
+ const alreadyExists = fs6.existsSync(configPath);
1965
2184
  if (alreadyExists) {
1966
2185
  console.log("\u26A0\uFE0F Configuration file already exists at .translationsrc.json\n");
1967
2186
  const shouldOverwrite = await confirm({
@@ -2129,16 +2348,26 @@ function getLanguageName(code) {
2129
2348
 
2130
2349
  // src/translations/cli/manage.ts
2131
2350
  init_utils();
2132
- import * as fs6 from "fs";
2133
- import * as path9 from "path";
2351
+ import * as fs7 from "fs";
2352
+ import * as path10 from "path";
2134
2353
  init_init();
2135
2354
  async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2136
- const { autoFill = false, apiKey, limit, concurrency = 5, language, skipTypes = false, dryRun = false } = options;
2355
+ const {
2356
+ autoFill = false,
2357
+ apiKey,
2358
+ limit,
2359
+ concurrency = 5,
2360
+ language,
2361
+ skipTypes = false,
2362
+ dryRun = false,
2363
+ retranslateChanged = false,
2364
+ force = false
2365
+ } = options;
2137
2366
  console.log("=====");
2138
2367
  console.log("Translation Management");
2139
2368
  console.log("=====");
2140
- const configPath = path9.join(projectRoot, ".translationsrc.json");
2141
- const isInitialized = fs6.existsSync(configPath);
2369
+ const configPath = path10.join(projectRoot, ".translationsrc.json");
2370
+ const isInitialized = fs7.existsSync(configPath);
2142
2371
  if (!isInitialized) {
2143
2372
  console.log("\u{1F4C1} No translation configuration found. Initializing...\n");
2144
2373
  initTranslations(projectRoot);
@@ -2147,9 +2376,9 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2147
2376
  console.log("\u2713 Translation structure initialized\n");
2148
2377
  }
2149
2378
  const config = loadConfig(projectRoot);
2150
- const translationsPath = path9.join(projectRoot, config.translationsPath);
2151
- const sourceLangPath = path9.join(translationsPath, config.sourceLanguage);
2152
- if (!fs6.existsSync(sourceLangPath)) {
2379
+ const translationsPath = path10.join(projectRoot, config.translationsPath);
2380
+ const sourceLangPath = path10.join(translationsPath, config.sourceLanguage);
2381
+ if (!fs7.existsSync(sourceLangPath)) {
2153
2382
  console.log(`\u26A0\uFE0F Source language directory not found: ${sourceLangPath}`);
2154
2383
  console.log("Please add translation files to the source language directory.\n");
2155
2384
  return false;
@@ -2171,11 +2400,25 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2171
2400
  if (syncResult.createdFiles.length === 0 && syncResult.cleanedKeys.length === 0 && syncResult.removedNamespaces.length === 0) {
2172
2401
  console.log("\u2713 Translation structure is already synchronized\n");
2173
2402
  }
2403
+ if (!dryRun) {
2404
+ const baseline = ensureBaselineMetadata(projectRoot);
2405
+ if (baseline.recorded > 0) {
2406
+ const verb = baseline.created ? "Created change-tracking baseline for" : "Added change tracking for";
2407
+ console.log(`\u{1F4CC} ${verb} ${baseline.recorded} existing translations
2408
+ `);
2409
+ }
2410
+ }
2174
2411
  console.log("\u{1F50D} Validating translations...\n");
2175
2412
  const validationResult = validateTranslations(projectRoot);
2176
- if (validationResult.valid) {
2413
+ const hasWorkToFill = !validationResult.valid || force || retranslateChanged && validationResult.stale.length > 0;
2414
+ if (validationResult.valid && !validationResult.stale.length) {
2177
2415
  console.log("\n\u2705 All translations are complete!\n");
2178
- } else {
2416
+ } else if (validationResult.valid) {
2417
+ console.log(`
2418
+ \u26A0\uFE0F ${validationResult.stale.length} translations may be outdated.
2419
+ `);
2420
+ }
2421
+ if (hasWorkToFill) {
2179
2422
  const totalMissing = validationResult.missing.length + validationResult.empty.length + validationResult.orphaned.length;
2180
2423
  if (autoFill) {
2181
2424
  if (!apiKey) {
@@ -2185,29 +2428,38 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2185
2428
  console.log(`Set ${envVarName} or pass --api-key to enable auto-fill.
2186
2429
  `);
2187
2430
  } else {
2188
- console.log(`
2189
- \u{1F916} Auto-filling ${totalMissing} missing translations...
2431
+ if (force) {
2432
+ console.log("\n\u{1F916} Force re-translating all keys...\n");
2433
+ } else {
2434
+ const staleCount = retranslateChanged ? validationResult.stale.length : 0;
2435
+ console.log(`
2436
+ \u{1F916} Auto-filling ${totalMissing} missing and ${staleCount} changed translations...
2190
2437
  `);
2438
+ }
2191
2439
  await autoFillTranslations(projectRoot, {
2192
2440
  apiKey,
2193
2441
  limit,
2194
2442
  concurrency,
2195
2443
  language,
2196
2444
  dryRun,
2197
- delayMs: 50
2445
+ delayMs: 50,
2446
+ retranslateChanged,
2447
+ force
2198
2448
  });
2199
2449
  if (!dryRun) {
2200
2450
  console.log("\n\u{1F50D} Re-validating after auto-fill...\n");
2201
2451
  const revalidation = validateTranslations(projectRoot);
2202
- if (revalidation.valid) {
2452
+ if (revalidation.valid && !revalidation.stale.length) {
2203
2453
  console.log("\n\u2705 All translations are now complete!\n");
2204
2454
  }
2205
2455
  }
2206
2456
  }
2207
- } else {
2457
+ } else if (!validationResult.valid) {
2208
2458
  console.log(`
2209
2459
  \u{1F4A1} Tip: Run with --auto-fill to automatically translate missing keys.
2210
2460
  `);
2461
+ } else if (validationResult.stale.length > 0) {
2462
+ console.log("\n\u{1F4A1} Tip: Run with --auto-fill --retranslate-changed to update outdated translations.\n");
2211
2463
  }
2212
2464
  }
2213
2465
  if (!skipTypes && !dryRun) {
@@ -2233,9 +2485,17 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2233
2485
  if (validationResult.orphaned.length > 0) {
2234
2486
  console.log(`\u26A0\uFE0F ${validationResult.orphaned.length} orphaned translations (will be auto-removed on next sync)`);
2235
2487
  }
2488
+ if (validationResult.stale.length > 0) {
2489
+ console.log(`\u26A0\uFE0F ${validationResult.stale.length} potentially outdated translations`);
2490
+ }
2236
2491
  console.log("\nNext steps:");
2237
2492
  console.log(" 1. Add missing translations manually, or");
2238
2493
  console.log(" 2. Run with --auto-fill to translate automatically");
2494
+ } else if (validationResult.valid && validationResult.stale.length > 0 && !autoFill) {
2495
+ console.log(`
2496
+ \u26A0\uFE0F ${validationResult.stale.length} potentially outdated translations`);
2497
+ console.log("\nNext steps:");
2498
+ console.log(" Run with --auto-fill --retranslate-changed to update them");
2239
2499
  } else if (validationResult.valid) {
2240
2500
  console.log("\n\u2705 All systems ready!");
2241
2501
  }
@@ -2250,6 +2510,7 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2250
2510
  // src/translations/index.ts
2251
2511
  init_schema();
2252
2512
  init_types();
2513
+ init_metadata();
2253
2514
  init_utils();
2254
2515
  export {
2255
2516
  DEEPL_LANGUAGES,
@@ -2257,6 +2518,7 @@ export {
2257
2518
  DEFAULT_LANGUAGES,
2258
2519
  GOOGLE_LANGUAGES,
2259
2520
  GoogleTranslateProvider,
2521
+ METADATA_FILE_NAME,
2260
2522
  SUPPORTED_LANGUAGES,
2261
2523
  TRANSLATION_CONFIG_SCHEMA,
2262
2524
  TRANSLATION_PROVIDERS,
@@ -2265,6 +2527,8 @@ export {
2265
2527
  autoFillTranslations,
2266
2528
  createEmptyTranslationStructure,
2267
2529
  detectExistingTranslations,
2530
+ emptyMetadata,
2531
+ ensureBaselineMetadata,
2268
2532
  ensureTranslationsStructure,
2269
2533
  extractPluralBaseKeys,
2270
2534
  extractVariables,
@@ -2274,11 +2538,14 @@ export {
2274
2538
  generateTranslationTypes,
2275
2539
  getAvailableLanguages,
2276
2540
  getFallbackMappings,
2541
+ getMetadataPath,
2277
2542
  getMissingForLanguage,
2278
2543
  getNamespaces,
2544
+ getSourceHash,
2279
2545
  getSupportedLanguages,
2280
2546
  getTranslationProvider,
2281
2547
  hasInterpolation,
2548
+ hashSourceValue,
2282
2549
  initTranslations,
2283
2550
  initTranslationsInteractive,
2284
2551
  isNestedObject,
@@ -2289,10 +2556,14 @@ export {
2289
2556
  loadConfig,
2290
2557
  logLanguageFallback,
2291
2558
  manageTranslations,
2559
+ metadataExists,
2292
2560
  printUnusedKeysResult,
2561
+ pruneMetadata,
2562
+ readMetadata,
2293
2563
  readTranslations,
2294
2564
  resetTranslationProvider,
2295
2565
  resolveLanguageWithFallback,
2566
+ setSourceHash,
2296
2567
  setTranslationProvider,
2297
2568
  sortKeys,
2298
2569
  syncTranslationStructure,
@@ -2303,6 +2574,7 @@ export {
2303
2574
  validateLanguagesForProvider,
2304
2575
  validateTranslations,
2305
2576
  validateVariables,
2577
+ writeMetadata,
2306
2578
  writeTranslation
2307
2579
  };
2308
2580
  //# sourceMappingURL=index.js.map