@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,677 @@
1
+ "use strict";
2
+ /**
3
+ * The gettext catalogue of a Django run: which messages the code uses, the
4
+ * existing translations in the project's .po files, and the merged .po (and,
5
+ * when the project commits them, .mo) files the run writes.
6
+ *
7
+ * Existing translations are the project's: a complete, non-fuzzy msgstr is
8
+ * never overwritten and never re-translated (it seeds the lockfile instead).
9
+ * Fuzzy entries are left for review. Only missing or empty entries are
10
+ * filled. Untouched entries are written back byte-for-byte.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.GettextCatalog = void 0;
47
+ exports.formatProblem = formatProblem;
48
+ exports.formatPoDate = formatPoDate;
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const hasher_1 = require("../modules/lockfile/hasher");
52
+ const file_utils_1 = require("../utils/file-utils");
53
+ const pseudo_markers_1 = require("../utils/pseudo-markers");
54
+ const untranslated_1 = require("../utils/untranslated");
55
+ const icu_message_1 = require("../utils/icu-message");
56
+ const extract_1 = require("./extract");
57
+ const keys_1 = require("./keys");
58
+ const locale_names_1 = require("./locale-names");
59
+ const plural_forms_1 = require("./plural-forms");
60
+ const po_file_1 = require("./po-file");
61
+ const mo_file_1 = require("./mo-file");
62
+ function compareRefs(a, b) {
63
+ const fa = a.slice(0, a.lastIndexOf(':'));
64
+ const fb = b.slice(0, b.lastIndexOf(':'));
65
+ if (fa !== fb)
66
+ return fa < fb ? -1 : 1;
67
+ return Number(a.slice(a.lastIndexOf(':') + 1)) - Number(b.slice(b.lastIndexOf(':') + 1));
68
+ }
69
+ function sortRefs(refs) {
70
+ return [...new Set(refs)].sort(compareRefs);
71
+ }
72
+ function isComplete(e, nplurals) {
73
+ if (e.msgstr.length === 0 || e.msgstr.some((s) => !s))
74
+ return false;
75
+ return e.msgidPlural === undefined ? e.msgstr.length >= 1 : e.msgstr.length === nplurals;
76
+ }
77
+ /** Text of an ICU branch, with `#` as the count placeholder of the source. */
78
+ function branchText(nodes, pound) {
79
+ let out = '';
80
+ for (const n of nodes) {
81
+ if (n.type === 'text')
82
+ out += n.value;
83
+ else if (n.type === 'pound') {
84
+ if (pound === null)
85
+ return null;
86
+ out += pound;
87
+ }
88
+ else
89
+ return null;
90
+ }
91
+ return out;
92
+ }
93
+ const PY_NAMED = /%\((\w+)\)[-#0 +]*\d*(?:\.\d+)?[sdiouxXeEfFgGcr]/g;
94
+ const PY_POSITIONAL = /%(?!\()[-#0 +]*\d*(?:\.\d+)?[sdiouxXeEfFgGcr]/g;
95
+ const BRACE_NAMED = /\{([A-Za-z_]\w*)(?:[.[][^{}]*)?(?:![rsa])?(?::[^{}]*)?\}/g;
96
+ const BRACE_POSITIONAL = /\{(\d*)(?:![rsa])?(?::[^{}]*)?\}/g;
97
+ function placeholders(text, flags) {
98
+ const t = text.replace(/%%/g, '').replace(/\{\{|\}\}/g, '');
99
+ if (flags.includes('python-format')) {
100
+ return {
101
+ named: [...t.matchAll(PY_NAMED)].map((m) => m[1]).sort(),
102
+ positional: [...t.matchAll(PY_POSITIONAL)].map((m) => m[0].slice(-1)),
103
+ };
104
+ }
105
+ if (flags.includes('python-brace-format')) {
106
+ return {
107
+ named: [...t.matchAll(BRACE_NAMED)].map((m) => m[1]).sort(),
108
+ positional: [...t.matchAll(BRACE_POSITIONAL)].map((m) => m[1] || '{}'),
109
+ };
110
+ }
111
+ return { named: [], positional: [] };
112
+ }
113
+ /**
114
+ * Why a translated form would fail `msgfmt --check-format` (and break
115
+ * `compilemessages`), or null if it's fine.
116
+ */
117
+ function formatProblem(source, translated, flags, allowDroppingCount, union) {
118
+ if (!flags.includes('python-format') && !flags.includes('python-brace-format'))
119
+ return null;
120
+ const want = placeholders(source, flags);
121
+ const got = placeholders(translated, flags);
122
+ const allowed = new Set(placeholders(union ?? source, flags).named);
123
+ const invented = got.named.filter((n) => !allowed.has(n));
124
+ if (invented.length)
125
+ return `adds placeholder ${invented.map((n) => `%(${n})s`).join(', ')} that is not in the source`;
126
+ const missing = want.named.filter((n) => !got.named.includes(n));
127
+ if (missing.length && !allowDroppingCount)
128
+ return `drops placeholder ${missing.map((n) => `%(${n})s`).join(', ')}`;
129
+ if (got.positional.join(',') !== want.positional.join(',') && !(allowDroppingCount && got.positional.length === 0)) {
130
+ return `positional placeholders differ (source has ${want.positional.length}, translation has ${got.positional.length})`;
131
+ }
132
+ return null;
133
+ }
134
+ class GettextCatalog {
135
+ constructor(workspace, entries) {
136
+ this.workspace = workspace;
137
+ this.byKey = new Map();
138
+ this.states = new Map();
139
+ this.entries = entries;
140
+ for (const entry of entries) {
141
+ for (const [form, key] of Object.entries(entry.keys)) {
142
+ if (key)
143
+ this.byKey.set(key, { entry, form: form });
144
+ }
145
+ }
146
+ }
147
+ /** Build the run's catalogue and read the existing .po file of every target locale. */
148
+ static async create(workspace, strings, keys, targetLocales) {
149
+ const byMessage = new Map();
150
+ for (const s of strings) {
151
+ const g = s.gettext;
152
+ if (!g)
153
+ continue;
154
+ const k = (0, po_file_1.entryKey)(g.msgctxt, g.msgid);
155
+ let entry = byMessage.get(k);
156
+ if (!entry) {
157
+ entry = { message: { ...g, flags: [...g.flags], comments: [...g.comments] }, references: [], keys: {} };
158
+ byMessage.set(k, entry);
159
+ }
160
+ else {
161
+ // The same msgid used as plural somewhere wins (gettext has one entry per msgid).
162
+ if (g.msgidPlural !== undefined && entry.message.msgidPlural === undefined)
163
+ entry.message = { ...entry.message, msgidPlural: g.msgidPlural, icu: g.icu };
164
+ for (const f of g.flags)
165
+ if (!entry.message.flags.includes(f))
166
+ entry.message.flags.push(f);
167
+ for (const c of g.comments)
168
+ if (!entry.message.comments.includes(c))
169
+ entry.message.comments.push(c);
170
+ entry.message.wrapped = entry.message.wrapped && g.wrapped;
171
+ }
172
+ entry.references.push(`${s.file}:${s.line}`);
173
+ }
174
+ for (const key of keys) {
175
+ if (!key.gettext)
176
+ continue;
177
+ const entry = byMessage.get((0, po_file_1.entryKey)(key.gettext.msgctxt, key.gettext.msgid));
178
+ if (!entry)
179
+ continue;
180
+ // A key for the singular-only variant of a message that is plural elsewhere is dropped.
181
+ if ((key.gettext.msgidPlural ?? null) !== (entry.message.msgidPlural ?? null))
182
+ continue;
183
+ const form = key.gettext.form;
184
+ entry.keys[form === 'single' ? 'single' : form] = key.key;
185
+ }
186
+ const entries = [...byMessage.values()].filter((e) => Object.values(e.keys).some(Boolean));
187
+ for (const e of entries)
188
+ e.references = sortRefs(e.references);
189
+ entries.sort((a, b) => compareRefs(a.references[0] ?? '', b.references[0] ?? ''));
190
+ const catalog = new GettextCatalog(workspace, entries);
191
+ for (const locale of targetLocales)
192
+ await catalog.loadLocale(locale);
193
+ return catalog;
194
+ }
195
+ /** The Django project's name: the package of its settings module (from manage.py). */
196
+ projectName() {
197
+ try {
198
+ const manage = fs.readFileSync(path.join(this.workspace.path, this.workspace.djangoRoot ?? '.', 'manage.py'), 'utf8');
199
+ const m = /DJANGO_SETTINGS_MODULE['"]\s*,\s*['"]([\w]+)/.exec(manage);
200
+ if (m)
201
+ return m[1];
202
+ }
203
+ catch {
204
+ // no manage.py
205
+ }
206
+ return 'django-project';
207
+ }
208
+ async loadLocale(locale) {
209
+ const existing = this.states.get(locale);
210
+ if (existing)
211
+ return existing;
212
+ const { file, relativeFile } = (0, file_utils_1.localeFilePath)(this.workspace.path, this.workspace.publicDir, 'po', locale);
213
+ let content = null;
214
+ let parsed = null;
215
+ try {
216
+ content = await fs.promises.readFile(file, 'utf8');
217
+ }
218
+ catch (err) {
219
+ if (err?.code !== 'ENOENT')
220
+ throw err;
221
+ }
222
+ if (content !== null) {
223
+ try {
224
+ parsed = (0, po_file_1.parsePo)(content);
225
+ }
226
+ catch (err) {
227
+ // Never overwrite a catalogue we can't read correctly.
228
+ throw new Error(`Could not parse ${relativeFile}: ${err.message}`);
229
+ }
230
+ }
231
+ let forms = null;
232
+ let guessed = false;
233
+ const header = (0, po_file_1.getHeaderField)(parsed?.header, 'Plural-Forms');
234
+ if (header) {
235
+ try {
236
+ forms = (0, plural_forms_1.parsePluralForms)(header);
237
+ }
238
+ catch {
239
+ forms = null;
240
+ }
241
+ }
242
+ if (!forms) {
243
+ const fallback = (0, plural_forms_1.pluralFormsFor)(locale);
244
+ forms = (0, plural_forms_1.parsePluralForms)(fallback.header);
245
+ guessed = !fallback.known;
246
+ }
247
+ const state = {
248
+ locale, file, relativeFile, existing: parsed, existingContent: content, forms,
249
+ categories: (0, plural_forms_1.indexCategories)(forms, locale), guessedForms: guessed,
250
+ };
251
+ this.states.set(locale, state);
252
+ return state;
253
+ }
254
+ existingEntry(state, entry) {
255
+ const k = (0, po_file_1.entryKey)(entry.message.msgctxt, entry.message.msgid);
256
+ return state.existing?.entries.find((e) => !e.obsolete && (0, po_file_1.entryKey)(e.msgctxt, e.msgid) === k);
257
+ }
258
+ /**
259
+ * Translations already in the .po files, by engine key: they count as done
260
+ * (seeded into the lockfile) so they are neither re-translated nor billed.
261
+ */
262
+ existingTranslations(locale) {
263
+ const state = this.states.get(locale);
264
+ const out = {};
265
+ if (!state?.existing)
266
+ return out;
267
+ for (const entry of this.entries) {
268
+ const e = this.existingEntry(state, entry);
269
+ if (!e || e.flags.includes('fuzzy'))
270
+ continue;
271
+ if ((e.msgidPlural ?? null) !== (entry.message.msgidPlural ?? null))
272
+ continue;
273
+ if (!isComplete(e, state.forms.nplurals))
274
+ continue;
275
+ if (entry.keys.single)
276
+ out[entry.keys.single] = e.msgstr[0];
277
+ if (entry.keys.singular || entry.keys.plural) {
278
+ const one = state.categories.indexOf('one');
279
+ const other = state.categories.lastIndexOf('other');
280
+ const pl = e.msgstr[other !== -1 ? other : e.msgstr.length - 1];
281
+ if (entry.keys.singular)
282
+ out[entry.keys.singular] = e.msgstr[one !== -1 ? one : 0];
283
+ if (entry.keys.plural)
284
+ out[entry.keys.plural] = pl;
285
+ }
286
+ if (entry.keys.icu) {
287
+ if (e.msgstr.some((f) => /[{}#]/.test(f)))
288
+ continue;
289
+ const branches = [];
290
+ const seen = new Set();
291
+ state.categories.forEach((cat, i) => {
292
+ if (seen.has(cat))
293
+ return;
294
+ seen.add(cat);
295
+ branches.push(`${cat} {${e.msgstr[i]}}`);
296
+ });
297
+ if (!seen.has('other'))
298
+ branches.push(`other {${e.msgstr[e.msgstr.length - 1]}}`);
299
+ out[entry.keys.icu] = `{${keys_1.ICU_PLURAL_ARG}, plural, ${branches.join(' ')}}`;
300
+ }
301
+ }
302
+ return out;
303
+ }
304
+ /**
305
+ * For each ICU-plural key: the placeholder that shows the count in its
306
+ * branches (`%(count)d`, or a lone `%d`). A plain-text translator gets a
307
+ * number of each plural category in its place, so every gettext form is
308
+ * inflected for its own numbers.
309
+ */
310
+ pluralValuePlaceholders() {
311
+ const out = {};
312
+ for (const entry of this.entries) {
313
+ const m = entry.message;
314
+ if (!entry.keys.icu || m.msgidPlural === undefined || !m.flags.includes('python-format'))
315
+ continue;
316
+ const text = m.msgidPlural.replace(/%%/g, '');
317
+ const named = [...text.matchAll(PY_NAMED)];
318
+ if (named.length > 0) {
319
+ if (new Set(named.map((x) => x[1])).size === 1)
320
+ out[entry.keys.icu] = named[0][0];
321
+ continue;
322
+ }
323
+ const positional = [...text.matchAll(PY_POSITIONAL)];
324
+ if (positional.length === 1)
325
+ out[entry.keys.icu] = positional[0][0];
326
+ }
327
+ return out;
328
+ }
329
+ /** Put existing .po translations into the lockfile (they win over its cached values). */
330
+ seedLockfile(lockfile, flatKeys) {
331
+ const next = JSON.parse(JSON.stringify(lockfile));
332
+ const now = new Date().toISOString();
333
+ for (const state of this.states.values()) {
334
+ for (const [key, value] of Object.entries(this.existingTranslations(state.locale))) {
335
+ const source = flatKeys[key];
336
+ if (source === undefined)
337
+ continue;
338
+ const hash = (0, hasher_1.hashSourceString)(source);
339
+ const entry = next.keys[key] ?? { sourceHash: hash, translatedAt: now, languages: {}, metadata: {} };
340
+ if (entry.sourceHash !== hash) {
341
+ entry.sourceHash = hash;
342
+ entry.languages = {};
343
+ entry.metadata = {};
344
+ }
345
+ // A value Babelize didn't write (a translator edited the .po file) is
346
+ // the project's own: glossary edits must not re-translate it.
347
+ if (entry.languages[state.locale] !== value) {
348
+ entry.metadata = { ...(entry.metadata ?? {}), [state.locale]: { external: true } };
349
+ }
350
+ entry.languages[state.locale] = value;
351
+ if (entry.metadata?.[state.locale]?.pseudo && !(0, pseudo_markers_1.isPseudoLocalized)(value))
352
+ delete entry.metadata[state.locale];
353
+ next.keys[key] = entry;
354
+ }
355
+ }
356
+ return next;
357
+ }
358
+ /**
359
+ * msgstr forms for an entry from the run's translations, or a reason it
360
+ * can't have them yet (null when simply untranslated).
361
+ */
362
+ formsFor(entry, locale, values) {
363
+ const state = this.states.get(locale);
364
+ const m = entry.message;
365
+ if (entry.keys.single) {
366
+ const v = values[entry.keys.single];
367
+ if (!v)
368
+ return null;
369
+ const problem = formatProblem(m.msgid, v, m.flags, false);
370
+ return problem ? { problem, key: entry.keys.single } : { forms: [v] };
371
+ }
372
+ const byCategory = new Map();
373
+ let failKey = entry.keys.icu ?? entry.keys.plural;
374
+ if (entry.keys.icu) {
375
+ const v = values[entry.keys.icu];
376
+ if (!v)
377
+ return null;
378
+ let nodes;
379
+ try {
380
+ nodes = (0, icu_message_1.parseIcu)(v);
381
+ }
382
+ catch (err) {
383
+ return { problem: `translation is not a valid ICU plural: ${err.message}`, key: entry.keys.icu };
384
+ }
385
+ // One plural; text around it belongs to every form ("Total: {count, plural, …}").
386
+ const pluralIdx = nodes.findIndex((n) => n.type === 'plural');
387
+ const plural = nodes[pluralIdx];
388
+ const outside = nodes.filter((_, i) => i !== pluralIdx);
389
+ if (!plural || outside.some((n) => n.type !== 'text')) {
390
+ return { problem: 'translation is not a single ICU plural', key: entry.keys.icu };
391
+ }
392
+ const prefix = nodes.slice(0, pluralIdx).map((n) => (n.type === 'text' ? n.value : '')).join('');
393
+ const suffix = nodes.slice(pluralIdx + 1).map((n) => (n.type === 'text' ? n.value : '')).join('');
394
+ const countNames = placeholders(m.msgidPlural ?? '', m.flags).named;
395
+ const pound = countNames.length === 1 ? `%(${countNames[0]})s` : null;
396
+ for (const [cat, branch] of Object.entries(plural.options)) {
397
+ const text = branchText(branch, pound);
398
+ if (text === null)
399
+ return { problem: `plural branch "${cat}" uses ICU syntax gettext can't express`, key: entry.keys.icu };
400
+ byCategory.set(cat === '=1' && !plural.options.one ? 'one' : cat, `${prefix}${text}${suffix}`);
401
+ }
402
+ }
403
+ else {
404
+ const sg = values[entry.keys.singular];
405
+ const pl = values[entry.keys.plural];
406
+ if (!sg || !pl)
407
+ return null;
408
+ byCategory.set('one', sg);
409
+ byCategory.set('other', pl);
410
+ }
411
+ const forms = [];
412
+ for (const cat of state.categories) {
413
+ const text = byCategory.get(cat) ?? byCategory.get('other');
414
+ if (!text)
415
+ return null;
416
+ const isOne = cat === 'one' || cat === 'zero' || cat === 'two';
417
+ const src = cat === 'one' ? m.msgid : m.msgidPlural;
418
+ if (!entry.keys.icu)
419
+ failKey = cat === 'one' ? entry.keys.singular : entry.keys.plural;
420
+ const problem = formatProblem(src, text, m.flags, isOne, `${m.msgid} ${m.msgidPlural}`);
421
+ if (problem)
422
+ return { problem: `${cat} form ${problem}`, key: failKey };
423
+ forms.push(text);
424
+ }
425
+ return { forms };
426
+ }
427
+ /**
428
+ * Check every fresh translation can go into a .po file that compiles:
429
+ * placeholders must match the source (msgfmt --check-format), plurals must
430
+ * map onto the language's forms. Bad values are removed and reported as
431
+ * failures (and retried next run) — never shipped, never replaced by the
432
+ * source text.
433
+ */
434
+ validate(translation, sourceValues) {
435
+ for (const localeFile of translation.locales) {
436
+ const state = this.states.get(localeFile.locale);
437
+ if (!state)
438
+ continue;
439
+ const values = localeFile.data;
440
+ const otherScript = !(0, untranslated_1.isLatinScript)(localeFile.locale);
441
+ for (const entry of this.entries) {
442
+ const keys = Object.values(entry.keys).filter((k) => !!k && typeof values[k] === 'string');
443
+ if (!keys.length)
444
+ continue;
445
+ // Into a non-Latin-script language, English text that came back unchanged
446
+ // was not translated: report it (and retry next run) rather than writing
447
+ // it into the catalogue as if it were a translation.
448
+ const unchanged = otherScript ? keys.find((k) => values[k] === sourceValues[k] && (0, untranslated_1.hasWords)(sourceValues[k])) : undefined;
449
+ if (unchanged) {
450
+ for (const k of keys)
451
+ delete values[k];
452
+ (translation.failures ?? (translation.failures = [])).push({
453
+ locale: localeFile.locale,
454
+ key: unchanged,
455
+ source: sourceValues[unchanged],
456
+ reason: 'untranslated',
457
+ retryable: true,
458
+ message: `Returned unchanged instead of translated into ${localeFile.locale}`,
459
+ });
460
+ continue;
461
+ }
462
+ const r = this.formsFor(entry, localeFile.locale, values);
463
+ if (r && 'problem' in r) {
464
+ for (const k of keys)
465
+ delete values[k];
466
+ (translation.failures ?? (translation.failures = [])).push({
467
+ locale: localeFile.locale,
468
+ key: r.key,
469
+ source: sourceValues[r.key] ?? entry.message.msgid,
470
+ reason: 'placeholder_lost',
471
+ retryable: true,
472
+ message: `Placeholder problem: ${r.problem}`,
473
+ });
474
+ }
475
+ }
476
+ }
477
+ }
478
+ /** Point references at where strings are after injection (lines move when imports/tags are added). */
479
+ refreshReferences(injection) {
480
+ const fresh = new Map();
481
+ const files = new Set();
482
+ for (const f of injection.modifiedFiles) {
483
+ const analysis = (0, extract_1.analyzeDjangoFile)(f.modifiedContent, f.file);
484
+ if (!analysis)
485
+ continue;
486
+ files.add(f.file);
487
+ for (const finding of analysis.findings) {
488
+ const k = (0, po_file_1.entryKey)(finding.message.msgctxt, finding.message.msgid);
489
+ fresh.set(k, [...(fresh.get(k) ?? []), `${f.file}:${finding.line}`]);
490
+ }
491
+ }
492
+ if (!files.size)
493
+ return;
494
+ for (const entry of this.entries) {
495
+ const kept = entry.references.filter((r) => !files.has(r.slice(0, r.lastIndexOf(':'))));
496
+ const added = fresh.get((0, po_file_1.entryKey)(entry.message.msgctxt, entry.message.msgid)) ?? [];
497
+ entry.references = sortRefs([...kept, ...added]);
498
+ }
499
+ }
500
+ /**
501
+ * Merge the run's translations into each target locale's .po file.
502
+ * Returns LocaleFiles carrying the exact content (and .mo when the project
503
+ * commits compiled catalogues); writes them unless `dryRun`.
504
+ */
505
+ async write(targetLocales, valuesByLocale, options) {
506
+ const notes = [];
507
+ const locales = [];
508
+ const now = options.now ?? new Date();
509
+ const stamp = formatPoDate(now);
510
+ let fuzzyKept = 0;
511
+ const approximated = [];
512
+ for (const locale of targetLocales) {
513
+ const state = await this.loadLocale(locale);
514
+ const values = valuesByLocale[locale] ?? {};
515
+ const file = state.existing ?? { entries: [], newline: '\n' };
516
+ let changed = !state.existing;
517
+ const insertAt = (() => {
518
+ let idx = file.entries.length;
519
+ while (idx > 0 && file.entries[idx - 1].obsolete)
520
+ idx--;
521
+ return idx;
522
+ })();
523
+ const added = [];
524
+ let plainPluralApprox = 0;
525
+ for (const entry of this.entries) {
526
+ const m = entry.message;
527
+ const r = this.formsFor(entry, locale, values);
528
+ const forms = r && 'forms' in r ? r.forms : null;
529
+ const empty = m.msgidPlural !== undefined ? Array(state.forms.nplurals).fill('') : [''];
530
+ if (forms && !entry.keys.icu && m.msgidPlural !== undefined && state.categories.some((c) => c !== 'one' && c !== 'other'))
531
+ plainPluralApprox++;
532
+ const e = this.existingEntry(state, entry);
533
+ if (e) {
534
+ const samePlural = (e.msgidPlural ?? null) === (m.msgidPlural ?? null);
535
+ const fuzzy = e.flags.includes('fuzzy');
536
+ const stalePseudo = !options.pseudo && e.msgstr.some((s) => (0, pseudo_markers_1.isPseudoLocalized)(s));
537
+ if (samePlural && !fuzzy && !stalePseudo && isComplete(e, state.forms.nplurals))
538
+ continue; // theirs
539
+ if (samePlural && fuzzy && e.msgstr.some(Boolean)) {
540
+ fuzzyKept++;
541
+ continue;
542
+ }
543
+ const nextMsgstr = forms ?? (samePlural && !stalePseudo ? padForms(e.msgstr, empty.length) : empty);
544
+ const nextFlags = mergeFlags(forms ? e.flags.filter((f) => f !== 'fuzzy') : e.flags, m.flags);
545
+ const nextRefs = entry.references;
546
+ const differs = JSON.stringify(nextMsgstr) !== JSON.stringify(e.msgstr) ||
547
+ (e.msgidPlural ?? null) !== (m.msgidPlural ?? null) ||
548
+ nextFlags.join() !== e.flags.join() ||
549
+ (forms !== null && nextRefs.join() !== e.references.join());
550
+ if (!differs)
551
+ continue;
552
+ e.msgstr = nextMsgstr;
553
+ e.msgidPlural = m.msgidPlural;
554
+ e.flags = nextFlags;
555
+ if (forms)
556
+ e.references = nextRefs;
557
+ if (m.comments.length)
558
+ e.extractedComments = m.comments;
559
+ e.dirty = true;
560
+ changed = true;
561
+ continue;
562
+ }
563
+ added.push({
564
+ translatorComments: [],
565
+ extractedComments: m.comments,
566
+ references: entry.references,
567
+ flags: [...m.flags],
568
+ previous: [],
569
+ msgctxt: m.msgctxt,
570
+ msgid: m.msgid,
571
+ msgidPlural: m.msgidPlural,
572
+ msgstr: forms ?? empty,
573
+ obsolete: false,
574
+ dirty: true,
575
+ });
576
+ changed = true;
577
+ }
578
+ if (plainPluralApprox > 0)
579
+ approximated.push(`${locale} (${plainPluralApprox})`);
580
+ file.entries.splice(insertAt, 0, ...added);
581
+ const langName = (0, locale_names_1.languageName)(locale);
582
+ if (!file.header) {
583
+ file.header = {
584
+ translatorComments: [`${langName} translations.`, 'Generated by Babelize (https://babelize.co).', ''],
585
+ extractedComments: [], references: [], flags: [], previous: [], msgid: '', msgstr: [''], obsolete: false, dirty: true,
586
+ };
587
+ (0, po_file_1.setHeaderFields)(file.header, [
588
+ ['Project-Id-Version', options.projectName ?? this.projectName()],
589
+ ['Report-Msgid-Bugs-To', ''],
590
+ ['POT-Creation-Date', stamp],
591
+ ['PO-Revision-Date', stamp],
592
+ ['Last-Translator', 'Babelize'],
593
+ ['Language-Team', langName],
594
+ ['Language', (0, locale_names_1.djangoLocaleName)(locale)],
595
+ ['MIME-Version', '1.0'],
596
+ ['Content-Type', 'text/plain; charset=UTF-8'],
597
+ ['Content-Transfer-Encoding', '8bit'],
598
+ ['Plural-Forms', state.forms.header],
599
+ ['X-Generator', 'Babelize'],
600
+ ]);
601
+ }
602
+ else {
603
+ const h = file.header;
604
+ const updates = [];
605
+ if (!(0, po_file_1.getHeaderField)(h, 'Language'))
606
+ updates.push(['Language', (0, locale_names_1.djangoLocaleName)(locale)]);
607
+ if (!(0, po_file_1.getHeaderField)(h, 'Plural-Forms'))
608
+ updates.push(['Plural-Forms', state.forms.header]);
609
+ const ct = (0, po_file_1.getHeaderField)(h, 'Content-Type');
610
+ if (!ct || /charset=CHARSET/i.test(ct))
611
+ updates.push(['Content-Type', 'text/plain; charset=UTF-8']);
612
+ if (!(0, po_file_1.getHeaderField)(h, 'Content-Transfer-Encoding'))
613
+ updates.push(['Content-Transfer-Encoding', '8bit']);
614
+ if (!(0, po_file_1.getHeaderField)(h, 'MIME-Version'))
615
+ updates.push(['MIME-Version', '1.0']);
616
+ if (updates.length && (0, po_file_1.setHeaderFields)(h, updates))
617
+ changed = true;
618
+ if (changed)
619
+ (0, po_file_1.setHeaderFields)(h, [['PO-Revision-Date', stamp], ['X-Generator', 'Babelize']]);
620
+ }
621
+ if (state.guessedForms) {
622
+ notes.push(`Plural forms for ${locale} are not known to Babelize; used "${state.forms.header}". Check the Plural-Forms header in ${state.relativeFile}.`);
623
+ }
624
+ const content = changed || state.existingContent === null ? (0, po_file_1.serializePo)(file) : state.existingContent;
625
+ const localeFile = {
626
+ locale,
627
+ path: state.relativeFile,
628
+ data: values,
629
+ isNew: state.existingContent === null,
630
+ content,
631
+ };
632
+ // Projects that commit compiled catalogues get their .mo kept in sync —
633
+ // a stale .mo would shadow the new translations.
634
+ const moPath = state.file.replace(/\.po$/, '.mo');
635
+ if (fs.existsSync(moPath) && content !== state.existingContent) {
636
+ const mo = (0, mo_file_1.compileMo)(content);
637
+ localeFile.extraFiles = [{ path: state.relativeFile.replace(/\.po$/, '.mo'), content: mo.toString('base64'), encoding: 'base64' }];
638
+ if (!options.dryRun)
639
+ await fs.promises.writeFile(moPath, mo);
640
+ }
641
+ if (!options.dryRun && content !== state.existingContent) {
642
+ await fs.promises.mkdir(path.dirname(state.file), { recursive: true });
643
+ await fs.promises.writeFile(state.file, content);
644
+ }
645
+ locales.push(localeFile);
646
+ }
647
+ if (fuzzyKept > 0) {
648
+ notes.push(`${fuzzyKept} fuzzy translation(s) in your .po files were left for review — Django ignores fuzzy entries until the flag is removed.`);
649
+ }
650
+ if (approximated.length > 0) {
651
+ notes.push(`Plural messages in ${approximated.join(', ')} use the translated plural text for every non-singular form: ` +
652
+ 'this translator translates singular and plural separately and can\'t produce each grammatical form these languages have. ' +
653
+ 'Review them, or translate with an AI provider for full plural forms.');
654
+ }
655
+ const compiled = locales.some((l) => l.extraFiles?.length);
656
+ if (!compiled && locales.length > 0) {
657
+ notes.push('Run `python manage.py compilemessages` to compile the .po files (Django loads the compiled .mo files).');
658
+ }
659
+ return { locales, notes };
660
+ }
661
+ }
662
+ exports.GettextCatalog = GettextCatalog;
663
+ function padForms(forms, n) {
664
+ return Array.from({ length: n }, (_, i) => forms[i] ?? '');
665
+ }
666
+ function mergeFlags(existing, wanted) {
667
+ const out = [...existing];
668
+ for (const f of wanted)
669
+ if (!out.includes(f))
670
+ out.push(f);
671
+ return out;
672
+ }
673
+ /** PO date format: 2026-10-10 14:03+0000 */
674
+ function formatPoDate(d) {
675
+ const p = (n) => String(n).padStart(2, '0');
676
+ return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}+0000`;
677
+ }