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