blume 1.6.4 → 1.6.6
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/CHANGELOG.md +41 -0
- package/bin/blume.mjs +3 -2
- package/dist/cli/chunk-0ewz4trd.js +679 -0
- package/dist/cli/chunk-0ewz4trd.js.map +15 -0
- package/dist/cli/chunk-27gtm2ym.js +69 -0
- package/dist/cli/chunk-27gtm2ym.js.map +11 -0
- package/dist/cli/chunk-2aj8ddew.js +72 -0
- package/dist/cli/chunk-2aj8ddew.js.map +10 -0
- package/dist/cli/chunk-3k0kzs6d.js +69 -0
- package/dist/cli/chunk-3k0kzs6d.js.map +11 -0
- package/dist/cli/chunk-3r94j3tc.js +221 -0
- package/dist/cli/chunk-3r94j3tc.js.map +10 -0
- package/dist/cli/chunk-4trphnvy.js +102 -0
- package/dist/cli/chunk-4trphnvy.js.map +11 -0
- package/dist/cli/chunk-4xyggvgf.js +21 -0
- package/dist/cli/chunk-4xyggvgf.js.map +10 -0
- package/dist/cli/chunk-5hs6gb7n.js +32 -0
- package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
- package/dist/cli/chunk-5yvt556e.js +185 -0
- package/dist/cli/chunk-5yvt556e.js.map +11 -0
- package/dist/cli/chunk-62qsssnh.js +3808 -0
- package/dist/cli/chunk-62qsssnh.js.map +36 -0
- package/dist/cli/chunk-6kzzpsx8.js +26 -0
- package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
- package/dist/cli/chunk-8gnpdsn1.js +952 -0
- package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
- package/dist/cli/chunk-9sh49q0h.js +30 -0
- package/dist/cli/chunk-9sh49q0h.js.map +10 -0
- package/dist/cli/chunk-aerwpe14.js +2370 -0
- package/dist/cli/chunk-aerwpe14.js.map +15 -0
- package/dist/cli/chunk-ag1zyr5x.js +176 -0
- package/dist/cli/chunk-ag1zyr5x.js.map +10 -0
- package/dist/cli/chunk-bawgnt8x.js +277 -0
- package/dist/cli/chunk-bawgnt8x.js.map +11 -0
- package/dist/cli/chunk-bcy492zc.js +16 -0
- package/dist/cli/chunk-bcy492zc.js.map +10 -0
- package/dist/cli/chunk-btfr9yvw.js +41 -0
- package/dist/cli/chunk-btfr9yvw.js.map +10 -0
- package/dist/cli/chunk-cbjnx4s8.js +73 -0
- package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
- package/dist/cli/chunk-cnvm6k3e.js +96 -0
- package/dist/cli/chunk-cnvm6k3e.js.map +10 -0
- package/dist/cli/chunk-etsqspj6.js +5170 -0
- package/dist/cli/chunk-etsqspj6.js.map +47 -0
- package/dist/cli/chunk-ev67ycx0.js +15 -0
- package/dist/cli/chunk-ev67ycx0.js.map +10 -0
- package/dist/cli/chunk-ey89bjj1.js +209 -0
- package/dist/cli/chunk-ey89bjj1.js.map +11 -0
- package/dist/cli/chunk-f75cqye8.js +76 -0
- package/dist/cli/chunk-f75cqye8.js.map +10 -0
- package/dist/cli/chunk-j00ezcg5.js +259 -0
- package/dist/cli/chunk-j00ezcg5.js.map +11 -0
- package/dist/cli/chunk-jtb45atp.js +467 -0
- package/dist/cli/chunk-jtb45atp.js.map +14 -0
- package/dist/cli/chunk-m3p3wahd.js +117 -0
- package/dist/cli/chunk-m3p3wahd.js.map +10 -0
- package/dist/cli/chunk-n0y172hf.js +387 -0
- package/dist/cli/chunk-n0y172hf.js.map +12 -0
- package/dist/cli/chunk-n4qjabmt.js +1062 -0
- package/dist/cli/chunk-n4qjabmt.js.map +25 -0
- package/dist/cli/chunk-nyqzjdhj.js +111 -0
- package/dist/cli/chunk-nyqzjdhj.js.map +11 -0
- package/dist/cli/chunk-pxj10x8y.js +35 -0
- package/dist/cli/chunk-pxj10x8y.js.map +10 -0
- package/dist/cli/chunk-s4jn7f1q.js +54 -0
- package/dist/cli/chunk-s4jn7f1q.js.map +10 -0
- package/dist/cli/chunk-s4k1pnvf.js +81 -0
- package/dist/cli/chunk-s4k1pnvf.js.map +10 -0
- package/dist/cli/chunk-s5e5jt53.js +227 -0
- package/dist/cli/chunk-s5e5jt53.js.map +11 -0
- package/dist/cli/chunk-sbdqrjbb.js +81 -0
- package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
- package/dist/cli/chunk-tc89yh2r.js +136 -0
- package/dist/cli/chunk-tc89yh2r.js.map +10 -0
- package/dist/cli/chunk-vt8fgygt.js +23 -0
- package/dist/cli/chunk-vt8fgygt.js.map +10 -0
- package/dist/cli/chunk-vv237fp3.js +1002 -0
- package/dist/cli/chunk-vv237fp3.js.map +13 -0
- package/dist/cli/chunk-vv3f8mb6.js +5314 -0
- package/dist/cli/chunk-vv3f8mb6.js.map +58 -0
- package/dist/cli/chunk-vxv4x1n8.js +17 -0
- package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
- package/dist/cli/chunk-wb067mv3.js +758 -0
- package/dist/cli/chunk-wb067mv3.js.map +13 -0
- package/dist/cli/chunk-wd27zjcz.js +60 -0
- package/dist/cli/chunk-wd27zjcz.js.map +10 -0
- package/dist/cli/chunk-wkq5tbtq.js +1141 -0
- package/dist/cli/chunk-wkq5tbtq.js.map +19 -0
- package/dist/cli/chunk-x1vrdjyk.js +1967 -0
- package/dist/cli/chunk-x1vrdjyk.js.map +34 -0
- package/dist/cli/chunk-x66c5yjn.js +23 -0
- package/dist/cli/chunk-x66c5yjn.js.map +10 -0
- package/dist/cli/index.js +55 -27587
- package/dist/cli/index.js.map +5 -243
- package/dist/types/ai/ask-context.d.ts +26 -0
- package/dist/types/core/code-fences.d.ts +11 -0
- package/dist/types/core/config-input.d.ts +10 -0
- package/dist/types/core/package-root.d.ts +1 -1
- package/dist/types/core/schema.d.ts +74 -1
- package/docs/02-deployment.mdx +1 -1
- package/docs/configuration/analytics.mdx +21 -2
- package/docs/configuration/ask-ai.mdx +1 -1
- package/docs/configuration/customization.mdx +2 -9
- package/docs/content/syntax.mdx +1 -1
- package/docs/reference/cli.mdx +1 -1
- package/package.json +16 -14
- package/src/ai/api/handlers.ts +4 -7
- package/src/ai/api/paths.ts +8 -0
- package/src/ai/api/spec.ts +2 -1
- package/src/ai/ask-context.ts +378 -22
- package/src/astro/generate.ts +25 -29
- package/src/astro/include-hmr.ts +10 -13
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +6 -1
- package/src/astro/integration.ts +269 -53
- package/src/astro/module-types.ts +74 -0
- package/src/astro/templates.ts +85 -97
- package/src/audit/image-size.ts +10 -8
- package/src/cli/command-meta.ts +77 -0
- package/src/cli/commands/add.ts +2 -4
- package/src/cli/commands/audit.ts +2 -4
- package/src/cli/commands/build.ts +42 -346
- package/src/cli/commands/check.ts +2 -4
- package/src/cli/commands/dev.ts +31 -42
- package/src/cli/commands/doctor.ts +2 -4
- package/src/cli/commands/eject.ts +3 -41
- package/src/cli/commands/eval.ts +2 -5
- package/src/cli/commands/init.ts +2 -4
- package/src/cli/commands/mcp-stdio.ts +2 -5
- package/src/cli/commands/preview.ts +3 -5
- package/src/cli/commands/sync.ts +2 -4
- package/src/cli/commands/translate.ts +2 -5
- package/src/cli/commands/validate.ts +2 -4
- package/src/cli/commands/version.ts +2 -4
- package/src/cli/eject-scripts.ts +0 -45
- package/src/cli/host-args.ts +16 -0
- package/src/cli/index.ts +84 -35
- package/src/cli/lazy-command.ts +47 -0
- package/src/components/content/GithubInfo.astro +4 -1
- package/src/components/content/mermaid-element.ts +8 -0
- package/src/components/layout/Analytics.astro +20 -1
- package/src/components/layout/PageLayout.astro +14 -3
- package/src/components/layout/ReferenceLayout.astro +15 -4
- package/src/components/layout/RootLayout.astro +15 -4
- package/src/components/layout/analytics-client.ts +2 -1
- package/src/components/layout/page-locale.ts +29 -0
- package/src/components/openapi/AsyncApiOperation.astro +5 -3
- package/src/components/openapi/Authorization.astro +4 -6
- package/src/components/openapi/Bindings.astro +2 -2
- package/src/components/openapi/Description.astro +109 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +5 -7
- package/src/components/openapi/GraphqlOperation.astro +4 -3
- package/src/components/openapi/GraphqlType.astro +4 -6
- package/src/components/openapi/ParametersTable.astro +5 -7
- package/src/components/openapi/RequestBody.astro +2 -4
- package/src/components/openapi/Responses.astro +4 -3
- package/src/components/openapi/SchemaProperty.astro +11 -6
- package/src/components/openapi/description.ts +91 -0
- package/src/core/api-name.ts +18 -0
- package/src/core/code-fences.ts +48 -0
- package/src/core/config-input.ts +10 -0
- package/src/core/content-assets.ts +3 -7
- package/src/core/includes.ts +3 -7
- package/src/core/package-root.ts +1 -1
- package/src/core/schema.ts +27 -0
- package/src/core/sources/normalize.ts +2 -37
- package/src/core/sources/obsidian.ts +3 -2
- package/src/core/svg-dimensions.ts +97 -0
- package/src/core/version-cut.ts +2 -2
- package/src/deploy/artifacts.ts +370 -0
- package/src/deploy/cloudflare-negotiation.ts +97 -32
- package/src/deploy/function-bundle.ts +66 -20
- package/src/deploy/sitemap.ts +6 -0
- package/src/deploy/vercel-negotiation.ts +8 -30
- package/src/og/card.ts +6 -12
- package/src/openapi/render-mdx.ts +9 -5
- package/src/registry/eject.ts +0 -2
- package/src/theme/entry.ts +9 -2
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DISALLOWED_TOOLS,
|
|
4
|
+
duration,
|
|
5
|
+
money,
|
|
6
|
+
readAgentOutput,
|
|
7
|
+
runAgentHeadless,
|
|
8
|
+
seconds
|
|
9
|
+
} from "./chunk-s5e5jt53.js";
|
|
10
|
+
import {
|
|
11
|
+
AGENTS
|
|
12
|
+
} from "./chunk-8gnpdsn1.js";
|
|
13
|
+
import {
|
|
14
|
+
reportInternalError
|
|
15
|
+
} from "./chunk-btfr9yvw.js";
|
|
16
|
+
import {
|
|
17
|
+
writeTextAtomic
|
|
18
|
+
} from "./chunk-ev67ycx0.js";
|
|
19
|
+
import {
|
|
20
|
+
frontmatter_default,
|
|
21
|
+
i18nEnabled,
|
|
22
|
+
localeCodes,
|
|
23
|
+
localeTargetPath,
|
|
24
|
+
scanBody,
|
|
25
|
+
scanProject
|
|
26
|
+
} from "./chunk-etsqspj6.js";
|
|
27
|
+
import {
|
|
28
|
+
createModuleLoader,
|
|
29
|
+
folderMetaSchema
|
|
30
|
+
} from "./chunk-vv3f8mb6.js";
|
|
31
|
+
import"./chunk-27gtm2ym.js";
|
|
32
|
+
import"./chunk-4xyggvgf.js";
|
|
33
|
+
import"./chunk-6kzzpsx8.js";
|
|
34
|
+
import {
|
|
35
|
+
BlumeError,
|
|
36
|
+
countBySeverity,
|
|
37
|
+
flushStdout,
|
|
38
|
+
logger
|
|
39
|
+
} from "./chunk-ey89bjj1.js";
|
|
40
|
+
import {
|
|
41
|
+
commandMeta
|
|
42
|
+
} from "./chunk-2aj8ddew.js";
|
|
43
|
+
|
|
44
|
+
// src/cli/commands/translate.ts
|
|
45
|
+
import { defineCommand } from "citty";
|
|
46
|
+
|
|
47
|
+
// src/translate/agents.ts
|
|
48
|
+
var DEFAULT_TRANSLATE_TIMEOUT_MS = 600000;
|
|
49
|
+
var claudeArgs = () => [
|
|
50
|
+
"-p",
|
|
51
|
+
"--output-format",
|
|
52
|
+
"json",
|
|
53
|
+
"--strict-mcp-config",
|
|
54
|
+
"--disallowedTools",
|
|
55
|
+
DISALLOWED_TOOLS.join(","),
|
|
56
|
+
"--max-turns",
|
|
57
|
+
"1"
|
|
58
|
+
];
|
|
59
|
+
var codexArgs = (lastMessagePath) => [
|
|
60
|
+
"exec",
|
|
61
|
+
"--skip-git-repo-check",
|
|
62
|
+
"--ignore-user-config",
|
|
63
|
+
"--ephemeral",
|
|
64
|
+
"--sandbox",
|
|
65
|
+
"read-only",
|
|
66
|
+
"--output-last-message",
|
|
67
|
+
lastMessagePath,
|
|
68
|
+
"-"
|
|
69
|
+
];
|
|
70
|
+
var translateAgentArgs = (kind, lastMessagePath) => kind === "claude" ? claudeArgs() : codexArgs(lastMessagePath);
|
|
71
|
+
|
|
72
|
+
// src/translate/ledger.ts
|
|
73
|
+
import { createHash } from "node:crypto";
|
|
74
|
+
import { readFile } from "node:fs/promises";
|
|
75
|
+
import { join } from "pathe";
|
|
76
|
+
import { z } from "zod";
|
|
77
|
+
var LEDGER_FILE = "blume.translations.json";
|
|
78
|
+
var ledgerSchema = z.object({
|
|
79
|
+
files: z.record(z.string(), z.record(z.string(), z.string())),
|
|
80
|
+
version: z.literal(1)
|
|
81
|
+
});
|
|
82
|
+
var emptyLedger = () => ({
|
|
83
|
+
files: {},
|
|
84
|
+
version: 1
|
|
85
|
+
});
|
|
86
|
+
var hashSource = (text) => createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
87
|
+
var readLedger = async (root) => {
|
|
88
|
+
let raw;
|
|
89
|
+
try {
|
|
90
|
+
raw = await readFile(join(root, LEDGER_FILE), "utf-8");
|
|
91
|
+
} catch {
|
|
92
|
+
return emptyLedger();
|
|
93
|
+
}
|
|
94
|
+
let data;
|
|
95
|
+
try {
|
|
96
|
+
data = JSON.parse(raw);
|
|
97
|
+
} catch {
|
|
98
|
+
return emptyLedger();
|
|
99
|
+
}
|
|
100
|
+
const parsed = ledgerSchema.safeParse(data);
|
|
101
|
+
return parsed.success ? parsed.data : emptyLedger();
|
|
102
|
+
};
|
|
103
|
+
var serializeLedger = (ledger) => {
|
|
104
|
+
const files = {};
|
|
105
|
+
for (const source of Object.keys(ledger.files).toSorted()) {
|
|
106
|
+
const locales = ledger.files[source] ?? {};
|
|
107
|
+
files[source] = Object.fromEntries(Object.keys(locales).toSorted().map((locale) => [locale, locales[locale] ?? ""]));
|
|
108
|
+
}
|
|
109
|
+
return `${JSON.stringify({ files, version: ledger.version }, null, 2)}
|
|
110
|
+
`;
|
|
111
|
+
};
|
|
112
|
+
var writeLedger = async (root, ledger) => {
|
|
113
|
+
const path = join(root, LEDGER_FILE);
|
|
114
|
+
const content = serializeLedger(ledger);
|
|
115
|
+
let existing = null;
|
|
116
|
+
try {
|
|
117
|
+
existing = await readFile(path, "utf-8");
|
|
118
|
+
} catch {
|
|
119
|
+
existing = null;
|
|
120
|
+
}
|
|
121
|
+
if (existing === content) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
await writeTextAtomic(path, content);
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
var stampLedger = (ledger, sourceRel, locale, hash) => {
|
|
128
|
+
const locales = ledger.files[sourceRel] ?? {};
|
|
129
|
+
locales[locale] = hash;
|
|
130
|
+
ledger.files[sourceRel] = locales;
|
|
131
|
+
};
|
|
132
|
+
var pruneLedger = (ledger, knownSources, knownLocales) => {
|
|
133
|
+
const files = {};
|
|
134
|
+
for (const [source, locales] of Object.entries(ledger.files)) {
|
|
135
|
+
if (!knownSources.has(source)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const kept = Object.fromEntries(Object.entries(locales).filter(([locale]) => knownLocales.has(locale)));
|
|
139
|
+
if (Object.keys(kept).length > 0) {
|
|
140
|
+
files[source] = kept;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { files, version: ledger.version };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/translate/report.ts
|
|
147
|
+
import { colors } from "consola/utils";
|
|
148
|
+
var ESC = String.fromCodePoint(27);
|
|
149
|
+
var GLYPH = {
|
|
150
|
+
failed: "✖",
|
|
151
|
+
partial: "!",
|
|
152
|
+
translated: "✔"
|
|
153
|
+
};
|
|
154
|
+
var STATUS_COLOR = {
|
|
155
|
+
failed: colors.red,
|
|
156
|
+
partial: colors.yellow,
|
|
157
|
+
translated: colors.green
|
|
158
|
+
};
|
|
159
|
+
var SPINNER_FRAMES = [
|
|
160
|
+
"⠋",
|
|
161
|
+
"⠙",
|
|
162
|
+
"⠹",
|
|
163
|
+
"⠸",
|
|
164
|
+
"⠼",
|
|
165
|
+
"⠴",
|
|
166
|
+
"⠦",
|
|
167
|
+
"⠧",
|
|
168
|
+
"⠇",
|
|
169
|
+
"⠏"
|
|
170
|
+
];
|
|
171
|
+
var SPINNER_INTERVAL_MS = 80;
|
|
172
|
+
var REWRITE = `\r${ESC}[K`;
|
|
173
|
+
var itemLabel = (item) => item.kind === "page" ? `${item.sourceRel} → ${item.locale}` : `meta title${item.entries.length === 1 ? "" : "s"} (${item.entries.length}) → ${item.locale}`;
|
|
174
|
+
var spinnerLine = (active, done, total, frame) => {
|
|
175
|
+
const first = active[0];
|
|
176
|
+
const more = active.length > 1 ? ` (+${active.length - 1} more)` : "";
|
|
177
|
+
return ` ${colors.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length])} ${itemLabel(first)}${more} ${colors.dim(`${done}/${total}`)}`;
|
|
178
|
+
};
|
|
179
|
+
var itemEndLine = (result) => {
|
|
180
|
+
const color = STATUS_COLOR[result.status];
|
|
181
|
+
const glyph = color(GLYPH[result.status]);
|
|
182
|
+
const label = itemLabel(result.item);
|
|
183
|
+
if (result.status === "translated") {
|
|
184
|
+
const cells = [seconds(result.durationMs), money(result.costUsd)].filter((cell) => cell !== "").join(" ");
|
|
185
|
+
return ` ${glyph} ${label} ${colors.dim(cells)}`;
|
|
186
|
+
}
|
|
187
|
+
const word = result.status === "partial" ? "partial" : "failed";
|
|
188
|
+
return ` ${glyph} ${label} ${color(word)}${result.detail ? colors.dim(`: ${result.detail}`) : ""}`;
|
|
189
|
+
};
|
|
190
|
+
var translateHeaderLine = (itemCount, localeCount, agent) => `${colors.bold("blume translate")} ${itemCount} item(s) · ${localeCount} locale(s) · ${AGENTS[agent].name}`;
|
|
191
|
+
var translateSummaryLine = (result, workList) => {
|
|
192
|
+
const { counts } = result;
|
|
193
|
+
const parts = [
|
|
194
|
+
`Translated ${counts.translated} file${counts.translated === 1 ? "" : "s"} into ${workList.targetLocales.length} locale${workList.targetLocales.length === 1 ? "" : "s"}`,
|
|
195
|
+
counts.failed > 0 ? `${counts.failed} failed` : "",
|
|
196
|
+
counts.partial > 0 ? `${counts.partial} partial` : "",
|
|
197
|
+
workList.untracked.length > 0 ? `${workList.untracked.length} adopted` : "",
|
|
198
|
+
workList.upToDate > 0 ? `${workList.upToDate} already up to date` : "",
|
|
199
|
+
duration(result.durationMs),
|
|
200
|
+
money(result.costUsd)
|
|
201
|
+
].filter((part) => part !== "");
|
|
202
|
+
return parts.join(" · ");
|
|
203
|
+
};
|
|
204
|
+
var diagnosticLines = (diagnostics) => diagnostics.map((diagnostic) => ` ${colors.yellow("⚠")} ${colors.dim(diagnostic.message)}`);
|
|
205
|
+
var driftRows = (workList) => workList.items.flatMap((item) => item.kind === "page" ? [
|
|
206
|
+
{
|
|
207
|
+
locale: item.locale,
|
|
208
|
+
sourceRel: item.sourceRel,
|
|
209
|
+
status: item.status
|
|
210
|
+
}
|
|
211
|
+
] : item.entries.map((entry) => ({
|
|
212
|
+
locale: item.locale,
|
|
213
|
+
sourceRel: entry.meta.sourceRel,
|
|
214
|
+
status: entry.status
|
|
215
|
+
})));
|
|
216
|
+
var checkLines = (workList) => [
|
|
217
|
+
...driftRows(workList).map((row) => ` ${colors.red("✖")} ${row.sourceRel} → ${row.locale} ${colors.dim(row.status)}`),
|
|
218
|
+
...workList.untracked.map((entry) => ` ${colors.dim(`⊘ ${entry.sourceRel} → ${entry.locale} untracked (adopted by the next translate run)`)}`)
|
|
219
|
+
];
|
|
220
|
+
var checkSummaryLine = (workList) => {
|
|
221
|
+
const rows = driftRows(workList);
|
|
222
|
+
const missing = rows.filter((row) => row.status === "missing").length;
|
|
223
|
+
const stale = rows.filter((row) => row.status === "stale").length;
|
|
224
|
+
const parts = [
|
|
225
|
+
missing > 0 ? `${missing} missing` : "",
|
|
226
|
+
stale > 0 ? `${stale} stale` : "",
|
|
227
|
+
workList.untracked.length > 0 ? `${workList.untracked.length} untracked` : "",
|
|
228
|
+
`${workList.upToDate} up to date`
|
|
229
|
+
].filter((part) => part !== "");
|
|
230
|
+
return parts.join(" · ");
|
|
231
|
+
};
|
|
232
|
+
var hasDrift = (workList) => workList.items.length > 0;
|
|
233
|
+
var checkReportJson = (workList) => {
|
|
234
|
+
const locales = {};
|
|
235
|
+
for (const locale of workList.targetLocales) {
|
|
236
|
+
locales[locale] = { missing: [], stale: [], untracked: [] };
|
|
237
|
+
}
|
|
238
|
+
for (const row of driftRows(workList)) {
|
|
239
|
+
locales[row.locale]?.[row.status].push(row.sourceRel);
|
|
240
|
+
}
|
|
241
|
+
for (const entry of workList.untracked) {
|
|
242
|
+
locales[entry.locale]?.untracked.push(entry.sourceRel);
|
|
243
|
+
}
|
|
244
|
+
return `${JSON.stringify({
|
|
245
|
+
diagnostics: workList.diagnostics,
|
|
246
|
+
summary: countBySeverity(workList.diagnostics),
|
|
247
|
+
translate: { locales, upToDate: workList.upToDate }
|
|
248
|
+
}, null, 2)}
|
|
249
|
+
`;
|
|
250
|
+
};
|
|
251
|
+
var resultJson = (result) => ({
|
|
252
|
+
costUsd: result.costUsd,
|
|
253
|
+
detail: result.detail,
|
|
254
|
+
durationMs: result.durationMs,
|
|
255
|
+
kind: result.item.kind,
|
|
256
|
+
locale: result.item.locale,
|
|
257
|
+
status: result.status,
|
|
258
|
+
...result.item.kind === "page" ? { source: result.item.sourceRel, target: result.item.targetRel } : {
|
|
259
|
+
sources: result.item.entries.map((entry) => entry.meta.sourceRel)
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
var translateReportJson = (result, workList) => {
|
|
263
|
+
const diagnostics = [...workList.diagnostics, ...result.diagnostics];
|
|
264
|
+
return `${JSON.stringify({
|
|
265
|
+
diagnostics,
|
|
266
|
+
summary: countBySeverity(diagnostics),
|
|
267
|
+
translate: {
|
|
268
|
+
adopted: workList.untracked.length,
|
|
269
|
+
agent: result.agent,
|
|
270
|
+
costUsd: result.costUsd,
|
|
271
|
+
counts: result.counts,
|
|
272
|
+
durationMs: result.durationMs,
|
|
273
|
+
results: result.results.map(resultJson),
|
|
274
|
+
upToDate: workList.upToDate
|
|
275
|
+
}
|
|
276
|
+
}, null, 2)}
|
|
277
|
+
`;
|
|
278
|
+
};
|
|
279
|
+
var createProgressRenderer = (options) => {
|
|
280
|
+
const now = options.now ?? (() => performance.now());
|
|
281
|
+
let timer;
|
|
282
|
+
const active = new Map;
|
|
283
|
+
let done = 0;
|
|
284
|
+
let total = 0;
|
|
285
|
+
let startedAt = 0;
|
|
286
|
+
const paint = () => {
|
|
287
|
+
if (active.size > 0) {
|
|
288
|
+
const frame = Math.floor((now() - startedAt) / SPINNER_INTERVAL_MS);
|
|
289
|
+
options.write(`${REWRITE}${spinnerLine([...active.values()], done, total, frame)}`);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const clearTimer = () => {
|
|
293
|
+
if (timer) {
|
|
294
|
+
clearInterval(timer);
|
|
295
|
+
timer = undefined;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
onProgress(event) {
|
|
300
|
+
({ total } = event);
|
|
301
|
+
if (event.kind === "item-start") {
|
|
302
|
+
active.set(event.index, event.item);
|
|
303
|
+
if (!options.isTTY) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!timer) {
|
|
307
|
+
startedAt = now();
|
|
308
|
+
timer = setInterval(paint, SPINNER_INTERVAL_MS);
|
|
309
|
+
timer.unref?.();
|
|
310
|
+
}
|
|
311
|
+
paint();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
active.delete(event.index);
|
|
315
|
+
done += 1;
|
|
316
|
+
const line = itemEndLine(event.result);
|
|
317
|
+
if (!options.isTTY) {
|
|
318
|
+
options.write(`${line}
|
|
319
|
+
`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
options.write(`${REWRITE}${line}
|
|
323
|
+
`);
|
|
324
|
+
if (active.size > 0) {
|
|
325
|
+
paint();
|
|
326
|
+
} else {
|
|
327
|
+
clearTimer();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
stop() {
|
|
331
|
+
clearTimer();
|
|
332
|
+
active.clear();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/translate/run.ts
|
|
338
|
+
import { existsSync } from "node:fs";
|
|
339
|
+
import { mkdtemp, readFile as readFile3 } from "node:fs/promises";
|
|
340
|
+
import { tmpdir } from "node:os";
|
|
341
|
+
import pLimit from "p-limit";
|
|
342
|
+
import pMap from "p-map";
|
|
343
|
+
import { join as join3 } from "pathe";
|
|
344
|
+
|
|
345
|
+
// src/translate/meta.ts
|
|
346
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
347
|
+
import { dirname, join as join2, relative } from "pathe";
|
|
348
|
+
import { glob } from "tinyglobby";
|
|
349
|
+
var META_FILES = ["**/meta.ts", "**/meta.js", "**/meta.mjs"];
|
|
350
|
+
var isFactoryModule = (value) => typeof value === "function";
|
|
351
|
+
var metaTargetPath = (meta, locale) => join2(meta.contentRoot, locale, meta.dir, "meta.ts");
|
|
352
|
+
var discoverTranslatableMeta = async (project) => {
|
|
353
|
+
const { i18n } = project.config;
|
|
354
|
+
if (!i18n || i18n.parser !== "dir") {
|
|
355
|
+
return { diagnostics: [], metas: [] };
|
|
356
|
+
}
|
|
357
|
+
const localeDirs = new Set(i18n.locales.flatMap((locale) => locale.code === i18n.defaultLocale ? [] : [locale.code.toLowerCase()]));
|
|
358
|
+
const load = createModuleLoader();
|
|
359
|
+
const metas = [];
|
|
360
|
+
const diagnostics = [];
|
|
361
|
+
const roots = project.sources.flatMap((source) => source.staged || !source.contentRoot ? [] : [source.contentRoot]);
|
|
362
|
+
for (const contentRoot of roots) {
|
|
363
|
+
const files = await glob(META_FILES, {
|
|
364
|
+
absolute: true,
|
|
365
|
+
cwd: contentRoot,
|
|
366
|
+
ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
|
|
367
|
+
onlyFiles: true
|
|
368
|
+
});
|
|
369
|
+
for (const file of files.toSorted()) {
|
|
370
|
+
const dir = relative(contentRoot, dirname(file));
|
|
371
|
+
const first = dir.split("/")[0]?.toLowerCase();
|
|
372
|
+
if (first && localeDirs.has(first)) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const [raw, mod] = await Promise.all([
|
|
376
|
+
readFile2(file, "utf-8"),
|
|
377
|
+
load(file)
|
|
378
|
+
]);
|
|
379
|
+
if (isFactoryModule(mod)) {
|
|
380
|
+
diagnostics.push({
|
|
381
|
+
code: "BLUME_TRANSLATE_META_FACTORY",
|
|
382
|
+
file,
|
|
383
|
+
message: "This meta file default-exports a function, so `blume translate` cannot generate per-locale copies of it.",
|
|
384
|
+
severity: "warning",
|
|
385
|
+
suggestion: "Export a plain object, or author the locale's meta file by hand."
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const parsed = folderMetaSchema.safeParse(mod);
|
|
390
|
+
if (!parsed.success || parsed.data.title === undefined) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
metas.push({
|
|
394
|
+
contentRoot,
|
|
395
|
+
data: parsed.data,
|
|
396
|
+
dir: dir === "." ? "" : dir,
|
|
397
|
+
file,
|
|
398
|
+
raw,
|
|
399
|
+
sourceRel: relative(project.context.root, file),
|
|
400
|
+
title: parsed.data.title
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { diagnostics, metas };
|
|
405
|
+
};
|
|
406
|
+
var generateMetaModule = (meta, translatedTitle) => {
|
|
407
|
+
const data = {
|
|
408
|
+
...meta,
|
|
409
|
+
title: translatedTitle
|
|
410
|
+
};
|
|
411
|
+
const lines = Object.entries(data).toSorted(([a], [b]) => a < b ? -1 : 1).filter(([, value]) => value !== undefined).map(([key, value]) => ` ${key}: ${JSON.stringify(value)},`);
|
|
412
|
+
return [
|
|
413
|
+
"// Generated by `blume translate` — edit the default locale's meta file",
|
|
414
|
+
"// and rerun the translation instead of editing this copy.",
|
|
415
|
+
"export default {",
|
|
416
|
+
...lines,
|
|
417
|
+
"};",
|
|
418
|
+
""
|
|
419
|
+
].join(`
|
|
420
|
+
`);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/translate/prompts.ts
|
|
424
|
+
var TRANSLATABLE_KEY_PATHS = [
|
|
425
|
+
["title"],
|
|
426
|
+
["description"],
|
|
427
|
+
["sidebar", "label"],
|
|
428
|
+
["sidebar", "badge"],
|
|
429
|
+
["seo", "title"],
|
|
430
|
+
["seo", "description"]
|
|
431
|
+
];
|
|
432
|
+
var localeName = (locale) => `${locale.label} (${locale.code})`;
|
|
433
|
+
var KEY_LIST = TRANSLATABLE_KEY_PATHS.map((path) => path.join(".")).join(", ");
|
|
434
|
+
var pagePrompt = (sourceText, target, source, previousTranslation) => {
|
|
435
|
+
const styleRule = target.style === undefined ? "" : `
|
|
436
|
+
- Write the translation in this style: ${target.style}`;
|
|
437
|
+
const previousRule = previousTranslation === undefined ? "" : `
|
|
438
|
+
- A translation of an earlier revision of this page is included below. Match its register, formality, dialect, and terminology exactly; re-translate only what the changed source requires and keep everything else word-for-word identical.${target.style === undefined ? "" : " Where the previous translation disagrees with the style rule above, the style rule wins."}`;
|
|
439
|
+
const previousSection = previousTranslation === undefined ? "" : `
|
|
440
|
+
The previous translation begins after this line and ends at the "page source" marker.
|
|
441
|
+
${previousTranslation}`;
|
|
442
|
+
return `Translate the following documentation page from ${localeName(source)} into ${localeName(target)}.
|
|
443
|
+
|
|
444
|
+
Rules:
|
|
445
|
+
- Translate the prose: headings, paragraphs, list items, table cells, admonitions, and image alt text.
|
|
446
|
+
- In the YAML frontmatter, translate ONLY the values of these keys: ${KEY_LIST}. Copy every other frontmatter key and value exactly as written.
|
|
447
|
+
- Never translate or alter: code blocks, inline code, import/export statements, JSX/MDX component names and their attributes, URLs, link targets, HTML tags, or frontmatter keys.
|
|
448
|
+
- A heading may end with anchor markers: \`[#some-id]\`, \`{#some-id}\` (also written \`\\{#some-id\\}\`), \`[!toc]\`, or \`[toc]\`. Copy every marker exactly as written, in the same position at the end of the translated heading — never translate, reorder, or drop a marker, and never change the id inside \`[#...]\` or \`{#...}\` (anchors must stay identical across languages).
|
|
449
|
+
|
|
450
|
+
- Preserve the document structure exactly: the same headings, the same lists and tables, and the same number of code fences.${styleRule}${previousRule}
|
|
451
|
+
- Output ONLY the complete translated file. Do not wrap it in a code fence. Do not add commentary before or after it.
|
|
452
|
+
${previousSection}
|
|
453
|
+
The page source begins after this line.
|
|
454
|
+
${sourceText}`;
|
|
455
|
+
};
|
|
456
|
+
var metaPrompt = (titles, target, source) => `Translate the following documentation sidebar section titles from ${localeName(source)} into ${localeName(target)}.
|
|
457
|
+
|
|
458
|
+
The input is a JSON object mapping a directory path to its title. Reply with ONLY a JSON object that has exactly the same keys, where each value is the title translated into ${localeName(target)}. Do not translate the keys. Do not add commentary.${target.style === undefined ? "" : `
|
|
459
|
+
Write the titles in this style: ${target.style}`}
|
|
460
|
+
|
|
461
|
+
${JSON.stringify(titles, null, 2)}`;
|
|
462
|
+
|
|
463
|
+
// src/translate/anchors.ts
|
|
464
|
+
var ATX_CLOSE = /\s+#+$/u;
|
|
465
|
+
var structureOf = (headings) => headings.map((heading) => heading.depth).join(",");
|
|
466
|
+
var pinLine = (line, id) => {
|
|
467
|
+
const eol = line.endsWith("\r") ? "\r" : "";
|
|
468
|
+
const body = line.slice(0, line.length - eol.length).trimEnd();
|
|
469
|
+
const close = body.match(ATX_CLOSE)?.[0] ?? "";
|
|
470
|
+
const head = body.slice(0, body.length - close.length);
|
|
471
|
+
return `${head} [#${id}]${close}${eol}`;
|
|
472
|
+
};
|
|
473
|
+
var pinHeadingAnchors = (sourceText, translatedText) => {
|
|
474
|
+
const source = scanBody(sourceText);
|
|
475
|
+
const translated = scanBody(translatedText);
|
|
476
|
+
if (structureOf(source.headings) !== structureOf(translated.headings)) {
|
|
477
|
+
return {
|
|
478
|
+
pinned: 0,
|
|
479
|
+
skipped: `heading structure differs (source has ${source.headings.length} headings, translation has ${translated.headings.length})`,
|
|
480
|
+
text: translatedText
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const lines = translatedText.split(`
|
|
484
|
+
`);
|
|
485
|
+
let pinned = 0;
|
|
486
|
+
for (const [index, heading] of translated.headings.entries()) {
|
|
487
|
+
const site = translated.sites[index];
|
|
488
|
+
const sourceId = source.headings[index]?.slug;
|
|
489
|
+
if (!site || site.pinned || sourceId === undefined || heading.slug === sourceId) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
lines[site.line - 1] = pinLine(lines[site.line - 1] ?? "", sourceId);
|
|
493
|
+
pinned += 1;
|
|
494
|
+
}
|
|
495
|
+
if (pinned === 0) {
|
|
496
|
+
return { pinned, text: translatedText };
|
|
497
|
+
}
|
|
498
|
+
const text = lines.join(`
|
|
499
|
+
`);
|
|
500
|
+
const rendered = scanBody(text).headings.map((heading) => heading.slug);
|
|
501
|
+
const expected = source.headings.map((heading) => heading.slug);
|
|
502
|
+
if (rendered.join(`
|
|
503
|
+
`) !== expected.join(`
|
|
504
|
+
`)) {
|
|
505
|
+
return {
|
|
506
|
+
pinned: 0,
|
|
507
|
+
skipped: "pinned anchors did not reproduce the source heading ids",
|
|
508
|
+
text: translatedText
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return { pinned, text };
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/translate/validate.ts
|
|
515
|
+
var FRONTMATTER_OPEN = /^---\r?\n/u;
|
|
516
|
+
var FENCE_LINE = /^\s*(?:```|~~~)/u;
|
|
517
|
+
var stripOuterFence = (text) => {
|
|
518
|
+
const trimmed = text.trim();
|
|
519
|
+
const match = trimmed.match(/^(?<fence>`{3,}|~{3,})[^\n]*\n(?<inner>[\s\S]*?)\n\k<fence>\s*$/u);
|
|
520
|
+
return match?.groups?.inner ?? trimmed;
|
|
521
|
+
};
|
|
522
|
+
var countFenceLines = (text) => text.split(`
|
|
523
|
+
`).filter((line) => FENCE_LINE.test(line)).length;
|
|
524
|
+
var isKeyedObject = (value) => typeof value === "object" && value !== null;
|
|
525
|
+
var isString = (value) => typeof value === "string";
|
|
526
|
+
var getPath = (data, path) => {
|
|
527
|
+
let value = data;
|
|
528
|
+
for (const key of path) {
|
|
529
|
+
if (!isKeyedObject(value)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
value = value[key];
|
|
533
|
+
}
|
|
534
|
+
return value;
|
|
535
|
+
};
|
|
536
|
+
var setPath = (data, path, value) => {
|
|
537
|
+
let parent = data;
|
|
538
|
+
for (const key of path.slice(0, -1)) {
|
|
539
|
+
parent = parent[key];
|
|
540
|
+
}
|
|
541
|
+
parent[path.at(-1)] = value;
|
|
542
|
+
};
|
|
543
|
+
var ensureTrailingNewline = (text) => text.endsWith(`
|
|
544
|
+
`) ? text : `${text}
|
|
545
|
+
`;
|
|
546
|
+
var validateTranslation = (sourceText, agentText) => {
|
|
547
|
+
const candidate = stripOuterFence(agentText);
|
|
548
|
+
if (candidate === "") {
|
|
549
|
+
return { ok: false, reason: "agent returned empty output" };
|
|
550
|
+
}
|
|
551
|
+
const sourceHasFrontmatter = FRONTMATTER_OPEN.test(sourceText);
|
|
552
|
+
const source = frontmatter_default(sourceText);
|
|
553
|
+
if (sourceHasFrontmatter && !FRONTMATTER_OPEN.test(candidate)) {
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
reason: "translation dropped the frontmatter (must start with ---)"
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
let parsed;
|
|
560
|
+
try {
|
|
561
|
+
parsed = frontmatter_default(candidate);
|
|
562
|
+
} catch {
|
|
563
|
+
return { ok: false, reason: "frontmatter does not parse as YAML" };
|
|
564
|
+
}
|
|
565
|
+
const rawBody = ensureTrailingNewline(parsed.content.replace(/^\r?\n/u, ""));
|
|
566
|
+
if (rawBody.trim() === "") {
|
|
567
|
+
return { ok: false, reason: "translation has an empty body" };
|
|
568
|
+
}
|
|
569
|
+
const sourceFences = countFenceLines(source.content);
|
|
570
|
+
const candidateFences = countFenceLines(rawBody);
|
|
571
|
+
if (sourceFences !== candidateFences) {
|
|
572
|
+
return {
|
|
573
|
+
ok: false,
|
|
574
|
+
reason: `code fence count changed (source has ${sourceFences}, translation has ${candidateFences})`
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const body = pinHeadingAnchors(source.content, rawBody).text;
|
|
578
|
+
if (!sourceHasFrontmatter) {
|
|
579
|
+
return { ok: true, text: body };
|
|
580
|
+
}
|
|
581
|
+
const data = structuredClone(source.data);
|
|
582
|
+
for (const path of TRANSLATABLE_KEY_PATHS) {
|
|
583
|
+
const original = getPath(source.data, path);
|
|
584
|
+
const translated = getPath(parsed.data, path);
|
|
585
|
+
if (isString(original) && isString(translated) && translated.trim() !== "") {
|
|
586
|
+
setPath(data, path, translated);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
ok: true,
|
|
591
|
+
text: ensureTrailingNewline(frontmatter_default.stringify(body, data))
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
var parseMetaTitles = (agentText, expectedKeys) => {
|
|
595
|
+
const start = agentText.indexOf("{");
|
|
596
|
+
const end = agentText.lastIndexOf("}");
|
|
597
|
+
let parsed;
|
|
598
|
+
if (start !== -1 && end > start) {
|
|
599
|
+
try {
|
|
600
|
+
parsed = JSON.parse(agentText.slice(start, end + 1));
|
|
601
|
+
} catch {
|
|
602
|
+
parsed = undefined;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const record = isKeyedObject(parsed) ? parsed : {};
|
|
606
|
+
const titles = {};
|
|
607
|
+
const missing = [];
|
|
608
|
+
for (const key of expectedKeys) {
|
|
609
|
+
const value = record[key];
|
|
610
|
+
if (isString(value) && value.trim() !== "") {
|
|
611
|
+
titles[key] = value;
|
|
612
|
+
} else {
|
|
613
|
+
missing.push(key);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return { missing, titles };
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/translate/run.ts
|
|
620
|
+
var metaDirKey = (dir) => dir === "" ? "." : dir;
|
|
621
|
+
var invokeAgent = async (context, prompt, index) => {
|
|
622
|
+
const messagePath = join3(context.dir, `message-${index}.txt`);
|
|
623
|
+
const result = await context.run(context.bin, translateAgentArgs(context.kind, messagePath), { cwd: context.dir, prompt, timeoutMs: context.timeoutMs });
|
|
624
|
+
return await readAgentOutput(context.kind, result, messagePath);
|
|
625
|
+
};
|
|
626
|
+
var runPageItem = async (item, index, context, ledger) => {
|
|
627
|
+
const started = performance.now();
|
|
628
|
+
const done = (status, detail, costUsd) => ({
|
|
629
|
+
costUsd,
|
|
630
|
+
detail,
|
|
631
|
+
durationMs: Math.round(performance.now() - started),
|
|
632
|
+
item,
|
|
633
|
+
status
|
|
634
|
+
});
|
|
635
|
+
const sourceText = await readFile3(item.sourcePath, "utf-8");
|
|
636
|
+
if (item.verbatim) {
|
|
637
|
+
await writeTextAtomic(item.targetPath, sourceText);
|
|
638
|
+
stampLedger(ledger, item.sourceRel, item.locale, hashSource(sourceText));
|
|
639
|
+
return done("translated");
|
|
640
|
+
}
|
|
641
|
+
const target = context.targets.get(item.locale);
|
|
642
|
+
const previousTranslation = existsSync(item.targetPath) ? await readFile3(item.targetPath, "utf-8") : undefined;
|
|
643
|
+
const output = await invokeAgent(context, pagePrompt(sourceText, target, context.source, previousTranslation), index);
|
|
644
|
+
if (output.isError) {
|
|
645
|
+
return done("failed", output.detail ?? "agent failed", output.costUsd);
|
|
646
|
+
}
|
|
647
|
+
const validated = validateTranslation(sourceText, output.text);
|
|
648
|
+
if (!validated.ok) {
|
|
649
|
+
return done("failed", validated.reason, output.costUsd);
|
|
650
|
+
}
|
|
651
|
+
await writeTextAtomic(item.targetPath, validated.text);
|
|
652
|
+
stampLedger(ledger, item.sourceRel, item.locale, hashSource(sourceText));
|
|
653
|
+
return done("translated", undefined, output.costUsd);
|
|
654
|
+
};
|
|
655
|
+
var runMetaItem = async (item, index, context, ledger) => {
|
|
656
|
+
const started = performance.now();
|
|
657
|
+
const done = (status, detail, costUsd) => ({
|
|
658
|
+
costUsd,
|
|
659
|
+
detail,
|
|
660
|
+
durationMs: Math.round(performance.now() - started),
|
|
661
|
+
item,
|
|
662
|
+
status
|
|
663
|
+
});
|
|
664
|
+
const titles = Object.fromEntries(item.entries.map((entry) => [metaDirKey(entry.meta.dir), entry.meta.title]));
|
|
665
|
+
const target = context.targets.get(item.locale);
|
|
666
|
+
const output = await invokeAgent(context, metaPrompt(titles, target, context.source), index);
|
|
667
|
+
if (output.isError) {
|
|
668
|
+
return done("failed", output.detail ?? "agent failed", output.costUsd);
|
|
669
|
+
}
|
|
670
|
+
const parsed = parseMetaTitles(output.text, Object.keys(titles));
|
|
671
|
+
for (const entry of item.entries) {
|
|
672
|
+
const translated = parsed.titles[metaDirKey(entry.meta.dir)];
|
|
673
|
+
if (translated === undefined) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
await writeTextAtomic(entry.targetPath, generateMetaModule(entry.meta.data, translated));
|
|
677
|
+
stampLedger(ledger, entry.meta.sourceRel, item.locale, hashSource(entry.meta.raw));
|
|
678
|
+
}
|
|
679
|
+
if (parsed.missing.length === item.entries.length) {
|
|
680
|
+
return done("failed", "reply contained no usable titles", output.costUsd);
|
|
681
|
+
}
|
|
682
|
+
if (parsed.missing.length > 0) {
|
|
683
|
+
return done("partial", `no translation for: ${parsed.missing.join(", ")}`, output.costUsd);
|
|
684
|
+
}
|
|
685
|
+
return done("translated", undefined, output.costUsd);
|
|
686
|
+
};
|
|
687
|
+
var itemDiagnostic = (result) => {
|
|
688
|
+
const { item } = result;
|
|
689
|
+
const site = item.kind === "page" ? { file: item.sourcePath, subject: item.sourceRel } : {
|
|
690
|
+
file: item.entries[0]?.meta.file,
|
|
691
|
+
subject: `meta titles (${item.entries.length})`
|
|
692
|
+
};
|
|
693
|
+
return {
|
|
694
|
+
code: result.status === "partial" ? "BLUME_TRANSLATE_META_PARTIAL" : "BLUME_TRANSLATE_FAILED",
|
|
695
|
+
file: site.file,
|
|
696
|
+
message: `Translating ${site.subject} into "${item.locale}" ${result.status === "partial" ? "partially failed" : "failed"}: ${result.detail ?? "unknown error"}.`,
|
|
697
|
+
severity: result.status === "partial" ? "warning" : "error"
|
|
698
|
+
};
|
|
699
|
+
};
|
|
700
|
+
var runTranslate = async (options) => {
|
|
701
|
+
const started = performance.now();
|
|
702
|
+
const { i18n } = options.project.config;
|
|
703
|
+
if (!i18n) {
|
|
704
|
+
throw new Error("blume translate requires i18n to be configured");
|
|
705
|
+
}
|
|
706
|
+
const source = i18n.locales.find((locale) => locale.code === i18n.defaultLocale);
|
|
707
|
+
const context = {
|
|
708
|
+
bin: AGENTS[options.agent].bin,
|
|
709
|
+
dir: await mkdtemp(join3(tmpdir(), "blume-translate-")),
|
|
710
|
+
kind: options.agent,
|
|
711
|
+
run: options.run ?? runAgentHeadless,
|
|
712
|
+
source,
|
|
713
|
+
targets: new Map(i18n.locales.map((locale) => [locale.code, locale])),
|
|
714
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TRANSLATE_TIMEOUT_MS
|
|
715
|
+
};
|
|
716
|
+
const { items } = options.workList;
|
|
717
|
+
const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, items.length || 1));
|
|
718
|
+
const persistLimit = pLimit(1);
|
|
719
|
+
const persist = () => persistLimit(() => options.persistLedger?.());
|
|
720
|
+
const results = await pMap(items, async (item, index) => {
|
|
721
|
+
options.onProgress?.({
|
|
722
|
+
index,
|
|
723
|
+
item,
|
|
724
|
+
kind: "item-start",
|
|
725
|
+
total: items.length
|
|
726
|
+
});
|
|
727
|
+
const result = await (item.kind === "page" ? runPageItem(item, index, context, options.ledger) : runMetaItem(item, index, context, options.ledger));
|
|
728
|
+
await persist();
|
|
729
|
+
options.onProgress?.({
|
|
730
|
+
index,
|
|
731
|
+
kind: "item-end",
|
|
732
|
+
result,
|
|
733
|
+
total: items.length
|
|
734
|
+
});
|
|
735
|
+
return result;
|
|
736
|
+
}, { concurrency });
|
|
737
|
+
const diagnostics = [];
|
|
738
|
+
for (const result of results) {
|
|
739
|
+
if (result.status !== "translated") {
|
|
740
|
+
diagnostics.push(itemDiagnostic(result));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const counts = {
|
|
744
|
+
failed: 0,
|
|
745
|
+
partial: 0,
|
|
746
|
+
translated: 0
|
|
747
|
+
};
|
|
748
|
+
for (const result of results) {
|
|
749
|
+
counts[result.status] += 1;
|
|
750
|
+
}
|
|
751
|
+
const costs = results.flatMap((result) => result.costUsd === undefined ? [] : [result.costUsd]);
|
|
752
|
+
return {
|
|
753
|
+
agent: options.agent,
|
|
754
|
+
costUsd: costs.length > 0 ? costs.reduce((total, cost) => total + cost, 0) : undefined,
|
|
755
|
+
counts,
|
|
756
|
+
diagnostics,
|
|
757
|
+
durationMs: Math.round(performance.now() - started),
|
|
758
|
+
results
|
|
759
|
+
};
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/translate/work-list.ts
|
|
763
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
764
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
765
|
+
import { dirname as dirname2, extname, join as join4, relative as relative2 } from "pathe";
|
|
766
|
+
var PAGE_EXTENSIONS = new Set([".md", ".mdx"]);
|
|
767
|
+
var translatablePages = (project, i18n) => {
|
|
768
|
+
const rootsByName = new Map(project.sources.flatMap((source) => source.staged || !source.contentRoot ? [] : [[source.name, source.contentRoot]]));
|
|
769
|
+
const seen = new Set;
|
|
770
|
+
const universe = [];
|
|
771
|
+
for (const page of project.graph.pages) {
|
|
772
|
+
if (page.locale !== i18n.defaultLocale || page.fallback || !page.sourcePath || seen.has(page.sourcePath) || page.version !== "") {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const ext = extname(page.sourcePath);
|
|
776
|
+
const base = page.sourcePath.slice(0, page.sourcePath.length - ext.length);
|
|
777
|
+
const contentRoot = rootsByName.get(page.source.name);
|
|
778
|
+
if (!PAGE_EXTENSIONS.has(ext) || base.endsWith(".$") || !contentRoot) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
seen.add(page.sourcePath);
|
|
782
|
+
universe.push({ contentRoot, ext, page, sourcePath: page.sourcePath });
|
|
783
|
+
}
|
|
784
|
+
return universe;
|
|
785
|
+
};
|
|
786
|
+
var classifyPair = (exists, stamp, hash, force) => {
|
|
787
|
+
if (!exists) {
|
|
788
|
+
return "missing";
|
|
789
|
+
}
|
|
790
|
+
if (stamp === undefined) {
|
|
791
|
+
return force ? "stale" : "untracked";
|
|
792
|
+
}
|
|
793
|
+
return stamp !== hash || force ? "stale" : "current";
|
|
794
|
+
};
|
|
795
|
+
var partialWorkItems = async (universe, project, i18n, ledger, targetLocales, force) => {
|
|
796
|
+
const work = {
|
|
797
|
+
items: [],
|
|
798
|
+
sources: [],
|
|
799
|
+
untracked: [],
|
|
800
|
+
upToDate: 0
|
|
801
|
+
};
|
|
802
|
+
if (i18n.parser !== "dir") {
|
|
803
|
+
return work;
|
|
804
|
+
}
|
|
805
|
+
const { root } = project.context;
|
|
806
|
+
const pagePaths = new Set(project.graph.pages.flatMap((page) => page.sourcePath ? [page.sourcePath] : []));
|
|
807
|
+
const partials = new Map;
|
|
808
|
+
for (const { page, contentRoot } of universe) {
|
|
809
|
+
for (const partial of page.includes ?? []) {
|
|
810
|
+
if (!(pagePaths.has(partial) || partials.has(partial))) {
|
|
811
|
+
partials.set(partial, contentRoot);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
for (const [partialPath, contentRoot] of partials) {
|
|
816
|
+
const sourceRel = relative2(root, partialPath);
|
|
817
|
+
work.sources.push(sourceRel);
|
|
818
|
+
const hash = hashSource(await readFile4(partialPath, "utf-8"));
|
|
819
|
+
const ext = extname(partialPath);
|
|
820
|
+
const verbatim = !PAGE_EXTENSIONS.has(ext.toLowerCase());
|
|
821
|
+
const contentRel = relative2(contentRoot, partialPath);
|
|
822
|
+
for (const locale of targetLocales) {
|
|
823
|
+
const targetPath = join4(contentRoot, localeTargetPath(contentRel, ext, locale, i18n));
|
|
824
|
+
const item = (status) => {
|
|
825
|
+
const partialItem = {
|
|
826
|
+
kind: "page",
|
|
827
|
+
locale,
|
|
828
|
+
sourcePath: partialPath,
|
|
829
|
+
sourceRel,
|
|
830
|
+
status,
|
|
831
|
+
targetPath,
|
|
832
|
+
targetRel: relative2(root, targetPath)
|
|
833
|
+
};
|
|
834
|
+
if (verbatim) {
|
|
835
|
+
partialItem.verbatim = true;
|
|
836
|
+
}
|
|
837
|
+
return partialItem;
|
|
838
|
+
};
|
|
839
|
+
const stamp = ledger.files[sourceRel]?.[locale];
|
|
840
|
+
const outcome = classifyPair(existsSync2(targetPath), stamp, hash, force);
|
|
841
|
+
if (outcome === "untracked") {
|
|
842
|
+
work.untracked.push({ hash, kind: "page", locale, sourceRel });
|
|
843
|
+
} else if (outcome === "current") {
|
|
844
|
+
work.upToDate += 1;
|
|
845
|
+
} else {
|
|
846
|
+
work.items.push(item(outcome));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return work;
|
|
851
|
+
};
|
|
852
|
+
var computeWorkList = async (project, ledger, options = {}) => {
|
|
853
|
+
const { i18n } = project.config;
|
|
854
|
+
if (!i18n) {
|
|
855
|
+
return {
|
|
856
|
+
diagnostics: [],
|
|
857
|
+
items: [],
|
|
858
|
+
knownSources: new Set,
|
|
859
|
+
targetLocales: [],
|
|
860
|
+
untracked: [],
|
|
861
|
+
upToDate: 0
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const targetLocales = localeCodes(i18n).filter((code) => code !== i18n.defaultLocale && (options.locales === undefined || options.locales.includes(code)));
|
|
865
|
+
const translated = new Set(project.graph.pages.flatMap((page) => page.fallback ? [] : [`${page.translationKey}\x00${page.locale}`]));
|
|
866
|
+
const { root } = project.context;
|
|
867
|
+
const knownSources = new Set;
|
|
868
|
+
const untracked = [];
|
|
869
|
+
const pageItems = [];
|
|
870
|
+
let upToDate = 0;
|
|
871
|
+
const universe = translatablePages(project, i18n);
|
|
872
|
+
for (const { page, contentRoot, ext, sourcePath } of universe) {
|
|
873
|
+
const sourceRel = relative2(root, sourcePath);
|
|
874
|
+
knownSources.add(sourceRel);
|
|
875
|
+
const hash = hashSource(await readFile4(sourcePath, "utf-8"));
|
|
876
|
+
const contentRel = relative2(contentRoot, sourcePath);
|
|
877
|
+
for (const locale of targetLocales) {
|
|
878
|
+
const targetPath = join4(contentRoot, localeTargetPath(contentRel, ext, locale, i18n));
|
|
879
|
+
const item = (status) => ({
|
|
880
|
+
kind: "page",
|
|
881
|
+
locale,
|
|
882
|
+
sourcePath,
|
|
883
|
+
sourceRel,
|
|
884
|
+
status,
|
|
885
|
+
targetPath,
|
|
886
|
+
targetRel: relative2(root, targetPath)
|
|
887
|
+
});
|
|
888
|
+
const exists = translated.has(`${page.translationKey}\x00${locale}`);
|
|
889
|
+
const stamp = ledger.files[sourceRel]?.[locale];
|
|
890
|
+
const outcome = classifyPair(exists, stamp, hash, options.force === true);
|
|
891
|
+
if (outcome === "untracked") {
|
|
892
|
+
untracked.push({ hash, kind: "page", locale, sourceRel });
|
|
893
|
+
} else if (outcome === "current") {
|
|
894
|
+
upToDate += 1;
|
|
895
|
+
} else {
|
|
896
|
+
pageItems.push(item(outcome));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const partialWork = await partialWorkItems(universe, project, i18n, ledger, targetLocales, options.force ?? false);
|
|
901
|
+
for (const rel of partialWork.sources) {
|
|
902
|
+
knownSources.add(rel);
|
|
903
|
+
}
|
|
904
|
+
pageItems.push(...partialWork.items);
|
|
905
|
+
untracked.push(...partialWork.untracked);
|
|
906
|
+
upToDate += partialWork.upToDate;
|
|
907
|
+
const meta = await discoverTranslatableMeta(project);
|
|
908
|
+
const metaItems = [];
|
|
909
|
+
for (const locale of targetLocales) {
|
|
910
|
+
const entries = [];
|
|
911
|
+
for (const source of meta.metas) {
|
|
912
|
+
knownSources.add(source.sourceRel);
|
|
913
|
+
const targetDir = dirname2(metaTargetPath(source, locale));
|
|
914
|
+
const exists = ["meta.ts", "meta.js", "meta.mjs"].some((name) => existsSync2(join4(targetDir, name)));
|
|
915
|
+
const hash = hashSource(source.raw);
|
|
916
|
+
const stamp = ledger.files[source.sourceRel]?.[locale];
|
|
917
|
+
const entry = (status) => ({
|
|
918
|
+
meta: source,
|
|
919
|
+
status,
|
|
920
|
+
targetPath: metaTargetPath(source, locale)
|
|
921
|
+
});
|
|
922
|
+
const outcome = classifyPair(exists, stamp, hash, options.force === true);
|
|
923
|
+
if (outcome === "untracked") {
|
|
924
|
+
untracked.push({
|
|
925
|
+
hash,
|
|
926
|
+
kind: "meta",
|
|
927
|
+
locale,
|
|
928
|
+
sourceRel: source.sourceRel
|
|
929
|
+
});
|
|
930
|
+
} else if (outcome === "current") {
|
|
931
|
+
upToDate += 1;
|
|
932
|
+
} else {
|
|
933
|
+
entries.push(entry(outcome));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (entries.length > 0) {
|
|
937
|
+
metaItems.push({ entries, kind: "meta", locale });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
pageItems.sort((a, b) => a.sourceRel === b.sourceRel ? a.locale.localeCompare(b.locale) : a.sourceRel.localeCompare(b.sourceRel));
|
|
941
|
+
metaItems.sort((a, b) => a.locale.localeCompare(b.locale));
|
|
942
|
+
return {
|
|
943
|
+
diagnostics: meta.diagnostics,
|
|
944
|
+
items: [...pageItems, ...metaItems],
|
|
945
|
+
knownSources,
|
|
946
|
+
targetLocales,
|
|
947
|
+
untracked,
|
|
948
|
+
upToDate
|
|
949
|
+
};
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/cli/commands/translate.ts
|
|
953
|
+
var DEFAULT_TIMEOUT_S = DEFAULT_TRANSLATE_TIMEOUT_MS / 1000;
|
|
954
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
955
|
+
var MAX_CONCURRENCY = 16;
|
|
956
|
+
var parseFlags = (args) => {
|
|
957
|
+
const agents = Object.keys(AGENTS).filter((kind) => args[kind]);
|
|
958
|
+
if (agents.length > 1) {
|
|
959
|
+
logger.error("Pass exactly one of --claude or --codex.");
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
if (args.check && agents.length > 0) {
|
|
963
|
+
logger.error("--check is read-only and never runs an agent; drop --claude/--codex.");
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
if (!args.check && agents.length === 0) {
|
|
967
|
+
logger.error("Pass --claude or --codex to choose the agent CLI that translates.");
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
const timeoutS = args.timeout === undefined ? DEFAULT_TIMEOUT_S : Number(args.timeout);
|
|
971
|
+
if (!Number.isInteger(timeoutS) || timeoutS <= 0) {
|
|
972
|
+
logger.error(`Invalid --timeout "${args.timeout}" (whole seconds).`);
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
const concurrency = args.concurrency === undefined ? DEFAULT_CONCURRENCY : Number(args.concurrency);
|
|
976
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
|
|
977
|
+
logger.error(`Invalid --concurrency "${args.concurrency}" (use 1..${MAX_CONCURRENCY}).`);
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
return { agent: agents[0], concurrency, timeoutS };
|
|
981
|
+
};
|
|
982
|
+
var parseLocales = (value, i18n) => {
|
|
983
|
+
if (value === undefined) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const configured = new Map(i18n.locales.map((locale) => [locale.code.toLowerCase(), locale.code]));
|
|
987
|
+
const resolved = [];
|
|
988
|
+
for (const part of value.split(",")) {
|
|
989
|
+
const code = configured.get(part.trim().toLowerCase());
|
|
990
|
+
if (code === undefined) {
|
|
991
|
+
logger.error(`Unknown --locale "${part.trim()}" (configured: ${localeCodes(i18n).join(", ")}).`);
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
if (code === i18n.defaultLocale) {
|
|
995
|
+
logger.error(`--locale "${code}" is the default locale — it is the translation source, not a target.`);
|
|
996
|
+
process.exit(1);
|
|
997
|
+
}
|
|
998
|
+
resolved.push(code);
|
|
999
|
+
}
|
|
1000
|
+
return resolved;
|
|
1001
|
+
};
|
|
1002
|
+
var notInstalled = (agent) => {
|
|
1003
|
+
const cli = AGENTS[agent];
|
|
1004
|
+
logger.error(`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`);
|
|
1005
|
+
return process.exit(1);
|
|
1006
|
+
};
|
|
1007
|
+
var translateCommand = defineCommand({
|
|
1008
|
+
args: {
|
|
1009
|
+
check: {
|
|
1010
|
+
description: "Report missing/stale translations without writing anything; exits 1 on drift (for CI).",
|
|
1011
|
+
type: "boolean"
|
|
1012
|
+
},
|
|
1013
|
+
claude: {
|
|
1014
|
+
description: "Translate with Claude Code.",
|
|
1015
|
+
type: "boolean"
|
|
1016
|
+
},
|
|
1017
|
+
codex: {
|
|
1018
|
+
description: "Translate with Codex.",
|
|
1019
|
+
type: "boolean"
|
|
1020
|
+
},
|
|
1021
|
+
concurrency: {
|
|
1022
|
+
description: `Parallel agent sessions. Defaults to ${DEFAULT_CONCURRENCY}, max ${MAX_CONCURRENCY}.`,
|
|
1023
|
+
type: "string"
|
|
1024
|
+
},
|
|
1025
|
+
force: {
|
|
1026
|
+
description: "Retranslate everything, up-to-date and hand-authored files included.",
|
|
1027
|
+
type: "boolean"
|
|
1028
|
+
},
|
|
1029
|
+
json: {
|
|
1030
|
+
description: "Emit the report as JSON on stdout (for CI/editors).",
|
|
1031
|
+
type: "boolean"
|
|
1032
|
+
},
|
|
1033
|
+
locale: {
|
|
1034
|
+
description: "Comma-separated target locale codes; defaults to every non-default locale.",
|
|
1035
|
+
type: "string"
|
|
1036
|
+
},
|
|
1037
|
+
timeout: {
|
|
1038
|
+
description: `Agent time limit per file, in seconds. Defaults to ${DEFAULT_TIMEOUT_S}.`,
|
|
1039
|
+
type: "string"
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
meta: commandMeta.translate,
|
|
1043
|
+
async run({ args }) {
|
|
1044
|
+
const root = process.cwd();
|
|
1045
|
+
const { agent, concurrency, timeoutS } = parseFlags(args);
|
|
1046
|
+
try {
|
|
1047
|
+
const project = await scanProject(root, { mode: "build" });
|
|
1048
|
+
if (!i18nEnabled(project.config)) {
|
|
1049
|
+
logger.error("i18n is not configured — add `i18n.locales` to blume.config to use `blume translate`.");
|
|
1050
|
+
process.exit(1);
|
|
1051
|
+
}
|
|
1052
|
+
const { i18n } = project.config;
|
|
1053
|
+
if (localeCodes(i18n).every((code) => code === i18n.defaultLocale)) {
|
|
1054
|
+
logger.error("i18n.locales has no locale besides the default — nothing to translate into.");
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
const locales = parseLocales(args.locale, i18n);
|
|
1058
|
+
const ledger = await readLedger(root);
|
|
1059
|
+
const workList = await computeWorkList(project, ledger, {
|
|
1060
|
+
force: !args.check && Boolean(args.force),
|
|
1061
|
+
locales
|
|
1062
|
+
});
|
|
1063
|
+
if (args.check) {
|
|
1064
|
+
const lines = [
|
|
1065
|
+
...diagnosticLines(workList.diagnostics),
|
|
1066
|
+
...checkLines(workList),
|
|
1067
|
+
"",
|
|
1068
|
+
` ${checkSummaryLine(workList)}`,
|
|
1069
|
+
""
|
|
1070
|
+
];
|
|
1071
|
+
process.stderr.write(`${lines.join(`
|
|
1072
|
+
`)}
|
|
1073
|
+
`);
|
|
1074
|
+
if (args.json) {
|
|
1075
|
+
process.stdout.write(checkReportJson(workList));
|
|
1076
|
+
}
|
|
1077
|
+
if (hasDrift(workList)) {
|
|
1078
|
+
await flushStdout();
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const kind = agent;
|
|
1084
|
+
process.stderr.write(`${translateHeaderLine(workList.items.length, workList.targetLocales.length, kind)}
|
|
1085
|
+
|
|
1086
|
+
`);
|
|
1087
|
+
for (const entry of workList.untracked) {
|
|
1088
|
+
stampLedger(ledger, entry.sourceRel, entry.locale, entry.hash);
|
|
1089
|
+
}
|
|
1090
|
+
await writeLedger(root, ledger);
|
|
1091
|
+
const renderer = createProgressRenderer({
|
|
1092
|
+
isTTY: process.stderr.isTTY === true,
|
|
1093
|
+
write: (chunk) => process.stderr.write(chunk)
|
|
1094
|
+
});
|
|
1095
|
+
const result = await runTranslate({
|
|
1096
|
+
agent: kind,
|
|
1097
|
+
concurrency,
|
|
1098
|
+
ledger,
|
|
1099
|
+
onProgress: (event) => renderer.onProgress(event),
|
|
1100
|
+
persistLedger: () => writeLedger(root, ledger),
|
|
1101
|
+
project,
|
|
1102
|
+
timeoutMs: timeoutS * 1000,
|
|
1103
|
+
workList
|
|
1104
|
+
});
|
|
1105
|
+
renderer.stop();
|
|
1106
|
+
const knownLocales = new Set(localeCodes(i18n).filter((code) => code !== i18n.defaultLocale));
|
|
1107
|
+
await writeLedger(root, pruneLedger(ledger, workList.knownSources, knownLocales));
|
|
1108
|
+
const tail = [
|
|
1109
|
+
"",
|
|
1110
|
+
...diagnosticLines(workList.diagnostics),
|
|
1111
|
+
` ${translateSummaryLine(result, workList)}`,
|
|
1112
|
+
""
|
|
1113
|
+
];
|
|
1114
|
+
process.stderr.write(tail.join(`
|
|
1115
|
+
`));
|
|
1116
|
+
if (args.json) {
|
|
1117
|
+
process.stdout.write(translateReportJson(result, workList));
|
|
1118
|
+
}
|
|
1119
|
+
if (result.counts.failed > 0 || result.counts.partial > 0) {
|
|
1120
|
+
await flushStdout();
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (error instanceof BlumeError) {
|
|
1125
|
+
logger.error(error.diagnostic.message);
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
if (error?.code === "ENOENT" && agent) {
|
|
1129
|
+
notInstalled(agent);
|
|
1130
|
+
}
|
|
1131
|
+
reportInternalError(error);
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
export {
|
|
1137
|
+
translateCommand
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
//# debugId=51E02F84D7BE6FE064756E2164756E21
|
|
1141
|
+
//# sourceMappingURL=chunk-wkq5tbtq.js.map
|