@menapo/vi18n 0.0.0-stage → 0.1.1

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 (100) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +110 -2
  3. package/dist/apply-Bre-lY9J.js +228 -0
  4. package/dist/captured-Bj8bO6vd.js +1034 -0
  5. package/dist/check-BIKEuu6B.js +201 -0
  6. package/dist/check-HsmmpJZg.js +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +41 -0
  9. package/dist/codec-DuvU2VQy.js +207 -0
  10. package/dist/collect-Cx4_SKOe.js +13 -0
  11. package/dist/collisions-CfpZU8e4.js +217 -0
  12. package/dist/compile-BaDh0cHG.js +245 -0
  13. package/dist/compile-Cc97JaW2.js +2502 -0
  14. package/dist/concurrency-C9UUWuDC.js +96 -0
  15. package/dist/concurrency-DONL7t4V.js +1 -0
  16. package/dist/config.d.ts +21 -0
  17. package/dist/config.js +39 -0
  18. package/dist/dir-zw6bGMKQ.js +290 -0
  19. package/dist/duplicates-yuRljQ6h.js +185 -0
  20. package/dist/engine-C2ENBRJe.js +531 -0
  21. package/dist/engine.d.ts +43 -0
  22. package/dist/engine.js +1241 -0
  23. package/dist/exclude-Du-Zd_qI.js +26 -0
  24. package/dist/export-C9Sy6UWv.js +103 -0
  25. package/dist/extract-C3S8btx0.js +298 -0
  26. package/dist/formats-9unqj_fZ.js +781 -0
  27. package/dist/i18next-9HAoxwUI.js +1 -0
  28. package/dist/i18next-BEJgv_S8.js +1596 -0
  29. package/dist/import-nANvQutw.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-C2ScuW6a.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui-zUiCl9Um.js +833 -0
  35. package/dist/lint.d.ts +105 -0
  36. package/dist/lint.js +971 -0
  37. package/dist/load-CQRz4PpM.js +2950 -0
  38. package/dist/load-gW-oJt1Q.js +747 -0
  39. package/dist/locales-DX4MOBFE.js +827 -0
  40. package/dist/lsp-RMMMc88P.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-CHVwBoRP.js +28 -0
  43. package/dist/merge-driver-BTX87aVm.js +422 -0
  44. package/dist/messages-BfjIExyB.js +166 -0
  45. package/dist/migrate-IpLDMXmq.js +1484 -0
  46. package/dist/migrate-rCQuOGsf.js +1 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DWE3SIUA.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C9Dbotkh.js +2031 -0
  52. package/dist/pipeline-DbX6rlo5.js +1 -0
  53. package/dist/project-Bw9UPIdx.js +1046 -0
  54. package/dist/project-C4GpaN-n.js +381 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-BP0u-D5m.js +519 -0
  58. package/dist/restore-CiYv-jxS.js +175 -0
  59. package/dist/rewrite-EsIw_lsq.js +803 -0
  60. package/dist/rich-DCUKl5HM.js +97 -0
  61. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  62. package/dist/run-BEcnBbu-.js +694 -0
  63. package/dist/runtime-DTMmliT1.js +64 -0
  64. package/dist/scan-worker.d.ts +62 -0
  65. package/dist/scan-worker.js +1 -0
  66. package/dist/scoped-BMYpSthj.js +44 -0
  67. package/dist/server-Da1Ot9IN.js +10485 -0
  68. package/dist/server-deBOq2vq.js +11823 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DJXzKoza.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-zr4jY59Y.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-B9V2KFwb.js +658 -0
  77. package/dist/t-DC-c9zE5.js +454 -0
  78. package/dist/test.d.ts +116 -0
  79. package/dist/test.js +319 -0
  80. package/dist/translate-CQnJfLjO.js +299 -0
  81. package/dist/ts-BaU5yOf0.js +4863 -0
  82. package/dist/types-CgUCkFU1.d.ts +707 -0
  83. package/dist/types-D3QGM8XO.js +146 -0
  84. package/dist/types-DsdWBG0b.d.ts +558 -0
  85. package/dist/types-DtvQ7UdD.js +1948 -0
  86. package/dist/types-wSh9fCBY.js +11 -0
  87. package/dist/units-CQ5Q99RC.js +150 -0
  88. package/dist/url-CRfCOhUI.js +172 -0
  89. package/dist/url-CsBkRaJy.js +225 -0
  90. package/dist/validate-BxpupdAf.js +2615 -0
  91. package/dist/validate-D6DJepZv.js +365 -0
  92. package/dist/version-Byqi2guX.js +28 -0
  93. package/dist/vite-scan-CT_vsp4g.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-g0iGy6CZ.js +651 -0
  97. package/dist/which-BOemZ5Q8.js +116 -0
  98. package/dist/worker-84U6uYB3.js +1656 -0
  99. package/package.json +97 -3
  100. package/schemas/catalog.json +57 -0
