scribe-cms 0.0.1 → 0.0.4
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/README.md +3 -20
- package/bin/scribe.js +20 -0
- package/dist/cli/index.cjs +1284 -171
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1284 -171
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt-translate.d.ts +16 -0
- package/dist/cli/prompt-translate.d.ts.map +1 -0
- package/dist/cli/translate-progress.d.ts +11 -0
- package/dist/cli/translate-progress.d.ts.map +1 -0
- package/dist/index.cjs +479 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +475 -102
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +12 -2
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +12 -2
- package/dist/runtime.js.map +1 -1
- package/dist/src/core/field.d.ts +2 -0
- package/dist/src/core/field.d.ts.map +1 -1
- package/dist/src/core/types.d.ts +1 -1
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/export/build-static-raw-exports.d.ts +37 -0
- package/dist/src/export/build-static-raw-exports.d.ts.map +1 -0
- package/dist/src/export/write-static-raw-exports.d.ts +12 -0
- package/dist/src/export/write-static-raw-exports.d.ts.map +1 -0
- package/dist/src/history/record-revision.d.ts +2 -0
- package/dist/src/history/record-revision.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/storage/sqlite.d.ts.map +1 -1
- package/dist/src/storage/translations.d.ts +6 -0
- package/dist/src/storage/translations.d.ts.map +1 -1
- package/dist/src/translate/gemini-client.d.ts +7 -0
- package/dist/src/translate/gemini-client.d.ts.map +1 -1
- package/dist/src/translate/gemini-models.d.ts +8 -0
- package/dist/src/translate/gemini-models.d.ts.map +1 -0
- package/dist/src/translate/gemini-pricing.d.ts +11 -0
- package/dist/src/translate/gemini-pricing.d.ts.map +1 -0
- package/dist/src/translate/page-translator.d.ts +38 -1
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/translate/prompts/translation-prompt.d.ts +1 -2
- package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
- package/dist/src/translate/response-schema.d.ts +7 -0
- package/dist/src/translate/response-schema.d.ts.map +1 -0
- package/dist/src/translate/validate-translation.d.ts +14 -0
- package/dist/src/translate/validate-translation.d.ts.map +1 -0
- package/dist/src/translate/worklist.d.ts +3 -0
- package/dist/src/translate/worklist.d.ts.map +1 -1
- package/dist/src/validate/validate-project.d.ts.map +1 -1
- package/dist/studio/server.cjs +611 -194
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +611 -194
- package/dist/studio/server.js.map +1 -1
- package/package.json +17 -5
package/dist/cli/index.cjs
CHANGED
|
@@ -12,6 +12,7 @@ var genai = require('@google/genai');
|
|
|
12
12
|
var dotenv = require('dotenv');
|
|
13
13
|
var nodeServer = require('@hono/node-server');
|
|
14
14
|
var hono = require('hono');
|
|
15
|
+
var prompts = require('@inquirer/prompts');
|
|
15
16
|
|
|
16
17
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
18
|
|
|
@@ -23,6 +24,7 @@ var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
|
|
|
23
24
|
|
|
24
25
|
var __defProp = Object.defineProperty;
|
|
25
26
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
27
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
26
28
|
var __esm = (fn, res) => function __init() {
|
|
27
29
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
28
30
|
};
|
|
@@ -30,6 +32,7 @@ var __export = (target, all) => {
|
|
|
30
32
|
for (var name in all)
|
|
31
33
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
32
34
|
};
|
|
35
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
33
36
|
|
|
34
37
|
// ../../node_modules/tsup/assets/cjs_shims.js
|
|
35
38
|
var getImportMetaUrl, importMetaUrl;
|
|
@@ -52,17 +55,27 @@ function resolveStorePath(config) {
|
|
|
52
55
|
}
|
|
53
56
|
function openStore(config, mode = "readwrite") {
|
|
54
57
|
const storePath = resolveStorePath(config);
|
|
55
|
-
|
|
58
|
+
if (mode === "readwrite") {
|
|
59
|
+
fs2__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
|
|
60
|
+
}
|
|
56
61
|
const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
|
|
57
62
|
if (mode === "readwrite") {
|
|
58
63
|
migrate(db);
|
|
59
64
|
}
|
|
60
65
|
return db;
|
|
61
66
|
}
|
|
67
|
+
function addColumnIfMissing(db, table, column, ddlType) {
|
|
68
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
69
|
+
if (!columns.some((c) => c.name === column)) {
|
|
70
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
62
73
|
function migrate(db) {
|
|
63
74
|
for (const sql of MIGRATIONS) {
|
|
64
75
|
db.exec(sql);
|
|
65
76
|
}
|
|
77
|
+
addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
|
|
78
|
+
addColumnIfMissing(db, "revisions", "body", "TEXT");
|
|
66
79
|
db.prepare(
|
|
67
80
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
68
81
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -75,7 +88,7 @@ var SCHEMA_VERSION, MIGRATIONS;
|
|
|
75
88
|
var init_sqlite = __esm({
|
|
76
89
|
"src/storage/sqlite.ts"() {
|
|
77
90
|
init_cjs_shims();
|
|
78
|
-
SCHEMA_VERSION =
|
|
91
|
+
SCHEMA_VERSION = 3;
|
|
79
92
|
MIGRATIONS = [
|
|
80
93
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
81
94
|
key TEXT PRIMARY KEY,
|
|
@@ -187,6 +200,22 @@ function unwrapSchema(schema) {
|
|
|
187
200
|
}
|
|
188
201
|
return current;
|
|
189
202
|
}
|
|
203
|
+
function peelOptionalWrappers(schema) {
|
|
204
|
+
let current = schema;
|
|
205
|
+
for (let i = 0; i < 8; i++) {
|
|
206
|
+
const type = current._def?.type;
|
|
207
|
+
if (type === "optional" || type === "nullable") {
|
|
208
|
+
current = current.unwrap();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (type === "default") {
|
|
212
|
+
current = current.removeDefault();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
return current;
|
|
218
|
+
}
|
|
190
219
|
|
|
191
220
|
// src/core/types.ts
|
|
192
221
|
init_cjs_shims();
|
|
@@ -203,14 +232,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
203
232
|
function isRoutableType(type) {
|
|
204
233
|
return typeof type.path === "string" && type.path.length > 0;
|
|
205
234
|
}
|
|
206
|
-
function assertValidPathTemplate(
|
|
207
|
-
if (!
|
|
208
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
235
|
+
function assertValidPathTemplate(path11, typeId) {
|
|
236
|
+
if (!path11.startsWith("/")) {
|
|
237
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path11}")`);
|
|
209
238
|
}
|
|
210
|
-
const count = (
|
|
239
|
+
const count = (path11.match(/\{slug\}/g) ?? []).length;
|
|
211
240
|
if (count !== 1) {
|
|
212
241
|
throw new Error(
|
|
213
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
242
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
|
|
214
243
|
);
|
|
215
244
|
}
|
|
216
245
|
}
|
|
@@ -338,10 +367,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
338
367
|
}
|
|
339
368
|
function extractByPaths(data, paths) {
|
|
340
369
|
const out = {};
|
|
341
|
-
for (const
|
|
342
|
-
const value = getAtPath(data,
|
|
370
|
+
for (const path11 of paths) {
|
|
371
|
+
const value = getAtPath(data, path11);
|
|
343
372
|
if (value !== void 0) {
|
|
344
|
-
setAtPath(out,
|
|
373
|
+
setAtPath(out, path11.filter((p) => p !== "*"), value);
|
|
345
374
|
}
|
|
346
375
|
}
|
|
347
376
|
return out;
|
|
@@ -359,13 +388,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
359
388
|
const translatable = pickTranslatable(localeData, schema);
|
|
360
389
|
return deepMerge(structural, translatable);
|
|
361
390
|
}
|
|
362
|
-
function getAtPath(obj,
|
|
391
|
+
function getAtPath(obj, path11) {
|
|
363
392
|
let current = obj;
|
|
364
|
-
for (const segment of
|
|
393
|
+
for (const segment of path11) {
|
|
365
394
|
if (segment === "*") {
|
|
366
395
|
if (!Array.isArray(current)) return void 0;
|
|
367
396
|
return current.map(
|
|
368
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
397
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
|
|
369
398
|
);
|
|
370
399
|
}
|
|
371
400
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -373,13 +402,13 @@ function getAtPath(obj, path10) {
|
|
|
373
402
|
}
|
|
374
403
|
return current;
|
|
375
404
|
}
|
|
376
|
-
function setAtPath(obj,
|
|
377
|
-
if (
|
|
378
|
-
if (
|
|
379
|
-
obj[
|
|
405
|
+
function setAtPath(obj, path11, value) {
|
|
406
|
+
if (path11.length === 0) return;
|
|
407
|
+
if (path11.length === 1) {
|
|
408
|
+
obj[path11[0]] = value;
|
|
380
409
|
return;
|
|
381
410
|
}
|
|
382
|
-
const [head, ...rest] =
|
|
411
|
+
const [head, ...rest] = path11;
|
|
383
412
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
384
413
|
obj[head] = {};
|
|
385
414
|
}
|
|
@@ -618,14 +647,18 @@ function listTranslationsForEnSlug(db, contentType, enSlug) {
|
|
|
618
647
|
`SELECT * FROM translations WHERE content_type = ? AND en_slug = ? ORDER BY locale`
|
|
619
648
|
).all(contentType, enSlug);
|
|
620
649
|
}
|
|
650
|
+
function listTranslationsForLocale(db, contentType, locale) {
|
|
651
|
+
return db.prepare(`SELECT * FROM translations WHERE content_type = ? AND locale = ? ORDER BY en_slug`).all(contentType, locale);
|
|
652
|
+
}
|
|
621
653
|
function bulkLoadTranslations(db) {
|
|
622
654
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
623
655
|
}
|
|
624
656
|
function appendRevision(db, input) {
|
|
625
657
|
const result = db.prepare(
|
|
626
658
|
`INSERT INTO revisions (
|
|
627
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
628
|
-
|
|
659
|
+
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
660
|
+
model, body_preview, frontmatter_json, body
|
|
661
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
629
662
|
).run(
|
|
630
663
|
input.contentType,
|
|
631
664
|
input.enSlug,
|
|
@@ -635,7 +668,9 @@ function appendRevision(db, input) {
|
|
|
635
668
|
input.bodyHash,
|
|
636
669
|
input.createdAt,
|
|
637
670
|
input.model ?? null,
|
|
638
|
-
input.bodyPreview ?? null
|
|
671
|
+
input.bodyPreview ?? null,
|
|
672
|
+
input.frontmatterJson ?? null,
|
|
673
|
+
input.body ?? null
|
|
639
674
|
);
|
|
640
675
|
return Number(result.lastInsertRowid);
|
|
641
676
|
}
|
|
@@ -1473,7 +1508,9 @@ function recordRevision(config, input) {
|
|
|
1473
1508
|
bodyHash: computeBodyHash(input.body),
|
|
1474
1509
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1475
1510
|
model: input.model,
|
|
1476
|
-
bodyPreview: input.body.slice(0, 200)
|
|
1511
|
+
bodyPreview: input.body.slice(0, 200),
|
|
1512
|
+
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1513
|
+
body: input.body
|
|
1477
1514
|
});
|
|
1478
1515
|
db.close();
|
|
1479
1516
|
return id;
|
|
@@ -1664,7 +1701,8 @@ function validateProject(config) {
|
|
|
1664
1701
|
locale,
|
|
1665
1702
|
revisionKind: "en_edit_detected",
|
|
1666
1703
|
enHash: currentEnHash,
|
|
1667
|
-
body: row.body
|
|
1704
|
+
body: row.body,
|
|
1705
|
+
frontmatter: JSON.parse(row.frontmatter_json)
|
|
1668
1706
|
});
|
|
1669
1707
|
}
|
|
1670
1708
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
@@ -1793,6 +1831,10 @@ function buildWorklist(config, options = {}) {
|
|
|
1793
1831
|
}
|
|
1794
1832
|
}
|
|
1795
1833
|
db.close();
|
|
1834
|
+
const strategy = options.strategy ?? "all";
|
|
1835
|
+
if (strategy === "missing-only") {
|
|
1836
|
+
return items.filter((item) => item.reason === "missing");
|
|
1837
|
+
}
|
|
1796
1838
|
return items;
|
|
1797
1839
|
}
|
|
1798
1840
|
function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
@@ -1809,7 +1851,31 @@ init_sqlite();
|
|
|
1809
1851
|
|
|
1810
1852
|
// src/translate/gemini-client.ts
|
|
1811
1853
|
init_cjs_shims();
|
|
1812
|
-
|
|
1854
|
+
|
|
1855
|
+
// src/translate/gemini-models.ts
|
|
1856
|
+
init_cjs_shims();
|
|
1857
|
+
var GEMINI_MODEL_IDS = {
|
|
1858
|
+
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
1859
|
+
"gemini-3.1-pro": "gemini-3.1-pro-preview"
|
|
1860
|
+
};
|
|
1861
|
+
var DEFAULT_GEMINI_MODEL = "gemini-3.1-pro";
|
|
1862
|
+
function stripModelsPrefix(model) {
|
|
1863
|
+
return model.replace(/^models\//, "");
|
|
1864
|
+
}
|
|
1865
|
+
function resolveGeminiModelId(model) {
|
|
1866
|
+
const name = stripModelsPrefix(model);
|
|
1867
|
+
return GEMINI_MODEL_IDS[name] ?? name;
|
|
1868
|
+
}
|
|
1869
|
+
function normalizeGeminiDisplayName(model) {
|
|
1870
|
+
const name = stripModelsPrefix(model);
|
|
1871
|
+
if (name in GEMINI_MODEL_IDS) return name;
|
|
1872
|
+
const alias = Object.entries(GEMINI_MODEL_IDS).find(([, apiId]) => apiId === name);
|
|
1873
|
+
if (alias) return alias[0];
|
|
1874
|
+
return name;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// src/translate/gemini-client.ts
|
|
1878
|
+
var DEFAULT_MODEL = DEFAULT_GEMINI_MODEL;
|
|
1813
1879
|
function extractJson(text) {
|
|
1814
1880
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1815
1881
|
if (fenced?.[1]) return fenced[1].trim();
|
|
@@ -1823,13 +1889,17 @@ async function translatePageWithGemini(input) {
|
|
|
1823
1889
|
if (!apiKey) {
|
|
1824
1890
|
throw new Error("GEMINI_API_KEY is required for scribe translate");
|
|
1825
1891
|
}
|
|
1826
|
-
const
|
|
1892
|
+
const displayModel = normalizeGeminiDisplayName(
|
|
1893
|
+
input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL
|
|
1894
|
+
);
|
|
1895
|
+
const apiModel = resolveGeminiModelId(displayModel);
|
|
1827
1896
|
const ai = new genai.GoogleGenAI({ apiKey });
|
|
1828
1897
|
const response = await ai.models.generateContent({
|
|
1829
|
-
model,
|
|
1898
|
+
model: apiModel,
|
|
1830
1899
|
contents: input.prompt,
|
|
1831
1900
|
config: {
|
|
1832
|
-
responseMimeType: "application/json"
|
|
1901
|
+
responseMimeType: "application/json",
|
|
1902
|
+
...input.responseSchema ? { responseSchema: input.responseSchema } : {}
|
|
1833
1903
|
}
|
|
1834
1904
|
});
|
|
1835
1905
|
const raw = response.text ?? "";
|
|
@@ -1837,7 +1907,50 @@ async function translatePageWithGemini(input) {
|
|
|
1837
1907
|
if (!parsed.frontmatter || typeof parsed.body !== "string") {
|
|
1838
1908
|
throw new Error("Gemini response missing frontmatter/body");
|
|
1839
1909
|
}
|
|
1840
|
-
|
|
1910
|
+
const usageMetadata = response.usageMetadata;
|
|
1911
|
+
const usage = {
|
|
1912
|
+
inputTokens: usageMetadata?.promptTokenCount ?? 0,
|
|
1913
|
+
outputTokens: usageMetadata?.candidatesTokenCount ?? 0,
|
|
1914
|
+
totalTokens: usageMetadata?.totalTokenCount ?? 0
|
|
1915
|
+
};
|
|
1916
|
+
return { model: displayModel, raw, parsed, usage };
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/translate/gemini-pricing.ts
|
|
1920
|
+
init_cjs_shims();
|
|
1921
|
+
var CONTEXT_TIER_TOKENS = 2e5;
|
|
1922
|
+
var GEMINI_PRICING_USD = {
|
|
1923
|
+
"gemini-3.1-pro": { input: [2, 4], output: [12, 18] }
|
|
1924
|
+
};
|
|
1925
|
+
function normalizeModelId(model) {
|
|
1926
|
+
return model.replace(/^models\//, "").toLowerCase();
|
|
1927
|
+
}
|
|
1928
|
+
function tierRate(tokens, tiers) {
|
|
1929
|
+
return tokens <= CONTEXT_TIER_TOKENS ? tiers[0] : tiers[1];
|
|
1930
|
+
}
|
|
1931
|
+
function resolveModelPricing(model) {
|
|
1932
|
+
const id = normalizeModelId(model);
|
|
1933
|
+
if (GEMINI_PRICING_USD[id]) return GEMINI_PRICING_USD[id];
|
|
1934
|
+
const match = Object.entries(GEMINI_PRICING_USD).find(([key]) => id.includes(key));
|
|
1935
|
+
return match?.[1];
|
|
1936
|
+
}
|
|
1937
|
+
function estimateTranslationCostUsd(model, inputTokens, outputTokens) {
|
|
1938
|
+
const pricing = resolveModelPricing(model);
|
|
1939
|
+
if (!pricing) return void 0;
|
|
1940
|
+
const inputRate = tierRate(inputTokens, pricing.input);
|
|
1941
|
+
const outputRate = tierRate(outputTokens, pricing.output);
|
|
1942
|
+
return inputTokens / 1e6 * inputRate + outputTokens / 1e6 * outputRate;
|
|
1943
|
+
}
|
|
1944
|
+
function formatTokenCount(tokens) {
|
|
1945
|
+
if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(2)}M`;
|
|
1946
|
+
if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(1)}k`;
|
|
1947
|
+
return String(tokens);
|
|
1948
|
+
}
|
|
1949
|
+
function formatUsd(amount) {
|
|
1950
|
+
if (amount === void 0) return "n/a";
|
|
1951
|
+
if (amount >= 1) return `$${amount.toFixed(2)}`;
|
|
1952
|
+
if (amount >= 0.01) return `$${amount.toFixed(3)}`;
|
|
1953
|
+
return `$${amount.toFixed(4)}`;
|
|
1841
1954
|
}
|
|
1842
1955
|
|
|
1843
1956
|
// src/translate/prompts/translation-prompt.ts
|
|
@@ -1874,6 +1987,7 @@ function buildPageTranslationPrompt(input) {
|
|
|
1874
1987
|
prompt,
|
|
1875
1988
|
"",
|
|
1876
1989
|
...input.resolved.context ? ["## Context", input.resolved.context, ""] : [],
|
|
1990
|
+
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
1877
1991
|
"## Rules",
|
|
1878
1992
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
1879
1993
|
"",
|
|
@@ -1881,10 +1995,6 @@ function buildPageTranslationPrompt(input) {
|
|
|
1881
1995
|
"Return ONLY valid JSON with keys:",
|
|
1882
1996
|
input.slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).",
|
|
1883
1997
|
"",
|
|
1884
|
-
"## EN metadata",
|
|
1885
|
-
`title: ${input.enTitle}`,
|
|
1886
|
-
`description: ${input.enDescription}`,
|
|
1887
|
-
"",
|
|
1888
1998
|
"## EN translatable frontmatter (JSON)",
|
|
1889
1999
|
JSON.stringify(input.translatableFrontmatter, null, 2),
|
|
1890
2000
|
"",
|
|
@@ -1894,6 +2004,74 @@ function buildPageTranslationPrompt(input) {
|
|
|
1894
2004
|
return lines.join("\n");
|
|
1895
2005
|
}
|
|
1896
2006
|
|
|
2007
|
+
// src/translate/response-schema.ts
|
|
2008
|
+
init_cjs_shims();
|
|
2009
|
+
function getObjectShape(schema) {
|
|
2010
|
+
const base = peelOptionalWrappers(schema);
|
|
2011
|
+
if (base instanceof Object && "shape" in base) {
|
|
2012
|
+
return base.shape;
|
|
2013
|
+
}
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
function getArraySchema(schema) {
|
|
2017
|
+
const base = peelOptionalWrappers(schema);
|
|
2018
|
+
if (base instanceof Object && "element" in base && base._def?.type === "array") {
|
|
2019
|
+
return base;
|
|
2020
|
+
}
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
function extractTranslatableFromStructural(schema) {
|
|
2024
|
+
const arraySchema = getArraySchema(schema);
|
|
2025
|
+
if (arraySchema) {
|
|
2026
|
+
const inner = buildTranslatableSubschema(arraySchema.element);
|
|
2027
|
+
if (inner) return zod.z.array(inner);
|
|
2028
|
+
return null;
|
|
2029
|
+
}
|
|
2030
|
+
if (getObjectShape(schema)) {
|
|
2031
|
+
return buildTranslatableSubschema(schema);
|
|
2032
|
+
}
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
function buildTranslatableSubschema(schema) {
|
|
2036
|
+
const shape = getObjectShape(schema);
|
|
2037
|
+
if (!shape) return null;
|
|
2038
|
+
const out = {};
|
|
2039
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
2040
|
+
const childSchema = child;
|
|
2041
|
+
const kind = getFieldKind(childSchema);
|
|
2042
|
+
if (kind === "translatable") {
|
|
2043
|
+
out[key] = peelOptionalWrappers(childSchema);
|
|
2044
|
+
} else if (kind === "structural") {
|
|
2045
|
+
const nested = extractTranslatableFromStructural(childSchema);
|
|
2046
|
+
if (nested) out[key] = nested;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
if (Object.keys(out).length === 0) return null;
|
|
2050
|
+
return zod.z.object(out);
|
|
2051
|
+
}
|
|
2052
|
+
function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
2053
|
+
try {
|
|
2054
|
+
const translatable = buildTranslatableSubschema(schema);
|
|
2055
|
+
if (!translatable) return null;
|
|
2056
|
+
const responseShape = {
|
|
2057
|
+
frontmatter: translatable,
|
|
2058
|
+
body: zod.z.string()
|
|
2059
|
+
};
|
|
2060
|
+
if (slugStrategy === "localized") {
|
|
2061
|
+
responseShape.slug = zod.z.string();
|
|
2062
|
+
}
|
|
2063
|
+
const jsonSchema = zod.z.toJSONSchema(zod.z.object(responseShape), {
|
|
2064
|
+
target: "draft-2020-12",
|
|
2065
|
+
unrepresentable: "any",
|
|
2066
|
+
io: "output"
|
|
2067
|
+
});
|
|
2068
|
+
delete jsonSchema.$schema;
|
|
2069
|
+
return jsonSchema;
|
|
2070
|
+
} catch {
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1897
2075
|
// src/translate/resolve-translate-config.ts
|
|
1898
2076
|
init_cjs_shims();
|
|
1899
2077
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
@@ -1934,86 +2112,313 @@ function resolveTranslateConfig(config, type) {
|
|
|
1934
2112
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
1935
2113
|
}
|
|
1936
2114
|
|
|
2115
|
+
// src/translate/validate-translation.ts
|
|
2116
|
+
init_cjs_shims();
|
|
2117
|
+
function formatZodIssues(error) {
|
|
2118
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2119
|
+
}
|
|
2120
|
+
function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
|
|
2121
|
+
return pickTranslatable(rawFrontmatter, schema);
|
|
2122
|
+
}
|
|
2123
|
+
function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
|
|
2124
|
+
const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
|
|
2125
|
+
const merged = mergeStructuralOntoLocale(
|
|
2126
|
+
frontmatter,
|
|
2127
|
+
enDoc.frontmatter,
|
|
2128
|
+
typeSchema
|
|
2129
|
+
);
|
|
2130
|
+
const parsed = typeSchema.safeParse(merged);
|
|
2131
|
+
if (!parsed.success) {
|
|
2132
|
+
return { ok: false, error: formatZodIssues(parsed.error) };
|
|
2133
|
+
}
|
|
2134
|
+
return { ok: true, frontmatter };
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1937
2137
|
// src/translate/page-translator.ts
|
|
2138
|
+
function summarizeResults(results, durationMs) {
|
|
2139
|
+
return results.reduce(
|
|
2140
|
+
(totals, result) => {
|
|
2141
|
+
if (result.failed) totals.failed += 1;
|
|
2142
|
+
else if (result.skipped) totals.skipped += 1;
|
|
2143
|
+
else totals.translated += 1;
|
|
2144
|
+
totals.inputTokens += result.usage?.inputTokens ?? 0;
|
|
2145
|
+
totals.outputTokens += result.usage?.outputTokens ?? 0;
|
|
2146
|
+
totals.estimatedCostUsd += result.estimatedCostUsd ?? 0;
|
|
2147
|
+
return totals;
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
translated: 0,
|
|
2151
|
+
skipped: 0,
|
|
2152
|
+
failed: 0,
|
|
2153
|
+
inputTokens: 0,
|
|
2154
|
+
outputTokens: 0,
|
|
2155
|
+
estimatedCostUsd: 0,
|
|
2156
|
+
durationMs
|
|
2157
|
+
}
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
function formatTranslateError(error) {
|
|
2161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2162
|
+
try {
|
|
2163
|
+
const parsed = JSON.parse(message);
|
|
2164
|
+
if (parsed.error?.message) return parsed.error.message;
|
|
2165
|
+
} catch {
|
|
2166
|
+
}
|
|
2167
|
+
const embedded = message.match(/"message"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
2168
|
+
if (embedded?.[1]) {
|
|
2169
|
+
return embedded[1].replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
2170
|
+
}
|
|
2171
|
+
return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
|
|
2172
|
+
}
|
|
2173
|
+
async function runPool(items, concurrency, worker) {
|
|
2174
|
+
if (items.length === 0) return;
|
|
2175
|
+
let nextIndex = 0;
|
|
2176
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
2177
|
+
while (nextIndex < items.length) {
|
|
2178
|
+
const index = nextIndex++;
|
|
2179
|
+
await worker(items[index], index);
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
await Promise.all(runners);
|
|
2183
|
+
}
|
|
2184
|
+
function resolveContextLabel(enDoc, enSlug) {
|
|
2185
|
+
const fm = enDoc.frontmatter;
|
|
2186
|
+
for (const key of ["title", "name", "h1"]) {
|
|
2187
|
+
const value = fm[key];
|
|
2188
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2189
|
+
}
|
|
2190
|
+
return enSlug;
|
|
2191
|
+
}
|
|
1938
2192
|
async function translatePage(config, item, options = {}) {
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2193
|
+
const startedAt = Date.now();
|
|
2194
|
+
const base = {
|
|
2195
|
+
contentType: item.contentType,
|
|
2196
|
+
enSlug: item.enSlug,
|
|
2197
|
+
locale: item.locale
|
|
2198
|
+
};
|
|
2199
|
+
try {
|
|
2200
|
+
const type = config.types.find((t) => t.id === item.contentType);
|
|
2201
|
+
if (!type) throw new Error(`Unknown content type ${item.contentType}`);
|
|
2202
|
+
const enDoc = readEnDocument(config, type, item.enSlug);
|
|
2203
|
+
if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
|
|
2204
|
+
const payload = getTranslatablePayload(enDoc, type);
|
|
2205
|
+
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
2206
|
+
const db = openStore(config, "readonly");
|
|
2207
|
+
const existing = getTranslation(db, type.id, item.enSlug, item.locale);
|
|
2208
|
+
db.close();
|
|
2209
|
+
if (!options.force && existing && existing.en_hash === currentEnHash) {
|
|
2210
|
+
return {
|
|
2211
|
+
...base,
|
|
2212
|
+
skipped: true,
|
|
2213
|
+
reason: "fresh",
|
|
2214
|
+
durationMs: Date.now() - startedAt
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
const resolvedTranslate = resolveTranslateConfig(config, type);
|
|
2218
|
+
const model = options.model ?? resolvedTranslate.model;
|
|
2219
|
+
const prompt = buildPageTranslationPrompt({
|
|
2220
|
+
resolved: resolvedTranslate,
|
|
2221
|
+
targetLocale: item.locale,
|
|
2222
|
+
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2223
|
+
translatableFrontmatter: payload.frontmatter,
|
|
2224
|
+
enBody: payload.body,
|
|
2225
|
+
slugStrategy: type.slugStrategy
|
|
2226
|
+
});
|
|
2227
|
+
if (options.dryRun) {
|
|
2228
|
+
return {
|
|
2229
|
+
...base,
|
|
2230
|
+
skipped: false,
|
|
2231
|
+
model,
|
|
2232
|
+
durationMs: Date.now() - startedAt
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2236
|
+
const result = await translatePageWithGemini({
|
|
2237
|
+
prompt,
|
|
2238
|
+
model,
|
|
2239
|
+
responseSchema: responseSchema ?? void 0
|
|
2240
|
+
});
|
|
2241
|
+
const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2242
|
+
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2243
|
+
if (!validated.ok) {
|
|
2244
|
+
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2245
|
+
}
|
|
2246
|
+
const writeDb = openStore(config, "readwrite");
|
|
2247
|
+
upsertTranslation(writeDb, {
|
|
2248
|
+
contentType: type.id,
|
|
1951
2249
|
enSlug: item.enSlug,
|
|
1952
2250
|
locale: item.locale,
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
translatableFrontmatter: payload.frontmatter,
|
|
1964
|
-
enBody: payload.body,
|
|
1965
|
-
slugStrategy: type.slugStrategy
|
|
1966
|
-
});
|
|
1967
|
-
if (options.dryRun) {
|
|
1968
|
-
return {
|
|
1969
|
-
contentType: item.contentType,
|
|
2251
|
+
slug,
|
|
2252
|
+
frontmatter: validated.frontmatter,
|
|
2253
|
+
body: result.parsed.body,
|
|
2254
|
+
enHash: currentEnHash,
|
|
2255
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2256
|
+
model: result.model
|
|
2257
|
+
});
|
|
2258
|
+
writeDb.close();
|
|
2259
|
+
recordRevision(config, {
|
|
2260
|
+
contentType: type.id,
|
|
1970
2261
|
enSlug: item.enSlug,
|
|
1971
2262
|
locale: item.locale,
|
|
2263
|
+
revisionKind: "translation",
|
|
2264
|
+
enHash: currentEnHash,
|
|
2265
|
+
body: result.parsed.body,
|
|
2266
|
+
frontmatter: validated.frontmatter,
|
|
2267
|
+
model: result.model
|
|
2268
|
+
});
|
|
2269
|
+
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2270
|
+
normalizeGeminiDisplayName(result.model),
|
|
2271
|
+
result.usage.inputTokens,
|
|
2272
|
+
result.usage.outputTokens
|
|
2273
|
+
);
|
|
2274
|
+
return {
|
|
2275
|
+
...base,
|
|
1972
2276
|
skipped: false,
|
|
1973
|
-
model:
|
|
2277
|
+
model: result.model,
|
|
2278
|
+
usage: result.usage,
|
|
2279
|
+
estimatedCostUsd,
|
|
2280
|
+
durationMs: Date.now() - startedAt
|
|
2281
|
+
};
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
return {
|
|
2284
|
+
...base,
|
|
2285
|
+
skipped: false,
|
|
2286
|
+
failed: true,
|
|
2287
|
+
error: formatTranslateError(error),
|
|
2288
|
+
durationMs: Date.now() - startedAt
|
|
1974
2289
|
};
|
|
1975
2290
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
const
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
model: result.model
|
|
2291
|
+
}
|
|
2292
|
+
function labelForItem(item) {
|
|
2293
|
+
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
2294
|
+
}
|
|
2295
|
+
async function translateWorklist(config, items, options = {}) {
|
|
2296
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
2297
|
+
const startedAt = Date.now();
|
|
2298
|
+
const results = new Array(items.length);
|
|
2299
|
+
const active = /* @__PURE__ */ new Set();
|
|
2300
|
+
options.onProgress?.({
|
|
2301
|
+
type: "start",
|
|
2302
|
+
total: items.length,
|
|
2303
|
+
concurrency,
|
|
2304
|
+
dryRun: Boolean(options.dryRun),
|
|
2305
|
+
model: options.model
|
|
1992
2306
|
});
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
model: result.model
|
|
2307
|
+
await runPool(items, concurrency, async (item, index) => {
|
|
2308
|
+
const label = labelForItem(item);
|
|
2309
|
+
active.add(label);
|
|
2310
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
2311
|
+
const result = await translatePage(config, item, options);
|
|
2312
|
+
results[index] = result;
|
|
2313
|
+
active.delete(label);
|
|
2314
|
+
options.onProgress?.({ type: "item-done", result });
|
|
2002
2315
|
});
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
locale: item.locale,
|
|
2007
|
-
skipped: false,
|
|
2008
|
-
model: result.model
|
|
2009
|
-
};
|
|
2316
|
+
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
2317
|
+
options.onProgress?.({ type: "done", results, totals });
|
|
2318
|
+
return results;
|
|
2010
2319
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2320
|
+
|
|
2321
|
+
// src/export/build-static-raw-exports.ts
|
|
2322
|
+
init_cjs_shims();
|
|
2323
|
+
|
|
2324
|
+
// src/loader/parse-mdx.ts
|
|
2325
|
+
init_cjs_shims();
|
|
2326
|
+
function serializeMdx(frontmatter, content) {
|
|
2327
|
+
return matter__default.default.stringify(content, frontmatter);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/export/build-static-raw-exports.ts
|
|
2331
|
+
function normalizeExtension(ext) {
|
|
2332
|
+
return ext.startsWith(".") ? ext : `.${ext}`;
|
|
2333
|
+
}
|
|
2334
|
+
function exportDirSegment(pathTemplate) {
|
|
2335
|
+
const prefix = pathPrefix(pathTemplate);
|
|
2336
|
+
return prefix.replace(/^\/+|\/+$/g, "");
|
|
2337
|
+
}
|
|
2338
|
+
function getStaticExportRoots(project, options = {}) {
|
|
2339
|
+
const { config } = project;
|
|
2340
|
+
const locales = options.locales ?? config.locales;
|
|
2341
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2342
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2343
|
+
for (const type of project.listTypes()) {
|
|
2344
|
+
if (!isRoutableType(type.config)) continue;
|
|
2345
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2346
|
+
const segment = exportDirSegment(type.config.path);
|
|
2347
|
+
roots.add(`${segment}/`);
|
|
2348
|
+
for (const locale of locales) {
|
|
2349
|
+
if (locale === config.defaultLocale) continue;
|
|
2350
|
+
roots.add(`${locale}/${segment}/`);
|
|
2351
|
+
}
|
|
2015
2352
|
}
|
|
2016
|
-
return
|
|
2353
|
+
return [...roots].sort();
|
|
2354
|
+
}
|
|
2355
|
+
function buildStaticRawExports(project, options = {}) {
|
|
2356
|
+
const { config } = project;
|
|
2357
|
+
const extension = normalizeExtension(options.extension ?? ".mdx");
|
|
2358
|
+
const locales = options.locales ?? config.locales;
|
|
2359
|
+
const excludeRedirected = options.excludeRedirected ?? true;
|
|
2360
|
+
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2361
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2362
|
+
const out = [];
|
|
2363
|
+
for (const type of project.listTypes()) {
|
|
2364
|
+
if (!isRoutableType(type.config)) continue;
|
|
2365
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2366
|
+
const pathTemplate = type.config.path;
|
|
2367
|
+
const all = type.load();
|
|
2368
|
+
const enIdx = all.get(config.defaultLocale);
|
|
2369
|
+
if (!enIdx) continue;
|
|
2370
|
+
for (const locale of locales) {
|
|
2371
|
+
for (const enDoc of enIdx.bySlug.values()) {
|
|
2372
|
+
const resolved = type.resolve(enDoc.slug, locale);
|
|
2373
|
+
if (!resolved.document) continue;
|
|
2374
|
+
if (excludeRedirected && resolved.shouldRedirectTo) continue;
|
|
2375
|
+
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2376
|
+
const doc = resolved.document;
|
|
2377
|
+
const slugWithExt = `${doc.slug}${extension}`;
|
|
2378
|
+
const urlPath = resolvePath(pathTemplate, slugWithExt, locale, config.defaultLocale);
|
|
2379
|
+
out.push({
|
|
2380
|
+
relativePath: urlPath.slice(1),
|
|
2381
|
+
urlPath,
|
|
2382
|
+
locale,
|
|
2383
|
+
typeId: type.id,
|
|
2384
|
+
enSlug: doc.enSlug,
|
|
2385
|
+
source: serializeMdx(doc.frontmatter, doc.content)
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return out;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/export/write-static-raw-exports.ts
|
|
2394
|
+
init_cjs_shims();
|
|
2395
|
+
function rmDirIfExists(dir) {
|
|
2396
|
+
if (fs2__default.default.existsSync(dir)) {
|
|
2397
|
+
fs2__default.default.rmSync(dir, { recursive: true, force: true });
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function writeStaticRawExports(project, options = {}) {
|
|
2401
|
+
const outDir = path3__default.default.resolve(options.outDir ?? "public");
|
|
2402
|
+
const typeFilter = options.types;
|
|
2403
|
+
for (const root of getStaticExportRoots(project, {
|
|
2404
|
+
types: typeFilter,
|
|
2405
|
+
locales: options.locales
|
|
2406
|
+
})) {
|
|
2407
|
+
rmDirIfExists(path3__default.default.join(outDir, root));
|
|
2408
|
+
}
|
|
2409
|
+
const exports = buildStaticRawExports(project, options);
|
|
2410
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2411
|
+
for (const item of exports) {
|
|
2412
|
+
const filePath = path3__default.default.join(outDir, item.relativePath);
|
|
2413
|
+
fs2__default.default.mkdirSync(path3__default.default.dirname(filePath), { recursive: true });
|
|
2414
|
+
fs2__default.default.writeFileSync(filePath, item.source, "utf8");
|
|
2415
|
+
const key = `${item.typeId}/${item.locale}`;
|
|
2416
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2417
|
+
}
|
|
2418
|
+
for (const [key, count] of [...counts.entries()].sort()) {
|
|
2419
|
+
console.log(` ${key}: ${count}`);
|
|
2420
|
+
}
|
|
2421
|
+
return { exports, written: exports.length };
|
|
2017
2422
|
}
|
|
2018
2423
|
|
|
2019
2424
|
// src/config/load-env.ts
|
|
@@ -2034,99 +2439,464 @@ function loadEnvFromCwd(cwd) {
|
|
|
2034
2439
|
// studio/server.ts
|
|
2035
2440
|
init_cjs_shims();
|
|
2036
2441
|
init_sqlite();
|
|
2037
|
-
function
|
|
2442
|
+
function escapeHtml(value) {
|
|
2443
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2444
|
+
}
|
|
2445
|
+
function encodePathSegment(value) {
|
|
2446
|
+
return encodeURIComponent(value);
|
|
2447
|
+
}
|
|
2448
|
+
function statusDot(status) {
|
|
2449
|
+
const labels = {
|
|
2450
|
+
source: "en",
|
|
2451
|
+
"up-to-date": "ok",
|
|
2452
|
+
stale: "stale",
|
|
2453
|
+
missing: "\u2014"
|
|
2454
|
+
};
|
|
2455
|
+
return `<span class="status" title="${status}"><span class="dot dot-${status}"></span>${labels[status]}</span>`;
|
|
2456
|
+
}
|
|
2457
|
+
function documentStatus(config, db, type, enSlug, locale) {
|
|
2458
|
+
if (locale === config.defaultLocale) {
|
|
2459
|
+
return { status: "source" };
|
|
2460
|
+
}
|
|
2461
|
+
const enDoc = readEnDocument(config, type.config, enSlug);
|
|
2462
|
+
if (!enDoc) {
|
|
2463
|
+
return { status: "missing" };
|
|
2464
|
+
}
|
|
2465
|
+
const payload = getTranslatablePayload(enDoc, type.config);
|
|
2466
|
+
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
2467
|
+
const row = getTranslation(db, type.id, enSlug, locale);
|
|
2468
|
+
if (!row) {
|
|
2469
|
+
return { status: "missing", currentEnHash };
|
|
2470
|
+
}
|
|
2471
|
+
if (row.en_hash !== currentEnHash) {
|
|
2472
|
+
return { status: "stale", currentEnHash, storedEnHash: row.en_hash };
|
|
2473
|
+
}
|
|
2474
|
+
return { status: "up-to-date", currentEnHash, storedEnHash: row.en_hash };
|
|
2475
|
+
}
|
|
2476
|
+
function flattenFrontmatter(data, prefix = "") {
|
|
2477
|
+
const rows = [];
|
|
2478
|
+
for (const [key, value] of Object.entries(data)) {
|
|
2479
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
2480
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
2481
|
+
rows.push(...flattenFrontmatter(value, fullKey));
|
|
2482
|
+
} else {
|
|
2483
|
+
const display = typeof value === "string" ? value : value === void 0 ? "" : JSON.stringify(value, null, 2);
|
|
2484
|
+
rows.push({ key: fullKey, value: display });
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return rows;
|
|
2488
|
+
}
|
|
2489
|
+
function translatableKeySet(schema) {
|
|
2490
|
+
return new Set(
|
|
2491
|
+
introspectSchema(schema).filter((field2) => field2.kind === "translatable").map((field2) => field2.path.join("."))
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
function renderFrontmatterTable(frontmatter, schema) {
|
|
2495
|
+
const translatable = translatableKeySet(schema);
|
|
2496
|
+
const rows = flattenFrontmatter(frontmatter).map((row) => {
|
|
2497
|
+
const flag = translatable.has(row.key) ? `<span class="flag t" title="Translatable">T</span>` : `<span class="flag s" title="Structural">S</span>`;
|
|
2498
|
+
return `<tr>
|
|
2499
|
+
<td class="k">${flag}${escapeHtml(row.key)}</td>
|
|
2500
|
+
<td class="v">${escapeHtml(row.value)}</td>
|
|
2501
|
+
</tr>`;
|
|
2502
|
+
}).join("");
|
|
2503
|
+
return `<table class="kv">
|
|
2504
|
+
<tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
|
|
2505
|
+
</table>`;
|
|
2506
|
+
}
|
|
2507
|
+
function renderRevisionSnapshot(revision) {
|
|
2508
|
+
if (revision.frontmatter_json && revision.body) {
|
|
2509
|
+
let frontmatter = {};
|
|
2510
|
+
try {
|
|
2511
|
+
frontmatter = JSON.parse(revision.frontmatter_json);
|
|
2512
|
+
} catch {
|
|
2513
|
+
frontmatter = {};
|
|
2514
|
+
}
|
|
2515
|
+
const fmRows = flattenFrontmatter(frontmatter).map(
|
|
2516
|
+
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2517
|
+
).join("");
|
|
2518
|
+
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2519
|
+
<pre class="code">${escapeHtml(revision.body)}</pre>`;
|
|
2520
|
+
}
|
|
2521
|
+
return `<p class="dim">${escapeHtml(revision.body_preview ?? "(no snapshot)")}</p>`;
|
|
2522
|
+
}
|
|
2523
|
+
function renderRevisionTimeline(revisions) {
|
|
2524
|
+
if (revisions.length === 0) {
|
|
2525
|
+
return `<p class="dim">No history.</p>`;
|
|
2526
|
+
}
|
|
2527
|
+
const items = revisions.map(
|
|
2528
|
+
(row) => `<tr>
|
|
2529
|
+
<td class="dim">${escapeHtml(row.created_at.slice(0, 19))}</td>
|
|
2530
|
+
<td>${escapeHtml(row.revision_kind)}</td>
|
|
2531
|
+
<td class="dim">${row.model ? escapeHtml(row.model) : "\u2014"}</td>
|
|
2532
|
+
<td class="dim mono">${escapeHtml(row.en_hash.slice(0, 8))}</td>
|
|
2533
|
+
<td><details><summary>view</summary>${renderRevisionSnapshot(row)}</details></td>
|
|
2534
|
+
</tr>`
|
|
2535
|
+
).join("");
|
|
2536
|
+
return `<table class="data"><thead><tr>
|
|
2537
|
+
<th>When</th><th>Kind</th><th>Model</th><th>Hash</th><th></th>
|
|
2538
|
+
</tr></thead><tbody>${items}</tbody></table>`;
|
|
2539
|
+
}
|
|
2540
|
+
function renderLayout(title, body, project, options = {}) {
|
|
2541
|
+
const typeLinks = project.listTypes().map((type) => {
|
|
2542
|
+
const active = type.id === options.activeTypeId ? " active" : "";
|
|
2543
|
+
return `<a class="tree-item${active}" href="/type/${encodePathSegment(type.id)}">
|
|
2544
|
+
<span class="tree-label">${escapeHtml(type.config.label)}</span>
|
|
2545
|
+
<span class="tree-meta">${escapeHtml(type.id)}</span>
|
|
2546
|
+
</a>`;
|
|
2547
|
+
}).join("");
|
|
2038
2548
|
return `<!doctype html>
|
|
2039
2549
|
<html lang="en">
|
|
2040
2550
|
<head>
|
|
2041
2551
|
<meta charset="utf-8" />
|
|
2042
2552
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2043
|
-
<title>${title} \u2014 Scribe
|
|
2553
|
+
<title>${escapeHtml(title)} \u2014 Scribe</title>
|
|
2044
2554
|
<style>
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2555
|
+
:root {
|
|
2556
|
+
--bg: #1e1e1e;
|
|
2557
|
+
--sidebar: #252526;
|
|
2558
|
+
--bar: #333333;
|
|
2559
|
+
--panel: #1e1e1e;
|
|
2560
|
+
--border: #3c3c3c;
|
|
2561
|
+
--text: #cccccc;
|
|
2562
|
+
--dim: #858585;
|
|
2563
|
+
--accent: #3794ff;
|
|
2564
|
+
--hover: #2a2d2e;
|
|
2565
|
+
--active: #37373d;
|
|
2566
|
+
--ok: #89d185;
|
|
2567
|
+
--stale: #cca700;
|
|
2568
|
+
--missing: #f48771;
|
|
2569
|
+
--source: #75beff;
|
|
2570
|
+
--mono: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace;
|
|
2571
|
+
--ui: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2572
|
+
--fs: 13px;
|
|
2573
|
+
--fs-sm: 11px;
|
|
2574
|
+
}
|
|
2575
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2576
|
+
body { font: var(--fs)/1.4 var(--ui); background: var(--bg); color: var(--text); }
|
|
2577
|
+
a { color: var(--accent); text-decoration: none; }
|
|
2578
|
+
a:hover { text-decoration: underline; }
|
|
2579
|
+
.app { display: flex; height: 100vh; overflow: hidden; }
|
|
2580
|
+
|
|
2581
|
+
/* activity bar */
|
|
2582
|
+
.actbar {
|
|
2583
|
+
width: 48px; flex-shrink: 0; background: var(--bar);
|
|
2584
|
+
display: flex; flex-direction: column; align-items: center;
|
|
2585
|
+
padding: 8px 0; gap: 4px; border-right: 1px solid var(--border);
|
|
2586
|
+
}
|
|
2587
|
+
.actbar a {
|
|
2588
|
+
width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;
|
|
2589
|
+
color: var(--dim); font-size: 18px; text-decoration: none; position: relative;
|
|
2590
|
+
}
|
|
2591
|
+
.actbar a:hover { color: var(--text); }
|
|
2592
|
+
.actbar a.active { color: var(--text); }
|
|
2593
|
+
.actbar a.active::before {
|
|
2594
|
+
content: ""; position: absolute; left: 0; top: 8px; bottom: 8px;
|
|
2595
|
+
width: 2px; background: var(--accent);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
/* sidebar */
|
|
2599
|
+
.sidebar {
|
|
2600
|
+
width: 200px; flex-shrink: 0; background: var(--sidebar);
|
|
2601
|
+
border-right: 1px solid var(--border); display: flex; flex-direction: column;
|
|
2602
|
+
overflow: hidden;
|
|
2603
|
+
}
|
|
2604
|
+
.sidebar-head {
|
|
2605
|
+
padding: 8px 12px; font-size: var(--fs-sm); font-weight: 600;
|
|
2606
|
+
text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim);
|
|
2607
|
+
}
|
|
2608
|
+
.sidebar-body { flex: 1; overflow-y: auto; padding: 2px 0; }
|
|
2609
|
+
.tree-item {
|
|
2610
|
+
display: flex; flex-direction: column; padding: 3px 12px 3px 20px;
|
|
2611
|
+
color: var(--text); text-decoration: none; line-height: 1.3;
|
|
2612
|
+
}
|
|
2613
|
+
.tree-item:hover { background: var(--hover); text-decoration: none; }
|
|
2614
|
+
.tree-item.active { background: var(--active); }
|
|
2615
|
+
.tree-label { font-size: var(--fs); }
|
|
2616
|
+
.tree-meta { font-size: var(--fs-sm); color: var(--dim); }
|
|
2617
|
+
|
|
2618
|
+
/* main */
|
|
2619
|
+
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
2620
|
+
.toolbar {
|
|
2621
|
+
display: flex; align-items: center; gap: 6px; padding: 0 12px;
|
|
2622
|
+
height: 35px; background: var(--sidebar); border-bottom: 1px solid var(--border);
|
|
2623
|
+
font-size: var(--fs-sm); color: var(--dim); flex-shrink: 0; overflow: hidden;
|
|
2624
|
+
}
|
|
2625
|
+
.toolbar a { color: var(--dim); }
|
|
2626
|
+
.toolbar a:hover { color: var(--text); }
|
|
2627
|
+
.toolbar .sep { color: var(--border); }
|
|
2628
|
+
.content { flex: 1; overflow-y: auto; padding: 0; }
|
|
2629
|
+
|
|
2630
|
+
/* tabs (locale switcher) */
|
|
2631
|
+
.tabs {
|
|
2632
|
+
display: flex; background: var(--sidebar); border-bottom: 1px solid var(--border);
|
|
2633
|
+
overflow-x: auto; flex-shrink: 0;
|
|
2634
|
+
}
|
|
2635
|
+
.tab {
|
|
2636
|
+
display: flex; align-items: center; gap: 6px; padding: 6px 12px;
|
|
2637
|
+
font-size: var(--fs-sm); color: var(--dim); border-right: 1px solid var(--border);
|
|
2638
|
+
text-decoration: none; white-space: nowrap;
|
|
2639
|
+
}
|
|
2640
|
+
.tab:hover { background: var(--hover); color: var(--text); text-decoration: none; }
|
|
2641
|
+
.tab.active {
|
|
2642
|
+
background: var(--bg); color: var(--text);
|
|
2643
|
+
border-bottom: 1px solid var(--bg); margin-bottom: -1px;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/* status dots */
|
|
2647
|
+
.status { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-sm); color: var(--dim); }
|
|
2648
|
+
.dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
|
2649
|
+
.dot-source { background: var(--source); }
|
|
2650
|
+
.dot-up-to-date { background: var(--ok); }
|
|
2651
|
+
.dot-stale { background: var(--stale); }
|
|
2652
|
+
.dot-missing { background: var(--missing); opacity: 0.7; }
|
|
2653
|
+
|
|
2654
|
+
/* sections */
|
|
2655
|
+
.section { border-bottom: 1px solid var(--border); }
|
|
2656
|
+
.section-head {
|
|
2657
|
+
padding: 4px 12px; font-size: var(--fs-sm); font-weight: 600;
|
|
2658
|
+
text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim);
|
|
2659
|
+
background: var(--sidebar);
|
|
2660
|
+
}
|
|
2661
|
+
.section-body { padding: 0; }
|
|
2662
|
+
|
|
2663
|
+
/* tables */
|
|
2664
|
+
table { border-collapse: collapse; width: 100%; font-size: var(--fs); }
|
|
2665
|
+
.data th, .data td { padding: 2px 12px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
2666
|
+
.data th { font-size: var(--fs-sm); font-weight: 600; color: var(--dim); background: var(--sidebar); height: 22px; }
|
|
2667
|
+
.data tr:hover td { background: var(--hover); }
|
|
2668
|
+
.data .mono { font-family: var(--mono); font-size: var(--fs-sm); }
|
|
2669
|
+
|
|
2670
|
+
/* key-value (frontmatter) */
|
|
2671
|
+
.kv td { padding: 1px 12px; font-family: var(--mono); font-size: var(--fs-sm); vertical-align: top; border-bottom: 1px solid var(--border); }
|
|
2672
|
+
.kv .k { width: 160px; color: #9cdcfe; white-space: nowrap; }
|
|
2673
|
+
.kv .v { color: #ce9178; white-space: pre-wrap; word-break: break-word; }
|
|
2674
|
+
.flag { font-size: 9px; font-weight: 700; margin-right: 4px; opacity: 0.5; }
|
|
2675
|
+
.flag.t { color: var(--ok); }
|
|
2676
|
+
.flag.s { color: var(--dim); }
|
|
2677
|
+
|
|
2678
|
+
/* code block */
|
|
2679
|
+
.code {
|
|
2680
|
+
margin: 0; padding: 8px 12px; font: var(--fs-sm)/1.5 var(--mono);
|
|
2681
|
+
white-space: pre-wrap; word-break: break-word; color: var(--text);
|
|
2682
|
+
background: var(--bg); border: none;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/* meta row */
|
|
2686
|
+
.meta { display: flex; flex-wrap: wrap; font-size: var(--fs-sm); border-bottom: 1px solid var(--border); padding: 4px 0; }
|
|
2687
|
+
.meta dt { padding: 0 4px 0 12px; color: var(--dim); }
|
|
2688
|
+
.meta dt::after { content: ":"; }
|
|
2689
|
+
.meta dd { padding: 0 12px 0 0; font-family: var(--mono); }
|
|
2690
|
+
|
|
2691
|
+
/* misc */
|
|
2692
|
+
.dim { color: var(--dim); }
|
|
2693
|
+
.page-title { padding: 8px 12px 4px; font-size: 14px; font-weight: 400; }
|
|
2694
|
+
.page-sub { padding: 0 12px 8px; font-size: var(--fs-sm); color: var(--dim); }
|
|
2695
|
+
details summary { cursor: pointer; color: var(--accent); font-size: var(--fs-sm); }
|
|
2696
|
+
details[open] summary { margin-bottom: 4px; }
|
|
2697
|
+
.tag { font-size: var(--fs-sm); color: var(--dim); margin-left: 6px; }
|
|
2698
|
+
.tag-warn { color: var(--stale); }
|
|
2699
|
+
.tag-err { color: var(--missing); }
|
|
2056
2700
|
</style>
|
|
2057
2701
|
</head>
|
|
2058
2702
|
<body>
|
|
2059
|
-
<
|
|
2060
|
-
<
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2703
|
+
<div class="app">
|
|
2704
|
+
<nav class="actbar">
|
|
2705
|
+
<a href="/" title="Overview">\u2302</a>
|
|
2706
|
+
<a href="/staleness" title="Staleness">\u26A0</a>
|
|
2707
|
+
</nav>
|
|
2708
|
+
<aside class="sidebar">
|
|
2709
|
+
<div class="sidebar-head">Types</div>
|
|
2710
|
+
<div class="sidebar-body">${typeLinks || `<span class="dim" style="padding:8px 12px">\u2014</span>`}</div>
|
|
2711
|
+
</aside>
|
|
2712
|
+
<div class="main">
|
|
2713
|
+
<div class="content">${body}</div>
|
|
2714
|
+
</div>
|
|
2715
|
+
</div>
|
|
2066
2716
|
</body>
|
|
2067
2717
|
</html>`;
|
|
2068
2718
|
}
|
|
2719
|
+
function docTitleFromFrontmatter(frontmatter, enSlug) {
|
|
2720
|
+
const title = frontmatter.title;
|
|
2721
|
+
if (typeof title === "string" && title.trim()) return title;
|
|
2722
|
+
return enSlug;
|
|
2723
|
+
}
|
|
2069
2724
|
async function startStudio(project, options = {}) {
|
|
2070
2725
|
const app = new hono.Hono();
|
|
2071
2726
|
const config = project.config;
|
|
2072
2727
|
app.get("/", (c) => {
|
|
2073
|
-
const
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2728
|
+
const db = openStore(config, "readonly");
|
|
2729
|
+
const worklist = buildWorklist(config);
|
|
2730
|
+
const rows = project.listTypes().map((type) => {
|
|
2731
|
+
const enCount = type.list().length;
|
|
2732
|
+
const localeCells = config.locales.filter((locale) => locale !== config.defaultLocale).map((locale) => {
|
|
2733
|
+
const translated = listTranslationsForLocale(db, type.id, locale).length;
|
|
2734
|
+
const stale = worklist.filter(
|
|
2735
|
+
(item) => item.contentType === type.id && item.locale === locale && item.reason === "stale"
|
|
2736
|
+
).length;
|
|
2737
|
+
const missing = worklist.filter(
|
|
2738
|
+
(item) => item.contentType === type.id && item.locale === locale && item.reason === "missing"
|
|
2739
|
+
).length;
|
|
2740
|
+
const tags = [
|
|
2741
|
+
stale ? `<span class="tag tag-warn">${stale}s</span>` : "",
|
|
2742
|
+
missing ? `<span class="tag tag-err">${missing}m</span>` : ""
|
|
2743
|
+
].join("");
|
|
2744
|
+
return `<td>${translated}${tags}</td>`;
|
|
2077
2745
|
}).join("");
|
|
2078
|
-
return `<
|
|
2746
|
+
return `<tr>
|
|
2747
|
+
<td><a href="/type/${encodePathSegment(type.id)}">${escapeHtml(type.config.label)}</a></td>
|
|
2748
|
+
<td class="dim">${escapeHtml(type.id)}</td>
|
|
2749
|
+
<td>${enCount}</td>
|
|
2750
|
+
${localeCells}
|
|
2751
|
+
</tr>`;
|
|
2079
2752
|
});
|
|
2080
|
-
|
|
2753
|
+
db.close();
|
|
2754
|
+
const localeHeaders = config.locales.filter((l) => l !== config.defaultLocale).map((l) => `<th>${escapeHtml(l)}</th>`).join("");
|
|
2755
|
+
const html = `<div class="toolbar">Overview</div>
|
|
2756
|
+
<table class="data">
|
|
2757
|
+
<thead><tr><th>Type</th><th>ID</th><th>EN</th>${localeHeaders}</tr></thead>
|
|
2758
|
+
<tbody>${rows.join("")}</tbody>
|
|
2759
|
+
</table>`;
|
|
2760
|
+
return c.html(renderLayout("Overview", html, project));
|
|
2081
2761
|
});
|
|
2082
|
-
app.get("/
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2762
|
+
app.get("/type/:id", (c) => {
|
|
2763
|
+
const typeId = c.req.param("id");
|
|
2764
|
+
const type = project.getType(typeId);
|
|
2765
|
+
if (!type) {
|
|
2766
|
+
return c.html(renderLayout("Not found", `<div class="toolbar">Unknown type</div>`, project), 404);
|
|
2767
|
+
}
|
|
2768
|
+
const db = openStore(config, "readonly");
|
|
2769
|
+
const locales = config.locales;
|
|
2770
|
+
const headerCells = locales.map((locale) => `<th>${escapeHtml(locale)}</th>`).join("");
|
|
2771
|
+
const rows = type.list().map((doc) => {
|
|
2772
|
+
const title = docTitleFromFrontmatter(doc.frontmatter, doc.slug);
|
|
2773
|
+
const statusCells = locales.map((locale) => {
|
|
2774
|
+
const { status } = documentStatus(config, db, type, doc.slug, locale);
|
|
2775
|
+
return `<td>${statusDot(status)}</td>`;
|
|
2776
|
+
}).join("");
|
|
2777
|
+
return `<tr>
|
|
2778
|
+
<td class="mono"><a href="/type/${encodePathSegment(typeId)}/doc/${encodePathSegment(doc.slug)}">${escapeHtml(doc.slug)}</a></td>
|
|
2779
|
+
<td>${escapeHtml(title)}</td>
|
|
2780
|
+
${statusCells}
|
|
2781
|
+
</tr>`;
|
|
2782
|
+
}).join("");
|
|
2783
|
+
db.close();
|
|
2784
|
+
const html = `<div class="toolbar">
|
|
2785
|
+
<a href="/">Overview</a><span class="sep">\u203A</span>${escapeHtml(type.config.label)}
|
|
2786
|
+
</div>
|
|
2787
|
+
<table class="data">
|
|
2788
|
+
<thead><tr><th>Slug</th><th>Title</th>${headerCells}</tr></thead>
|
|
2789
|
+
<tbody>${rows || `<tr><td colspan="${2 + locales.length}" class="dim">No documents</td></tr>`}</tbody>
|
|
2790
|
+
</table>`;
|
|
2791
|
+
return c.html(renderLayout(type.config.label, html, project, { activeTypeId: typeId }));
|
|
2090
2792
|
});
|
|
2091
|
-
app.get("/
|
|
2092
|
-
const typeId = c.req.
|
|
2093
|
-
const enSlug = c.req.
|
|
2094
|
-
|
|
2793
|
+
app.get("/type/:id/doc/:enSlug", (c) => {
|
|
2794
|
+
const typeId = c.req.param("id");
|
|
2795
|
+
const enSlug = c.req.param("enSlug");
|
|
2796
|
+
const locale = c.req.query("locale") ?? config.defaultLocale;
|
|
2797
|
+
const showRaw = c.req.query("raw") === "1";
|
|
2798
|
+
const type = project.getType(typeId);
|
|
2799
|
+
if (!type) {
|
|
2800
|
+
return c.html(renderLayout("Not found", `<div class="toolbar">Unknown type</div>`, project), 404);
|
|
2801
|
+
}
|
|
2095
2802
|
const db = openStore(config, "readonly");
|
|
2096
|
-
|
|
2097
|
-
if (
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2803
|
+
const enDoc = readEnDocument(config, type.config, enSlug);
|
|
2804
|
+
if (!enDoc) {
|
|
2805
|
+
db.close();
|
|
2806
|
+
return c.html(
|
|
2807
|
+
renderLayout("Not found", `<div class="toolbar">Not found</div><p class="dim" style="padding:12px">${escapeHtml(enSlug)}</p>`, project, {
|
|
2808
|
+
activeTypeId: typeId
|
|
2809
|
+
}),
|
|
2810
|
+
404
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
const localeTabs = config.locales.map((loc) => {
|
|
2814
|
+
const { status } = documentStatus(config, db, type, enSlug, loc);
|
|
2815
|
+
const active = loc === locale ? " active" : "";
|
|
2816
|
+
const href = `/type/${encodePathSegment(typeId)}/doc/${encodePathSegment(enSlug)}?locale=${encodePathSegment(loc)}`;
|
|
2817
|
+
return `<a class="tab${active}" href="${href}">${escapeHtml(loc)} ${statusDot(status)}</a>`;
|
|
2818
|
+
}).join("");
|
|
2819
|
+
let contentPanel = "";
|
|
2820
|
+
let metaPanel = "";
|
|
2821
|
+
let historyPanel = "";
|
|
2822
|
+
if (locale === config.defaultLocale) {
|
|
2823
|
+
contentPanel = `<div class="section">
|
|
2824
|
+
<div class="section-head">Frontmatter</div>
|
|
2825
|
+
<div class="section-body">${renderFrontmatterTable(enDoc.frontmatter, type.config.schema)}</div>
|
|
2826
|
+
<div class="section-head">Body</div>
|
|
2827
|
+
<pre class="code">${escapeHtml(enDoc.content)}</pre>
|
|
2828
|
+
</div>`;
|
|
2102
2829
|
} else {
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2830
|
+
const translation = getTranslation(db, typeId, enSlug, locale);
|
|
2831
|
+
const { status, currentEnHash, storedEnHash } = documentStatus(config, db, type, enSlug, locale);
|
|
2832
|
+
if (translation) {
|
|
2833
|
+
const rawFrontmatter = JSON.parse(translation.frontmatter_json);
|
|
2834
|
+
const displayFrontmatter = showRaw ? rawFrontmatter : mergeStructuralOntoLocale(rawFrontmatter, enDoc.frontmatter, type.config.schema);
|
|
2835
|
+
const rawToggle = showRaw ? `<span class="dim"> \xB7 <a href="?locale=${encodePathSegment(locale)}">merged</a></span>` : `<span class="dim"> \xB7 <a href="?locale=${encodePathSegment(locale)}&raw=1">raw</a></span>`;
|
|
2836
|
+
contentPanel = `<div class="section">
|
|
2837
|
+
<div class="section-head">Frontmatter${rawToggle}</div>
|
|
2838
|
+
<div class="section-body">${renderFrontmatterTable(displayFrontmatter, type.config.schema)}</div>
|
|
2839
|
+
<div class="section-head">Body</div>
|
|
2840
|
+
<pre class="code">${escapeHtml(translation.body)}</pre>
|
|
2841
|
+
</div>`;
|
|
2842
|
+
metaPanel = `<dl class="meta">
|
|
2843
|
+
<dt>status</dt><dd>${statusDot(status)}</dd>
|
|
2844
|
+
<dt>model</dt><dd>${escapeHtml(translation.model)}</dd>
|
|
2845
|
+
<dt>translated</dt><dd>${escapeHtml(translation.translated_at.slice(0, 19))}</dd>
|
|
2846
|
+
<dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
|
|
2847
|
+
<dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
|
|
2848
|
+
</dl>`;
|
|
2849
|
+
const revisions = listRevisions(db, typeId, enSlug, locale);
|
|
2850
|
+
historyPanel = `<div class="section">
|
|
2851
|
+
<div class="section-head">History</div>
|
|
2852
|
+
<div class="section-body">${renderRevisionTimeline(revisions)}</div>
|
|
2853
|
+
</div>`;
|
|
2854
|
+
} else {
|
|
2855
|
+
contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
|
|
2856
|
+
metaPanel = `<dl class="meta"><dt>status</dt><dd>${statusDot(status)}</dd></dl>`;
|
|
2857
|
+
}
|
|
2105
2858
|
}
|
|
2106
2859
|
db.close();
|
|
2107
|
-
|
|
2860
|
+
const title = docTitleFromFrontmatter(enDoc.frontmatter, enSlug);
|
|
2861
|
+
const html = `<div class="toolbar">
|
|
2862
|
+
<a href="/">Overview</a><span class="sep">\u203A</span>
|
|
2863
|
+
<a href="/type/${encodePathSegment(typeId)}">${escapeHtml(type.config.label)}</a><span class="sep">\u203A</span>
|
|
2864
|
+
<span>${escapeHtml(enSlug)}</span>
|
|
2865
|
+
</div>
|
|
2866
|
+
<div class="tabs">${localeTabs}</div>
|
|
2867
|
+
${metaPanel}
|
|
2868
|
+
${contentPanel}
|
|
2869
|
+
${historyPanel}`;
|
|
2870
|
+
return c.html(renderLayout(title, html, project, { activeTypeId: typeId }));
|
|
2871
|
+
});
|
|
2872
|
+
app.get("/staleness", (c) => {
|
|
2873
|
+
const worklist = buildWorklist(config);
|
|
2874
|
+
const rows = worklist.slice(0, 500).map((item) => {
|
|
2875
|
+
const href = `/type/${encodePathSegment(item.contentType)}/doc/${encodePathSegment(item.enSlug)}?locale=${encodePathSegment(item.locale)}`;
|
|
2876
|
+
return `<tr>
|
|
2877
|
+
<td>${escapeHtml(item.contentType)}</td>
|
|
2878
|
+
<td class="mono"><a href="${href}">${escapeHtml(item.enSlug)}</a></td>
|
|
2879
|
+
<td>${escapeHtml(item.locale)}</td>
|
|
2880
|
+
<td>${statusDot(item.reason === "stale" ? "stale" : "missing")}</td>
|
|
2881
|
+
</tr>`;
|
|
2882
|
+
}).join("");
|
|
2883
|
+
const html = `<div class="toolbar">Staleness <span class="dim">\xB7 ${worklist.length} entries</span></div>
|
|
2884
|
+
<table class="data">
|
|
2885
|
+
<thead><tr><th>Type</th><th>Slug</th><th>Locale</th><th>Status</th></tr></thead>
|
|
2886
|
+
<tbody>${rows || `<tr><td colspan="4" class="dim">All up to date</td></tr>`}</tbody>
|
|
2887
|
+
</table>`;
|
|
2888
|
+
return c.html(renderLayout("Staleness", html, project));
|
|
2108
2889
|
});
|
|
2109
2890
|
app.get("/api/staleness-matrix", (c) => {
|
|
2891
|
+
const worklist = buildWorklist(config);
|
|
2110
2892
|
const matrix = {};
|
|
2111
2893
|
for (const type of project.listTypes()) {
|
|
2112
2894
|
matrix[type.id] = {};
|
|
2113
2895
|
for (const locale of config.locales) {
|
|
2114
2896
|
if (locale === config.defaultLocale) continue;
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
const enDoc = readEnDocument(config, type.config, enSlug);
|
|
2119
|
-
if (!enDoc) continue;
|
|
2120
|
-
const payload = getTranslatablePayload(enDoc, type.config);
|
|
2121
|
-
const hash = computePageEnHash(payload.frontmatter, payload.body);
|
|
2122
|
-
const db = openStore(config, "readonly");
|
|
2123
|
-
const row = db.prepare(
|
|
2124
|
-
`SELECT en_hash FROM translations WHERE content_type = ? AND en_slug = ? AND locale = ?`
|
|
2125
|
-
).get(type.id, enSlug, locale);
|
|
2126
|
-
db.close();
|
|
2127
|
-
if (!row || row.en_hash !== hash) stale++;
|
|
2128
|
-
}
|
|
2129
|
-
matrix[type.id][locale] = stale;
|
|
2897
|
+
matrix[type.id][locale] = worklist.filter(
|
|
2898
|
+
(item) => item.contentType === type.id && item.locale === locale
|
|
2899
|
+
).length;
|
|
2130
2900
|
}
|
|
2131
2901
|
}
|
|
2132
2902
|
return c.json(matrix);
|
|
@@ -2138,6 +2908,276 @@ async function startStudio(project, options = {}) {
|
|
|
2138
2908
|
});
|
|
2139
2909
|
}
|
|
2140
2910
|
|
|
2911
|
+
// cli/prompt-translate.ts
|
|
2912
|
+
init_cjs_shims();
|
|
2913
|
+
|
|
2914
|
+
// ../../node_modules/@inquirer/core/dist/lib/errors.js
|
|
2915
|
+
init_cjs_shims();
|
|
2916
|
+
var CancelPromptError = class extends Error {
|
|
2917
|
+
constructor() {
|
|
2918
|
+
super(...arguments);
|
|
2919
|
+
__publicField(this, "name", "CancelPromptError");
|
|
2920
|
+
__publicField(this, "message", "Prompt was canceled");
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
function isInteractive() {
|
|
2924
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2925
|
+
}
|
|
2926
|
+
function nonDefaultLocales(config) {
|
|
2927
|
+
return config.locales.filter((locale) => locale !== config.defaultLocale);
|
|
2928
|
+
}
|
|
2929
|
+
async function runPrompt(prompt) {
|
|
2930
|
+
try {
|
|
2931
|
+
return await prompt;
|
|
2932
|
+
} catch (error) {
|
|
2933
|
+
if (error instanceof CancelPromptError) process.exit(0);
|
|
2934
|
+
throw error;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
async function promptContentType(config) {
|
|
2938
|
+
return runPrompt(
|
|
2939
|
+
prompts.select({
|
|
2940
|
+
message: "Content type to translate",
|
|
2941
|
+
choices: [
|
|
2942
|
+
{ name: "All content types", value: void 0 },
|
|
2943
|
+
...config.types.map((type) => ({ name: type.id, value: type.id }))
|
|
2944
|
+
]
|
|
2945
|
+
})
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
async function promptLocalePreset(config) {
|
|
2949
|
+
const presets = Object.entries(config.localePresets ?? {}).filter(
|
|
2950
|
+
(entry) => Array.isArray(entry[1])
|
|
2951
|
+
);
|
|
2952
|
+
if (presets.length === 0) {
|
|
2953
|
+
const locales = nonDefaultLocales(config);
|
|
2954
|
+
if (locales.length <= 1) return {};
|
|
2955
|
+
const picked = await runPrompt(
|
|
2956
|
+
prompts.checkbox({
|
|
2957
|
+
message: "Locales to translate",
|
|
2958
|
+
choices: locales.map((locale) => ({ name: locale, value: locale, checked: true })),
|
|
2959
|
+
validate: (values) => values.length > 0 || "Pick at least one locale"
|
|
2960
|
+
})
|
|
2961
|
+
);
|
|
2962
|
+
return picked.length === locales.length ? {} : { locale: picked };
|
|
2963
|
+
}
|
|
2964
|
+
const choice = await runPrompt(
|
|
2965
|
+
prompts.select({
|
|
2966
|
+
message: "Locale preset",
|
|
2967
|
+
choices: [
|
|
2968
|
+
{ name: "All locales", value: void 0 },
|
|
2969
|
+
...presets.map(([name, locales]) => ({
|
|
2970
|
+
name: `${name} (${locales.join(", ")})`,
|
|
2971
|
+
value: name
|
|
2972
|
+
}))
|
|
2973
|
+
]
|
|
2974
|
+
})
|
|
2975
|
+
);
|
|
2976
|
+
return choice ? { preset: choice } : {};
|
|
2977
|
+
}
|
|
2978
|
+
async function promptStrategy() {
|
|
2979
|
+
return runPrompt(
|
|
2980
|
+
prompts.select({
|
|
2981
|
+
message: "Translation strategy",
|
|
2982
|
+
choices: [
|
|
2983
|
+
{ name: "Stale and missing", value: "all" },
|
|
2984
|
+
{ name: "Missing only (skip stale)", value: "missing-only" }
|
|
2985
|
+
],
|
|
2986
|
+
default: "all"
|
|
2987
|
+
})
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
async function promptTranslateSelection(config, flags) {
|
|
2991
|
+
const selection = {
|
|
2992
|
+
contentType: flags.type,
|
|
2993
|
+
preset: flags.preset,
|
|
2994
|
+
locale: flags.locale,
|
|
2995
|
+
strategy: flags.strategy
|
|
2996
|
+
};
|
|
2997
|
+
if (!isInteractive()) return selection;
|
|
2998
|
+
if (!selection.contentType) {
|
|
2999
|
+
selection.contentType = await promptContentType(config);
|
|
3000
|
+
}
|
|
3001
|
+
if (!selection.preset && !selection.locale?.length) {
|
|
3002
|
+
Object.assign(selection, await promptLocalePreset(config));
|
|
3003
|
+
}
|
|
3004
|
+
if (!selection.strategy) {
|
|
3005
|
+
selection.strategy = await promptStrategy();
|
|
3006
|
+
}
|
|
3007
|
+
return selection;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// cli/translate-progress.ts
|
|
3011
|
+
init_cjs_shims();
|
|
3012
|
+
var dim = (text) => `\x1B[2m${text}\x1B[0m`;
|
|
3013
|
+
var green = (text) => `\x1B[32m${text}\x1B[0m`;
|
|
3014
|
+
var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
|
|
3015
|
+
var red = (text) => `\x1B[31m${text}\x1B[0m`;
|
|
3016
|
+
var cyan = (text) => `\x1B[36m${text}\x1B[0m`;
|
|
3017
|
+
function isInteractive2() {
|
|
3018
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
3019
|
+
}
|
|
3020
|
+
function progressBar(ratio, width = 28) {
|
|
3021
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
3022
|
+
const filled = Math.round(clamped * width);
|
|
3023
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}`;
|
|
3024
|
+
}
|
|
3025
|
+
function labelForResult(result) {
|
|
3026
|
+
return `${result.contentType}/${result.enSlug}@${result.locale}`;
|
|
3027
|
+
}
|
|
3028
|
+
function statusForResult(result, dryRun) {
|
|
3029
|
+
if (result.failed) return red("failed");
|
|
3030
|
+
if (result.skipped) return dim("skipped");
|
|
3031
|
+
if (dryRun) return yellow("would translate");
|
|
3032
|
+
return green("translated");
|
|
3033
|
+
}
|
|
3034
|
+
function detailForResult(result) {
|
|
3035
|
+
const parts = [];
|
|
3036
|
+
if (result.durationMs !== void 0) parts.push(`${(result.durationMs / 1e3).toFixed(1)}s`);
|
|
3037
|
+
if (result.usage) {
|
|
3038
|
+
parts.push(`${formatTokenCount(result.usage.inputTokens)} in`);
|
|
3039
|
+
parts.push(`${formatTokenCount(result.usage.outputTokens)} out`);
|
|
3040
|
+
}
|
|
3041
|
+
if (result.estimatedCostUsd !== void 0) parts.push(formatUsd(result.estimatedCostUsd));
|
|
3042
|
+
if (result.error) parts.push(red(result.error));
|
|
3043
|
+
return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
|
|
3044
|
+
}
|
|
3045
|
+
function formatSummaryLine(totals, dryRun) {
|
|
3046
|
+
const action = dryRun ? "Would translate" : "Translated";
|
|
3047
|
+
const parts = [
|
|
3048
|
+
`${action} ${totals.translated}`,
|
|
3049
|
+
`skipped ${totals.skipped}`
|
|
3050
|
+
];
|
|
3051
|
+
if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
|
|
3052
|
+
parts.push(`${formatTokenCount(totals.inputTokens)} in / ${formatTokenCount(totals.outputTokens)} out`);
|
|
3053
|
+
parts.push(`${formatUsd(totals.estimatedCostUsd)} est.`);
|
|
3054
|
+
parts.push(`${(totals.durationMs / 1e3).toFixed(1)}s`);
|
|
3055
|
+
return parts.join(" \xB7 ");
|
|
3056
|
+
}
|
|
3057
|
+
function createTranslateProgressReporter(options = {}) {
|
|
3058
|
+
const enabled = options.enabled ?? isInteractive2();
|
|
3059
|
+
const dryRun = Boolean(options.dryRun);
|
|
3060
|
+
const recentLimit = options.recentLimit ?? 6;
|
|
3061
|
+
if (!enabled) {
|
|
3062
|
+
return {
|
|
3063
|
+
onEvent(event) {
|
|
3064
|
+
if (event.type === "item-done") {
|
|
3065
|
+
const label = labelForResult(event.result);
|
|
3066
|
+
const status = statusForResult(event.result, dryRun);
|
|
3067
|
+
const detail = detailForResult(event.result);
|
|
3068
|
+
console.log(`${label}: ${status}${detail ? ` (${detail.replace(/\x1b\[[0-9;]*m/g, "")})` : ""}`);
|
|
3069
|
+
if (event.result.failed && event.result.error) {
|
|
3070
|
+
console.error(event.result.error);
|
|
3071
|
+
}
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
if (event.type === "done") {
|
|
3075
|
+
console.log(formatSummaryLine(event.totals, dryRun));
|
|
3076
|
+
}
|
|
3077
|
+
},
|
|
3078
|
+
finish() {
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
let total = 0;
|
|
3083
|
+
let concurrency = 1;
|
|
3084
|
+
let model;
|
|
3085
|
+
let done = 0;
|
|
3086
|
+
let inputTokens = 0;
|
|
3087
|
+
let outputTokens = 0;
|
|
3088
|
+
let estimatedCostUsd = 0;
|
|
3089
|
+
let active = [];
|
|
3090
|
+
const recent = [];
|
|
3091
|
+
let renderedLines = 0;
|
|
3092
|
+
let cursorHidden = false;
|
|
3093
|
+
function hideCursor() {
|
|
3094
|
+
if (!cursorHidden) {
|
|
3095
|
+
process.stdout.write("\x1B[?25l");
|
|
3096
|
+
cursorHidden = true;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function showCursor() {
|
|
3100
|
+
if (cursorHidden) {
|
|
3101
|
+
process.stdout.write("\x1B[?25h");
|
|
3102
|
+
cursorHidden = false;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
function render() {
|
|
3106
|
+
hideCursor();
|
|
3107
|
+
if (renderedLines > 0) {
|
|
3108
|
+
process.stdout.write(`\x1B[${renderedLines}A`);
|
|
3109
|
+
}
|
|
3110
|
+
const lines = [];
|
|
3111
|
+
const title = dryRun ? "Dry run" : "Translating";
|
|
3112
|
+
lines.push(cyan(`${title} ${done}/${total}`) + dim(` \xB7 ${concurrency} parallel`) + (model ? dim(` \xB7 ${model}`) : ""));
|
|
3113
|
+
lines.push(progressBar(total === 0 ? 0 : done / total) + dim(` ${Math.round(total === 0 ? 0 : done / total * 100)}%`));
|
|
3114
|
+
lines.push(
|
|
3115
|
+
dim("Tokens ") + `${formatTokenCount(inputTokens)} in \xB7 ${formatTokenCount(outputTokens)} out` + dim(" \xB7 Cost ") + formatUsd(estimatedCostUsd) + dim(" est.")
|
|
3116
|
+
);
|
|
3117
|
+
if (active.length > 0) {
|
|
3118
|
+
lines.push(dim("Active ") + active.slice(0, 3).join(", ") + (active.length > 3 ? dim(` +${active.length - 3}`) : ""));
|
|
3119
|
+
} else if (done < total) {
|
|
3120
|
+
lines.push(dim("Active ") + "starting\u2026");
|
|
3121
|
+
} else {
|
|
3122
|
+
lines.push("");
|
|
3123
|
+
}
|
|
3124
|
+
lines.push("");
|
|
3125
|
+
if (recent.length === 0) {
|
|
3126
|
+
lines.push(dim("Waiting for first page\u2026"));
|
|
3127
|
+
} else {
|
|
3128
|
+
for (const result of recent) {
|
|
3129
|
+
const detail = detailForResult(result);
|
|
3130
|
+
lines.push(`${statusForResult(result, dryRun)} ${labelForResult(result)}${detail ? ` ${detail}` : ""}`);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
for (const line of lines) {
|
|
3134
|
+
process.stdout.write("\x1B[2K" + line + "\n");
|
|
3135
|
+
}
|
|
3136
|
+
renderedLines = lines.length;
|
|
3137
|
+
}
|
|
3138
|
+
return {
|
|
3139
|
+
onEvent(event) {
|
|
3140
|
+
switch (event.type) {
|
|
3141
|
+
case "start":
|
|
3142
|
+
total = event.total;
|
|
3143
|
+
concurrency = event.concurrency;
|
|
3144
|
+
model = event.model;
|
|
3145
|
+
render();
|
|
3146
|
+
break;
|
|
3147
|
+
case "item-start":
|
|
3148
|
+
active = event.active;
|
|
3149
|
+
render();
|
|
3150
|
+
break;
|
|
3151
|
+
case "item-done": {
|
|
3152
|
+
done += 1;
|
|
3153
|
+
active = active.filter((label) => label !== labelForResult(event.result));
|
|
3154
|
+
inputTokens += event.result.usage?.inputTokens ?? 0;
|
|
3155
|
+
outputTokens += event.result.usage?.outputTokens ?? 0;
|
|
3156
|
+
estimatedCostUsd += event.result.estimatedCostUsd ?? 0;
|
|
3157
|
+
recent.unshift(event.result);
|
|
3158
|
+
recent.splice(recentLimit);
|
|
3159
|
+
render();
|
|
3160
|
+
break;
|
|
3161
|
+
}
|
|
3162
|
+
case "done":
|
|
3163
|
+
showCursor();
|
|
3164
|
+
if (renderedLines > 0) {
|
|
3165
|
+
process.stdout.write(`\x1B[${renderedLines}A`);
|
|
3166
|
+
}
|
|
3167
|
+
process.stdout.write("\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun) + "\n");
|
|
3168
|
+
renderedLines = 0;
|
|
3169
|
+
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
3170
|
+
process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
|
|
3171
|
+
}
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
},
|
|
3175
|
+
finish() {
|
|
3176
|
+
showCursor();
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
|
|
2141
3181
|
// cli/index.ts
|
|
2142
3182
|
function parseArgs(argv) {
|
|
2143
3183
|
const options = { cwd: process.cwd() };
|
|
@@ -2194,6 +3234,35 @@ function parseArgs(argv) {
|
|
|
2194
3234
|
i++;
|
|
2195
3235
|
continue;
|
|
2196
3236
|
}
|
|
3237
|
+
if (arg === "--concurrency") {
|
|
3238
|
+
options.concurrency = Number(argv[++i]);
|
|
3239
|
+
i++;
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
if (arg === "--no-progress") {
|
|
3243
|
+
options.noProgress = true;
|
|
3244
|
+
i++;
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
if (arg === "--strategy") {
|
|
3248
|
+
const value = argv[++i];
|
|
3249
|
+
if (value !== "all" && value !== "missing-only") {
|
|
3250
|
+
throw new Error(`Unknown strategy: ${value} (expected all or missing-only)`);
|
|
3251
|
+
}
|
|
3252
|
+
options.strategy = value;
|
|
3253
|
+
i++;
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
if (arg === "--out") {
|
|
3257
|
+
options.out = argv[++i];
|
|
3258
|
+
i++;
|
|
3259
|
+
continue;
|
|
3260
|
+
}
|
|
3261
|
+
if (arg === "--extension") {
|
|
3262
|
+
options.extension = argv[++i];
|
|
3263
|
+
i++;
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
2197
3266
|
if (arg.startsWith("--")) {
|
|
2198
3267
|
throw new Error(`Unknown flag: ${arg}`);
|
|
2199
3268
|
}
|
|
@@ -2219,9 +3288,28 @@ async function main() {
|
|
|
2219
3288
|
Commands:
|
|
2220
3289
|
status Show EN docs + translation counts
|
|
2221
3290
|
validate Validate EN files and sqlite consistency
|
|
3291
|
+
export-static Write raw MDX files for static hosting
|
|
2222
3292
|
translate Translate stale/missing locale pages
|
|
2223
3293
|
history <type> <slug> Show revision timeline
|
|
2224
3294
|
studio Start read-only local studio
|
|
3295
|
+
|
|
3296
|
+
Export-static flags:
|
|
3297
|
+
--out <dir> Output directory (default: public)
|
|
3298
|
+
--extension <ext> File extension (default: mdx)
|
|
3299
|
+
--type <id> Comma-separated content types (default: all routable)
|
|
3300
|
+
--locale <code>... Locales to export (default: all configured)
|
|
3301
|
+
|
|
3302
|
+
Translate flags:
|
|
3303
|
+
--type <id> Content type (interactive picker in a TTY when omitted)
|
|
3304
|
+
--preset <name> Locale preset from config (interactive picker in a TTY)
|
|
3305
|
+
--locale <code>... Target locale(s); overrides --preset
|
|
3306
|
+
--slug <en-slug> Single English document
|
|
3307
|
+
--model <id> Gemini model override
|
|
3308
|
+
--concurrency <n> Parallel translations (default: 3)
|
|
3309
|
+
--dry-run List work without writing
|
|
3310
|
+
--force Re-translate even when hashes match
|
|
3311
|
+
--strategy <mode> all (default) or missing-only
|
|
3312
|
+
--no-progress Plain line logging instead of live progress
|
|
2225
3313
|
`);
|
|
2226
3314
|
return;
|
|
2227
3315
|
}
|
|
@@ -2251,24 +3339,49 @@ Commands:
|
|
|
2251
3339
|
if (!result.ok) process.exitCode = 1;
|
|
2252
3340
|
break;
|
|
2253
3341
|
}
|
|
3342
|
+
case "export-static": {
|
|
3343
|
+
const types = options.type?.split(",").map((t) => t.trim()).filter(Boolean);
|
|
3344
|
+
const { written } = writeStaticRawExports(project, {
|
|
3345
|
+
outDir: options.out,
|
|
3346
|
+
extension: options.extension ? `.${options.extension.replace(/^\./, "")}` : void 0,
|
|
3347
|
+
types,
|
|
3348
|
+
locales: options.locale
|
|
3349
|
+
});
|
|
3350
|
+
console.log(`Wrote ${written} static raw exports to ${options.out ?? "public"}`);
|
|
3351
|
+
break;
|
|
3352
|
+
}
|
|
2254
3353
|
case "translate": {
|
|
2255
|
-
const
|
|
3354
|
+
const selection = await promptTranslateSelection(config, {
|
|
3355
|
+
type: options.type,
|
|
3356
|
+
preset: options.preset,
|
|
3357
|
+
locale: options.locale,
|
|
3358
|
+
strategy: options.strategy
|
|
3359
|
+
});
|
|
3360
|
+
const locales = resolveLocalesFromPreset(config, selection.preset, selection.locale);
|
|
2256
3361
|
const worklist = buildWorklist(config, {
|
|
2257
|
-
contentType:
|
|
3362
|
+
contentType: selection.contentType,
|
|
2258
3363
|
locales,
|
|
2259
|
-
enSlug: options.slug
|
|
3364
|
+
enSlug: options.slug,
|
|
3365
|
+
strategy: selection.strategy ?? "all"
|
|
3366
|
+
});
|
|
3367
|
+
if (worklist.length === 0) {
|
|
3368
|
+
console.log("Nothing to translate.");
|
|
3369
|
+
break;
|
|
3370
|
+
}
|
|
3371
|
+
const reporter = createTranslateProgressReporter({
|
|
3372
|
+
enabled: !options.noProgress,
|
|
3373
|
+
dryRun: options.dryRun
|
|
2260
3374
|
});
|
|
2261
|
-
console.log(`Translating ${worklist.length} page(s)...`);
|
|
2262
3375
|
const results = await translateWorklist(config, worklist, {
|
|
2263
3376
|
model: options.model,
|
|
2264
3377
|
dryRun: options.dryRun,
|
|
2265
|
-
force: options.force
|
|
3378
|
+
force: options.force,
|
|
3379
|
+
concurrency: options.concurrency,
|
|
3380
|
+
onProgress: reporter.onEvent
|
|
2266
3381
|
});
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
);
|
|
2271
|
-
}
|
|
3382
|
+
reporter.finish();
|
|
3383
|
+
const failed = results.filter((result) => result.failed);
|
|
3384
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
2272
3385
|
break;
|
|
2273
3386
|
}
|
|
2274
3387
|
case "history": {
|