@babelize/engine 0.0.0-stage → 0.1.2

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.
Files changed (270) hide show
  1. package/LICENSE +202 -0
  2. package/dist/BabelizeEngine.d.ts +12 -0
  3. package/dist/BabelizeEngine.js +47 -0
  4. package/dist/analysis/analyze.d.ts +106 -0
  5. package/dist/analysis/analyze.js +323 -0
  6. package/dist/config/env.d.ts +9 -0
  7. package/dist/config/env.js +16 -0
  8. package/dist/config.d.ts +74 -0
  9. package/dist/config.js +47 -0
  10. package/dist/detection/stack.d.ts +42 -0
  11. package/dist/detection/stack.js +277 -0
  12. package/dist/django/catalog.d.ts +92 -0
  13. package/dist/django/catalog.js +677 -0
  14. package/dist/django/extract.d.ts +20 -0
  15. package/dist/django/extract.js +79 -0
  16. package/dist/django/inject-file.d.ts +6 -0
  17. package/dist/django/inject-file.js +52 -0
  18. package/dist/django/inject.d.ts +11 -0
  19. package/dist/django/inject.js +205 -0
  20. package/dist/django/keys.d.ts +21 -0
  21. package/dist/django/keys.js +63 -0
  22. package/dist/django/locale-names.d.ts +13 -0
  23. package/dist/django/locale-names.js +60 -0
  24. package/dist/django/mo-file.d.ts +6 -0
  25. package/dist/django/mo-file.js +60 -0
  26. package/dist/django/plural-forms.d.ts +34 -0
  27. package/dist/django/plural-forms.js +216 -0
  28. package/dist/django/po-file.d.ts +38 -0
  29. package/dist/django/po-file.js +273 -0
  30. package/dist/django/python-extract.d.ts +34 -0
  31. package/dist/django/python-extract.js +619 -0
  32. package/dist/django/python-lexer.d.ts +65 -0
  33. package/dist/django/python-lexer.js +398 -0
  34. package/dist/django/template-extract.d.ts +32 -0
  35. package/dist/django/template-extract.js +680 -0
  36. package/dist/django/types.d.ts +49 -0
  37. package/dist/django/types.js +15 -0
  38. package/dist/index.d.ts +59 -0
  39. package/dist/index.js +140 -0
  40. package/dist/injection/InjectionStrategy.d.ts +62 -0
  41. package/dist/injection/InjectionStrategy.js +2 -0
  42. package/dist/injection/StrategyFactory.d.ts +4 -0
  43. package/dist/injection/StrategyFactory.js +29 -0
  44. package/dist/injection/html/html-injector.d.ts +11 -0
  45. package/dist/injection/html/html-injector.js +173 -0
  46. package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
  47. package/dist/injection/strategies/NextIntlStrategy.js +141 -0
  48. package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
  49. package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
  50. package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
  51. package/dist/injection/strategies/VanillaStrategy.js +73 -0
  52. package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
  53. package/dist/injection/strategies/VueI18nStrategy.js +84 -0
  54. package/dist/local/formatter.d.ts +26 -0
  55. package/dist/local/formatter.js +105 -0
  56. package/dist/local/git.d.ts +23 -0
  57. package/dist/local/git.js +136 -0
  58. package/dist/local/hosted-provider.d.ts +68 -0
  59. package/dist/local/hosted-provider.js +215 -0
  60. package/dist/local/index.d.ts +23 -0
  61. package/dist/local/index.js +75 -0
  62. package/dist/local/provider.d.ts +56 -0
  63. package/dist/local/provider.js +119 -0
  64. package/dist/local/pseudo-provider.d.ts +18 -0
  65. package/dist/local/pseudo-provider.js +208 -0
  66. package/dist/local/report.d.ts +177 -0
  67. package/dist/local/report.js +186 -0
  68. package/dist/local/translate-core.d.ts +218 -0
  69. package/dist/local/translate-core.js +499 -0
  70. package/dist/local/translate-messages.d.ts +49 -0
  71. package/dist/local/translate-messages.js +89 -0
  72. package/dist/local/workspace-copy.d.ts +91 -0
  73. package/dist/local/workspace-copy.js +355 -0
  74. package/dist/modules/extractor/extract.d.ts +20 -0
  75. package/dist/modules/extractor/extract.js +709 -0
  76. package/dist/modules/extractor/index.d.ts +2 -0
  77. package/dist/modules/extractor/index.js +63 -0
  78. package/dist/modules/finalization/django-finalize.d.ts +28 -0
  79. package/dist/modules/finalization/django-finalize.js +412 -0
  80. package/dist/modules/finalization/finalize.d.ts +38 -0
  81. package/dist/modules/finalization/finalize.js +745 -0
  82. package/dist/modules/finalization/index.d.ts +1 -0
  83. package/dist/modules/finalization/index.js +5 -0
  84. package/dist/modules/finalization/nextjs-finalize.d.ts +40 -0
  85. package/dist/modules/finalization/nextjs-finalize.js +328 -0
  86. package/dist/modules/finalization/static-runtime.d.ts +9 -0
  87. package/dist/modules/finalization/static-runtime.js +261 -0
  88. package/dist/modules/finalization/writes.d.ts +29 -0
  89. package/dist/modules/finalization/writes.js +78 -0
  90. package/dist/modules/format-injection/index.d.ts +19 -0
  91. package/dist/modules/format-injection/index.js +273 -0
  92. package/dist/modules/format-injection/inject-formats.d.ts +31 -0
  93. package/dist/modules/format-injection/inject-formats.js +208 -0
  94. package/dist/modules/glossary/index.d.ts +2 -0
  95. package/dist/modules/glossary/index.js +16 -0
  96. package/dist/modules/glossary/rules.d.ts +81 -0
  97. package/dist/modules/glossary/rules.js +244 -0
  98. package/dist/modules/glossary/types.d.ts +22 -0
  99. package/dist/modules/glossary/types.js +2 -0
  100. package/dist/modules/injection/index.d.ts +1 -0
  101. package/dist/modules/injection/index.js +6 -0
  102. package/dist/modules/injection/inject.d.ts +29 -0
  103. package/dist/modules/injection/inject.js +1050 -0
  104. package/dist/modules/keygen/index.d.ts +1 -0
  105. package/dist/modules/keygen/index.js +11 -0
  106. package/dist/modules/keygen/keygen.d.ts +4 -0
  107. package/dist/modules/keygen/keygen.js +123 -0
  108. package/dist/modules/keygen/locale-data.d.ts +7 -0
  109. package/dist/modules/keygen/locale-data.js +73 -0
  110. package/dist/modules/keygen/public.d.ts +2 -0
  111. package/dist/modules/keygen/public.js +12 -0
  112. package/dist/modules/locale-builder/index.d.ts +1 -0
  113. package/dist/modules/locale-builder/index.js +7 -0
  114. package/dist/modules/locale-builder/locales.d.ts +13 -0
  115. package/dist/modules/locale-builder/locales.js +130 -0
  116. package/dist/modules/lockfile/hasher.d.ts +7 -0
  117. package/dist/modules/lockfile/hasher.js +13 -0
  118. package/dist/modules/lockfile/index.d.ts +3 -0
  119. package/dist/modules/lockfile/index.js +22 -0
  120. package/dist/modules/lockfile/lockfile-manager.d.ts +99 -0
  121. package/dist/modules/lockfile/lockfile-manager.js +497 -0
  122. package/dist/modules/lockfile/schema.d.ts +196 -0
  123. package/dist/modules/lockfile/schema.js +56 -0
  124. package/dist/modules/parser/index.d.ts +2 -0
  125. package/dist/modules/parser/index.js +9 -0
  126. package/dist/modules/summary/index.d.ts +1 -0
  127. package/dist/modules/summary/index.js +7 -0
  128. package/dist/modules/summary/summary.d.ts +9 -0
  129. package/dist/modules/summary/summary.js +195 -0
  130. package/dist/modules/sync-detector/index.d.ts +2 -0
  131. package/dist/modules/sync-detector/index.js +7 -0
  132. package/dist/modules/sync-detector/reporter.d.ts +3 -0
  133. package/dist/modules/sync-detector/reporter.js +16 -0
  134. package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
  135. package/dist/modules/sync-detector/sync-detector.js +374 -0
  136. package/dist/modules/translator/batch-optimizer.d.ts +12 -0
  137. package/dist/modules/translator/batch-optimizer.js +30 -0
  138. package/dist/modules/translator/client/byok.provider.d.ts +64 -0
  139. package/dist/modules/translator/client/byok.provider.js +319 -0
  140. package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
  141. package/dist/modules/translator/client/direct-translator.client.js +71 -0
  142. package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
  143. package/dist/modules/translator/client/gemini.provider.js +110 -0
  144. package/dist/modules/translator/client/groq.provider.d.ts +1 -0
  145. package/dist/modules/translator/client/groq.provider.js +111 -0
  146. package/dist/modules/translator/client/request.types.d.ts +1 -0
  147. package/dist/modules/translator/client/request.types.js +2 -0
  148. package/dist/modules/translator/client/retry.d.ts +29 -0
  149. package/dist/modules/translator/client/retry.js +94 -0
  150. package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
  151. package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
  152. package/dist/modules/translator/client/translator.interface.d.ts +1 -0
  153. package/dist/modules/translator/client/translator.interface.js +2 -0
  154. package/dist/modules/translator/icu-branches.d.ts +138 -0
  155. package/dist/modules/translator/icu-branches.js +393 -0
  156. package/dist/modules/translator/language-codes/errors.d.ts +10 -0
  157. package/dist/modules/translator/language-codes/errors.js +18 -0
  158. package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
  159. package/dist/modules/translator/language-codes/flores-map.js +107 -0
  160. package/dist/modules/translator/language-codes/index.d.ts +11 -0
  161. package/dist/modules/translator/language-codes/index.js +24 -0
  162. package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
  163. package/dist/modules/translator/language-codes/language-mapper.js +132 -0
  164. package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
  165. package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
  166. package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
  167. package/dist/modules/translator/memory/memory-matcher.js +218 -0
  168. package/dist/modules/translator/memory/types.d.ts +22 -0
  169. package/dist/modules/translator/memory/types.js +2 -0
  170. package/dist/modules/translator/translate.d.ts +74 -0
  171. package/dist/modules/translator/translate.js +1047 -0
  172. package/dist/modules/vue/migrate.d.ts +13 -0
  173. package/dist/modules/vue/migrate.js +40 -0
  174. package/dist/modules/vue/script-inject.d.ts +16 -0
  175. package/dist/modules/vue/script-inject.js +115 -0
  176. package/dist/modules/vue/sfc.d.ts +11 -0
  177. package/dist/modules/vue/sfc.js +51 -0
  178. package/dist/modules/vue/template-rewrite.d.ts +25 -0
  179. package/dist/modules/vue/template-rewrite.js +76 -0
  180. package/dist/modules/vue/template-scan.d.ts +29 -0
  181. package/dist/modules/vue/template-scan.js +264 -0
  182. package/dist/modules/workspace/index.d.ts +1 -0
  183. package/dist/modules/workspace/index.js +5 -0
  184. package/dist/modules/workspace/validate.d.ts +13 -0
  185. package/dist/modules/workspace/validate.js +333 -0
  186. package/dist/orchestrator.d.ts +13 -0
  187. package/dist/orchestrator.js +727 -0
  188. package/dist/providers/default-translation-provider.d.ts +20 -0
  189. package/dist/providers/default-translation-provider.js +30 -0
  190. package/dist/providers/index.d.ts +2 -0
  191. package/dist/providers/index.js +21 -0
  192. package/dist/providers/types.d.ts +153 -0
  193. package/dist/providers/types.js +2 -0
  194. package/dist/qa/check-project.d.ts +50 -0
  195. package/dist/qa/check-project.js +193 -0
  196. package/dist/qa/gettext-files.d.ts +15 -0
  197. package/dist/qa/gettext-files.js +115 -0
  198. package/dist/qa/locale-files.d.ts +44 -0
  199. package/dist/qa/locale-files.js +159 -0
  200. package/dist/qa/locale-qa.d.ts +61 -0
  201. package/dist/qa/locale-qa.js +342 -0
  202. package/dist/qa/review.d.ts +43 -0
  203. package/dist/qa/review.js +175 -0
  204. package/dist/runtime/config.d.ts +8 -0
  205. package/dist/runtime/config.js +37 -0
  206. package/dist/runtime/index.d.ts +3 -0
  207. package/dist/runtime/index.js +8 -0
  208. package/dist/runtime/types.d.ts +427 -0
  209. package/dist/runtime/types.js +38 -0
  210. package/dist/types.d.ts +134 -0
  211. package/dist/types.js +12 -0
  212. package/dist/utils/ast-helpers.d.ts +57 -0
  213. package/dist/utils/ast-helpers.js +608 -0
  214. package/dist/utils/cache-keys.d.ts +8 -0
  215. package/dist/utils/cache-keys.js +18 -0
  216. package/dist/utils/circuit-breaker.d.ts +43 -0
  217. package/dist/utils/circuit-breaker.js +119 -0
  218. package/dist/utils/concurrent-map.d.ts +4 -0
  219. package/dist/utils/concurrent-map.js +21 -0
  220. package/dist/utils/detect-format.d.ts +5 -0
  221. package/dist/utils/detect-format.js +125 -0
  222. package/dist/utils/extraction-options.d.ts +29 -0
  223. package/dist/utils/extraction-options.js +33 -0
  224. package/dist/utils/file-utils.d.ts +33 -0
  225. package/dist/utils/file-utils.js +267 -0
  226. package/dist/utils/glossary-match.d.ts +9 -0
  227. package/dist/utils/glossary-match.js +25 -0
  228. package/dist/utils/html-extractor.d.ts +8 -0
  229. package/dist/utils/html-extractor.js +317 -0
  230. package/dist/utils/i18next-icu.d.ts +38 -0
  231. package/dist/utils/i18next-icu.js +159 -0
  232. package/dist/utils/icu-message.d.ts +65 -0
  233. package/dist/utils/icu-message.js +346 -0
  234. package/dist/utils/icu-spans.d.ts +10 -0
  235. package/dist/utils/icu-spans.js +126 -0
  236. package/dist/utils/jsx-message.d.ts +93 -0
  237. package/dist/utils/jsx-message.js +628 -0
  238. package/dist/utils/key-usage.d.ts +35 -0
  239. package/dist/utils/key-usage.js +247 -0
  240. package/dist/utils/logger.d.ts +24 -0
  241. package/dist/utils/logger.js +33 -0
  242. package/dist/utils/metadata.d.ts +82 -0
  243. package/dist/utils/metadata.js +261 -0
  244. package/dist/utils/module-constants.d.ts +30 -0
  245. package/dist/utils/module-constants.js +265 -0
  246. package/dist/utils/not-injected.d.ts +6 -0
  247. package/dist/utils/not-injected.js +38 -0
  248. package/dist/utils/placeholder-protection.d.ts +74 -0
  249. package/dist/utils/placeholder-protection.js +181 -0
  250. package/dist/utils/pseudo-markers.d.ts +9 -0
  251. package/dist/utils/pseudo-markers.js +16 -0
  252. package/dist/utils/string-utils.d.ts +52 -0
  253. package/dist/utils/string-utils.js +270 -0
  254. package/dist/utils/translation-quality.d.ts +34 -0
  255. package/dist/utils/translation-quality.js +156 -0
  256. package/dist/utils/translation-sanitizer.d.ts +11 -0
  257. package/dist/utils/translation-sanitizer.js +71 -0
  258. package/dist/utils/translator-caller.d.ts +32 -0
  259. package/dist/utils/translator-caller.js +217 -0
  260. package/dist/utils/translator-load-balancer.d.ts +73 -0
  261. package/dist/utils/translator-load-balancer.js +297 -0
  262. package/dist/utils/ui-copy.d.ts +32 -0
  263. package/dist/utils/ui-copy.js +126 -0
  264. package/dist/utils/untranslated.d.ts +15 -0
  265. package/dist/utils/untranslated.js +34 -0
  266. package/dist/utils/workspace.d.ts +14 -0
  267. package/dist/utils/workspace.js +89 -0
  268. package/package.json +64 -4
  269. package/schemas/translate-report.schema.json +297 -0
  270. package/README.md +0 -3
