scribe-cms 0.0.2 → 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 +2 -3
- package/bin/scribe.js +2 -11
- 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 +16 -5
- package/bin/ensure-built.mjs +0 -31
package/dist/index.js
CHANGED
|
@@ -98,6 +98,22 @@ function unwrapSchema(schema) {
|
|
|
98
98
|
}
|
|
99
99
|
return current;
|
|
100
100
|
}
|
|
101
|
+
function peelOptionalWrappers(schema) {
|
|
102
|
+
let current = schema;
|
|
103
|
+
for (let i = 0; i < 8; i++) {
|
|
104
|
+
const type = current._def?.type;
|
|
105
|
+
if (type === "optional" || type === "nullable") {
|
|
106
|
+
current = current.unwrap();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (type === "default") {
|
|
110
|
+
current = current.removeDefault();
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
return current;
|
|
116
|
+
}
|
|
101
117
|
|
|
102
118
|
// src/core/types.ts
|
|
103
119
|
function defineConfig(config) {
|
|
@@ -112,14 +128,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
112
128
|
function isRoutableType(type) {
|
|
113
129
|
return typeof type.path === "string" && type.path.length > 0;
|
|
114
130
|
}
|
|
115
|
-
function assertValidPathTemplate(
|
|
116
|
-
if (!
|
|
117
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
131
|
+
function assertValidPathTemplate(path10, typeId) {
|
|
132
|
+
if (!path10.startsWith("/")) {
|
|
133
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path10}")`);
|
|
118
134
|
}
|
|
119
|
-
const count = (
|
|
135
|
+
const count = (path10.match(/\{slug\}/g) ?? []).length;
|
|
120
136
|
if (count !== 1) {
|
|
121
137
|
throw new Error(
|
|
122
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
138
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path10}")`
|
|
123
139
|
);
|
|
124
140
|
}
|
|
125
141
|
}
|
|
@@ -243,10 +259,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
243
259
|
}
|
|
244
260
|
function extractByPaths(data, paths) {
|
|
245
261
|
const out = {};
|
|
246
|
-
for (const
|
|
247
|
-
const value = getAtPath(data,
|
|
262
|
+
for (const path10 of paths) {
|
|
263
|
+
const value = getAtPath(data, path10);
|
|
248
264
|
if (value !== void 0) {
|
|
249
|
-
setAtPath(out,
|
|
265
|
+
setAtPath(out, path10.filter((p) => p !== "*"), value);
|
|
250
266
|
}
|
|
251
267
|
}
|
|
252
268
|
return out;
|
|
@@ -264,13 +280,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
264
280
|
const translatable = pickTranslatable(localeData, schema);
|
|
265
281
|
return deepMerge(structural, translatable);
|
|
266
282
|
}
|
|
267
|
-
function getAtPath(obj,
|
|
283
|
+
function getAtPath(obj, path10) {
|
|
268
284
|
let current = obj;
|
|
269
|
-
for (const segment of
|
|
285
|
+
for (const segment of path10) {
|
|
270
286
|
if (segment === "*") {
|
|
271
287
|
if (!Array.isArray(current)) return void 0;
|
|
272
288
|
return current.map(
|
|
273
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
289
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path10.slice(path10.indexOf("*") + 1)) : void 0
|
|
274
290
|
);
|
|
275
291
|
}
|
|
276
292
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -278,13 +294,13 @@ function getAtPath(obj, path9) {
|
|
|
278
294
|
}
|
|
279
295
|
return current;
|
|
280
296
|
}
|
|
281
|
-
function setAtPath(obj,
|
|
282
|
-
if (
|
|
283
|
-
if (
|
|
284
|
-
obj[
|
|
297
|
+
function setAtPath(obj, path10, value) {
|
|
298
|
+
if (path10.length === 0) return;
|
|
299
|
+
if (path10.length === 1) {
|
|
300
|
+
obj[path10[0]] = value;
|
|
285
301
|
return;
|
|
286
302
|
}
|
|
287
|
-
const [head, ...rest] =
|
|
303
|
+
const [head, ...rest] = path10;
|
|
288
304
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
289
305
|
obj[head] = {};
|
|
290
306
|
}
|
|
@@ -480,7 +496,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
480
496
|
redirectTo: enDoc.redirectTo
|
|
481
497
|
};
|
|
482
498
|
}
|
|
483
|
-
var SCHEMA_VERSION =
|
|
499
|
+
var SCHEMA_VERSION = 3;
|
|
484
500
|
var MIGRATIONS = [
|
|
485
501
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
486
502
|
key TEXT PRIMARY KEY,
|
|
@@ -537,17 +553,27 @@ function resolveStorePath(config) {
|
|
|
537
553
|
}
|
|
538
554
|
function openStore(config, mode = "readwrite") {
|
|
539
555
|
const storePath = resolveStorePath(config);
|
|
540
|
-
|
|
556
|
+
if (mode === "readwrite") {
|
|
557
|
+
fs2.mkdirSync(path3.dirname(storePath), { recursive: true });
|
|
558
|
+
}
|
|
541
559
|
const db = new Database(storePath, { readonly: mode === "readonly" });
|
|
542
560
|
if (mode === "readwrite") {
|
|
543
561
|
migrate(db);
|
|
544
562
|
}
|
|
545
563
|
return db;
|
|
546
564
|
}
|
|
565
|
+
function addColumnIfMissing(db, table, column, ddlType) {
|
|
566
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
567
|
+
if (!columns.some((c) => c.name === column)) {
|
|
568
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
547
571
|
function migrate(db) {
|
|
548
572
|
for (const sql of MIGRATIONS) {
|
|
549
573
|
db.exec(sql);
|
|
550
574
|
}
|
|
575
|
+
addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
|
|
576
|
+
addColumnIfMissing(db, "revisions", "body", "TEXT");
|
|
551
577
|
db.prepare(
|
|
552
578
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
553
579
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -598,8 +624,9 @@ function bulkLoadTranslations(db) {
|
|
|
598
624
|
function appendRevision(db, input) {
|
|
599
625
|
const result = db.prepare(
|
|
600
626
|
`INSERT INTO revisions (
|
|
601
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
602
|
-
|
|
627
|
+
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
628
|
+
model, body_preview, frontmatter_json, body
|
|
629
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
603
630
|
).run(
|
|
604
631
|
input.contentType,
|
|
605
632
|
input.enSlug,
|
|
@@ -609,7 +636,9 @@ function appendRevision(db, input) {
|
|
|
609
636
|
input.bodyHash,
|
|
610
637
|
input.createdAt,
|
|
611
638
|
input.model ?? null,
|
|
612
|
-
input.bodyPreview ?? null
|
|
639
|
+
input.bodyPreview ?? null,
|
|
640
|
+
input.frontmatterJson ?? null,
|
|
641
|
+
input.body ?? null
|
|
613
642
|
);
|
|
614
643
|
return Number(result.lastInsertRowid);
|
|
615
644
|
}
|
|
@@ -1542,8 +1571,8 @@ function getRedirectSourceSlugs(project) {
|
|
|
1542
1571
|
// src/sitemap/join-base-url.ts
|
|
1543
1572
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1544
1573
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1545
|
-
const
|
|
1546
|
-
return `${origin}${
|
|
1574
|
+
const path10 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1575
|
+
return `${origin}${path10}`;
|
|
1547
1576
|
}
|
|
1548
1577
|
|
|
1549
1578
|
// src/sitemap/generate-sitemap.ts
|
|
@@ -1710,7 +1739,9 @@ function recordRevision(config, input) {
|
|
|
1710
1739
|
bodyHash: computeBodyHash(input.body),
|
|
1711
1740
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1712
1741
|
model: input.model,
|
|
1713
|
-
bodyPreview: input.body.slice(0, 200)
|
|
1742
|
+
bodyPreview: input.body.slice(0, 200),
|
|
1743
|
+
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1744
|
+
body: input.body
|
|
1714
1745
|
});
|
|
1715
1746
|
db.close();
|
|
1716
1747
|
return id;
|
|
@@ -1894,7 +1925,8 @@ function validateProject(config) {
|
|
|
1894
1925
|
locale,
|
|
1895
1926
|
revisionKind: "en_edit_detected",
|
|
1896
1927
|
enHash: currentEnHash,
|
|
1897
|
-
body: row.body
|
|
1928
|
+
body: row.body,
|
|
1929
|
+
frontmatter: JSON.parse(row.frontmatter_json)
|
|
1898
1930
|
});
|
|
1899
1931
|
}
|
|
1900
1932
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
@@ -2019,6 +2051,10 @@ function buildWorklist(config, options = {}) {
|
|
|
2019
2051
|
}
|
|
2020
2052
|
}
|
|
2021
2053
|
db.close();
|
|
2054
|
+
const strategy = options.strategy ?? "all";
|
|
2055
|
+
if (strategy === "missing-only") {
|
|
2056
|
+
return items.filter((item) => item.reason === "missing");
|
|
2057
|
+
}
|
|
2022
2058
|
return items;
|
|
2023
2059
|
}
|
|
2024
2060
|
function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
@@ -2028,7 +2064,30 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2028
2064
|
}
|
|
2029
2065
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2030
2066
|
}
|
|
2031
|
-
|
|
2067
|
+
|
|
2068
|
+
// src/translate/gemini-models.ts
|
|
2069
|
+
var GEMINI_MODEL_IDS = {
|
|
2070
|
+
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
2071
|
+
"gemini-3.1-pro": "gemini-3.1-pro-preview"
|
|
2072
|
+
};
|
|
2073
|
+
var DEFAULT_GEMINI_MODEL = "gemini-3.1-pro";
|
|
2074
|
+
function stripModelsPrefix(model) {
|
|
2075
|
+
return model.replace(/^models\//, "");
|
|
2076
|
+
}
|
|
2077
|
+
function resolveGeminiModelId(model) {
|
|
2078
|
+
const name = stripModelsPrefix(model);
|
|
2079
|
+
return GEMINI_MODEL_IDS[name] ?? name;
|
|
2080
|
+
}
|
|
2081
|
+
function normalizeGeminiDisplayName(model) {
|
|
2082
|
+
const name = stripModelsPrefix(model);
|
|
2083
|
+
if (name in GEMINI_MODEL_IDS) return name;
|
|
2084
|
+
const alias = Object.entries(GEMINI_MODEL_IDS).find(([, apiId]) => apiId === name);
|
|
2085
|
+
if (alias) return alias[0];
|
|
2086
|
+
return name;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/translate/gemini-client.ts
|
|
2090
|
+
var DEFAULT_MODEL = DEFAULT_GEMINI_MODEL;
|
|
2032
2091
|
function extractJson(text) {
|
|
2033
2092
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
2034
2093
|
if (fenced?.[1]) return fenced[1].trim();
|
|
@@ -2042,13 +2101,17 @@ async function translatePageWithGemini(input) {
|
|
|
2042
2101
|
if (!apiKey) {
|
|
2043
2102
|
throw new Error("GEMINI_API_KEY is required for scribe translate");
|
|
2044
2103
|
}
|
|
2045
|
-
const
|
|
2104
|
+
const displayModel = normalizeGeminiDisplayName(
|
|
2105
|
+
input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL
|
|
2106
|
+
);
|
|
2107
|
+
const apiModel = resolveGeminiModelId(displayModel);
|
|
2046
2108
|
const ai = new GoogleGenAI({ apiKey });
|
|
2047
2109
|
const response = await ai.models.generateContent({
|
|
2048
|
-
model,
|
|
2110
|
+
model: apiModel,
|
|
2049
2111
|
contents: input.prompt,
|
|
2050
2112
|
config: {
|
|
2051
|
-
responseMimeType: "application/json"
|
|
2113
|
+
responseMimeType: "application/json",
|
|
2114
|
+
...input.responseSchema ? { responseSchema: input.responseSchema } : {}
|
|
2052
2115
|
}
|
|
2053
2116
|
});
|
|
2054
2117
|
const raw = response.text ?? "";
|
|
@@ -2056,7 +2119,38 @@ async function translatePageWithGemini(input) {
|
|
|
2056
2119
|
if (!parsed.frontmatter || typeof parsed.body !== "string") {
|
|
2057
2120
|
throw new Error("Gemini response missing frontmatter/body");
|
|
2058
2121
|
}
|
|
2059
|
-
|
|
2122
|
+
const usageMetadata = response.usageMetadata;
|
|
2123
|
+
const usage = {
|
|
2124
|
+
inputTokens: usageMetadata?.promptTokenCount ?? 0,
|
|
2125
|
+
outputTokens: usageMetadata?.candidatesTokenCount ?? 0,
|
|
2126
|
+
totalTokens: usageMetadata?.totalTokenCount ?? 0
|
|
2127
|
+
};
|
|
2128
|
+
return { model: displayModel, raw, parsed, usage };
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// src/translate/gemini-pricing.ts
|
|
2132
|
+
var CONTEXT_TIER_TOKENS = 2e5;
|
|
2133
|
+
var GEMINI_PRICING_USD = {
|
|
2134
|
+
"gemini-3.1-pro": { input: [2, 4], output: [12, 18] }
|
|
2135
|
+
};
|
|
2136
|
+
function normalizeModelId(model) {
|
|
2137
|
+
return model.replace(/^models\//, "").toLowerCase();
|
|
2138
|
+
}
|
|
2139
|
+
function tierRate(tokens, tiers) {
|
|
2140
|
+
return tokens <= CONTEXT_TIER_TOKENS ? tiers[0] : tiers[1];
|
|
2141
|
+
}
|
|
2142
|
+
function resolveModelPricing(model) {
|
|
2143
|
+
const id = normalizeModelId(model);
|
|
2144
|
+
if (GEMINI_PRICING_USD[id]) return GEMINI_PRICING_USD[id];
|
|
2145
|
+
const match = Object.entries(GEMINI_PRICING_USD).find(([key]) => id.includes(key));
|
|
2146
|
+
return match?.[1];
|
|
2147
|
+
}
|
|
2148
|
+
function estimateTranslationCostUsd(model, inputTokens, outputTokens) {
|
|
2149
|
+
const pricing = resolveModelPricing(model);
|
|
2150
|
+
if (!pricing) return void 0;
|
|
2151
|
+
const inputRate = tierRate(inputTokens, pricing.input);
|
|
2152
|
+
const outputRate = tierRate(outputTokens, pricing.output);
|
|
2153
|
+
return inputTokens / 1e6 * inputRate + outputTokens / 1e6 * outputRate;
|
|
2060
2154
|
}
|
|
2061
2155
|
|
|
2062
2156
|
// src/translate/prompts/translation-prompt.ts
|
|
@@ -2092,6 +2186,7 @@ function buildPageTranslationPrompt(input) {
|
|
|
2092
2186
|
prompt,
|
|
2093
2187
|
"",
|
|
2094
2188
|
...input.resolved.context ? ["## Context", input.resolved.context, ""] : [],
|
|
2189
|
+
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2095
2190
|
"## Rules",
|
|
2096
2191
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2097
2192
|
"",
|
|
@@ -2099,10 +2194,6 @@ function buildPageTranslationPrompt(input) {
|
|
|
2099
2194
|
"Return ONLY valid JSON with keys:",
|
|
2100
2195
|
input.slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).",
|
|
2101
2196
|
"",
|
|
2102
|
-
"## EN metadata",
|
|
2103
|
-
`title: ${input.enTitle}`,
|
|
2104
|
-
`description: ${input.enDescription}`,
|
|
2105
|
-
"",
|
|
2106
2197
|
"## EN translatable frontmatter (JSON)",
|
|
2107
2198
|
JSON.stringify(input.translatableFrontmatter, null, 2),
|
|
2108
2199
|
"",
|
|
@@ -2111,6 +2202,71 @@ function buildPageTranslationPrompt(input) {
|
|
|
2111
2202
|
];
|
|
2112
2203
|
return lines.join("\n");
|
|
2113
2204
|
}
|
|
2205
|
+
function getObjectShape(schema) {
|
|
2206
|
+
const base = peelOptionalWrappers(schema);
|
|
2207
|
+
if (base instanceof Object && "shape" in base) {
|
|
2208
|
+
return base.shape;
|
|
2209
|
+
}
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
function getArraySchema(schema) {
|
|
2213
|
+
const base = peelOptionalWrappers(schema);
|
|
2214
|
+
if (base instanceof Object && "element" in base && base._def?.type === "array") {
|
|
2215
|
+
return base;
|
|
2216
|
+
}
|
|
2217
|
+
return null;
|
|
2218
|
+
}
|
|
2219
|
+
function extractTranslatableFromStructural(schema) {
|
|
2220
|
+
const arraySchema = getArraySchema(schema);
|
|
2221
|
+
if (arraySchema) {
|
|
2222
|
+
const inner = buildTranslatableSubschema(arraySchema.element);
|
|
2223
|
+
if (inner) return z.array(inner);
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
if (getObjectShape(schema)) {
|
|
2227
|
+
return buildTranslatableSubschema(schema);
|
|
2228
|
+
}
|
|
2229
|
+
return null;
|
|
2230
|
+
}
|
|
2231
|
+
function buildTranslatableSubschema(schema) {
|
|
2232
|
+
const shape = getObjectShape(schema);
|
|
2233
|
+
if (!shape) return null;
|
|
2234
|
+
const out = {};
|
|
2235
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
2236
|
+
const childSchema = child;
|
|
2237
|
+
const kind = getFieldKind(childSchema);
|
|
2238
|
+
if (kind === "translatable") {
|
|
2239
|
+
out[key] = peelOptionalWrappers(childSchema);
|
|
2240
|
+
} else if (kind === "structural") {
|
|
2241
|
+
const nested = extractTranslatableFromStructural(childSchema);
|
|
2242
|
+
if (nested) out[key] = nested;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (Object.keys(out).length === 0) return null;
|
|
2246
|
+
return z.object(out);
|
|
2247
|
+
}
|
|
2248
|
+
function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
2249
|
+
try {
|
|
2250
|
+
const translatable = buildTranslatableSubschema(schema);
|
|
2251
|
+
if (!translatable) return null;
|
|
2252
|
+
const responseShape = {
|
|
2253
|
+
frontmatter: translatable,
|
|
2254
|
+
body: z.string()
|
|
2255
|
+
};
|
|
2256
|
+
if (slugStrategy === "localized") {
|
|
2257
|
+
responseShape.slug = z.string();
|
|
2258
|
+
}
|
|
2259
|
+
const jsonSchema = z.toJSONSchema(z.object(responseShape), {
|
|
2260
|
+
target: "draft-2020-12",
|
|
2261
|
+
unrepresentable: "any",
|
|
2262
|
+
io: "output"
|
|
2263
|
+
});
|
|
2264
|
+
delete jsonSchema.$schema;
|
|
2265
|
+
return jsonSchema;
|
|
2266
|
+
} catch {
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2114
2270
|
|
|
2115
2271
|
// src/translate/resolve-translate-config.ts
|
|
2116
2272
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
@@ -2151,88 +2307,305 @@ function resolveTranslateConfig(config, type) {
|
|
|
2151
2307
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2152
2308
|
}
|
|
2153
2309
|
|
|
2310
|
+
// src/translate/validate-translation.ts
|
|
2311
|
+
function formatZodIssues(error) {
|
|
2312
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2313
|
+
}
|
|
2314
|
+
function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
|
|
2315
|
+
return pickTranslatable(rawFrontmatter, schema);
|
|
2316
|
+
}
|
|
2317
|
+
function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
|
|
2318
|
+
const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
|
|
2319
|
+
const merged = mergeStructuralOntoLocale(
|
|
2320
|
+
frontmatter,
|
|
2321
|
+
enDoc.frontmatter,
|
|
2322
|
+
typeSchema
|
|
2323
|
+
);
|
|
2324
|
+
const parsed = typeSchema.safeParse(merged);
|
|
2325
|
+
if (!parsed.success) {
|
|
2326
|
+
return { ok: false, error: formatZodIssues(parsed.error) };
|
|
2327
|
+
}
|
|
2328
|
+
return { ok: true, frontmatter };
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2154
2331
|
// src/translate/page-translator.ts
|
|
2332
|
+
function summarizeResults(results, durationMs) {
|
|
2333
|
+
return results.reduce(
|
|
2334
|
+
(totals, result) => {
|
|
2335
|
+
if (result.failed) totals.failed += 1;
|
|
2336
|
+
else if (result.skipped) totals.skipped += 1;
|
|
2337
|
+
else totals.translated += 1;
|
|
2338
|
+
totals.inputTokens += result.usage?.inputTokens ?? 0;
|
|
2339
|
+
totals.outputTokens += result.usage?.outputTokens ?? 0;
|
|
2340
|
+
totals.estimatedCostUsd += result.estimatedCostUsd ?? 0;
|
|
2341
|
+
return totals;
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
translated: 0,
|
|
2345
|
+
skipped: 0,
|
|
2346
|
+
failed: 0,
|
|
2347
|
+
inputTokens: 0,
|
|
2348
|
+
outputTokens: 0,
|
|
2349
|
+
estimatedCostUsd: 0,
|
|
2350
|
+
durationMs
|
|
2351
|
+
}
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
function formatTranslateError(error) {
|
|
2355
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2356
|
+
try {
|
|
2357
|
+
const parsed = JSON.parse(message);
|
|
2358
|
+
if (parsed.error?.message) return parsed.error.message;
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2361
|
+
const embedded = message.match(/"message"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
2362
|
+
if (embedded?.[1]) {
|
|
2363
|
+
return embedded[1].replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
2364
|
+
}
|
|
2365
|
+
return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
|
|
2366
|
+
}
|
|
2367
|
+
async function runPool(items, concurrency, worker) {
|
|
2368
|
+
if (items.length === 0) return;
|
|
2369
|
+
let nextIndex = 0;
|
|
2370
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
2371
|
+
while (nextIndex < items.length) {
|
|
2372
|
+
const index = nextIndex++;
|
|
2373
|
+
await worker(items[index], index);
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
await Promise.all(runners);
|
|
2377
|
+
}
|
|
2378
|
+
function resolveContextLabel(enDoc, enSlug) {
|
|
2379
|
+
const fm = enDoc.frontmatter;
|
|
2380
|
+
for (const key of ["title", "name", "h1"]) {
|
|
2381
|
+
const value = fm[key];
|
|
2382
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2383
|
+
}
|
|
2384
|
+
return enSlug;
|
|
2385
|
+
}
|
|
2155
2386
|
async function translatePage(config, item, options = {}) {
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2387
|
+
const startedAt = Date.now();
|
|
2388
|
+
const base = {
|
|
2389
|
+
contentType: item.contentType,
|
|
2390
|
+
enSlug: item.enSlug,
|
|
2391
|
+
locale: item.locale
|
|
2392
|
+
};
|
|
2393
|
+
try {
|
|
2394
|
+
const type = config.types.find((t) => t.id === item.contentType);
|
|
2395
|
+
if (!type) throw new Error(`Unknown content type ${item.contentType}`);
|
|
2396
|
+
const enDoc = readEnDocument(config, type, item.enSlug);
|
|
2397
|
+
if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
|
|
2398
|
+
const payload = getTranslatablePayload(enDoc, type);
|
|
2399
|
+
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
2400
|
+
const db = openStore(config, "readonly");
|
|
2401
|
+
const existing = getTranslation(db, type.id, item.enSlug, item.locale);
|
|
2402
|
+
db.close();
|
|
2403
|
+
if (!options.force && existing && existing.en_hash === currentEnHash) {
|
|
2404
|
+
return {
|
|
2405
|
+
...base,
|
|
2406
|
+
skipped: true,
|
|
2407
|
+
reason: "fresh",
|
|
2408
|
+
durationMs: Date.now() - startedAt
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const resolvedTranslate = resolveTranslateConfig(config, type);
|
|
2412
|
+
const model = options.model ?? resolvedTranslate.model;
|
|
2413
|
+
const prompt = buildPageTranslationPrompt({
|
|
2414
|
+
resolved: resolvedTranslate,
|
|
2415
|
+
targetLocale: item.locale,
|
|
2416
|
+
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2417
|
+
translatableFrontmatter: payload.frontmatter,
|
|
2418
|
+
enBody: payload.body,
|
|
2419
|
+
slugStrategy: type.slugStrategy
|
|
2420
|
+
});
|
|
2421
|
+
if (options.dryRun) {
|
|
2422
|
+
return {
|
|
2423
|
+
...base,
|
|
2424
|
+
skipped: false,
|
|
2425
|
+
model,
|
|
2426
|
+
durationMs: Date.now() - startedAt
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2430
|
+
const result = await translatePageWithGemini({
|
|
2431
|
+
prompt,
|
|
2432
|
+
model,
|
|
2433
|
+
responseSchema: responseSchema ?? void 0
|
|
2434
|
+
});
|
|
2435
|
+
const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2436
|
+
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2437
|
+
if (!validated.ok) {
|
|
2438
|
+
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2439
|
+
}
|
|
2440
|
+
const writeDb = openStore(config, "readwrite");
|
|
2441
|
+
upsertTranslation(writeDb, {
|
|
2442
|
+
contentType: type.id,
|
|
2168
2443
|
enSlug: item.enSlug,
|
|
2169
2444
|
locale: item.locale,
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
translatableFrontmatter: payload.frontmatter,
|
|
2181
|
-
enBody: payload.body,
|
|
2182
|
-
slugStrategy: type.slugStrategy
|
|
2183
|
-
});
|
|
2184
|
-
if (options.dryRun) {
|
|
2185
|
-
return {
|
|
2186
|
-
contentType: item.contentType,
|
|
2445
|
+
slug,
|
|
2446
|
+
frontmatter: validated.frontmatter,
|
|
2447
|
+
body: result.parsed.body,
|
|
2448
|
+
enHash: currentEnHash,
|
|
2449
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2450
|
+
model: result.model
|
|
2451
|
+
});
|
|
2452
|
+
writeDb.close();
|
|
2453
|
+
recordRevision(config, {
|
|
2454
|
+
contentType: type.id,
|
|
2187
2455
|
enSlug: item.enSlug,
|
|
2188
2456
|
locale: item.locale,
|
|
2457
|
+
revisionKind: "translation",
|
|
2458
|
+
enHash: currentEnHash,
|
|
2459
|
+
body: result.parsed.body,
|
|
2460
|
+
frontmatter: validated.frontmatter,
|
|
2461
|
+
model: result.model
|
|
2462
|
+
});
|
|
2463
|
+
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2464
|
+
normalizeGeminiDisplayName(result.model),
|
|
2465
|
+
result.usage.inputTokens,
|
|
2466
|
+
result.usage.outputTokens
|
|
2467
|
+
);
|
|
2468
|
+
return {
|
|
2469
|
+
...base,
|
|
2470
|
+
skipped: false,
|
|
2471
|
+
model: result.model,
|
|
2472
|
+
usage: result.usage,
|
|
2473
|
+
estimatedCostUsd,
|
|
2474
|
+
durationMs: Date.now() - startedAt
|
|
2475
|
+
};
|
|
2476
|
+
} catch (error) {
|
|
2477
|
+
return {
|
|
2478
|
+
...base,
|
|
2189
2479
|
skipped: false,
|
|
2190
|
-
|
|
2480
|
+
failed: true,
|
|
2481
|
+
error: formatTranslateError(error),
|
|
2482
|
+
durationMs: Date.now() - startedAt
|
|
2191
2483
|
};
|
|
2192
2484
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
model: result.model
|
|
2485
|
+
}
|
|
2486
|
+
function labelForItem(item) {
|
|
2487
|
+
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
2488
|
+
}
|
|
2489
|
+
async function translateWorklist(config, items, options = {}) {
|
|
2490
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
2491
|
+
const startedAt = Date.now();
|
|
2492
|
+
const results = new Array(items.length);
|
|
2493
|
+
const active = /* @__PURE__ */ new Set();
|
|
2494
|
+
options.onProgress?.({
|
|
2495
|
+
type: "start",
|
|
2496
|
+
total: items.length,
|
|
2497
|
+
concurrency,
|
|
2498
|
+
dryRun: Boolean(options.dryRun),
|
|
2499
|
+
model: options.model
|
|
2209
2500
|
});
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
model: result.model
|
|
2501
|
+
await runPool(items, concurrency, async (item, index) => {
|
|
2502
|
+
const label = labelForItem(item);
|
|
2503
|
+
active.add(label);
|
|
2504
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
2505
|
+
const result = await translatePage(config, item, options);
|
|
2506
|
+
results[index] = result;
|
|
2507
|
+
active.delete(label);
|
|
2508
|
+
options.onProgress?.({ type: "item-done", result });
|
|
2219
2509
|
});
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
locale: item.locale,
|
|
2224
|
-
skipped: false,
|
|
2225
|
-
model: result.model
|
|
2226
|
-
};
|
|
2510
|
+
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
2511
|
+
options.onProgress?.({ type: "done", results, totals });
|
|
2512
|
+
return results;
|
|
2227
2513
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2514
|
+
function serializeMdx(frontmatter, content) {
|
|
2515
|
+
return matter.stringify(content, frontmatter);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/export/build-static-raw-exports.ts
|
|
2519
|
+
function normalizeExtension(ext) {
|
|
2520
|
+
return ext.startsWith(".") ? ext : `.${ext}`;
|
|
2521
|
+
}
|
|
2522
|
+
function exportDirSegment(pathTemplate) {
|
|
2523
|
+
const prefix = pathPrefix(pathTemplate);
|
|
2524
|
+
return prefix.replace(/^\/+|\/+$/g, "");
|
|
2525
|
+
}
|
|
2526
|
+
function getStaticExportRoots(project, options = {}) {
|
|
2527
|
+
const { config } = project;
|
|
2528
|
+
const locales = options.locales ?? config.locales;
|
|
2529
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2530
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2531
|
+
for (const type of project.listTypes()) {
|
|
2532
|
+
if (!isRoutableType(type.config)) continue;
|
|
2533
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2534
|
+
const segment = exportDirSegment(type.config.path);
|
|
2535
|
+
roots.add(`${segment}/`);
|
|
2536
|
+
for (const locale of locales) {
|
|
2537
|
+
if (locale === config.defaultLocale) continue;
|
|
2538
|
+
roots.add(`${locale}/${segment}/`);
|
|
2539
|
+
}
|
|
2232
2540
|
}
|
|
2233
|
-
return
|
|
2541
|
+
return [...roots].sort();
|
|
2542
|
+
}
|
|
2543
|
+
function buildStaticRawExports(project, options = {}) {
|
|
2544
|
+
const { config } = project;
|
|
2545
|
+
const extension = normalizeExtension(options.extension ?? ".mdx");
|
|
2546
|
+
const locales = options.locales ?? config.locales;
|
|
2547
|
+
const excludeRedirected = options.excludeRedirected ?? true;
|
|
2548
|
+
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2549
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2550
|
+
const out = [];
|
|
2551
|
+
for (const type of project.listTypes()) {
|
|
2552
|
+
if (!isRoutableType(type.config)) continue;
|
|
2553
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2554
|
+
const pathTemplate = type.config.path;
|
|
2555
|
+
const all = type.load();
|
|
2556
|
+
const enIdx = all.get(config.defaultLocale);
|
|
2557
|
+
if (!enIdx) continue;
|
|
2558
|
+
for (const locale of locales) {
|
|
2559
|
+
for (const enDoc of enIdx.bySlug.values()) {
|
|
2560
|
+
const resolved = type.resolve(enDoc.slug, locale);
|
|
2561
|
+
if (!resolved.document) continue;
|
|
2562
|
+
if (excludeRedirected && resolved.shouldRedirectTo) continue;
|
|
2563
|
+
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2564
|
+
const doc = resolved.document;
|
|
2565
|
+
const slugWithExt = `${doc.slug}${extension}`;
|
|
2566
|
+
const urlPath = resolvePath(pathTemplate, slugWithExt, locale, config.defaultLocale);
|
|
2567
|
+
out.push({
|
|
2568
|
+
relativePath: urlPath.slice(1),
|
|
2569
|
+
urlPath,
|
|
2570
|
+
locale,
|
|
2571
|
+
typeId: type.id,
|
|
2572
|
+
enSlug: doc.enSlug,
|
|
2573
|
+
source: serializeMdx(doc.frontmatter, doc.content)
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
return out;
|
|
2579
|
+
}
|
|
2580
|
+
function rmDirIfExists(dir) {
|
|
2581
|
+
if (fs2.existsSync(dir)) {
|
|
2582
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
function writeStaticRawExports(project, options = {}) {
|
|
2586
|
+
const outDir = path3.resolve(options.outDir ?? "public");
|
|
2587
|
+
const typeFilter = options.types;
|
|
2588
|
+
for (const root of getStaticExportRoots(project, {
|
|
2589
|
+
types: typeFilter,
|
|
2590
|
+
locales: options.locales
|
|
2591
|
+
})) {
|
|
2592
|
+
rmDirIfExists(path3.join(outDir, root));
|
|
2593
|
+
}
|
|
2594
|
+
const exports = buildStaticRawExports(project, options);
|
|
2595
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2596
|
+
for (const item of exports) {
|
|
2597
|
+
const filePath = path3.join(outDir, item.relativePath);
|
|
2598
|
+
fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
2599
|
+
fs2.writeFileSync(filePath, item.source, "utf8");
|
|
2600
|
+
const key = `${item.typeId}/${item.locale}`;
|
|
2601
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2602
|
+
}
|
|
2603
|
+
for (const [key, count] of [...counts.entries()].sort()) {
|
|
2604
|
+
console.log(` ${key}: ${count}`);
|
|
2605
|
+
}
|
|
2606
|
+
return { exports, written: exports.length };
|
|
2234
2607
|
}
|
|
2235
2608
|
|
|
2236
|
-
export { buildAllContentRedirects, buildWorklist, createProject, createScribe, defineConfig, defineContentType, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, isResolvedConfig, loadConfigSync, resolveConfig, resolveLocalesFromPreset, translatePage, translateWorklist, unwrapSchema, validateProject };
|
|
2609
|
+
export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, loadConfigSync, resolveConfig, resolveLocalesFromPreset, serializeMdx, translatePage, translateWorklist, unwrapSchema, validateProject, writeStaticRawExports };
|
|
2237
2610
|
//# sourceMappingURL=index.js.map
|
|
2238
2611
|
//# sourceMappingURL=index.js.map
|