@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,325 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.flattenLocaleMessages = flattenLocaleMessages;
4
+ exports.checkLocalePair = checkLocalePair;
5
+ exports.tallyLocaleIssues = tallyLocaleIssues;
6
+ /**
7
+ * QA of an existing target locale against its source locale, without
8
+ * translating anything: missing/extra keys, empty and untranslated values,
9
+ * lost or invented placeholders, broken ICU messages and wrong plural forms
10
+ * (ICU and i18next `_one`/`_other` suffix keys), and glossary violations.
11
+ *
12
+ * Pure: takes flat key → text maps, returns issues in plain words. Used by the
13
+ * MCP `babelize_check` / `babelize_review` tools; the checks are the same ones
14
+ * the translate phase applies to fresh translations (placeholder protection,
15
+ * ICU comparison).
16
+ */
17
+ const icu_message_1 = require("../utils/icu-message");
18
+ const glossary_match_1 = require("../utils/glossary-match");
19
+ const placeholder_protection_1 = require("../utils/placeholder-protection");
20
+ const pseudo_markers_1 = require("../utils/pseudo-markers");
21
+ const icu_spans_1 = require("../utils/icu-spans");
22
+ const PLURAL_CATEGORIES = ['zero', 'one', 'two', 'few', 'many', 'other'];
23
+ const PLURAL_SUFFIX = /^(.+)_(zero|one|two|few|many|other)$/;
24
+ /**
25
+ * Flatten nested locale JSON into dotted keys with string values. The
26
+ * engine's `_meta` block is dropped; non-string leaves (numbers, arrays) are
27
+ * not messages and are skipped.
28
+ */
29
+ function flattenLocaleMessages(data, prefix = '', out = {}) {
30
+ if (!data || typeof data !== 'object' || Array.isArray(data))
31
+ return out;
32
+ for (const [k, v] of Object.entries(data)) {
33
+ if (!prefix && k === '_meta')
34
+ continue;
35
+ const key = prefix ? `${prefix}.${k}` : k;
36
+ if (typeof v === 'string')
37
+ out[key] = v;
38
+ else if (v && typeof v === 'object' && !Array.isArray(v))
39
+ flattenLocaleMessages(v, key, out);
40
+ }
41
+ return out;
42
+ }
43
+ function hasLetters(text) {
44
+ return /\p{L}/u.test(text);
45
+ }
46
+ function language(locale) {
47
+ return locale.toLowerCase().split(/[-_]/)[0];
48
+ }
49
+ function escapeRegex(s) {
50
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+ }
52
+ function containsTerm(text, term, ignoreCase) {
53
+ return new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(term)}(?![\\p{L}\\p{N}_])`, ignoreCase ? 'iu' : 'u').test(text);
54
+ }
55
+ /** Protected spans (variables, tags, URLs, …) with how often each occurs. */
56
+ function spanCounts(text, icu) {
57
+ const counts = new Map();
58
+ for (const span of (0, placeholder_protection_1.protectText)(text, [], { icu }).tokens) {
59
+ // Whitespace inside a variable is not meaningful ({ name } == {name}).
60
+ const norm = /^[{%$]/.test(span) ? span.replace(/\s+/g, '') : span;
61
+ counts.set(norm, (counts.get(norm) ?? 0) + 1);
62
+ }
63
+ return counts;
64
+ }
65
+ /** Variables and tags break the UI when lost; a changed URL or email may be deliberate. */
66
+ function isStructural(span) {
67
+ return /^[{%$<]/.test(span);
68
+ }
69
+ function quoteList(spans) {
70
+ return spans.map((s) => (s.length > 40 ? `${s.slice(0, 37)}…` : s)).join(', ');
71
+ }
72
+ function icuIssueCode(issue) {
73
+ switch (issue.code) {
74
+ case 'icu_invalid':
75
+ return 'icu_invalid';
76
+ case 'icu_plural_category_invalid':
77
+ return 'plural_category_invalid';
78
+ case 'icu_plural_category_missing':
79
+ return 'plural_category_missing';
80
+ default:
81
+ return 'icu_mismatch';
82
+ }
83
+ }
84
+ const COUNT_SPAN = /^\{\{?-?\s*count\s*\}?\}$/;
85
+ function pluralSpans(forms) {
86
+ const counts = forms.map((f) => spanCounts(f, false));
87
+ const required = new Map();
88
+ const allowed = new Set();
89
+ for (const c of counts)
90
+ for (const span of c.keys())
91
+ allowed.add(span);
92
+ for (const span of allowed) {
93
+ if (COUNT_SPAN.test(span))
94
+ continue;
95
+ const min = Math.min(...counts.map((c) => c.get(span) ?? 0));
96
+ if (min > 0)
97
+ required.set(span, min);
98
+ }
99
+ return { required, allowed };
100
+ }
101
+ function checkValue(key, source, target, locale, options, push, plural) {
102
+ if (target.trim() === '') {
103
+ if (source.trim() !== '')
104
+ push({ key, code: 'empty_value', severity: 'error', message: 'Value is empty; the app shows a blank string.' });
105
+ return;
106
+ }
107
+ if ((0, pseudo_markers_1.isPseudoLocalized)(target) && !(0, pseudo_markers_1.isPseudoLocalized)(source)) {
108
+ push({
109
+ key,
110
+ code: 'pseudo_translation',
111
+ severity: 'error',
112
+ message: 'Pseudo-localized test text (⟦…⟧ from --provider pseudo), not a translation. Run babelize translate with a real provider to replace it.',
113
+ });
114
+ }
115
+ const icu = (0, icu_message_1.isIcuMessage)(source) || (0, icu_message_1.isIcuMessage)(target);
116
+ if (icu) {
117
+ // The translate phase lets a translation add a plural over a count-like
118
+ // argument of a plain source ("{unreadCount} notes" → German one/other);
119
+ // QA accepts the same, or a correct run would fail `babelize check`.
120
+ const pluralizable = (0, icu_message_1.isIcuMessage)(source) ? [] : (0, icu_message_1.countArguments)(source);
121
+ for (const issue of (0, icu_message_1.compareIcuMessages)(source, target, locale, { pluralizable })) {
122
+ push({ key, code: icuIssueCode(issue), severity: issue.severity, message: `${issue.message.charAt(0).toUpperCase()}${issue.message.slice(1)}.` });
123
+ }
124
+ }
125
+ // Placeholders, tags, URLs: every span in the source must be in the
126
+ // translation exactly as often (in ICU messages braces are structure and
127
+ // were compared above, so only tags/URLs/code are counted).
128
+ const tgt = spanCounts(target, icu);
129
+ const lost = [];
130
+ const added = [];
131
+ if (plural && !icu) {
132
+ // A plural form is checked against all of the key's source forms.
133
+ for (const [span, n] of plural.required)
134
+ if ((tgt.get(span) ?? 0) < n)
135
+ lost.push(span);
136
+ for (const span of tgt.keys())
137
+ if (!plural.allowed.has(span) && !COUNT_SPAN.test(span))
138
+ added.push(span);
139
+ }
140
+ else if (icu) {
141
+ // An ICU message repeats a tag once per branch, and a plural added over a
142
+ // plain source does too: compare which spans appear, not how often (as
143
+ // the translate phase does).
144
+ const src = spanCounts(source, icu);
145
+ for (const span of src.keys())
146
+ if (!tgt.has(span))
147
+ lost.push(span);
148
+ for (const span of tgt.keys())
149
+ if (!src.has(span))
150
+ added.push(span);
151
+ }
152
+ else {
153
+ const src = spanCounts(source, icu);
154
+ for (const [span, n] of src)
155
+ if ((tgt.get(span) ?? 0) < n)
156
+ lost.push(span);
157
+ for (const [span, n] of tgt)
158
+ if ((src.get(span) ?? 0) < n)
159
+ added.push(span);
160
+ }
161
+ if (lost.length || added.length) {
162
+ const structural = [...lost, ...added].some(isStructural);
163
+ const parts = [
164
+ lost.length ? `lost ${quoteList(lost)}` : '',
165
+ added.length ? `added ${quoteList(added)}` : '',
166
+ ].filter(Boolean);
167
+ push({
168
+ key,
169
+ code: 'placeholder_mismatch',
170
+ severity: structural ? 'error' : 'warning',
171
+ message: `Placeholders differ from the source: ${parts.join('; ')}.`,
172
+ });
173
+ }
174
+ else if (icu) {
175
+ // Present somewhere is not enough: each branch must carry its source
176
+ // branch's spans (or the whole plain source, for an added plural).
177
+ const branch = (0, icu_spans_1.branchSpanProblems)(source, target);
178
+ if (branch.length) {
179
+ const all = branch.flatMap((p) => [...p.lost, ...p.added]);
180
+ push({
181
+ key,
182
+ code: 'placeholder_mismatch',
183
+ severity: all.some(isStructural) ? 'error' : 'warning',
184
+ message: `Placeholders differ from the source in ${branch.map(icu_spans_1.describeBranchSpanProblem).join('; ')}.`,
185
+ });
186
+ }
187
+ }
188
+ const neverTranslate = options.glossary?.neverTranslate ?? [];
189
+ if (target === source &&
190
+ hasLetters(source) &&
191
+ !(options.sourceLocale && language(options.sourceLocale) === language(locale)) &&
192
+ hasLetters((0, placeholder_protection_1.protectText)(source, neverTranslate, { icu }).text.replace(/\[#VAR_\d+#\]/g, ''))) {
193
+ const words = source.trim().split(/\s+/).filter(hasLetters).length;
194
+ push({
195
+ key,
196
+ code: 'untranslated',
197
+ severity: words >= 2 ? 'warning' : 'info',
198
+ message: words >= 2
199
+ ? 'Same text as the source; looks untranslated.'
200
+ : 'Same text as the source (fine for names or codes, otherwise untranslated).',
201
+ });
202
+ }
203
+ for (const term of neverTranslate) {
204
+ if (term.trim() && containsTerm(source, term, false) && !containsTerm(target, term, false)) {
205
+ push({ key, code: 'glossary_violation', severity: 'warning', message: `"${term}" must stay untranslated but is missing from the translation.` });
206
+ }
207
+ }
208
+ for (const entry of options.glossary?.alwaysTranslateAs ?? []) {
209
+ const expected = entry.translations[locale] ?? entry.translations[language(locale)];
210
+ if (!expected || !entry.term.trim())
211
+ continue;
212
+ // Same matcher as the translate phase: inflected forms count (Polish "obszaru roboczego").
213
+ if (containsTerm(source, entry.term, true) && !(0, glossary_match_1.containsGlossaryTranslation)(target, expected)) {
214
+ push({ key, code: 'glossary_violation', severity: 'warning', message: `"${entry.term}" should be translated as "${expected}".` });
215
+ }
216
+ }
217
+ }
218
+ /** Plural groups (i18next `key_one` / `key_other`) in a flat map: base → category → key. */
219
+ function pluralGroups(flat) {
220
+ const groups = new Map();
221
+ for (const key of Object.keys(flat)) {
222
+ const m = PLURAL_SUFFIX.exec(key);
223
+ if (!m)
224
+ continue;
225
+ const group = groups.get(m[1]) ?? new Map();
226
+ group.set(m[2], key);
227
+ groups.set(m[1], group);
228
+ }
229
+ return groups;
230
+ }
231
+ /**
232
+ * Check one target locale against the source locale. Both maps are flat
233
+ * (see {@link flattenLocaleMessages}). Issues come in key order, errors and
234
+ * warnings mixed; callers cap and sort as they need.
235
+ */
236
+ function checkLocalePair(source, target, locale, options = {}) {
237
+ const issues = [];
238
+ const push = (issue) => issues.push({ locale, ...issue });
239
+ const only = options.keys ? new Set(options.keys) : null;
240
+ const inScope = (key) => !only || only.has(key);
241
+ // A suffix group counts as plural only when the source has its `_other`
242
+ // form; `step_one` alone is just a key name.
243
+ const sourceGroups = new Map([...pluralGroups(source)].filter(([, g]) => g.has('other')));
244
+ const targetGroups = pluralGroups(target);
245
+ const pluralKeys = new Set();
246
+ for (const [base, group] of sourceGroups) {
247
+ for (const key of group.values())
248
+ pluralKeys.add(key);
249
+ for (const key of targetGroups.get(base)?.values() ?? [])
250
+ pluralKeys.add(key);
251
+ }
252
+ let total = 0;
253
+ let translated = 0;
254
+ for (const key of Object.keys(source)) {
255
+ if (pluralKeys.has(key) || !inScope(key))
256
+ continue;
257
+ total++;
258
+ const value = target[key];
259
+ if (value === undefined) {
260
+ push({ key, code: 'missing_key', severity: 'error', message: 'Missing in this locale; the app falls back to the source text.' });
261
+ continue;
262
+ }
263
+ if (value.trim() !== '')
264
+ translated++;
265
+ checkValue(key, source[key], value, locale, options, push);
266
+ }
267
+ const required = (0, icu_message_1.requiredPluralCategories)(locale).cardinal;
268
+ for (const [base, group] of sourceGroups) {
269
+ const groupKeys = [...group.values(), ...(targetGroups.get(base)?.values() ?? [])];
270
+ if (only && !groupKeys.some((k) => only.has(k)))
271
+ continue;
272
+ total++;
273
+ const label = `${base}_*`;
274
+ const present = targetGroups.get(base) ?? new Map();
275
+ if (present.size === 0) {
276
+ push({ key: label, code: 'missing_key', severity: 'error', message: 'Plural forms are missing in this locale; the app falls back to the source text.' });
277
+ continue;
278
+ }
279
+ translated++;
280
+ const missing = required.filter((c) => !present.has(c));
281
+ if (missing.length) {
282
+ push({
283
+ key: label,
284
+ code: 'plural_category_missing',
285
+ severity: 'error',
286
+ message: `Missing plural form${missing.length > 1 ? 's' : ''} ${missing.map((c) => `_${c}`).join(', ')} that ${locale} needs; those counts fall back to the source text.`,
287
+ });
288
+ }
289
+ // i18next resolves `_zero` for count 0 in every language: never unused.
290
+ const unused = [...present.keys()].filter((c) => c !== 'zero' && !required.includes(c) && PLURAL_CATEGORIES.includes(c));
291
+ if (unused.length) {
292
+ push({
293
+ key: label,
294
+ code: 'plural_category_invalid',
295
+ severity: 'warning',
296
+ message: `Has plural form${unused.length > 1 ? 's' : ''} ${unused.map((c) => `_${c}`).join(', ')}, which ${locale} doesn't use (valid: ${required.join(', ')}).`,
297
+ });
298
+ }
299
+ const spans = pluralSpans([...group.values()].map((k) => source[k]));
300
+ for (const [category, key] of present) {
301
+ const src = source[group.get(category) ?? group.get('other')];
302
+ checkValue(key, src, target[key], locale, options, push, spans);
303
+ }
304
+ }
305
+ for (const key of Object.keys(target)) {
306
+ if (key in source || pluralKeys.has(key) || !inScope(key))
307
+ continue;
308
+ push({ key, code: 'extra_key', severity: 'info', message: 'Not in the source locale (stale or renamed key).' });
309
+ }
310
+ return tallyLocaleIssues({ locale, total, translated, counts: {}, errors: 0, warnings: 0, issues });
311
+ }
312
+ /** Recompute `counts`/`errors`/`warnings` from `issues` (after adding issues to a report). */
313
+ function tallyLocaleIssues(report) {
314
+ const counts = {};
315
+ let errors = 0;
316
+ let warnings = 0;
317
+ for (const issue of report.issues) {
318
+ counts[issue.code] = (counts[issue.code] ?? 0) + 1;
319
+ if (issue.severity === 'error')
320
+ errors++;
321
+ else if (issue.severity === 'warning')
322
+ warnings++;
323
+ }
324
+ return { ...report, counts, errors, warnings };
325
+ }
@@ -0,0 +1,43 @@
1
+ import { type FindLocaleOptions, type LocaleFileSet } from './locale-files';
2
+ import { type LocaleGlossary, type LocalePairReport } from './locale-qa';
3
+ export interface ReviewOptions {
4
+ /** Ref to compare against (branch, tag, commit). Default HEAD (uncommitted changes). */
5
+ base?: string;
6
+ sourceLocale?: string;
7
+ /** Only these target locales. */
8
+ locales?: string[];
9
+ glossary?: LocaleGlossary;
10
+ /** Directory names skipped when looking for locale files. */
11
+ ignoreDirs?: FindLocaleOptions['ignoreDirs'];
12
+ }
13
+ export interface LocaleSetChangeReview {
14
+ dir: string;
15
+ layout: LocaleFileSet['layout'];
16
+ sourceLocale: string;
17
+ /** Source-locale keys added, changed and removed by the diff. */
18
+ source: {
19
+ added: string[];
20
+ changed: string[];
21
+ removed: string[];
22
+ };
23
+ /** Target locales touched by the diff (or affected by source changes). */
24
+ locales: Array<LocalePairReport & {
25
+ changedKeys: number;
26
+ removedKeys: number;
27
+ fileDeleted: boolean;
28
+ }>;
29
+ }
30
+ export interface LocaleChangeReview {
31
+ /** What the working tree was compared with. */
32
+ base: string;
33
+ /** Changed locale files, relative to the project. */
34
+ changedLocaleFiles: string[];
35
+ /** Other changed files (code), relative to the project. */
36
+ changedOtherFiles: number;
37
+ sets: LocaleSetChangeReview[];
38
+ }
39
+ export declare class ReviewError extends Error {
40
+ readonly code: 'not_a_repo' | 'git_missing' | 'bad_base';
41
+ constructor(message: string, code: 'not_a_repo' | 'git_missing' | 'bad_base');
42
+ }
43
+ export declare function reviewLocaleChanges(projectDir: string, options?: ReviewOptions): Promise<LocaleChangeReview>;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReviewError = void 0;
4
+ exports.reviewLocaleChanges = reviewLocaleChanges;
5
+ /**
6
+ * Review the locale changes in a git diff: QA only the entries that changed
7
+ * (same checks as checkProjectLocales), plus source strings whose
8
+ * translations weren't updated and new source keys a locale is missing.
9
+ * Read-only; compares the working tree with HEAD or a base ref.
10
+ */
11
+ const child_process_1 = require("child_process");
12
+ const util_1 = require("util");
13
+ const git_1 = require("../local/git");
14
+ const check_project_1 = require("./check-project");
15
+ const locale_files_1 = require("./locale-files");
16
+ const locale_qa_1 = require("./locale-qa");
17
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
18
+ class ReviewError extends Error {
19
+ constructor(message, code) {
20
+ super(message);
21
+ this.code = code;
22
+ this.name = 'ReviewError';
23
+ }
24
+ }
25
+ exports.ReviewError = ReviewError;
26
+ /** Git's empty tree: the base of a repository without commits. */
27
+ const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
28
+ async function git(cwd, args) {
29
+ const { stdout } = await execFileAsync('git', args, { cwd, maxBuffer: 256 * 1024 * 1024 });
30
+ return stdout;
31
+ }
32
+ async function resolveBase(projectDir, base) {
33
+ if (base !== undefined) {
34
+ // A ref, never an option (`--output=…`) or a range.
35
+ if (!/^[\w./@^~{}-]+$/.test(base) || base.startsWith('-') || base.includes('..')) {
36
+ throw new ReviewError(`"${base}" is not a valid git ref.`, 'bad_base');
37
+ }
38
+ try {
39
+ const sha = (await git(projectDir, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${base}^{commit}`])).trim();
40
+ return { ref: sha, label: base };
41
+ }
42
+ catch {
43
+ throw new ReviewError(`Unknown git ref "${base}". Use a branch, tag or commit (e.g. main, origin/main, HEAD~1).`, 'bad_base');
44
+ }
45
+ }
46
+ try {
47
+ const sha = (await git(projectDir, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'])).trim();
48
+ return { ref: sha, label: 'HEAD' };
49
+ }
50
+ catch {
51
+ return { ref: EMPTY_TREE, label: 'empty repository' };
52
+ }
53
+ }
54
+ const lines = (out) => out.split('\n').map((l) => l.trim()).filter(Boolean);
55
+ function diffKeys(now, old) {
56
+ const added = [];
57
+ const changed = [];
58
+ const removed = [];
59
+ for (const [k, v] of Object.entries(now)) {
60
+ if (!(k in old))
61
+ added.push(k);
62
+ else if (old[k] !== v)
63
+ changed.push(k);
64
+ }
65
+ for (const k of Object.keys(old))
66
+ if (!(k in now))
67
+ removed.push(k);
68
+ return { added, changed, removed };
69
+ }
70
+ function clip(text, max = 60) {
71
+ const one = text.replace(/\s+/g, ' ').trim();
72
+ return one.length > max ? `${one.slice(0, max - 1)}…` : one;
73
+ }
74
+ async function reviewLocaleChanges(projectDir, options = {}) {
75
+ try {
76
+ await (0, git_1.assertGitRepo)(projectDir);
77
+ }
78
+ catch (err) {
79
+ const code = err.code === 'git_missing' ? 'git_missing' : 'not_a_repo';
80
+ throw new ReviewError(err.message, code);
81
+ }
82
+ const { ref, label } = await resolveBase(projectDir, options.base);
83
+ // Paths relative to projectDir (--relative also limits the diff to it).
84
+ const changed = new Set([
85
+ ...lines(await git(projectDir, ['diff', '--name-only', '--relative', ref, '--', '.'])),
86
+ ...lines(await git(projectDir, ['ls-files', '--others', '--exclude-standard', '--', '.'])),
87
+ ]);
88
+ const readOld = async (rel) => {
89
+ if (ref === EMPTY_TREE)
90
+ return null;
91
+ try {
92
+ return await git(projectDir, ['show', `${ref}:./${rel}`]);
93
+ }
94
+ catch {
95
+ return null; // new file
96
+ }
97
+ };
98
+ const config = await (0, check_project_1.readProjectLocaleConfig)(projectDir);
99
+ // Sets from the working tree and the base, so deleted locale files count.
100
+ const basePaths = ref === EMPTY_TREE ? [] : lines(await git(projectDir, ['ls-tree', '-r', '--name-only', ref, '--', '.']));
101
+ const nowSets = await (0, locale_files_1.findLocaleFileSets)(projectDir, { ignoreDirs: options.ignoreDirs });
102
+ const sets = (0, locale_files_1.groupLocaleFiles)([...nowSets.flatMap((st) => Object.values(st.files).flat()), ...basePaths], { ignoreDirs: options.ignoreDirs });
103
+ const sourceLocale = options.sourceLocale ?? config.sourceLocale ?? 'en';
104
+ const glossary = options.glossary ?? config.glossary;
105
+ const localeFiles = new Set(sets.flatMap((st) => Object.values(st.files).flat()));
106
+ const reviews = [];
107
+ for (const set of sets) {
108
+ const setFiles = Object.values(set.files).flat();
109
+ if (!set.files[sourceLocale] || !setFiles.some((f) => changed.has(f)))
110
+ continue;
111
+ const srcNow = (await (0, locale_files_1.loadLocale)(projectDir, set, sourceLocale)).messages;
112
+ const srcOld = (await (0, locale_files_1.loadLocale)(projectDir, set, sourceLocale, readOld)).messages;
113
+ const source = diffKeys(srcNow, srcOld);
114
+ const locales = [];
115
+ const targets = Object.keys(set.files)
116
+ .filter((l) => l !== sourceLocale && (!options.locales?.length || options.locales.includes(l)))
117
+ .sort();
118
+ for (const locale of targets) {
119
+ const nowLoaded = await (0, locale_files_1.loadLocale)(projectDir, set, locale);
120
+ const tgtNow = nowLoaded.messages;
121
+ const tgtOld = (await (0, locale_files_1.loadLocale)(projectDir, set, locale, readOld)).messages;
122
+ const fileDeleted = set.files[locale].every((f) => changed.has(f)) && Object.keys(tgtNow).length === 0 && nowLoaded.errors.length === 0 && Object.keys(tgtOld).length > 0;
123
+ const changedTarget = Object.keys(tgtNow).filter((k) => tgtNow[k] !== tgtOld[k]);
124
+ // Deleted translations whose source key still exists.
125
+ const removedTarget = Object.keys(tgtOld).filter((k) => !(k in tgtNow) && k in srcNow);
126
+ const removed = new Set(removedTarget);
127
+ const keys = fileDeleted
128
+ ? new Set(Object.keys(srcNow))
129
+ : new Set([...changedTarget, ...source.added, ...source.changed, ...source.removed.filter((k) => k in tgtNow)].filter((k) => !removed.has(k)));
130
+ if (keys.size === 0 && removedTarget.length === 0)
131
+ continue;
132
+ const report = (0, locale_qa_1.checkLocalePair)(srcNow, tgtNow, locale, { sourceLocale, glossary, keys: [...keys] });
133
+ if (fileDeleted) {
134
+ report.issues.unshift({
135
+ locale,
136
+ key: '*',
137
+ code: 'translation_removed',
138
+ severity: 'error',
139
+ message: `The ${locale} locale file was deleted: all ${Object.keys(srcNow).length} source keys are now missing in ${locale}.`,
140
+ });
141
+ }
142
+ else {
143
+ for (const key of removedTarget) {
144
+ report.issues.push({
145
+ locale,
146
+ key,
147
+ code: 'translation_removed',
148
+ severity: 'error',
149
+ message: `Translation "${clip(tgtOld[key])}" was deleted but the source still has this key; the app falls back to the source text.`,
150
+ });
151
+ }
152
+ }
153
+ for (const key of source.changed) {
154
+ if (tgtNow[key] !== undefined && tgtNow[key] === tgtOld[key]) {
155
+ report.issues.push({
156
+ locale,
157
+ key,
158
+ code: 'stale_translation',
159
+ severity: 'warning',
160
+ message: `Source changed from "${clip(srcOld[key])}" to "${clip(srcNow[key])}" but this translation wasn't updated.`,
161
+ });
162
+ }
163
+ }
164
+ locales.push({ ...(0, locale_qa_1.tallyLocaleIssues)(report), changedKeys: changedTarget.length, removedKeys: removedTarget.length, fileDeleted });
165
+ }
166
+ reviews.push({ dir: set.dir, layout: set.layout, sourceLocale, source, locales });
167
+ }
168
+ const changedLocaleFiles = [...changed].filter((f) => localeFiles.has(f)).sort();
169
+ return {
170
+ base: label,
171
+ changedLocaleFiles,
172
+ changedOtherFiles: changed.size - changedLocaleFiles.length,
173
+ sets: reviews,
174
+ };
175
+ }
@@ -0,0 +1,8 @@
1
+ import type { EngineConfigV2 } from './types';
2
+ /**
3
+ * Runtime config validation for the backend engine (v2).
4
+ *
5
+ * This mirrors the existing backend contract used by workers/controllers.
6
+ * (A separate Zod schema exists for the internal v2 modules.)
7
+ */
8
+ export declare function validateEngineConfigV2(config: Partial<EngineConfigV2>): EngineConfigV2;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateEngineConfigV2 = validateEngineConfigV2;
4
+ /**
5
+ * Runtime config validation for the backend engine (v2).
6
+ *
7
+ * This mirrors the existing backend contract used by workers/controllers.
8
+ * (A separate Zod schema exists for the internal v2 modules.)
9
+ */
10
+ function validateEngineConfigV2(config) {
11
+ if (!config.workspacePath) {
12
+ throw new Error('workspacePath is required');
13
+ }
14
+ const confidenceThreshold = config.confidenceThreshold ?? 80;
15
+ if (confidenceThreshold < 0 || confidenceThreshold > 100) {
16
+ throw new Error(`confidenceThreshold must be between 0 and 100, got ${confidenceThreshold}`);
17
+ }
18
+ return {
19
+ workspacePath: config.workspacePath,
20
+ inputType: config.inputType || 'github',
21
+ sourceLocale: config.sourceLocale || 'en',
22
+ targetLocales: config.targetLocales || [],
23
+ framework: config.framework,
24
+ confidenceThreshold,
25
+ dryRun: config.dryRun ?? false,
26
+ forceTranslation: config.forceTranslation ?? false,
27
+ onProgress: config.onProgress,
28
+ projectId: config.projectId,
29
+ filesToProcess: config.filesToProcess,
30
+ byok: config.byok,
31
+ providers: config.providers,
32
+ formatConfig: config.formatConfig,
33
+ staticRuntime: config.staticRuntime,
34
+ installDependencies: config.installDependencies,
35
+ };
36
+ }
@@ -0,0 +1,3 @@
1
+ export type { EngineConfigV2, EngineResultV2 } from './types';
2
+ export { validateEngineConfigV2 } from './config';
3
+ export { runEngineV2 } from '../orchestrator';
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runEngineV2 = exports.validateEngineConfigV2 = void 0;
4
+ var config_1 = require("./config");
5
+ Object.defineProperty(exports, "validateEngineConfigV2", { enumerable: true, get: function () { return config_1.validateEngineConfigV2; } });
6
+ // `runEngineV2` is implemented in `../orchestrator` as the single wiring point.
7
+ var orchestrator_1 = require("../orchestrator");
8
+ Object.defineProperty(exports, "runEngineV2", { enumerable: true, get: function () { return orchestrator_1.runEngineV2; } });