@@ -0,0 +1,497 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildKeysToTranslateByTargetLocale = buildKeysToTranslateByTargetLocale;
37
+ exports.getLockfilePath = getLockfilePath;
38
+ exports.loadLockfile = loadLockfile;
39
+ exports.saveLockfile = saveLockfile;
40
+ exports.clearLockfile = clearLockfile;
41
+ exports.removeLocaleFromLockfile = removeLocaleFromLockfile;
42
+ exports.glossaryChanged = glossaryChanged;
43
+ exports.isStalePseudoEntry = isStalePseudoEntry;
44
+ exports.partitionKeys = partitionKeys;
45
+ exports.dropFailedTranslations = dropFailedTranslations;
46
+ exports.lockfileChanged = lockfileChanged;
47
+ exports.updateLockfile = updateLockfile;
48
+ exports.collectCachedTranslations = collectCachedTranslations;
49
+ exports.computeLockfileStats = computeLockfileStats;
50
+ const path = __importStar(require("path"));
51
+ const pseudo_markers_1 = require("../../utils/pseudo-markers");
52
+ const fs = __importStar(require("fs-extra"));
53
+ const logger_1 = require("../../utils/logger");
54
+ const hasher_1 = require("./hasher");
55
+ const schema_1 = require("./schema");
56
+ const LOCK_RETRIES = 20;
57
+ const LOCK_RETRY_DELAY_MS = 50;
58
+ async function acquireLock(lockDir) {
59
+ let lastErr = null;
60
+ for (let i = 0; i < LOCK_RETRIES; i++) {
61
+ try {
62
+ await fs.mkdir(lockDir, { recursive: false });
63
+ return;
64
+ }
65
+ catch (err) {
66
+ lastErr = err;
67
+ if (err.code !== 'EEXIST')
68
+ throw err;
69
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_DELAY_MS));
70
+ }
71
+ }
72
+ throw new Error(`Could not acquire lockfile lock after ${LOCK_RETRIES} retries: ${lastErr?.message}`);
73
+ }
74
+ async function releaseLock(lockDir) {
75
+ try {
76
+ await fs.rmdir(lockDir);
77
+ }
78
+ catch {
79
+ // Best-effort cleanup
80
+ }
81
+ }
82
+ /**
83
+ * For each target locale, list of translation keys that must go through the translator
84
+ * (new keys, source-changed keys, or keys missing that locale in the lockfile).
85
+ */
86
+ function buildKeysToTranslateByTargetLocale(partition, targetLocales) {
87
+ const byLocale = {};
88
+ for (const loc of targetLocales) {
89
+ byLocale[loc] = new Set();
90
+ }
91
+ for (const { key } of partition.newKeys) {
92
+ for (const loc of targetLocales) {
93
+ byLocale[loc].add(key);
94
+ }
95
+ }
96
+ for (const { key } of partition.changed) {
97
+ for (const loc of targetLocales) {
98
+ byLocale[loc].add(key);
99
+ }
100
+ }
101
+ for (const { key, missingLocales } of partition.missingLocales) {
102
+ for (const loc of missingLocales) {
103
+ if (byLocale[loc])
104
+ byLocale[loc].add(key);
105
+ }
106
+ }
107
+ const result = {};
108
+ for (const loc of targetLocales) {
109
+ result[loc] = Array.from(byLocale[loc]);
110
+ }
111
+ return result;
112
+ }
113
+ // ─────────────────────────────────────────────────────────────
114
+ // Path helpers
115
+ // ─────────────────────────────────────────────────────────────
116
+ /**
117
+ * Resolve the lockfile path for a project:
118
+ * `<dataDir>/.babelize/{projectId}/babelize-lock.json`. `dataDir` defaults to
119
+ * $BABELIZE_DATA_DIR, else `process.cwd()` (the cloud backend). Local runs
120
+ * pass it explicitly so concurrent runs in one process never share it.
121
+ */
122
+ function getLockfilePath(projectId, dataDir) {
123
+ const baseDir = dataDir || process.env.BABELIZE_DATA_DIR || process.cwd();
124
+ return path.join(baseDir, '.babelize', projectId, 'babelize-lock.json');
125
+ }
126
+ // ─────────────────────────────────────────────────────────────
127
+ // Load
128
+ // ─────────────────────────────────────────────────────────────
129
+ /**
130
+ * Load and validate the lockfile from disk.
131
+ * Returns an empty lockfile if the file doesn't exist or is malformed.
132
+ */
133
+ async function loadLockfile(projectId, dataDir) {
134
+ const lockfilePath = getLockfilePath(projectId, dataDir);
135
+ try {
136
+ if (!(await fs.pathExists(lockfilePath))) {
137
+ logger_1.engineLogger.info('No existing lockfile found, starting fresh', { projectId });
138
+ return (0, schema_1.createEmptyLockfile)(projectId);
139
+ }
140
+ const raw = await fs.readJson(lockfilePath);
141
+ const parsed = schema_1.lockfileDataSchema.safeParse(raw);
142
+ if (!parsed.success) {
143
+ logger_1.engineLogger.warn('Lockfile validation failed, starting fresh', {
144
+ projectId,
145
+ errors: parsed.error.issues,
146
+ });
147
+ return (0, schema_1.createEmptyLockfile)(projectId);
148
+ }
149
+ logger_1.engineLogger.info('Loaded lockfile', {
150
+ projectId,
151
+ keyCount: Object.keys(parsed.data.keys).length,
152
+ });
153
+ return parsed.data;
154
+ }
155
+ catch (err) {
156
+ logger_1.engineLogger.warn('Failed to read lockfile, starting fresh', {
157
+ projectId,
158
+ error: err instanceof Error ? err.message : String(err),
159
+ });
160
+ return (0, schema_1.createEmptyLockfile)(projectId);
161
+ }
162
+ }
163
+ // ─────────────────────────────────────────────────────────────
164
+ // Save
165
+ // ─────────────────────────────────────────────────────────────
166
+ /**
167
+ * Persist lockfile to disk atomically (write→tmp→rename).
168
+ * Uses advisory file locking via atomic mkdir to prevent concurrent writes
169
+ * from overwriting each other (two simultaneous runs for the same project).
170
+ */
171
+ async function saveLockfile(projectId, data, dataDir) {
172
+ const lockfilePath = getLockfilePath(projectId, dataDir);
173
+ const lockDir = lockfilePath + '.lock';
174
+ const tmpPath = lockfilePath + '.tmp';
175
+ await fs.ensureDir(path.dirname(lockfilePath));
176
+ await acquireLock(lockDir);
177
+ try {
178
+ // Re-read the current lockfile and merge any keys written by a concurrent run
179
+ // between when we loaded data and now.
180
+ const merged = data;
181
+ try {
182
+ if (await fs.pathExists(lockfilePath)) {
183
+ const current = await fs.readJson(lockfilePath);
184
+ for (const [key, entry] of Object.entries(current.keys || {})) {
185
+ if (!merged.keys[key]) {
186
+ merged.keys[key] = entry;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ catch (readErr) {
192
+ logger_1.engineLogger.warn('Failed to re-read lockfile for merge, proceeding with current data', {
193
+ projectId,
194
+ error: readErr instanceof Error ? readErr.message : String(readErr),
195
+ });
196
+ }
197
+ merged.updatedAt = new Date().toISOString();
198
+ await fs.writeJson(tmpPath, merged, { spaces: 2 });
199
+ await fs.rename(tmpPath, lockfilePath);
200
+ logger_1.engineLogger.info('Saved lockfile', {
201
+ projectId,
202
+ keyCount: Object.keys(merged.keys).length,
203
+ });
204
+ }
205
+ catch (err) {
206
+ logger_1.engineLogger.error('Failed to save lockfile', {
207
+ projectId,
208
+ error: err instanceof Error ? err.message : String(err),
209
+ });
210
+ await fs.remove(tmpPath).catch((cleanupErr) => {
211
+ logger_1.engineLogger.warn('Failed to clean up temp lockfile', { tmpPath, error: cleanupErr });
212
+ });
213
+ throw err;
214
+ }
215
+ finally {
216
+ await releaseLock(lockDir);
217
+ }
218
+ }
219
+ // ─────────────────────────────────────────────────────────────
220
+ // Clear (force re-translate)
221
+ // ─────────────────────────────────────────────────────────────
222
+ /**
223
+ * Delete the lockfile for a project (used by "Force full re-translate").
224
+ */
225
+ async function clearLockfile(projectId, dataDir) {
226
+ const lockfilePath = getLockfilePath(projectId, dataDir);
227
+ try {
228
+ if (await fs.pathExists(lockfilePath)) {
229
+ await fs.remove(lockfilePath);
230
+ logger_1.engineLogger.info('Cleared lockfile', { projectId });
231
+ }
232
+ }
233
+ catch (err) {
234
+ logger_1.engineLogger.warn('Failed to clear lockfile', {
235
+ projectId,
236
+ error: err instanceof Error ? err.message : String(err),
237
+ });
238
+ }
239
+ }
240
+ /**
241
+ * Purge a specific locale's translations from the lockfile.
242
+ * Used when a language is removed from a project so re-adding it
243
+ * triggers fresh translation instead of resurrecting stale cache.
244
+ */
245
+ async function removeLocaleFromLockfile(projectId, localeCode, dataDir) {
246
+ const lockfilePath = getLockfilePath(projectId, dataDir);
247
+ try {
248
+ if (!(await fs.pathExists(lockfilePath))) {
249
+ return;
250
+ }
251
+ const lockfile = await loadLockfile(projectId, dataDir);
252
+ let removedCount = 0;
253
+ for (const [key, entry] of Object.entries(lockfile.keys)) {
254
+ if (localeCode in entry.languages) {
255
+ delete entry.languages[localeCode];
256
+ removedCount++;
257
+ }
258
+ if (entry.metadata && localeCode in entry.metadata) {
259
+ delete entry.metadata[localeCode];
260
+ }
261
+ if (Object.keys(entry.languages).length === 0) {
262
+ delete lockfile.keys[key];
263
+ }
264
+ }
265
+ if (removedCount > 0) {
266
+ lockfile.updatedAt = new Date().toISOString();
267
+ // Write directly (bypass saveLockfile's merge logic which would
268
+ // re-add deleted keys from the on-disk file)
269
+ const lockDir = lockfilePath + '.lock';
270
+ const tmpPath = lockfilePath + '.tmp';
271
+ await fs.ensureDir(path.dirname(lockfilePath));
272
+ await acquireLock(lockDir);
273
+ try {
274
+ await fs.writeJson(tmpPath, lockfile, { spaces: 2 });
275
+ await fs.rename(tmpPath, lockfilePath);
276
+ }
277
+ finally {
278
+ await releaseLock(lockDir);
279
+ }
280
+ logger_1.engineLogger.info('Purged locale from lockfile', {
281
+ projectId,
282
+ localeCode,
283
+ keysAffected: removedCount,
284
+ });
285
+ }
286
+ }
287
+ catch (err) {
288
+ logger_1.engineLogger.warn('Failed to purge locale from lockfile', {
289
+ projectId,
290
+ localeCode,
291
+ error: err instanceof Error ? err.message : String(err),
292
+ });
293
+ }
294
+ }
295
+ /** True when the glossary rules for this key and locale changed since its translation was stored. */
296
+ function glossaryChanged(entry, source, locale, options = {}) {
297
+ if (!options.glossaryHash)
298
+ return false;
299
+ const meta = entry.metadata?.[locale];
300
+ if (meta?.external)
301
+ return false;
302
+ return (meta?.glossaryHash ?? '') !== options.glossaryHash(source, locale);
303
+ }
304
+ /**
305
+ * True when the stored value for `locale` is pseudo-localization and this run
306
+ * is a real one: it must be translated, never reused. Recognised by the
307
+ * `pseudo` metadata flag, or by the pseudo markers for lockfiles written
308
+ * before the flag existed.
309
+ */
310
+ function isStalePseudoEntry(entry, locale, options = {}) {
311
+ if (options.pseudo)
312
+ return false;
313
+ return entry.metadata?.[locale]?.pseudo === true || (0, pseudo_markers_1.isPseudoLocalized)(entry.languages[locale] ?? '');
314
+ }
315
+ function partitionKeys(lockfile, keys, targetLocales, options = {}) {
316
+ const result = {
317
+ unchanged: [],
318
+ newKeys: [],
319
+ changed: [],
320
+ missingLocales: [],
321
+ };
322
+ for (const [key, value] of Object.entries(keys)) {
323
+ const currentHash = (0, hasher_1.hashSourceString)(value);
324
+ const entry = lockfile.keys[key];
325
+ if (!entry) {
326
+ // Key not seen before
327
+ result.newKeys.push({ key, value });
328
+ continue;
329
+ }
330
+ if (entry.sourceHash !== currentHash) {
331
+ // Source string changed
332
+ result.changed.push({ key, value });
333
+ continue;
334
+ }
335
+ // Hash matches — check if all target locales are present
336
+ const missing = targetLocales.filter((locale) => !(locale in entry.languages) || isStalePseudoEntry(entry, locale, options) ||
337
+ glossaryChanged(entry, value, locale, options));
338
+ if (missing.length === 0) {
339
+ // Fully translated and unchanged
340
+ result.unchanged.push({ key, value });
341
+ }
342
+ else {
343
+ // Some locales missing
344
+ result.missingLocales.push({ key, value, missingLocales: missing });
345
+ }
346
+ }
347
+ return result;
348
+ }
349
+ // ─────────────────────────────────────────────────────────────
350
+ // Update lockfile with new translations
351
+ // ─────────────────────────────────────────────────────────────
352
+ /**
353
+ * Merge newly translated values and metadata into the lockfile.
354
+ *
355
+ * @param lockfile - Current lockfile (will be mutated)
356
+ * @param keys - Flat map of key → source value (for hashing)
357
+ * @param translations - Map of locale → { key → translated string }
358
+ * @param projectId - Project identifier
359
+ * @param metadata - Map of locale → { key → metadata object }
360
+ */
361
+ /**
362
+ * Remove the stored translation of every key that failed this run.
363
+ * updateLockfile records the new source fingerprint for every key and keeps
364
+ * the previous translation where a locale produced none — for a key whose
365
+ * source text changed and then failed, that previous value translates the
366
+ * *old* text. Dropping it makes the next run treat the locale as missing and
367
+ * retry it, instead of silently reusing a stale translation.
368
+ */
369
+ function dropFailedTranslations(lockfile, failures) {
370
+ if (failures.length === 0)
371
+ return lockfile;
372
+ const cloned = JSON.parse(JSON.stringify(lockfile));
373
+ for (const { key, locale } of failures) {
374
+ const entry = cloned.keys[key];
375
+ if (entry?.languages)
376
+ delete entry.languages[locale];
377
+ if (entry?.metadata)
378
+ delete entry.metadata[locale];
379
+ }
380
+ return cloned;
381
+ }
382
+ /** The metadata fields the lockfile schema keeps (anything else is dropped on load anyway). */
383
+ function pickLockfileMetadata(meta) {
384
+ const out = {};
385
+ if (meta.pseudo === true)
386
+ out.pseudo = true;
387
+ if (typeof meta.fromMemory === 'boolean')
388
+ out.fromMemory = meta.fromMemory;
389
+ if (typeof meta.sourceHash === 'string')
390
+ out.sourceHash = meta.sourceHash;
391
+ if (typeof meta.inTm === 'boolean')
392
+ out.inTm = meta.inTm;
393
+ if (typeof meta.glossaryHash === 'string' && meta.glossaryHash)
394
+ out.glossaryHash = meta.glossaryHash;
395
+ if (meta.external === true)
396
+ out.external = true;
397
+ return out;
398
+ }
399
+ function sameJson(a, b) {
400
+ return JSON.stringify(a ?? {}) === JSON.stringify(b ?? {});
401
+ }
402
+ /** True when `next` differs from `previous` in anything but timestamps worth saving. */
403
+ function lockfileChanged(previous, next) {
404
+ return previous.projectId !== next.projectId || JSON.stringify(previous.keys) !== JSON.stringify(next.keys);
405
+ }
406
+ function updateLockfile(lockfile, keys, translations, projectId, metadata) {
407
+ // Deep-clone to prevent in-place mutation of the caller's reference.
408
+ // Prevents races when multiple concurrent runs share the same lockfile object.
409
+ const cloned = JSON.parse(JSON.stringify(lockfile));
410
+ const now = new Date().toISOString();
411
+ cloned.projectId = projectId;
412
+ for (const [key, value] of Object.entries(keys)) {
413
+ const sourceHash = (0, hasher_1.hashSourceString)(value);
414
+ const existing = cloned.keys[key];
415
+ // An entry is only touched (and re-stamped) when something in it
416
+ // actually changes, so a no-change rerun leaves the committed
417
+ // lockfile byte-identical.
418
+ const entry = existing ?? { sourceHash, translatedAt: now, languages: {}, metadata: {} };
419
+ let changed = !existing || existing.sourceHash !== sourceHash;
420
+ entry.sourceHash = sourceHash;
421
+ // Merge per-locale translations and metadata
422
+ for (const [locale, translatedKeys] of Object.entries(translations)) {
423
+ if (translatedKeys[key]) {
424
+ // Skip caching identity translations (same as source text) for non-trivial strings.
425
+ // Identity translations mean the translator fell back to source text
426
+ // (e.g., because the service was unavailable). Caching them would cause
427
+ // subsequent runs to skip re-translation and produce fake locale files.
428
+ const translated = translatedKeys[key];
429
+ if (translated === value && value.length > 4) {
430
+ logger_1.engineLogger.warn(`Skipping cache for "${key}" → "${locale}": identity translation (same as source). Translator may be unavailable.`);
431
+ continue;
432
+ }
433
+ if (entry.languages[locale] !== translated) {
434
+ entry.languages[locale] = translated;
435
+ changed = true;
436
+ }
437
+ // Metadata from this run replaces the previous run's rather than
438
+ // merging into it, so a stale `fromMemory: true` doesn't survive
439
+ // the key being re-translated. Keys reused from the lockfile
440
+ // carry no new metadata and keep what they had.
441
+ if (metadata?.[locale]?.[key]) {
442
+ const next = pickLockfileMetadata(metadata[locale][key]);
443
+ if (!sameJson(entry.metadata?.[locale], next)) {
444
+ if (!entry.metadata)
445
+ entry.metadata = {};
446
+ entry.metadata[locale] = next;
447
+ changed = true;
448
+ }
449
+ }
450
+ }
451
+ }
452
+ if (changed) {
453
+ entry.translatedAt = now;
454
+ if (!entry.metadata)
455
+ entry.metadata = {};
456
+ cloned.keys[key] = entry;
457
+ }
458
+ }
459
+ return cloned;
460
+ }
461
+ // ─────────────────────────────────────────────────────────────
462
+ // Collect cached translations from lockfile
463
+ // ─────────────────────────────────────────────────────────────
464
+ /**
465
+ * Extract cached translations for unchanged keys from the lockfile.
466
+ *
467
+ * @returns Map of locale → { key → translated string }
468
+ */
469
+ function collectCachedTranslations(lockfile, unchangedKeys, targetLocales, options = {}) {
470
+ const cached = {};
471
+ for (const locale of targetLocales) {
472
+ cached[locale] = {};
473
+ }
474
+ for (const { key } of unchangedKeys) {
475
+ const entry = lockfile.keys[key];
476
+ if (!entry)
477
+ continue;
478
+ for (const locale of targetLocales) {
479
+ if (entry.languages[locale] && !isStalePseudoEntry(entry, locale, options)) {
480
+ cached[locale][key] = entry.languages[locale];
481
+ }
482
+ }
483
+ }
484
+ return cached;
485
+ }
486
+ /**
487
+ * Compute stats for the UI: X translated, Y reused, Z changed.
488
+ */
489
+ function computeLockfileStats(partition) {
490
+ return {
491
+ reused: partition.unchanged.length,
492
+ translated: partition.newKeys.length +
493
+ partition.changed.length +
494
+ partition.missingLocales.length,
495
+ changed: partition.changed.length,
496
+ };
497
+ }
@@ -0,0 +1,196 @@
1
+ import { z } from 'zod';
2
+ export declare const lockfileEntrySchema: z.ZodObject<{
3
+ /** SHA-256 hex digest of the source string */
4
+ sourceHash: z.ZodString;
5
+ /** ISO-8601 timestamp of last translation */
6
+ translatedAt: z.ZodString;
7
+ /** Translated values keyed by locale code */
8
+ languages: z.ZodRecord<z.ZodString, z.ZodString>;
9
+ /** Metadata per locale */
10
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
11
+ fromMemory: z.ZodOptional<z.ZodBoolean>;
12
+ sourceHash: z.ZodOptional<z.ZodString>;
13
+ /** This key's translation for the locale is known to be in TM; skip backfill. */
14
+ inTm: z.ZodOptional<z.ZodBoolean>;
15
+ /** Pseudo-localized (`--provider pseudo`), not a translation: any real run re-translates it. */
16
+ pseudo: z.ZodOptional<z.ZodBoolean>;
17
+ /**
18
+ * Fingerprint of the glossary rules that applied to this key's source
19
+ * for the locale (see glossaryFingerprint). Absent: none applied.
20
+ * A run whose rules give a different fingerprint re-translates it.
21
+ */
22
+ glossaryHash: z.ZodOptional<z.ZodString>;
23
+ /**
24
+ * The translation came from the project's own files (e.g. a .po
25
+ * catalogue), not from Babelize: glossary edits don't overwrite it.
26
+ */
27
+ external: z.ZodOptional<z.ZodBoolean>;
28
+ }, "strip", z.ZodTypeAny, {
29
+ sourceHash?: string | undefined;
30
+ fromMemory?: boolean | undefined;
31
+ inTm?: boolean | undefined;
32
+ pseudo?: boolean | undefined;
33
+ glossaryHash?: string | undefined;
34
+ external?: boolean | undefined;
35
+ }, {
36
+ sourceHash?: string | undefined;
37
+ fromMemory?: boolean | undefined;
38
+ inTm?: boolean | undefined;
39
+ pseudo?: boolean | undefined;
40
+ glossaryHash?: string | undefined;
41
+ external?: boolean | undefined;
42
+ }>>>;
43
+ }, "strip", z.ZodTypeAny, {
44
+ sourceHash: string;
45
+ translatedAt: string;
46
+ languages: Record<string, string>;
47
+ metadata?: Record<string, {
48
+ sourceHash?: string | undefined;
49
+ fromMemory?: boolean | undefined;
50
+ inTm?: boolean | undefined;
51
+ pseudo?: boolean | undefined;
52
+ glossaryHash?: string | undefined;
53
+ external?: boolean | undefined;
54
+ }> | undefined;
55
+ }, {
56
+ sourceHash: string;
57
+ translatedAt: string;
58
+ languages: Record<string, string>;
59
+ metadata?: Record<string, {
60
+ sourceHash?: string | undefined;
61
+ fromMemory?: boolean | undefined;
62
+ inTm?: boolean | undefined;
63
+ pseudo?: boolean | undefined;
64
+ glossaryHash?: string | undefined;
65
+ external?: boolean | undefined;
66
+ }> | undefined;
67
+ }>;
68
+ export type LockfileEntry = z.infer<typeof lockfileEntrySchema>;
69
+ export declare const lockfileDataSchema: z.ZodObject<{
70
+ version: z.ZodDefault<z.ZodString>;
71
+ projectId: z.ZodString;
72
+ updatedAt: z.ZodString;
73
+ keys: z.ZodRecord<z.ZodString, z.ZodObject<{
74
+ /** SHA-256 hex digest of the source string */
75
+ sourceHash: z.ZodString;
76
+ /** ISO-8601 timestamp of last translation */
77
+ translatedAt: z.ZodString;
78
+ /** Translated values keyed by locale code */
79
+ languages: z.ZodRecord<z.ZodString, z.ZodString>;
80
+ /** Metadata per locale */
81
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
82
+ fromMemory: z.ZodOptional<z.ZodBoolean>;
83
+ sourceHash: z.ZodOptional<z.ZodString>;
84
+ /** This key's translation for the locale is known to be in TM; skip backfill. */
85
+ inTm: z.ZodOptional<z.ZodBoolean>;
86
+ /** Pseudo-localized (`--provider pseudo`), not a translation: any real run re-translates it. */
87
+ pseudo: z.ZodOptional<z.ZodBoolean>;
88
+ /**
89
+ * Fingerprint of the glossary rules that applied to this key's source
90
+ * for the locale (see glossaryFingerprint). Absent: none applied.
91
+ * A run whose rules give a different fingerprint re-translates it.
92
+ */
93
+ glossaryHash: z.ZodOptional<z.ZodString>;
94
+ /**
95
+ * The translation came from the project's own files (e.g. a .po
96
+ * catalogue), not from Babelize: glossary edits don't overwrite it.
97
+ */
98
+ external: z.ZodOptional<z.ZodBoolean>;
99
+ }, "strip", z.ZodTypeAny, {
100
+ sourceHash?: string | undefined;
101
+ fromMemory?: boolean | undefined;
102
+ inTm?: boolean | undefined;
103
+ pseudo?: boolean | undefined;
104
+ glossaryHash?: string | undefined;
105
+ external?: boolean | undefined;
106
+ }, {
107
+ sourceHash?: string | undefined;
108
+ fromMemory?: boolean | undefined;
109
+ inTm?: boolean | undefined;
110
+ pseudo?: boolean | undefined;
111
+ glossaryHash?: string | undefined;
112
+ external?: boolean | undefined;
113
+ }>>>;
114
+ }, "strip", z.ZodTypeAny, {
115
+ sourceHash: string;
116
+ translatedAt: string;
117
+ languages: Record<string, string>;
118
+ metadata?: Record<string, {
119
+ sourceHash?: string | undefined;
120
+ fromMemory?: boolean | undefined;
121
+ inTm?: boolean | undefined;
122
+ pseudo?: boolean | undefined;
123
+ glossaryHash?: string | undefined;
124
+ external?: boolean | undefined;
125
+ }> | undefined;
126
+ }, {
127
+ sourceHash: string;
128
+ translatedAt: string;
129
+ languages: Record<string, string>;
130
+ metadata?: Record<string, {
131
+ sourceHash?: string | undefined;
132
+ fromMemory?: boolean | undefined;
133
+ inTm?: boolean | undefined;
134
+ pseudo?: boolean | undefined;
135
+ glossaryHash?: string | undefined;
136
+ external?: boolean | undefined;
137
+ }> | undefined;
138
+ }>>;
139
+ }, "strip", z.ZodTypeAny, {
140
+ keys: Record<string, {
141
+ sourceHash: string;
142
+ translatedAt: string;
143
+ languages: Record<string, string>;
144
+ metadata?: Record<string, {
145
+ sourceHash?: string | undefined;
146
+ fromMemory?: boolean | undefined;
147
+ inTm?: boolean | undefined;
148
+ pseudo?: boolean | undefined;
149
+ glossaryHash?: string | undefined;
150
+ external?: boolean | undefined;
151
+ }> | undefined;
152
+ }>;
153
+ projectId: string;
154
+ version: string;
155
+ updatedAt: string;
156
+ }, {
157
+ keys: Record<string, {
158
+ sourceHash: string;
159
+ translatedAt: string;
160
+ languages: Record<string, string>;
161
+ metadata?: Record<string, {
162
+ sourceHash?: string | undefined;
163
+ fromMemory?: boolean | undefined;
164
+ inTm?: boolean | undefined;
165
+ pseudo?: boolean | undefined;
166
+ glossaryHash?: string | undefined;
167
+ external?: boolean | undefined;
168
+ }> | undefined;
169
+ }>;
170
+ projectId: string;
171
+ updatedAt: string;
172
+ version?: string | undefined;
173
+ }>;
174
+ export type LockfileData = z.infer<typeof lockfileDataSchema>;
175
+ export interface PartitionedKey {
176
+ key: string;
177
+ value: string;
178
+ }
179
+ export interface LockfilePartition {
180
+ /** Keys whose hash matches and all target locales exist in lockfile */
181
+ unchanged: PartitionedKey[];
182
+ /** Keys not present in lockfile at all */
183
+ newKeys: PartitionedKey[];
184
+ /** Keys whose source hash has changed */
185
+ changed: PartitionedKey[];
186
+ /** Keys whose hash matches but are missing some target locales */
187
+ missingLocales: Array<PartitionedKey & {
188
+ missingLocales: string[];
189
+ }>;
190
+ }
191
+ export interface LockfileStats {
192
+ reused: number;
193
+ translated: number;
194
+ changed: number;
195
+ }
196
+ export declare function createEmptyLockfile(projectId: string): LockfileData;