@@ -0,0 +1,31 @@
1
+ import { t as EXIT } from "./types-wSh9fCBY.js";
2
+ import { t as protocolConsole } from "./stdio-CVaJfxA7.js";
3
+ //#region src/cli/commands/lsp.ts
4
+ /**
5
+ * `vi18n lsp [--stdio]` (INTERNALS §2.17): runs the language server on the
6
+ * process's stdin and stdout until the editor exits it. The server and
7
+ * `vscode-languageserver` load only here, so other commands never pay for
8
+ * them (and `vp pack` gives them a chunk of their own).
9
+ */
10
+ async function lsp(ctx, _flags) {
11
+ const streams = ctx.io.streams;
12
+ if (streams === void 0) throw new Error("vi18n lsp needs the process's stdin and stdout");
13
+ const { startLanguageServer } = await import("./server-deBOq2vq.js");
14
+ const restore = protocolConsole(streams.output);
15
+ try {
16
+ return {
17
+ exitCode: await startLanguageServer({
18
+ input: streams.input,
19
+ output: streams.output,
20
+ cwd: ctx.global.cwd,
21
+ configFile: ctx.global.config,
22
+ ...ctx.io.signal === void 0 ? {} : { signal: ctx.io.signal }
23
+ }) === 0 ? EXIT.ok : EXIT.failure,
24
+ data: null
25
+ };
26
+ } finally {
27
+ restore();
28
+ }
29
+ }
30
+ //#endregion
31
+ export { lsp };
@@ -0,0 +1,89 @@
1
+ import { d as splitLocalePath, l as matchRoute, t as ROUTES, u as routePath } from "./url-CsBkRaJy.js";
2
+ //#region src/runtime/match.ts
3
+ /**
4
+ * The first candidate that reaches a configured locale: exactly, through its
5
+ * maximized form's truncations (`zh-TW` → `zh-Hant-TW` → `zh-Hant` → `zh`),
6
+ * through `fallbacks`, or through the first locale sharing its language.
7
+ * Candidates are canonicalised first; anything that is not a locale is skipped.
8
+ */
9
+ function matchLocale(candidates, config) {
10
+ const { locales, fallbacks } = config;
11
+ for (const candidate of candidates) {
12
+ let tag;
13
+ try {
14
+ tag = Intl.getCanonicalLocales(candidate)[0];
15
+ } catch {
16
+ continue;
17
+ }
18
+ if (!tag) continue;
19
+ if (locales.includes(tag)) return tag;
20
+ try {
21
+ let m = new Intl.Locale(tag).maximize().baseName;
22
+ for (let i = m.length; i > 0; i = m.lastIndexOf("-")) {
23
+ m = m.slice(0, i);
24
+ if (locales.includes(m)) return m;
25
+ }
26
+ } catch {}
27
+ if (Object.hasOwn(fallbacks, tag)) {
28
+ for (const f of fallbacks[tag]) if (locales.includes(f)) return f;
29
+ }
30
+ const lang = tag.split("-")[0];
31
+ for (const l of locales) if (l.split("-")[0] === lang) return l;
32
+ }
33
+ return null;
34
+ }
35
+ /** The strategy chain for a pathname: the first matching `routeStrategies` glob, else `strategy`. */
36
+ function strategyFor(config, pathname) {
37
+ const { routeStrategies, strategy } = config.routing;
38
+ const p = matchRoute(Object.keys(routeStrategies), routePath(pathname, config));
39
+ return p === null ? strategy : routeStrategies[p];
40
+ }
41
+ /**
42
+ * The `url` step: a prefixed path names its locale; an unprefixed one is the
43
+ * source locale's canonical page under `prefix_except_default` and
44
+ * `prefix_and_default` (on a listed host, the host's locale's), and says
45
+ * nothing under `prefix` and `no_prefix`.
46
+ */
47
+ function urlLocale(config, pathname) {
48
+ const url = config.routing.url;
49
+ if (url === "no_prefix") return null;
50
+ return splitLocalePath(pathname, config).locale ?? (url === "prefix" ? null : ROUTES && config.routing.home || config.sourceLocale);
51
+ }
52
+ /** One cookie's value from a `document.cookie` string, or null. */
53
+ function readCookie(cookies, name) {
54
+ for (const part of cookies.split(";")) {
55
+ const i = part.indexOf("=");
56
+ if (i > 0 && part.slice(0, i).trim() === name) try {
57
+ return decodeURIComponent(part.slice(i + 1).trim());
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ /** Storage key of the `localStorage` strategy (INTERNALS §6 D29). */
65
+ const STORAGE_KEY = "vi18n_locale";
66
+ /** The client steps of §2.11.9: the effective chain, first result wins. */
67
+ function clientLocale(config, input) {
68
+ const { locales, sourceLocale } = config;
69
+ for (const step of strategyFor(config, input.pathname)) {
70
+ let l = null;
71
+ if (step === "url") l = urlLocale(config, input.pathname);
72
+ else if (step === "cookie") l = readCookie(input.cookie, config.routing.cookie.name);
73
+ else if (step === "header") l = matchLocale(input.languages, config);
74
+ else if (step === "localStorage") try {
75
+ l = input.storage(STORAGE_KEY);
76
+ } catch {}
77
+ else if (step === "default") l = sourceLocale;
78
+ if (locales.includes(l)) return {
79
+ locale: l,
80
+ source: step
81
+ };
82
+ }
83
+ return {
84
+ locale: sourceLocale,
85
+ source: "default"
86
+ };
87
+ }
88
+ //#endregion
89
+ export { strategyFor as a, readCookie as i, clientLocale as n, urlLocale as o, matchLocale as r, STORAGE_KEY as t };
@@ -0,0 +1,28 @@
1
+ import { t as EXIT } from "./types-wSh9fCBY.js";
2
+ import { t as protocolConsole } from "./stdio-CVaJfxA7.js";
3
+ //#region src/cli/commands/mcp.ts
4
+ async function mcp(ctx, _flags) {
5
+ const streams = ctx.io.streams;
6
+ if (streams === void 0) throw new Error("vi18n mcp needs the process's stdin and stdout");
7
+ const { serveMcp } = await import("./server-Da1Ot9IN.js");
8
+ const restore = protocolConsole(streams.output);
9
+ try {
10
+ await serveMcp({
11
+ input: streams.input,
12
+ output: streams.output,
13
+ cwd: ctx.global.cwd,
14
+ configFile: ctx.global.config,
15
+ env: ctx.io.env,
16
+ log: ctx.io.stderr,
17
+ ...ctx.io.signal === void 0 ? {} : { signal: ctx.io.signal }
18
+ });
19
+ return {
20
+ exitCode: EXIT.ok,
21
+ data: null
22
+ };
23
+ } finally {
24
+ restore();
25
+ }
26
+ }
27
+ //#endregion
28
+ export { mcp };
@@ -0,0 +1,422 @@
1
+ import { d as parseCatalogBytes, o as catalogLayout, p as serializeCatalog } from "./library-D5CWvTQs.js";
2
+ import { n as loadConfig } from "./load-gW-oJt1Q.js";
3
+ import { t as VERSION } from "./version-Byqi2guX.js";
4
+ import { r as plural } from "./output-DWE3SIUA.js";
5
+ import { t as EXIT } from "./types-wSh9fCBY.js";
6
+ import { dirname, resolve } from "node:path";
7
+ import { readFile, writeFile } from "node:fs/promises";
8
+ import { promisify } from "node:util";
9
+ import { execFile } from "node:child_process";
10
+ //#region src/core/catalog/merge.ts
11
+ /** The identity git-level tools use: `msgctxt` + `msgid`, or the JSON key. */
12
+ function mergeKey(entry, layout) {
13
+ if (layout === "json") return entry.manualId ?? entry.label ?? "";
14
+ return `${entry.manualId ?? entry.foreignContext ?? ""}\u0004${(layout === "id" ? entry.label : entry.source) ?? ""}`;
15
+ }
16
+ function index(catalog, layout) {
17
+ const side = {
18
+ first: /* @__PURE__ */ new Map(),
19
+ rest: []
20
+ };
21
+ for (const entry of catalog?.entries ?? []) {
22
+ const key = mergeKey(entry, layout);
23
+ if (side.first.has(key)) side.rest.push(entry);
24
+ else side.first.set(key, entry);
25
+ }
26
+ return side;
27
+ }
28
+ /** Everything the writer emits for an entry, so equal fingerprints mean equal output. */
29
+ function fingerprint(entry) {
30
+ return JSON.stringify([
31
+ entry.manualId,
32
+ entry.foreignContext,
33
+ entry.label,
34
+ entry.source,
35
+ entry.translation,
36
+ entry.state,
37
+ entry.fuzzy,
38
+ entry.contexts,
39
+ entry.comments,
40
+ entry.refs,
41
+ entry.maxLength,
42
+ entry.flags,
43
+ entry.extra ?? null
44
+ ]);
45
+ }
46
+ /**
47
+ * Three-way merge of a value; `base` is absent when the entry is new on both
48
+ * sides. Null when both sides changed it differently.
49
+ */
50
+ function pick(base, ours, theirs, same) {
51
+ if (same(ours, theirs)) return { value: ours };
52
+ if (base !== void 0 && same(base.value, ours)) return { value: theirs };
53
+ if (base !== void 0 && same(base.value, theirs)) return { value: ours };
54
+ return null;
55
+ }
56
+ function from(entry) {
57
+ return (get) => entry === void 0 ? void 0 : { value: get(entry) };
58
+ }
59
+ const equal = (a, b) => a === b;
60
+ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
61
+ /** Set merge in order: the base's survivors, then each side's additions. */
62
+ function mergeList(base, ours, theirs, id) {
63
+ const inBase = new Set(base.map(id));
64
+ const inOurs = new Set(ours.map(id));
65
+ const inTheirs = new Set(theirs.map(id));
66
+ const out = [];
67
+ const seen = /* @__PURE__ */ new Set();
68
+ const add = (item) => {
69
+ const key = id(item);
70
+ if (!seen.has(key)) {
71
+ seen.add(key);
72
+ out.push(item);
73
+ }
74
+ };
75
+ for (const item of ours) {
76
+ const key = id(item);
77
+ if (!inBase.has(key) || inTheirs.has(key)) add(item);
78
+ }
79
+ for (const item of theirs) {
80
+ const key = id(item);
81
+ if (!inBase.has(key) || inOurs.has(key)) add(item);
82
+ }
83
+ return out;
84
+ }
85
+ const text = (value) => value;
86
+ const flagId = (flag) => `${flag.line}${flag.name}`;
87
+ const RANK = {
88
+ translated: 2,
89
+ ai: 1,
90
+ untranslated: 0
91
+ };
92
+ function mergeEntry(base, ours, theirs) {
93
+ const sorted = (values) => values.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
94
+ const common = {
95
+ ...ours,
96
+ refs: sorted(mergeList(base?.refs ?? [], ours.refs, theirs.refs, text)),
97
+ contexts: mergeList(base?.contexts ?? [], ours.contexts, theirs.contexts, text),
98
+ comments: mergeList(base?.comments ?? [], ours.comments, theirs.comments, text),
99
+ flags: mergeList(base?.flags ?? [], ours.flags, theirs.flags, flagId),
100
+ line: 0,
101
+ wrapped: false
102
+ };
103
+ const was = from(base);
104
+ const maxLength = pick(was((e) => e.maxLength), ours.maxLength, theirs.maxLength, equal);
105
+ common.maxLength = maxLength !== null ? maxLength.value : ours.maxLength === null ? theirs.maxLength : theirs.maxLength === null ? ours.maxLength : Math.min(ours.maxLength, theirs.maxLength);
106
+ const extra = pick(was((e) => e.extra), ours.extra, theirs.extra, sameJson);
107
+ const extraValue = extra === null ? ours.extra : extra.value;
108
+ if (extraValue === void 0) delete common.extra;
109
+ else common.extra = extraValue;
110
+ let conflict = false;
111
+ const oursView = { ...common };
112
+ const theirsView = { ...common };
113
+ const sourceOf = (entry) => ({
114
+ source: entry.source,
115
+ label: entry.label
116
+ });
117
+ const valueOf = (entry) => ({
118
+ translation: entry.translation,
119
+ state: entry.state,
120
+ fuzzy: entry.fuzzy
121
+ });
122
+ const source = pick(was(sourceOf), sourceOf(ours), sourceOf(theirs), sameJson);
123
+ let value = pick(was(valueOf), valueOf(ours), valueOf(theirs), sameJson);
124
+ if (value === null && ours.translation === theirs.translation && ours.fuzzy === theirs.fuzzy) value = { value: RANK[ours.state] >= RANK[theirs.state] ? valueOf(ours) : valueOf(theirs) };
125
+ let stale = false;
126
+ if (base !== void 0 && source !== null && value !== null) {
127
+ const moved = (entry) => !sameJson(sourceOf(base), sourceOf(entry));
128
+ const edited = (entry) => !sameJson(valueOf(base), valueOf(entry));
129
+ stale = moved(ours) && !moved(theirs) && edited(theirs) || moved(theirs) && !moved(ours) && edited(ours);
130
+ }
131
+ if (source === null || value === null || stale) {
132
+ conflict = true;
133
+ Object.assign(oursView, sourceOf(ours), valueOf(ours));
134
+ Object.assign(theirsView, sourceOf(theirs), valueOf(theirs));
135
+ } else {
136
+ Object.assign(oursView, source.value, value.value);
137
+ Object.assign(theirsView, source.value, value.value);
138
+ }
139
+ return {
140
+ ours: oursView,
141
+ theirs: theirsView,
142
+ conflict
143
+ };
144
+ }
145
+ function mergeHeader(base, ours, theirs) {
146
+ const was = from(base);
147
+ const soft = (get) => (pick(was(get), get(ours), get(theirs), sameJson) ?? { value: get(ours) }).value;
148
+ const extra = [];
149
+ const names = /* @__PURE__ */ new Set([...ours.extra.map(([n]) => n), ...theirs.extra.map(([n]) => n)]);
150
+ const field = (header, name) => header?.extra.find(([n]) => n === name)?.[1];
151
+ for (const name of names) {
152
+ const a = field(ours, name);
153
+ const merged = pick(was((h) => field(h, name)), a, field(theirs, name), sameJson);
154
+ const value = merged === null ? a : merged.value;
155
+ if (value !== void 0) extra.push([name, value]);
156
+ }
157
+ const common = {
158
+ language: soft((h) => h.language),
159
+ idScheme: ours.idScheme,
160
+ sourceLocale: soft((h) => h.sourceLocale),
161
+ aiModel: soft((h) => h.aiModel),
162
+ extra
163
+ };
164
+ const scheme = pick(was((h) => h.idScheme), ours.idScheme, theirs.idScheme, equal);
165
+ if (scheme === null) return {
166
+ ours: common,
167
+ theirs: {
168
+ ...common,
169
+ idScheme: theirs.idScheme
170
+ },
171
+ conflict: true
172
+ };
173
+ const idScheme = scheme.value;
174
+ return {
175
+ ours: {
176
+ ...common,
177
+ idScheme
178
+ },
179
+ theirs: {
180
+ ...common,
181
+ idScheme
182
+ },
183
+ conflict: false
184
+ };
185
+ }
186
+ /** The writer's output cut into entries: PO blocks between blank lines, JSON message members. */
187
+ function blocks(output, layout) {
188
+ const body = output.endsWith("\n") ? output.slice(0, -1) : output;
189
+ if (layout !== "json") return body.split("\n\n").map((block) => block.split("\n"));
190
+ const out = [];
191
+ let member = null;
192
+ for (const line of body.split("\n")) if (member !== null) {
193
+ member.push(line);
194
+ if (/^ {4}\},?$/.test(line)) {
195
+ out.push(member);
196
+ member = null;
197
+ }
198
+ } else if (/^ {4}"/.test(line) && line.endsWith("{")) member = [line];
199
+ else out.push([line]);
200
+ if (member !== null) out.push(member);
201
+ return out;
202
+ }
203
+ /** Git's markers around the differing middle of a block whose two versions differ. */
204
+ function conflictLines(ours, theirs) {
205
+ let start = 0;
206
+ while (start < ours.length && start < theirs.length && ours[start] === theirs[start]) start++;
207
+ let end = 0;
208
+ while (end < ours.length - start && end < theirs.length - start && ours[ours.length - 1 - end] === theirs[theirs.length - 1 - end]) end++;
209
+ return [
210
+ ...ours.slice(0, start),
211
+ "<<<<<<< ours",
212
+ ...ours.slice(start, ours.length - end),
213
+ "=======",
214
+ ...theirs.slice(start, theirs.length - end),
215
+ ">>>>>>> theirs",
216
+ ...ours.slice(ours.length - end)
217
+ ];
218
+ }
219
+ function mergeCatalogs(input) {
220
+ const { base, ours, theirs } = input;
221
+ const layout = ours.layout;
222
+ const o = index(base, layout);
223
+ const a = index(ours, layout);
224
+ const b = index(theirs, layout);
225
+ const oursEntries = [];
226
+ const theirsEntries = [];
227
+ const conflicting = [];
228
+ const both = (entry) => {
229
+ oursEntries.push(entry);
230
+ theirsEntries.push(entry);
231
+ };
232
+ const keys = [...a.first.keys(), ...[...b.first.keys()].filter((key) => !a.first.has(key))];
233
+ for (const key of keys) {
234
+ const was = o.first.get(key);
235
+ const mine = a.first.get(key);
236
+ const other = b.first.get(key);
237
+ if (mine === void 0 || other === void 0) {
238
+ const kept = mine ?? other;
239
+ if (was !== void 0 && fingerprint(was) === fingerprint(kept)) continue;
240
+ both(kept);
241
+ continue;
242
+ }
243
+ if (fingerprint(mine) === fingerprint(other)) {
244
+ both(mine);
245
+ continue;
246
+ }
247
+ const merged = mergeEntry(was, mine, other);
248
+ oursEntries.push(merged.ours);
249
+ theirsEntries.push(merged.theirs);
250
+ if (merged.conflict) conflicting.push(mine.manualId ?? mine.label ?? mine.source ?? key);
251
+ }
252
+ const restSeen = /* @__PURE__ */ new Set();
253
+ for (const entry of [...a.rest, ...b.rest]) {
254
+ const print = fingerprint(entry);
255
+ if (restSeen.has(print)) continue;
256
+ restSeen.add(print);
257
+ both(entry);
258
+ }
259
+ const header = mergeHeader(base?.header, ours.header, theirs.header);
260
+ const obsolete = mergeList(base?.obsolete ?? [], ours.obsolete, theirs.obsolete, text);
261
+ const view = (h, entries) => ({
262
+ ...ours,
263
+ header: h,
264
+ entries,
265
+ obsolete,
266
+ droppedObsolete: obsolete.length
267
+ });
268
+ const options = {
269
+ isSourceLocale: input.isSourceLocale,
270
+ version: input.version
271
+ };
272
+ const oursText = serializeCatalog(view(header.ours, oursEntries), options);
273
+ const conflicts = conflicting.length + (header.conflict ? 1 : 0);
274
+ if (conflicts === 0) return {
275
+ text: oursText,
276
+ conflicts,
277
+ conflicting
278
+ };
279
+ const theirsText = serializeCatalog(view(header.theirs, theirsEntries), options);
280
+ const left = blocks(oursText, layout);
281
+ const right = blocks(theirsText, layout);
282
+ let lines;
283
+ if (left.length !== right.length) {
284
+ lines = conflictLines(oursText.split("\n"), theirsText.split("\n"));
285
+ return {
286
+ text: `${lines.join("\n")}\n`,
287
+ conflicts,
288
+ conflicting
289
+ };
290
+ }
291
+ const separator = layout === "json" ? "\n" : "\n\n";
292
+ return {
293
+ text: `${left.map((block, i) => {
294
+ const other = right[i];
295
+ return (block.length === other.length && block.every((line, k) => line === other[k]) ? block : conflictLines(block, other)).join("\n");
296
+ }).join(separator)}\n`,
297
+ conflicts,
298
+ conflicting
299
+ };
300
+ }
301
+ //#endregion
302
+ //#region src/cli/commands/merge-driver.ts
303
+ /**
304
+ * `vi18n merge-driver <base> <ours> <theirs> [<path>]` (PLAN §4.4 P2,
305
+ * INTERNALS §2.6.10): the catalog merge driver git runs as
306
+ * `vi18n merge-driver %O %A %B %P`. It merges entry by entry
307
+ * (`core/catalog/merge.ts`), writes the result over `<ours>` as git
308
+ * requires, and exits 1 when a true conflict is left in it. Files it cannot
309
+ * read as catalogs (no config found, a parse error, conflict markers from an
310
+ * earlier merge) get git's own line merge (`git merge-file`), so a driver
311
+ * failure never loses either side.
312
+ */
313
+ const run = promisify(execFile);
314
+ async function findConfig(ctx, start) {
315
+ try {
316
+ const config = await loadConfig({
317
+ cwd: start,
318
+ configFile: ctx.global.config
319
+ });
320
+ return config.configFile === null ? null : config;
321
+ } catch {
322
+ return null;
323
+ }
324
+ }
325
+ /** Parses one side; null when it is not a catalog vi18n reads without errors. */
326
+ function read(bytes, layout, locale, isSourceLocale) {
327
+ const parsed = parseCatalogBytes(bytes, {
328
+ locale,
329
+ layout,
330
+ isSourceLocale
331
+ });
332
+ return parsed.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? null : parsed.catalog;
333
+ }
334
+ async function mergeDriver(ctx, flags) {
335
+ const cwd = ctx.global.cwd;
336
+ const base = resolve(cwd, flags.base);
337
+ const ours = resolve(cwd, flags.ours);
338
+ const theirs = resolve(cwd, flags.theirs);
339
+ const data = {
340
+ path: flags.path,
341
+ conflicts: 0,
342
+ conflicting: [],
343
+ fallback: false
344
+ };
345
+ const shown = flags.path ?? flags.ours;
346
+ const fallback = async (reason) => {
347
+ data.fallback = true;
348
+ ctx.out.note(`vi18n merge-driver: ${shown}: ${reason}; merging lines with git merge-file`);
349
+ try {
350
+ await run("git", [
351
+ "merge-file",
352
+ "-L",
353
+ "ours",
354
+ "-L",
355
+ "base",
356
+ "-L",
357
+ "theirs",
358
+ ours,
359
+ base,
360
+ theirs
361
+ ]);
362
+ return {
363
+ exitCode: EXIT.ok,
364
+ data
365
+ };
366
+ } catch (error) {
367
+ const status = error.code;
368
+ if (typeof status === "number" && status > 0) {
369
+ data.conflicts = status;
370
+ return {
371
+ exitCode: EXIT.failure,
372
+ data
373
+ };
374
+ }
375
+ throw error;
376
+ }
377
+ };
378
+ const config = await findConfig(ctx, flags.path === null ? cwd : dirname(resolve(cwd, flags.path)));
379
+ if (config === null) return fallback("no vi18n.config.ts found");
380
+ const layout = catalogLayout(config);
381
+ const [baseBytes, oursBytes, theirsBytes] = await Promise.all([
382
+ base,
383
+ ours,
384
+ theirs
385
+ ].map((file) => readFile(file)));
386
+ const probe = read(oursBytes, layout, "", false) ?? read(theirsBytes, layout, "", false);
387
+ if (probe === null) return fallback("a side is not a catalog vi18n can read");
388
+ const locale = probe.header.language;
389
+ const isSourceLocale = locale === config.sourceLocale;
390
+ const mine = read(oursBytes, layout, locale, isSourceLocale);
391
+ const other = read(theirsBytes, layout, locale, isSourceLocale);
392
+ const empty = baseBytes.toString("utf8").trim() === "";
393
+ const was = empty ? null : read(baseBytes, layout, locale, isSourceLocale);
394
+ if (mine === null || other === null || !empty && was === null) return fallback("a side is not a catalog vi18n can read");
395
+ const outcome = mergeCatalogs({
396
+ base: was,
397
+ ours: mine,
398
+ theirs: other,
399
+ isSourceLocale,
400
+ version: VERSION
401
+ });
402
+ await writeFile(ours, outcome.text);
403
+ data.conflicts = outcome.conflicts;
404
+ data.conflicting = outcome.conflicting;
405
+ if (outcome.conflicts === 0) return {
406
+ exitCode: EXIT.ok,
407
+ data
408
+ };
409
+ const names = outcome.conflicting.slice(0, 5).join(", ");
410
+ ctx.out.report([{
411
+ code: "merge-conflict",
412
+ severity: "warning",
413
+ message: `${plural(outcome.conflicts, "entry changed", "entries changed")} differently on both sides${names === "" ? " (the id scheme)" : ` (${names}${outcome.conflicting.length > 5 ? ", …" : ""})`}; resolve the markers, then run vi18n extract`,
414
+ file: shown
415
+ }]);
416
+ return {
417
+ exitCode: EXIT.failure,
418
+ data
419
+ };
420
+ }
421
+ //#endregion
422
+ export { mergeDriver };