@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.
- package/LICENSE +49 -0
- package/README.md +110 -2
- package/dist/apply-Bre-lY9J.js +228 -0
- package/dist/captured-Bj8bO6vd.js +1034 -0
- package/dist/check-BIKEuu6B.js +201 -0
- package/dist/check-HsmmpJZg.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +41 -0
- package/dist/codec-DuvU2VQy.js +207 -0
- package/dist/collect-Cx4_SKOe.js +13 -0
- package/dist/collisions-CfpZU8e4.js +217 -0
- package/dist/compile-BaDh0cHG.js +245 -0
- package/dist/compile-Cc97JaW2.js +2502 -0
- package/dist/concurrency-C9UUWuDC.js +96 -0
- package/dist/concurrency-DONL7t4V.js +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +39 -0
- package/dist/dir-zw6bGMKQ.js +290 -0
- package/dist/duplicates-yuRljQ6h.js +185 -0
- package/dist/engine-C2ENBRJe.js +531 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +1241 -0
- package/dist/exclude-Du-Zd_qI.js +26 -0
- package/dist/export-C9Sy6UWv.js +103 -0
- package/dist/extract-C3S8btx0.js +298 -0
- package/dist/formats-9unqj_fZ.js +781 -0
- package/dist/i18next-9HAoxwUI.js +1 -0
- package/dist/i18next-BEJgv_S8.js +1596 -0
- package/dist/import-nANvQutw.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-C2ScuW6a.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui-zUiCl9Um.js +833 -0
- package/dist/lint.d.ts +105 -0
- package/dist/lint.js +971 -0
- package/dist/load-CQRz4PpM.js +2950 -0
- package/dist/load-gW-oJt1Q.js +747 -0
- package/dist/locales-DX4MOBFE.js +827 -0
- package/dist/lsp-RMMMc88P.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-CHVwBoRP.js +28 -0
- package/dist/merge-driver-BTX87aVm.js +422 -0
- package/dist/messages-BfjIExyB.js +166 -0
- package/dist/migrate-IpLDMXmq.js +1484 -0
- package/dist/migrate-rCQuOGsf.js +1 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DWE3SIUA.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C9Dbotkh.js +2031 -0
- package/dist/pipeline-DbX6rlo5.js +1 -0
- package/dist/project-Bw9UPIdx.js +1046 -0
- package/dist/project-C4GpaN-n.js +381 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-BP0u-D5m.js +519 -0
- package/dist/restore-CiYv-jxS.js +175 -0
- package/dist/rewrite-EsIw_lsq.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-BEcnBbu-.js +694 -0
- package/dist/runtime-DTMmliT1.js +64 -0
- package/dist/scan-worker.d.ts +62 -0
- package/dist/scan-worker.js +1 -0
- package/dist/scoped-BMYpSthj.js +44 -0
- package/dist/server-Da1Ot9IN.js +10485 -0
- package/dist/server-deBOq2vq.js +11823 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DJXzKoza.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-zr4jY59Y.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-B9V2KFwb.js +658 -0
- package/dist/t-DC-c9zE5.js +454 -0
- package/dist/test.d.ts +116 -0
- package/dist/test.js +319 -0
- package/dist/translate-CQnJfLjO.js +299 -0
- package/dist/ts-BaU5yOf0.js +4863 -0
- package/dist/types-CgUCkFU1.d.ts +707 -0
- package/dist/types-D3QGM8XO.js +146 -0
- package/dist/types-DsdWBG0b.d.ts +558 -0
- package/dist/types-DtvQ7UdD.js +1948 -0
- package/dist/types-wSh9fCBY.js +11 -0
- package/dist/units-CQ5Q99RC.js +150 -0
- package/dist/url-CRfCOhUI.js +172 -0
- package/dist/url-CsBkRaJy.js +225 -0
- package/dist/validate-BxpupdAf.js +2615 -0
- package/dist/validate-D6DJepZv.js +365 -0
- package/dist/version-Byqi2guX.js +28 -0
- package/dist/vite-scan-CT_vsp4g.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-g0iGy6CZ.js +651 -0
- package/dist/which-BOemZ5Q8.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- 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 };
|