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/dist/index.js CHANGED
@@ -631,9 +631,99 @@ var init_language_fallback = __esm({
631
631
  }
632
632
  });
633
633
 
634
- // src/translations/utils/utils.ts
634
+ // src/translations/utils/metadata.ts
635
+ import { createHash } from "crypto";
635
636
  import * as fs from "fs";
636
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";
637
727
  function flattenObject(obj, prefix = "") {
638
728
  const result = {};
639
729
  for (const [key, value] of Object.entries(obj)) {
@@ -666,16 +756,16 @@ function isNestedObject(obj) {
666
756
  return Object.values(obj).some((value) => typeof value === "object" && value !== null);
667
757
  }
668
758
  function readTranslations(translationsPath, language) {
669
- const langPath = path.join(translationsPath, language);
670
- if (!fs.existsSync(langPath)) {
759
+ const langPath = path2.join(translationsPath, language);
760
+ if (!fs2.existsSync(langPath)) {
671
761
  return {};
672
762
  }
673
- const files = fs.readdirSync(langPath).filter((f) => f.endsWith(".json"));
763
+ const files = fs2.readdirSync(langPath).filter((f) => f.endsWith(".json"));
674
764
  const translations = {};
675
765
  for (const file of files) {
676
- const namespace = path.basename(file, ".json");
677
- const filePath = path.join(langPath, file);
678
- 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");
679
769
  const parsed = JSON.parse(content);
680
770
  if (isNestedObject(parsed)) {
681
771
  translations[namespace] = flattenObject(parsed);
@@ -686,26 +776,26 @@ function readTranslations(translationsPath, language) {
686
776
  return translations;
687
777
  }
688
778
  function writeTranslation(translationsPath, language, namespace, translations) {
689
- const langPath = path.join(translationsPath, language);
690
- if (!fs.existsSync(langPath)) {
691
- fs.mkdirSync(langPath, { recursive: true });
779
+ const langPath = path2.join(translationsPath, language);
780
+ if (!fs2.existsSync(langPath)) {
781
+ fs2.mkdirSync(langPath, { recursive: true });
692
782
  }
693
- const filePath = path.join(langPath, `${namespace}.json`);
694
- 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)}
695
785
  `, "utf-8");
696
786
  }
697
787
  function getAvailableLanguages(translationsPath) {
698
- if (!fs.existsSync(translationsPath)) {
788
+ if (!fs2.existsSync(translationsPath)) {
699
789
  return [];
700
790
  }
701
- 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);
702
792
  }
703
793
  function getNamespaces(translationsPath, language) {
704
- const langPath = path.join(translationsPath, language);
705
- if (!fs.existsSync(langPath)) {
794
+ const langPath = path2.join(translationsPath, language);
795
+ if (!fs2.existsSync(langPath)) {
706
796
  return [];
707
797
  }
708
- 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"));
709
799
  }
710
800
  function hasInterpolation(text) {
711
801
  return /\{\{[^}]+\}\}/g.test(text);
@@ -732,13 +822,13 @@ function sortKeys(obj) {
732
822
  return sorted;
733
823
  }
734
824
  function ensureTranslationsStructure(translationsPath, languages) {
735
- if (!fs.existsSync(translationsPath)) {
736
- fs.mkdirSync(translationsPath, { recursive: true });
825
+ if (!fs2.existsSync(translationsPath)) {
826
+ fs2.mkdirSync(translationsPath, { recursive: true });
737
827
  }
738
828
  for (const lang of languages) {
739
- const langPath = path.join(translationsPath, lang);
740
- if (!fs.existsSync(langPath)) {
741
- fs.mkdirSync(langPath, { recursive: true });
829
+ const langPath = path2.join(translationsPath, lang);
830
+ if (!fs2.existsSync(langPath)) {
831
+ fs2.mkdirSync(langPath, { recursive: true });
742
832
  }
743
833
  }
744
834
  }
@@ -769,8 +859,8 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
769
859
  const targetNamespaces = getNamespaces(translationsPath, language);
770
860
  for (const namespace of targetNamespaces) {
771
861
  if (!sourceNamespaces.includes(namespace)) {
772
- const filePath = path.join(translationsPath, language, `${namespace}.json`);
773
- fs.unlinkSync(filePath);
862
+ const filePath = path2.join(translationsPath, language, `${namespace}.json`);
863
+ fs2.unlinkSync(filePath);
774
864
  result.removedNamespaces.push({
775
865
  language,
776
866
  namespace,
@@ -779,9 +869,9 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
779
869
  }
780
870
  }
781
871
  for (const namespace of sourceNamespaces) {
782
- const filePath = path.join(translationsPath, language, `${namespace}.json`);
872
+ const filePath = path2.join(translationsPath, language, `${namespace}.json`);
783
873
  const sourceFile = sourceTranslations[namespace] || {};
784
- if (fs.existsSync(filePath)) {
874
+ if (fs2.existsSync(filePath)) {
785
875
  const targetFile = targetTranslations[namespace] || {};
786
876
  let hasOrphanedKeys = false;
787
877
  const cleanedFile = {};
@@ -821,11 +911,22 @@ function syncTranslationStructure(translationsPath, languages, sourceLanguage) {
821
911
  });
822
912
  }
823
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
+ }
824
924
  return result;
825
925
  }
826
926
  var init_utils = __esm({
827
927
  "src/translations/utils/utils.ts"() {
828
928
  "use strict";
929
+ init_metadata();
829
930
  }
830
931
  });
831
932
 
@@ -856,13 +957,13 @@ __export(init_exports, {
856
957
  initTranslations: () => initTranslations,
857
958
  loadConfig: () => loadConfig
858
959
  });
859
- import * as fs2 from "fs";
860
- import * as path2 from "path";
960
+ import * as fs3 from "fs";
961
+ import * as path3 from "path";
861
962
  function detectExistingTranslations(projectRoot) {
862
963
  const possiblePaths = ["public/static/locales", "public/locales", "src/locales", "locales", "i18n", "translations"];
863
964
  for (const possiblePath of possiblePaths) {
864
- const fullPath = path2.join(projectRoot, possiblePath);
865
- if (fs2.existsSync(fullPath)) {
965
+ const fullPath = path3.join(projectRoot, possiblePath);
966
+ if (fs3.existsSync(fullPath)) {
866
967
  const languages = getAvailableLanguages(fullPath);
867
968
  if (languages.length > 0) {
868
969
  return { path: possiblePath, languages };
@@ -892,7 +993,7 @@ function initTranslations(projectRoot, config = {}) {
892
993
  languages: validLanguages.length > 0 ? validLanguages : finalConfig.languages
893
994
  };
894
995
  }
895
- const translationsPath = path2.join(projectRoot, finalConfig.translationsPath);
996
+ const translationsPath = path3.join(projectRoot, finalConfig.translationsPath);
896
997
  const languages = finalConfig.languages.length > 0 ? finalConfig.languages : [...DEFAULT_LANGUAGES];
897
998
  console.log(`Project root: ${projectRoot}`);
898
999
  console.log(`Translations path: ${translationsPath}`);
@@ -904,9 +1005,9 @@ function initTranslations(projectRoot, config = {}) {
904
1005
  }
905
1006
  ensureTranslationsStructure(translationsPath, languages);
906
1007
  const sourceLanguage = finalConfig.sourceLanguage;
907
- const sourcePath = path2.join(translationsPath, sourceLanguage);
908
- const commonPath = path2.join(sourcePath, "common.json");
909
- if (!fs2.existsSync(commonPath)) {
1008
+ const sourcePath = path3.join(translationsPath, sourceLanguage);
1009
+ const commonPath = path3.join(sourcePath, "common.json");
1010
+ if (!fs3.existsSync(commonPath)) {
910
1011
  const sampleTranslations = {
911
1012
  LOADING: "Loading",
912
1013
  SAVE: "Save",
@@ -915,7 +1016,7 @@ function initTranslations(projectRoot, config = {}) {
915
1016
  ERROR: "Error",
916
1017
  SUCCESS: "Success"
917
1018
  };
918
- fs2.writeFileSync(commonPath, `${JSON.stringify(sampleTranslations, null, 2)}
1019
+ fs3.writeFileSync(commonPath, `${JSON.stringify(sampleTranslations, null, 2)}
919
1020
  `, "utf-8");
920
1021
  console.log(`Created sample file: ${commonPath}`);
921
1022
  }
@@ -929,8 +1030,8 @@ function initTranslations(projectRoot, config = {}) {
929
1030
  console.log(` ${lang}: ${langFiles.map((f) => f.namespace).join(", ")}`);
930
1031
  }
931
1032
  }
932
- const configPath = path2.join(projectRoot, ".translationsrc.json");
933
- if (!fs2.existsSync(configPath)) {
1033
+ const configPath = path3.join(projectRoot, ".translationsrc.json");
1034
+ if (!fs3.existsSync(configPath)) {
934
1035
  const configContent = {
935
1036
  $schema: "./node_modules/poly-lexis/dist/translations/core/translations-config.schema.json",
936
1037
  translationsPath: finalConfig.translationsPath,
@@ -939,7 +1040,7 @@ function initTranslations(projectRoot, config = {}) {
939
1040
  typesOutputPath: finalConfig.typesOutputPath,
940
1041
  provider: finalConfig.provider
941
1042
  };
942
- fs2.writeFileSync(configPath, `${JSON.stringify(configContent, null, 2)}
1043
+ fs3.writeFileSync(configPath, `${JSON.stringify(configContent, null, 2)}
943
1044
  `, "utf-8");
944
1045
  console.log(`Created config file: ${configPath}`);
945
1046
  }
@@ -948,8 +1049,8 @@ function initTranslations(projectRoot, config = {}) {
948
1049
  console.log("=====");
949
1050
  }
950
1051
  function loadConfig(projectRoot) {
951
- const configPath = path2.join(projectRoot, ".translationsrc.json");
952
- if (!fs2.existsSync(configPath)) {
1052
+ const configPath = path3.join(projectRoot, ".translationsrc.json");
1053
+ if (!fs3.existsSync(configPath)) {
953
1054
  const existing = detectExistingTranslations(projectRoot);
954
1055
  if (existing.path && existing.languages.length > 0) {
955
1056
  console.log(`\u2139\uFE0F No config found, but detected translations at ${existing.path}`);
@@ -961,7 +1062,7 @@ function loadConfig(projectRoot) {
961
1062
  }
962
1063
  return DEFAULT_CONFIG;
963
1064
  }
964
- const configContent = fs2.readFileSync(configPath, "utf-8");
1065
+ const configContent = fs3.readFileSync(configPath, "utf-8");
965
1066
  const config = JSON.parse(configContent);
966
1067
  if (config.languages) {
967
1068
  const validation = validateLanguages(config.languages);
@@ -982,7 +1083,7 @@ var init_init = __esm({
982
1083
  });
983
1084
 
984
1085
  // src/translations/cli/add-key.ts
985
- import * as path4 from "path";
1086
+ import * as path5 from "path";
986
1087
 
987
1088
  // src/translations/utils/deepl-translate-provider.ts
988
1089
  init_language_fallback();
@@ -1188,6 +1289,9 @@ var GoogleTranslateProvider = class {
1188
1289
  }
1189
1290
  };
1190
1291
 
1292
+ // src/translations/cli/add-key.ts
1293
+ init_metadata();
1294
+
1191
1295
  // src/translations/utils/translator.ts
1192
1296
  var defaultProvider = new GoogleTranslateProvider();
1193
1297
  var customProvider = null;
@@ -1223,8 +1327,8 @@ init_utils();
1223
1327
  init_utils();
1224
1328
  init_init();
1225
1329
  import { execSync } from "child_process";
1226
- import * as fs3 from "fs";
1227
- import * as path3 from "path";
1330
+ import * as fs4 from "fs";
1331
+ import * as path4 from "path";
1228
1332
  var PLURAL_SUFFIXES = ["_zero", "_one", "_two", "_few", "_many", "_other"];
1229
1333
  function extractPluralBaseKeys(keys) {
1230
1334
  const keySet = new Set(keys);
@@ -1257,11 +1361,11 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
1257
1361
  console.log("Generating i18n types");
1258
1362
  console.log("=====");
1259
1363
  const config = loadConfig(projectRoot);
1260
- const translationsPath = path3.join(projectRoot, config.translationsPath);
1364
+ const translationsPath = path4.join(projectRoot, config.translationsPath);
1261
1365
  const sourceLanguage = config.sourceLanguage;
1262
- const outputFilePath = path3.join(projectRoot, config.typesOutputPath);
1263
- const dirPath = path3.join(translationsPath, sourceLanguage);
1264
- if (!fs3.existsSync(dirPath)) {
1366
+ const outputFilePath = path4.join(projectRoot, config.typesOutputPath);
1367
+ const dirPath = path4.join(translationsPath, sourceLanguage);
1368
+ if (!fs4.existsSync(dirPath)) {
1265
1369
  throw new Error(`Source language directory not found: ${dirPath}`);
1266
1370
  }
1267
1371
  const namespaces = getNamespaces(translationsPath, sourceLanguage);
@@ -1276,12 +1380,12 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
1276
1380
  }
1277
1381
  const pluralBaseKeys = extractPluralBaseKeys(allKeys);
1278
1382
  allKeys = allKeys.concat(pluralBaseKeys);
1279
- const outputDir = path3.dirname(outputFilePath);
1280
- if (!fs3.existsSync(outputDir)) {
1281
- fs3.mkdirSync(outputDir, { recursive: true });
1383
+ const outputDir = path4.dirname(outputFilePath);
1384
+ if (!fs4.existsSync(outputDir)) {
1385
+ fs4.mkdirSync(outputDir, { recursive: true });
1282
1386
  }
1283
1387
  const typeString = typeTemplate(allKeys, namespaces, config.languages);
1284
- fs3.writeFileSync(outputFilePath, typeString, "utf8");
1388
+ fs4.writeFileSync(outputFilePath, typeString, "utf8");
1285
1389
  console.log(`Generated types with ${allKeys.length} keys and ${namespaces.length} namespaces`);
1286
1390
  console.log(`Output: ${outputFilePath}`);
1287
1391
  try {
@@ -1300,7 +1404,7 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
1300
1404
  init_init();
1301
1405
  async function addTranslationKey(projectRoot, options) {
1302
1406
  const config = loadConfig(projectRoot);
1303
- const translationsPath = path4.join(projectRoot, config.translationsPath);
1407
+ const translationsPath = path5.join(projectRoot, config.translationsPath);
1304
1408
  const { namespace, key, value, autoTranslate = false, apiKey } = options;
1305
1409
  const currentProvider = getTranslationProvider();
1306
1410
  const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
@@ -1334,6 +1438,7 @@ async function addTranslationKey(projectRoot, options) {
1334
1438
  const otherLanguages = config.languages.filter((lang) => lang !== sourceLang);
1335
1439
  if (autoTranslate && apiKey) {
1336
1440
  console.log("\nAuto-translating to other languages...");
1441
+ const metadata = readMetadata(translationsPath);
1337
1442
  for (const lang of otherLanguages) {
1338
1443
  try {
1339
1444
  const targetTranslations = readTranslations(translationsPath, lang);
@@ -1352,6 +1457,7 @@ async function addTranslationKey(projectRoot, options) {
1352
1457
  targetTranslations[namespace][key] = translated;
1353
1458
  const sorted = sortKeys(targetTranslations[namespace]);
1354
1459
  writeTranslation(translationsPath, lang, namespace, sorted);
1460
+ setSourceHash(metadata, lang, namespace, key, value);
1355
1461
  console.log(` \u2713 ${lang}: "${translated}"`);
1356
1462
  await new Promise((resolve) => setTimeout(resolve, 100));
1357
1463
  } else {
@@ -1361,6 +1467,7 @@ async function addTranslationKey(projectRoot, options) {
1361
1467
  console.error(` \u2717 ${lang}: Translation failed - ${error instanceof Error ? error.message : "Unknown error"}`);
1362
1468
  }
1363
1469
  }
1470
+ writeMetadata(translationsPath, metadata);
1364
1471
  } else {
1365
1472
  console.log("\nAdding empty values to other languages...");
1366
1473
  for (const lang of otherLanguages) {
@@ -1405,23 +1512,27 @@ async function addTranslationKeys(projectRoot, entries, autoTranslate = false, a
1405
1512
  }
1406
1513
 
1407
1514
  // src/translations/cli/auto-fill.ts
1408
- import * as path6 from "path";
1515
+ import * as path7 from "path";
1516
+ init_metadata();
1409
1517
  init_utils();
1410
1518
  init_init();
1411
1519
 
1412
1520
  // src/translations/cli/validate.ts
1521
+ init_metadata();
1413
1522
  init_utils();
1414
1523
  init_init();
1415
- import * as path5 from "path";
1524
+ import * as path6 from "path";
1416
1525
  function validateTranslations(projectRoot = process.cwd()) {
1417
1526
  const config = loadConfig(projectRoot);
1418
- const translationsPath = path5.join(projectRoot, config.translationsPath);
1527
+ const translationsPath = path6.join(projectRoot, config.translationsPath);
1419
1528
  const sourceLanguage = config.sourceLanguage;
1420
1529
  const missing = [];
1421
1530
  const empty = [];
1422
1531
  const orphaned = [];
1532
+ const stale = [];
1423
1533
  const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
1424
1534
  const sourceNamespaces = getNamespaces(translationsPath, sourceLanguage);
1535
+ const metadata = readMetadata(translationsPath);
1425
1536
  const languages = config.languages.filter((lang) => lang !== sourceLanguage);
1426
1537
  const syncResult = syncTranslationStructure(translationsPath, config.languages, sourceLanguage);
1427
1538
  if (syncResult.createdFiles.length > 0) {
@@ -1458,6 +1569,17 @@ function validateTranslations(projectRoot = process.cwd()) {
1458
1569
  language,
1459
1570
  sourceValue
1460
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
+ }
1461
1583
  }
1462
1584
  }
1463
1585
  for (const [key, targetValue] of Object.entries(targetKeys)) {
@@ -1473,7 +1595,7 @@ function validateTranslations(projectRoot = process.cwd()) {
1473
1595
  }
1474
1596
  }
1475
1597
  const valid = !missing.length && !empty.length && !orphaned.length;
1476
- if (valid) {
1598
+ if (valid && !stale.length) {
1477
1599
  console.log("\u2713 All translations are valid!");
1478
1600
  } else {
1479
1601
  if (missing.length > 0) {
@@ -1506,18 +1628,73 @@ function validateTranslations(projectRoot = process.cwd()) {
1506
1628
  console.log(` ... and ${orphaned.length - 10} more`);
1507
1629
  }
1508
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
+ }
1509
1644
  }
1510
1645
  console.log("=====");
1511
- return { valid, missing, empty, orphaned };
1646
+ return { valid, missing, empty, orphaned, stale };
1512
1647
  }
1513
- function getMissingForLanguage(projectRoot, language) {
1648
+ function getMissingForLanguage(projectRoot, language, options = {}) {
1514
1649
  const result = validateTranslations(projectRoot);
1515
1650
  const items = [
1516
1651
  ...result.missing.filter((m) => m.language === language).map((m) => ({ ...m, type: "missing" })),
1517
1652
  ...result.empty.filter((e) => e.language === language).map((e) => ({ ...e, type: "empty" }))
1518
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
+ }
1519
1665
  return items;
1520
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
+ }
1521
1698
 
1522
1699
  // src/translations/cli/auto-fill.ts
1523
1700
  async function processConcurrently(items, concurrency, processor) {
@@ -1540,10 +1717,31 @@ async function processConcurrently(items, concurrency, processor) {
1540
1717
  await Promise.all(executing);
1541
1718
  return results;
1542
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
+ }
1543
1733
  async function autoFillTranslations(projectRoot = process.cwd(), options = {}) {
1544
1734
  const config = loadConfig(projectRoot);
1545
- const translationsPath = path6.join(projectRoot, config.translationsPath);
1546
- 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;
1547
1745
  const currentProvider = getTranslationProvider();
1548
1746
  const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
1549
1747
  if (isDefaultGoogleProvider) {
@@ -1566,14 +1764,17 @@ async function autoFillTranslations(projectRoot = process.cwd(), options = {}) {
1566
1764
  console.log(`Created ${syncResult.createdFiles.length} namespace files
1567
1765
  `);
1568
1766
  }
1767
+ const mode = force ? "all keys (--force)" : retranslateChanged ? "missing, empty & changed" : "missing & empty";
1569
1768
  console.log("=====");
1570
1769
  console.log("Auto-filling translations");
1571
1770
  console.log("=====");
1572
1771
  console.log(`Languages: ${languagesToProcess.join(", ")}`);
1772
+ console.log(`Mode: ${mode}`);
1573
1773
  console.log(`Limit: ${limit === Infinity ? "unlimited" : limit}`);
1574
1774
  console.log(`Concurrency: ${concurrency}`);
1575
1775
  console.log(`Dry run: ${dryRun}`);
1576
1776
  console.log("=====");
1777
+ const metadata = readMetadata(translationsPath);
1577
1778
  let totalProcessed = 0;
1578
1779
  let totalTranslated = 0;
1579
1780
  for (const language of languagesToProcess) {
@@ -1584,9 +1785,12 @@ Reached limit of ${limit} translations`);
1584
1785
  }
1585
1786
  console.log(`
1586
1787
  Processing language: ${language}`);
1587
- const missing = getMissingForLanguage(projectRoot, language);
1788
+ const missing = getFillItemsForLanguage(projectRoot, translationsPath, config.sourceLanguage, language, {
1789
+ retranslateChanged,
1790
+ force
1791
+ });
1588
1792
  if (!missing.length) {
1589
- console.log(" No missing or empty translations");
1793
+ console.log(" Nothing to translate");
1590
1794
  continue;
1591
1795
  }
1592
1796
  console.log(` Found ${missing.length} translations to fill`);
@@ -1615,6 +1819,7 @@ Processing language: ${language}`);
1615
1819
  translations[item.namespace][item.key] = translated;
1616
1820
  const sorted = sortKeys(translations[item.namespace]);
1617
1821
  writeTranslation(translationsPath, language, item.namespace, sorted);
1822
+ setSourceHash(metadata, language, item.namespace, item.key, item.sourceValue);
1618
1823
  console.log(" \u2713 Saved");
1619
1824
  } else {
1620
1825
  console.log(" \u2713 Dry run - not saved");
@@ -1630,6 +1835,9 @@ Processing language: ${language}`);
1630
1835
  });
1631
1836
  totalProcessed += itemsToProcess.length;
1632
1837
  totalTranslated += results.filter((r) => r.success).length;
1838
+ if (!dryRun) {
1839
+ writeMetadata(translationsPath, metadata);
1840
+ }
1633
1841
  }
1634
1842
  console.log("\n=====");
1635
1843
  console.log(`Total processed: ${totalProcessed}`);
@@ -1641,7 +1849,7 @@ Processing language: ${language}`);
1641
1849
  }
1642
1850
  async function fillNamespace(projectRoot, language, namespace, apiKey) {
1643
1851
  const config = loadConfig(projectRoot);
1644
- const translationsPath = path6.join(projectRoot, config.translationsPath);
1852
+ const translationsPath = path7.join(projectRoot, config.translationsPath);
1645
1853
  const currentProvider = getTranslationProvider();
1646
1854
  const isDefaultGoogleProvider = currentProvider.constructor.name === "GoogleTranslateProvider";
1647
1855
  if (isDefaultGoogleProvider) {
@@ -1657,6 +1865,7 @@ async function fillNamespace(projectRoot, language, namespace, apiKey) {
1657
1865
  const targetTranslations = readTranslations(translationsPath, language);
1658
1866
  const sourceKeys = sourceTranslations[namespace] || {};
1659
1867
  const targetKeys = targetTranslations[namespace] || {};
1868
+ const metadata = readMetadata(translationsPath);
1660
1869
  let count = 0;
1661
1870
  for (const [key, sourceValue] of Object.entries(sourceKeys)) {
1662
1871
  const targetValue = targetKeys[key];
@@ -1673,12 +1882,14 @@ async function fillNamespace(projectRoot, language, namespace, apiKey) {
1673
1882
  config.protectedTerms ?? []
1674
1883
  );
1675
1884
  targetKeys[key] = translated;
1885
+ setSourceHash(metadata, language, namespace, key, sourceValue);
1676
1886
  count++;
1677
1887
  await new Promise((resolve) => setTimeout(resolve, 100));
1678
1888
  }
1679
1889
  if (count > 0) {
1680
1890
  const sorted = sortKeys(targetKeys);
1681
1891
  writeTranslation(translationsPath, language, namespace, sorted);
1892
+ writeMetadata(translationsPath, metadata);
1682
1893
  console.log(`\u2713 Filled ${count} translations`);
1683
1894
  } else {
1684
1895
  console.log("No translations to fill");
@@ -1689,8 +1900,8 @@ async function fillNamespace(projectRoot, language, namespace, apiKey) {
1689
1900
  init_utils();
1690
1901
  init_init();
1691
1902
  import { execSync as execSync2, spawnSync } from "child_process";
1692
- import * as fs4 from "fs";
1693
- import * as path7 from "path";
1903
+ import * as fs5 from "fs";
1904
+ import * as path8 from "path";
1694
1905
  function isRipgrepAvailable() {
1695
1906
  try {
1696
1907
  execSync2("rg --version", { stdio: "ignore" });
@@ -1751,15 +1962,15 @@ function findFiles(rootDir, searchPaths, extensions) {
1751
1962
  ]);
1752
1963
  function scanDirectory(dir) {
1753
1964
  try {
1754
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
1965
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1755
1966
  for (const entry of entries) {
1756
- const fullPath = path7.join(dir, entry.name);
1967
+ const fullPath = path8.join(dir, entry.name);
1757
1968
  if (entry.isDirectory()) {
1758
1969
  if (!excludeDirs.has(entry.name)) {
1759
1970
  scanDirectory(fullPath);
1760
1971
  }
1761
1972
  } else if (entry.isFile()) {
1762
- const ext = path7.extname(entry.name);
1973
+ const ext = path8.extname(entry.name);
1763
1974
  if (extensionSet.has(ext)) {
1764
1975
  files.push(fullPath);
1765
1976
  }
@@ -1769,8 +1980,8 @@ function findFiles(rootDir, searchPaths, extensions) {
1769
1980
  }
1770
1981
  }
1771
1982
  for (const searchPath of searchPaths) {
1772
- const fullSearchPath = path7.join(rootDir, searchPath);
1773
- if (fs4.existsSync(fullSearchPath)) {
1983
+ const fullSearchPath = path8.join(rootDir, searchPath);
1984
+ if (fs5.existsSync(fullSearchPath)) {
1774
1985
  scanDirectory(fullSearchPath);
1775
1986
  }
1776
1987
  }
@@ -1778,7 +1989,7 @@ function findFiles(rootDir, searchPaths, extensions) {
1778
1989
  }
1779
1990
  function checkKeyInFile(key, filePath) {
1780
1991
  try {
1781
- const content = fs4.readFileSync(filePath, "utf-8");
1992
+ const content = fs5.readFileSync(filePath, "utf-8");
1782
1993
  const exactPatterns = [new RegExp(`['"\`]${key}['"\`]`, "g"), new RegExp(`\\b${key}\\b`, "g")];
1783
1994
  for (const pattern of exactPatterns) {
1784
1995
  if (pattern.test(content)) {
@@ -1801,7 +2012,7 @@ function checkPartialMatches(key, files) {
1801
2012
  const foundParts = /* @__PURE__ */ new Set();
1802
2013
  for (const file of filesToCheck) {
1803
2014
  try {
1804
- const content = fs4.readFileSync(file, "utf-8");
2015
+ const content = fs5.readFileSync(file, "utf-8");
1805
2016
  for (const part of significantParts) {
1806
2017
  if (foundParts.has(part)) continue;
1807
2018
  const partPattern = new RegExp(`\\b${part}\\b`, "gi");
@@ -1821,7 +2032,7 @@ function checkPartialMatches(key, files) {
1821
2032
  }
1822
2033
  function findUnusedKeys(projectRoot = process.cwd()) {
1823
2034
  const config = loadConfig(projectRoot);
1824
- const translationsPath = path7.join(projectRoot, config.translationsPath);
2035
+ const translationsPath = path8.join(projectRoot, config.translationsPath);
1825
2036
  const sourceLanguage = config.sourceLanguage;
1826
2037
  const searchPaths = config.searchPaths || ["src", "app", "pages", "components"];
1827
2038
  const searchExtensions = config.searchExtensions || [".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"];
@@ -1963,13 +2174,13 @@ init_init();
1963
2174
  init_schema();
1964
2175
  init_types();
1965
2176
  init_init();
1966
- import * as fs5 from "fs";
1967
- import * as path8 from "path";
2177
+ import * as fs6 from "fs";
2178
+ import * as path9 from "path";
1968
2179
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
1969
2180
  async function initTranslationsInteractive(projectRoot = process.cwd()) {
1970
2181
  console.log("\n\u{1F30D} Translation System Setup\n");
1971
- const configPath = path8.join(projectRoot, ".translationsrc.json");
1972
- const alreadyExists = fs5.existsSync(configPath);
2182
+ const configPath = path9.join(projectRoot, ".translationsrc.json");
2183
+ const alreadyExists = fs6.existsSync(configPath);
1973
2184
  if (alreadyExists) {
1974
2185
  console.log("\u26A0\uFE0F Configuration file already exists at .translationsrc.json\n");
1975
2186
  const shouldOverwrite = await confirm({
@@ -2137,16 +2348,26 @@ function getLanguageName(code) {
2137
2348
 
2138
2349
  // src/translations/cli/manage.ts
2139
2350
  init_utils();
2140
- import * as fs6 from "fs";
2141
- import * as path9 from "path";
2351
+ import * as fs7 from "fs";
2352
+ import * as path10 from "path";
2142
2353
  init_init();
2143
2354
  async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2144
- 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;
2145
2366
  console.log("=====");
2146
2367
  console.log("Translation Management");
2147
2368
  console.log("=====");
2148
- const configPath = path9.join(projectRoot, ".translationsrc.json");
2149
- const isInitialized = fs6.existsSync(configPath);
2369
+ const configPath = path10.join(projectRoot, ".translationsrc.json");
2370
+ const isInitialized = fs7.existsSync(configPath);
2150
2371
  if (!isInitialized) {
2151
2372
  console.log("\u{1F4C1} No translation configuration found. Initializing...\n");
2152
2373
  initTranslations(projectRoot);
@@ -2155,9 +2376,9 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2155
2376
  console.log("\u2713 Translation structure initialized\n");
2156
2377
  }
2157
2378
  const config = loadConfig(projectRoot);
2158
- const translationsPath = path9.join(projectRoot, config.translationsPath);
2159
- const sourceLangPath = path9.join(translationsPath, config.sourceLanguage);
2160
- 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)) {
2161
2382
  console.log(`\u26A0\uFE0F Source language directory not found: ${sourceLangPath}`);
2162
2383
  console.log("Please add translation files to the source language directory.\n");
2163
2384
  return false;
@@ -2179,11 +2400,25 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2179
2400
  if (syncResult.createdFiles.length === 0 && syncResult.cleanedKeys.length === 0 && syncResult.removedNamespaces.length === 0) {
2180
2401
  console.log("\u2713 Translation structure is already synchronized\n");
2181
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
+ }
2182
2411
  console.log("\u{1F50D} Validating translations...\n");
2183
2412
  const validationResult = validateTranslations(projectRoot);
2184
- if (validationResult.valid) {
2413
+ const hasWorkToFill = !validationResult.valid || force || retranslateChanged && validationResult.stale.length > 0;
2414
+ if (validationResult.valid && !validationResult.stale.length) {
2185
2415
  console.log("\n\u2705 All translations are complete!\n");
2186
- } else {
2416
+ } else if (validationResult.valid) {
2417
+ console.log(`
2418
+ \u26A0\uFE0F ${validationResult.stale.length} translations may be outdated.
2419
+ `);
2420
+ }
2421
+ if (hasWorkToFill) {
2187
2422
  const totalMissing = validationResult.missing.length + validationResult.empty.length + validationResult.orphaned.length;
2188
2423
  if (autoFill) {
2189
2424
  if (!apiKey) {
@@ -2193,29 +2428,38 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2193
2428
  console.log(`Set ${envVarName} or pass --api-key to enable auto-fill.
2194
2429
  `);
2195
2430
  } else {
2196
- console.log(`
2197
- \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...
2198
2437
  `);
2438
+ }
2199
2439
  await autoFillTranslations(projectRoot, {
2200
2440
  apiKey,
2201
2441
  limit,
2202
2442
  concurrency,
2203
2443
  language,
2204
2444
  dryRun,
2205
- delayMs: 50
2445
+ delayMs: 50,
2446
+ retranslateChanged,
2447
+ force
2206
2448
  });
2207
2449
  if (!dryRun) {
2208
2450
  console.log("\n\u{1F50D} Re-validating after auto-fill...\n");
2209
2451
  const revalidation = validateTranslations(projectRoot);
2210
- if (revalidation.valid) {
2452
+ if (revalidation.valid && !revalidation.stale.length) {
2211
2453
  console.log("\n\u2705 All translations are now complete!\n");
2212
2454
  }
2213
2455
  }
2214
2456
  }
2215
- } else {
2457
+ } else if (!validationResult.valid) {
2216
2458
  console.log(`
2217
2459
  \u{1F4A1} Tip: Run with --auto-fill to automatically translate missing keys.
2218
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");
2219
2463
  }
2220
2464
  }
2221
2465
  if (!skipTypes && !dryRun) {
@@ -2241,9 +2485,17 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2241
2485
  if (validationResult.orphaned.length > 0) {
2242
2486
  console.log(`\u26A0\uFE0F ${validationResult.orphaned.length} orphaned translations (will be auto-removed on next sync)`);
2243
2487
  }
2488
+ if (validationResult.stale.length > 0) {
2489
+ console.log(`\u26A0\uFE0F ${validationResult.stale.length} potentially outdated translations`);
2490
+ }
2244
2491
  console.log("\nNext steps:");
2245
2492
  console.log(" 1. Add missing translations manually, or");
2246
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");
2247
2499
  } else if (validationResult.valid) {
2248
2500
  console.log("\n\u2705 All systems ready!");
2249
2501
  }
@@ -2258,6 +2510,7 @@ async function manageTranslations(projectRoot = process.cwd(), options = {}) {
2258
2510
  // src/translations/index.ts
2259
2511
  init_schema();
2260
2512
  init_types();
2513
+ init_metadata();
2261
2514
  init_utils();
2262
2515
  export {
2263
2516
  DEEPL_LANGUAGES,
@@ -2265,6 +2518,7 @@ export {
2265
2518
  DEFAULT_LANGUAGES,
2266
2519
  GOOGLE_LANGUAGES,
2267
2520
  GoogleTranslateProvider,
2521
+ METADATA_FILE_NAME,
2268
2522
  SUPPORTED_LANGUAGES,
2269
2523
  TRANSLATION_CONFIG_SCHEMA,
2270
2524
  TRANSLATION_PROVIDERS,
@@ -2273,6 +2527,8 @@ export {
2273
2527
  autoFillTranslations,
2274
2528
  createEmptyTranslationStructure,
2275
2529
  detectExistingTranslations,
2530
+ emptyMetadata,
2531
+ ensureBaselineMetadata,
2276
2532
  ensureTranslationsStructure,
2277
2533
  extractPluralBaseKeys,
2278
2534
  extractVariables,
@@ -2282,11 +2538,14 @@ export {
2282
2538
  generateTranslationTypes,
2283
2539
  getAvailableLanguages,
2284
2540
  getFallbackMappings,
2541
+ getMetadataPath,
2285
2542
  getMissingForLanguage,
2286
2543
  getNamespaces,
2544
+ getSourceHash,
2287
2545
  getSupportedLanguages,
2288
2546
  getTranslationProvider,
2289
2547
  hasInterpolation,
2548
+ hashSourceValue,
2290
2549
  initTranslations,
2291
2550
  initTranslationsInteractive,
2292
2551
  isNestedObject,
@@ -2297,10 +2556,14 @@ export {
2297
2556
  loadConfig,
2298
2557
  logLanguageFallback,
2299
2558
  manageTranslations,
2559
+ metadataExists,
2300
2560
  printUnusedKeysResult,
2561
+ pruneMetadata,
2562
+ readMetadata,
2301
2563
  readTranslations,
2302
2564
  resetTranslationProvider,
2303
2565
  resolveLanguageWithFallback,
2566
+ setSourceHash,
2304
2567
  setTranslationProvider,
2305
2568
  sortKeys,
2306
2569
  syncTranslationStructure,
@@ -2311,6 +2574,7 @@ export {
2311
2574
  validateLanguagesForProvider,
2312
2575
  validateTranslations,
2313
2576
  validateVariables,
2577
+ writeMetadata,
2314
2578
  writeTranslation
2315
2579
  };
2316
2580
  //# sourceMappingURL=index.js.map