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/index.cjs
CHANGED
|
@@ -109,6 +109,22 @@ function unwrapSchema(schema) {
|
|
|
109
109
|
}
|
|
110
110
|
return current;
|
|
111
111
|
}
|
|
112
|
+
function peelOptionalWrappers(schema) {
|
|
113
|
+
let current = schema;
|
|
114
|
+
for (let i = 0; i < 8; i++) {
|
|
115
|
+
const type = current._def?.type;
|
|
116
|
+
if (type === "optional" || type === "nullable") {
|
|
117
|
+
current = current.unwrap();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (type === "default") {
|
|
121
|
+
current = current.removeDefault();
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
return current;
|
|
127
|
+
}
|
|
112
128
|
|
|
113
129
|
// src/core/types.ts
|
|
114
130
|
function defineConfig(config) {
|
|
@@ -123,14 +139,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
123
139
|
function isRoutableType(type) {
|
|
124
140
|
return typeof type.path === "string" && type.path.length > 0;
|
|
125
141
|
}
|
|
126
|
-
function assertValidPathTemplate(
|
|
127
|
-
if (!
|
|
128
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
142
|
+
function assertValidPathTemplate(path10, typeId) {
|
|
143
|
+
if (!path10.startsWith("/")) {
|
|
144
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path10}")`);
|
|
129
145
|
}
|
|
130
|
-
const count = (
|
|
146
|
+
const count = (path10.match(/\{slug\}/g) ?? []).length;
|
|
131
147
|
if (count !== 1) {
|
|
132
148
|
throw new Error(
|
|
133
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
149
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path10}")`
|
|
134
150
|
);
|
|
135
151
|
}
|
|
136
152
|
}
|
|
@@ -254,10 +270,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
254
270
|
}
|
|
255
271
|
function extractByPaths(data, paths) {
|
|
256
272
|
const out = {};
|
|
257
|
-
for (const
|
|
258
|
-
const value = getAtPath(data,
|
|
273
|
+
for (const path10 of paths) {
|
|
274
|
+
const value = getAtPath(data, path10);
|
|
259
275
|
if (value !== void 0) {
|
|
260
|
-
setAtPath(out,
|
|
276
|
+
setAtPath(out, path10.filter((p) => p !== "*"), value);
|
|
261
277
|
}
|
|
262
278
|
}
|
|
263
279
|
return out;
|
|
@@ -275,13 +291,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
275
291
|
const translatable = pickTranslatable(localeData, schema);
|
|
276
292
|
return deepMerge(structural, translatable);
|
|
277
293
|
}
|
|
278
|
-
function getAtPath(obj,
|
|
294
|
+
function getAtPath(obj, path10) {
|
|
279
295
|
let current = obj;
|
|
280
|
-
for (const segment of
|
|
296
|
+
for (const segment of path10) {
|
|
281
297
|
if (segment === "*") {
|
|
282
298
|
if (!Array.isArray(current)) return void 0;
|
|
283
299
|
return current.map(
|
|
284
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
300
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path10.slice(path10.indexOf("*") + 1)) : void 0
|
|
285
301
|
);
|
|
286
302
|
}
|
|
287
303
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -289,13 +305,13 @@ function getAtPath(obj, path9) {
|
|
|
289
305
|
}
|
|
290
306
|
return current;
|
|
291
307
|
}
|
|
292
|
-
function setAtPath(obj,
|
|
293
|
-
if (
|
|
294
|
-
if (
|
|
295
|
-
obj[
|
|
308
|
+
function setAtPath(obj, path10, value) {
|
|
309
|
+
if (path10.length === 0) return;
|
|
310
|
+
if (path10.length === 1) {
|
|
311
|
+
obj[path10[0]] = value;
|
|
296
312
|
return;
|
|
297
313
|
}
|
|
298
|
-
const [head, ...rest] =
|
|
314
|
+
const [head, ...rest] = path10;
|
|
299
315
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
300
316
|
obj[head] = {};
|
|
301
317
|
}
|
|
@@ -491,7 +507,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
491
507
|
redirectTo: enDoc.redirectTo
|
|
492
508
|
};
|
|
493
509
|
}
|
|
494
|
-
var SCHEMA_VERSION =
|
|
510
|
+
var SCHEMA_VERSION = 3;
|
|
495
511
|
var MIGRATIONS = [
|
|
496
512
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
497
513
|
key TEXT PRIMARY KEY,
|
|
@@ -548,17 +564,27 @@ function resolveStorePath(config) {
|
|
|
548
564
|
}
|
|
549
565
|
function openStore(config, mode = "readwrite") {
|
|
550
566
|
const storePath = resolveStorePath(config);
|
|
551
|
-
|
|
567
|
+
if (mode === "readwrite") {
|
|
568
|
+
fs2__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
|
|
569
|
+
}
|
|
552
570
|
const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
|
|
553
571
|
if (mode === "readwrite") {
|
|
554
572
|
migrate(db);
|
|
555
573
|
}
|
|
556
574
|
return db;
|
|
557
575
|
}
|
|
576
|
+
function addColumnIfMissing(db, table, column, ddlType) {
|
|
577
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
578
|
+
if (!columns.some((c) => c.name === column)) {
|
|
579
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
558
582
|
function migrate(db) {
|
|
559
583
|
for (const sql of MIGRATIONS) {
|
|
560
584
|
db.exec(sql);
|
|
561
585
|
}
|
|
586
|
+
addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
|
|
587
|
+
addColumnIfMissing(db, "revisions", "body", "TEXT");
|
|
562
588
|
db.prepare(
|
|
563
589
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
564
590
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -609,8 +635,9 @@ function bulkLoadTranslations(db) {
|
|
|
609
635
|
function appendRevision(db, input) {
|
|
610
636
|
const result = db.prepare(
|
|
611
637
|
`INSERT INTO revisions (
|
|
612
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
613
|
-
|
|
638
|
+
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
639
|
+
model, body_preview, frontmatter_json, body
|
|
640
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
614
641
|
).run(
|
|
615
642
|
input.contentType,
|
|
616
643
|
input.enSlug,
|
|
@@ -620,7 +647,9 @@ function appendRevision(db, input) {
|
|
|
620
647
|
input.bodyHash,
|
|
621
648
|
input.createdAt,
|
|
622
649
|
input.model ?? null,
|
|
623
|
-
input.bodyPreview ?? null
|
|
650
|
+
input.bodyPreview ?? null,
|
|
651
|
+
input.frontmatterJson ?? null,
|
|
652
|
+
input.body ?? null
|
|
624
653
|
);
|
|
625
654
|
return Number(result.lastInsertRowid);
|
|
626
655
|
}
|
|
@@ -1553,8 +1582,8 @@ function getRedirectSourceSlugs(project) {
|
|
|
1553
1582
|
// src/sitemap/join-base-url.ts
|
|
1554
1583
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1555
1584
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1556
|
-
const
|
|
1557
|
-
return `${origin}${
|
|
1585
|
+
const path10 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1586
|
+
return `${origin}${path10}`;
|
|
1558
1587
|
}
|
|
1559
1588
|
|
|
1560
1589
|
// src/sitemap/generate-sitemap.ts
|
|
@@ -1721,7 +1750,9 @@ function recordRevision(config, input) {
|
|
|
1721
1750
|
bodyHash: computeBodyHash(input.body),
|
|
1722
1751
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1723
1752
|
model: input.model,
|
|
1724
|
-
bodyPreview: input.body.slice(0, 200)
|
|
1753
|
+
bodyPreview: input.body.slice(0, 200),
|
|
1754
|
+
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1755
|
+
body: input.body
|
|
1725
1756
|
});
|
|
1726
1757
|
db.close();
|
|
1727
1758
|
return id;
|
|
@@ -1905,7 +1936,8 @@ function validateProject(config) {
|
|
|
1905
1936
|
locale,
|
|
1906
1937
|
revisionKind: "en_edit_detected",
|
|
1907
1938
|
enHash: currentEnHash,
|
|
1908
|
-
body: row.body
|
|
1939
|
+
body: row.body,
|
|
1940
|
+
frontmatter: JSON.parse(row.frontmatter_json)
|
|
1909
1941
|
});
|
|
1910
1942
|
}
|
|
1911
1943
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
@@ -2030,6 +2062,10 @@ function buildWorklist(config, options = {}) {
|
|
|
2030
2062
|
}
|
|
2031
2063
|
}
|
|
2032
2064
|
db.close();
|
|
2065
|
+
const strategy = options.strategy ?? "all";
|
|
2066
|
+
if (strategy === "missing-only") {
|
|
2067
|
+
return items.filter((item) => item.reason === "missing");
|
|
2068
|
+
}
|
|
2033
2069
|
return items;
|
|
2034
2070
|
}
|
|
2035
2071
|
function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
@@ -2039,7 +2075,30 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2039
2075
|
}
|
|
2040
2076
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2041
2077
|
}
|
|
2042
|
-
|
|
2078
|
+
|
|
2079
|
+
// src/translate/gemini-models.ts
|
|
2080
|
+
var GEMINI_MODEL_IDS = {
|
|
2081
|
+
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
2082
|
+
"gemini-3.1-pro": "gemini-3.1-pro-preview"
|
|
2083
|
+
};
|
|
2084
|
+
var DEFAULT_GEMINI_MODEL = "gemini-3.1-pro";
|
|
2085
|
+
function stripModelsPrefix(model) {
|
|
2086
|
+
return model.replace(/^models\//, "");
|
|
2087
|
+
}
|
|
2088
|
+
function resolveGeminiModelId(model) {
|
|
2089
|
+
const name = stripModelsPrefix(model);
|
|
2090
|
+
return GEMINI_MODEL_IDS[name] ?? name;
|
|
2091
|
+
}
|
|
2092
|
+
function normalizeGeminiDisplayName(model) {
|
|
2093
|
+
const name = stripModelsPrefix(model);
|
|
2094
|
+
if (name in GEMINI_MODEL_IDS) return name;
|
|
2095
|
+
const alias = Object.entries(GEMINI_MODEL_IDS).find(([, apiId]) => apiId === name);
|
|
2096
|
+
if (alias) return alias[0];
|
|
2097
|
+
return name;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// src/translate/gemini-client.ts
|
|
2101
|
+
var DEFAULT_MODEL = DEFAULT_GEMINI_MODEL;
|
|
2043
2102
|
function extractJson(text) {
|
|
2044
2103
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
2045
2104
|
if (fenced?.[1]) return fenced[1].trim();
|
|
@@ -2053,13 +2112,17 @@ async function translatePageWithGemini(input) {
|
|
|
2053
2112
|
if (!apiKey) {
|
|
2054
2113
|
throw new Error("GEMINI_API_KEY is required for scribe translate");
|
|
2055
2114
|
}
|
|
2056
|
-
const
|
|
2115
|
+
const displayModel = normalizeGeminiDisplayName(
|
|
2116
|
+
input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL
|
|
2117
|
+
);
|
|
2118
|
+
const apiModel = resolveGeminiModelId(displayModel);
|
|
2057
2119
|
const ai = new genai.GoogleGenAI({ apiKey });
|
|
2058
2120
|
const response = await ai.models.generateContent({
|
|
2059
|
-
model,
|
|
2121
|
+
model: apiModel,
|
|
2060
2122
|
contents: input.prompt,
|
|
2061
2123
|
config: {
|
|
2062
|
-
responseMimeType: "application/json"
|
|
2124
|
+
responseMimeType: "application/json",
|
|
2125
|
+
...input.responseSchema ? { responseSchema: input.responseSchema } : {}
|
|
2063
2126
|
}
|
|
2064
2127
|
});
|
|
2065
2128
|
const raw = response.text ?? "";
|
|
@@ -2067,7 +2130,38 @@ async function translatePageWithGemini(input) {
|
|
|
2067
2130
|
if (!parsed.frontmatter || typeof parsed.body !== "string") {
|
|
2068
2131
|
throw new Error("Gemini response missing frontmatter/body");
|
|
2069
2132
|
}
|
|
2070
|
-
|
|
2133
|
+
const usageMetadata = response.usageMetadata;
|
|
2134
|
+
const usage = {
|
|
2135
|
+
inputTokens: usageMetadata?.promptTokenCount ?? 0,
|
|
2136
|
+
outputTokens: usageMetadata?.candidatesTokenCount ?? 0,
|
|
2137
|
+
totalTokens: usageMetadata?.totalTokenCount ?? 0
|
|
2138
|
+
};
|
|
2139
|
+
return { model: displayModel, raw, parsed, usage };
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// src/translate/gemini-pricing.ts
|
|
2143
|
+
var CONTEXT_TIER_TOKENS = 2e5;
|
|
2144
|
+
var GEMINI_PRICING_USD = {
|
|
2145
|
+
"gemini-3.1-pro": { input: [2, 4], output: [12, 18] }
|
|
2146
|
+
};
|
|
2147
|
+
function normalizeModelId(model) {
|
|
2148
|
+
return model.replace(/^models\//, "").toLowerCase();
|
|
2149
|
+
}
|
|
2150
|
+
function tierRate(tokens, tiers) {
|
|
2151
|
+
return tokens <= CONTEXT_TIER_TOKENS ? tiers[0] : tiers[1];
|
|
2152
|
+
}
|
|
2153
|
+
function resolveModelPricing(model) {
|
|
2154
|
+
const id = normalizeModelId(model);
|
|
2155
|
+
if (GEMINI_PRICING_USD[id]) return GEMINI_PRICING_USD[id];
|
|
2156
|
+
const match = Object.entries(GEMINI_PRICING_USD).find(([key]) => id.includes(key));
|
|
2157
|
+
return match?.[1];
|
|
2158
|
+
}
|
|
2159
|
+
function estimateTranslationCostUsd(model, inputTokens, outputTokens) {
|
|
2160
|
+
const pricing = resolveModelPricing(model);
|
|
2161
|
+
if (!pricing) return void 0;
|
|
2162
|
+
const inputRate = tierRate(inputTokens, pricing.input);
|
|
2163
|
+
const outputRate = tierRate(outputTokens, pricing.output);
|
|
2164
|
+
return inputTokens / 1e6 * inputRate + outputTokens / 1e6 * outputRate;
|
|
2071
2165
|
}
|
|
2072
2166
|
|
|
2073
2167
|
// src/translate/prompts/translation-prompt.ts
|
|
@@ -2103,6 +2197,7 @@ function buildPageTranslationPrompt(input) {
|
|
|
2103
2197
|
prompt,
|
|
2104
2198
|
"",
|
|
2105
2199
|
...input.resolved.context ? ["## Context", input.resolved.context, ""] : [],
|
|
2200
|
+
...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
|
|
2106
2201
|
"## Rules",
|
|
2107
2202
|
...input.resolved.rules.map((rule) => `- ${rule}`),
|
|
2108
2203
|
"",
|
|
@@ -2110,10 +2205,6 @@ function buildPageTranslationPrompt(input) {
|
|
|
2110
2205
|
"Return ONLY valid JSON with keys:",
|
|
2111
2206
|
input.slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).",
|
|
2112
2207
|
"",
|
|
2113
|
-
"## EN metadata",
|
|
2114
|
-
`title: ${input.enTitle}`,
|
|
2115
|
-
`description: ${input.enDescription}`,
|
|
2116
|
-
"",
|
|
2117
2208
|
"## EN translatable frontmatter (JSON)",
|
|
2118
2209
|
JSON.stringify(input.translatableFrontmatter, null, 2),
|
|
2119
2210
|
"",
|
|
@@ -2122,6 +2213,71 @@ function buildPageTranslationPrompt(input) {
|
|
|
2122
2213
|
];
|
|
2123
2214
|
return lines.join("\n");
|
|
2124
2215
|
}
|
|
2216
|
+
function getObjectShape(schema) {
|
|
2217
|
+
const base = peelOptionalWrappers(schema);
|
|
2218
|
+
if (base instanceof Object && "shape" in base) {
|
|
2219
|
+
return base.shape;
|
|
2220
|
+
}
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
function getArraySchema(schema) {
|
|
2224
|
+
const base = peelOptionalWrappers(schema);
|
|
2225
|
+
if (base instanceof Object && "element" in base && base._def?.type === "array") {
|
|
2226
|
+
return base;
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
function extractTranslatableFromStructural(schema) {
|
|
2231
|
+
const arraySchema = getArraySchema(schema);
|
|
2232
|
+
if (arraySchema) {
|
|
2233
|
+
const inner = buildTranslatableSubschema(arraySchema.element);
|
|
2234
|
+
if (inner) return zod.z.array(inner);
|
|
2235
|
+
return null;
|
|
2236
|
+
}
|
|
2237
|
+
if (getObjectShape(schema)) {
|
|
2238
|
+
return buildTranslatableSubschema(schema);
|
|
2239
|
+
}
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
function buildTranslatableSubschema(schema) {
|
|
2243
|
+
const shape = getObjectShape(schema);
|
|
2244
|
+
if (!shape) return null;
|
|
2245
|
+
const out = {};
|
|
2246
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
2247
|
+
const childSchema = child;
|
|
2248
|
+
const kind = getFieldKind(childSchema);
|
|
2249
|
+
if (kind === "translatable") {
|
|
2250
|
+
out[key] = peelOptionalWrappers(childSchema);
|
|
2251
|
+
} else if (kind === "structural") {
|
|
2252
|
+
const nested = extractTranslatableFromStructural(childSchema);
|
|
2253
|
+
if (nested) out[key] = nested;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
if (Object.keys(out).length === 0) return null;
|
|
2257
|
+
return zod.z.object(out);
|
|
2258
|
+
}
|
|
2259
|
+
function buildGeminiResponseSchema(schema, slugStrategy) {
|
|
2260
|
+
try {
|
|
2261
|
+
const translatable = buildTranslatableSubschema(schema);
|
|
2262
|
+
if (!translatable) return null;
|
|
2263
|
+
const responseShape = {
|
|
2264
|
+
frontmatter: translatable,
|
|
2265
|
+
body: zod.z.string()
|
|
2266
|
+
};
|
|
2267
|
+
if (slugStrategy === "localized") {
|
|
2268
|
+
responseShape.slug = zod.z.string();
|
|
2269
|
+
}
|
|
2270
|
+
const jsonSchema = zod.z.toJSONSchema(zod.z.object(responseShape), {
|
|
2271
|
+
target: "draft-2020-12",
|
|
2272
|
+
unrepresentable: "any",
|
|
2273
|
+
io: "output"
|
|
2274
|
+
});
|
|
2275
|
+
delete jsonSchema.$schema;
|
|
2276
|
+
return jsonSchema;
|
|
2277
|
+
} catch {
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2125
2281
|
|
|
2126
2282
|
// src/translate/resolve-translate-config.ts
|
|
2127
2283
|
function slugStrategyRules(slugStrategy, preserveTerms) {
|
|
@@ -2162,107 +2318,329 @@ function resolveTranslateConfig(config, type) {
|
|
|
2162
2318
|
return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
|
|
2163
2319
|
}
|
|
2164
2320
|
|
|
2321
|
+
// src/translate/validate-translation.ts
|
|
2322
|
+
function formatZodIssues(error) {
|
|
2323
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2324
|
+
}
|
|
2325
|
+
function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
|
|
2326
|
+
return pickTranslatable(rawFrontmatter, schema);
|
|
2327
|
+
}
|
|
2328
|
+
function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
|
|
2329
|
+
const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
|
|
2330
|
+
const merged = mergeStructuralOntoLocale(
|
|
2331
|
+
frontmatter,
|
|
2332
|
+
enDoc.frontmatter,
|
|
2333
|
+
typeSchema
|
|
2334
|
+
);
|
|
2335
|
+
const parsed = typeSchema.safeParse(merged);
|
|
2336
|
+
if (!parsed.success) {
|
|
2337
|
+
return { ok: false, error: formatZodIssues(parsed.error) };
|
|
2338
|
+
}
|
|
2339
|
+
return { ok: true, frontmatter };
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2165
2342
|
// src/translate/page-translator.ts
|
|
2343
|
+
function summarizeResults(results, durationMs) {
|
|
2344
|
+
return results.reduce(
|
|
2345
|
+
(totals, result) => {
|
|
2346
|
+
if (result.failed) totals.failed += 1;
|
|
2347
|
+
else if (result.skipped) totals.skipped += 1;
|
|
2348
|
+
else totals.translated += 1;
|
|
2349
|
+
totals.inputTokens += result.usage?.inputTokens ?? 0;
|
|
2350
|
+
totals.outputTokens += result.usage?.outputTokens ?? 0;
|
|
2351
|
+
totals.estimatedCostUsd += result.estimatedCostUsd ?? 0;
|
|
2352
|
+
return totals;
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
translated: 0,
|
|
2356
|
+
skipped: 0,
|
|
2357
|
+
failed: 0,
|
|
2358
|
+
inputTokens: 0,
|
|
2359
|
+
outputTokens: 0,
|
|
2360
|
+
estimatedCostUsd: 0,
|
|
2361
|
+
durationMs
|
|
2362
|
+
}
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
function formatTranslateError(error) {
|
|
2366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2367
|
+
try {
|
|
2368
|
+
const parsed = JSON.parse(message);
|
|
2369
|
+
if (parsed.error?.message) return parsed.error.message;
|
|
2370
|
+
} catch {
|
|
2371
|
+
}
|
|
2372
|
+
const embedded = message.match(/"message"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
2373
|
+
if (embedded?.[1]) {
|
|
2374
|
+
return embedded[1].replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
2375
|
+
}
|
|
2376
|
+
return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
|
|
2377
|
+
}
|
|
2378
|
+
async function runPool(items, concurrency, worker) {
|
|
2379
|
+
if (items.length === 0) return;
|
|
2380
|
+
let nextIndex = 0;
|
|
2381
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
2382
|
+
while (nextIndex < items.length) {
|
|
2383
|
+
const index = nextIndex++;
|
|
2384
|
+
await worker(items[index], index);
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
await Promise.all(runners);
|
|
2388
|
+
}
|
|
2389
|
+
function resolveContextLabel(enDoc, enSlug) {
|
|
2390
|
+
const fm = enDoc.frontmatter;
|
|
2391
|
+
for (const key of ["title", "name", "h1"]) {
|
|
2392
|
+
const value = fm[key];
|
|
2393
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2394
|
+
}
|
|
2395
|
+
return enSlug;
|
|
2396
|
+
}
|
|
2166
2397
|
async function translatePage(config, item, options = {}) {
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2398
|
+
const startedAt = Date.now();
|
|
2399
|
+
const base = {
|
|
2400
|
+
contentType: item.contentType,
|
|
2401
|
+
enSlug: item.enSlug,
|
|
2402
|
+
locale: item.locale
|
|
2403
|
+
};
|
|
2404
|
+
try {
|
|
2405
|
+
const type = config.types.find((t) => t.id === item.contentType);
|
|
2406
|
+
if (!type) throw new Error(`Unknown content type ${item.contentType}`);
|
|
2407
|
+
const enDoc = readEnDocument(config, type, item.enSlug);
|
|
2408
|
+
if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
|
|
2409
|
+
const payload = getTranslatablePayload(enDoc, type);
|
|
2410
|
+
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
2411
|
+
const db = openStore(config, "readonly");
|
|
2412
|
+
const existing = getTranslation(db, type.id, item.enSlug, item.locale);
|
|
2413
|
+
db.close();
|
|
2414
|
+
if (!options.force && existing && existing.en_hash === currentEnHash) {
|
|
2415
|
+
return {
|
|
2416
|
+
...base,
|
|
2417
|
+
skipped: true,
|
|
2418
|
+
reason: "fresh",
|
|
2419
|
+
durationMs: Date.now() - startedAt
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
const resolvedTranslate = resolveTranslateConfig(config, type);
|
|
2423
|
+
const model = options.model ?? resolvedTranslate.model;
|
|
2424
|
+
const prompt = buildPageTranslationPrompt({
|
|
2425
|
+
resolved: resolvedTranslate,
|
|
2426
|
+
targetLocale: item.locale,
|
|
2427
|
+
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2428
|
+
translatableFrontmatter: payload.frontmatter,
|
|
2429
|
+
enBody: payload.body,
|
|
2430
|
+
slugStrategy: type.slugStrategy
|
|
2431
|
+
});
|
|
2432
|
+
if (options.dryRun) {
|
|
2433
|
+
return {
|
|
2434
|
+
...base,
|
|
2435
|
+
skipped: false,
|
|
2436
|
+
model,
|
|
2437
|
+
durationMs: Date.now() - startedAt
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2441
|
+
const result = await translatePageWithGemini({
|
|
2442
|
+
prompt,
|
|
2443
|
+
model,
|
|
2444
|
+
responseSchema: responseSchema ?? void 0
|
|
2445
|
+
});
|
|
2446
|
+
const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
|
|
2447
|
+
const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
|
|
2448
|
+
if (!validated.ok) {
|
|
2449
|
+
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2450
|
+
}
|
|
2451
|
+
const writeDb = openStore(config, "readwrite");
|
|
2452
|
+
upsertTranslation(writeDb, {
|
|
2453
|
+
contentType: type.id,
|
|
2179
2454
|
enSlug: item.enSlug,
|
|
2180
2455
|
locale: item.locale,
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
translatableFrontmatter: payload.frontmatter,
|
|
2192
|
-
enBody: payload.body,
|
|
2193
|
-
slugStrategy: type.slugStrategy
|
|
2194
|
-
});
|
|
2195
|
-
if (options.dryRun) {
|
|
2196
|
-
return {
|
|
2197
|
-
contentType: item.contentType,
|
|
2456
|
+
slug,
|
|
2457
|
+
frontmatter: validated.frontmatter,
|
|
2458
|
+
body: result.parsed.body,
|
|
2459
|
+
enHash: currentEnHash,
|
|
2460
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2461
|
+
model: result.model
|
|
2462
|
+
});
|
|
2463
|
+
writeDb.close();
|
|
2464
|
+
recordRevision(config, {
|
|
2465
|
+
contentType: type.id,
|
|
2198
2466
|
enSlug: item.enSlug,
|
|
2199
2467
|
locale: item.locale,
|
|
2468
|
+
revisionKind: "translation",
|
|
2469
|
+
enHash: currentEnHash,
|
|
2470
|
+
body: result.parsed.body,
|
|
2471
|
+
frontmatter: validated.frontmatter,
|
|
2472
|
+
model: result.model
|
|
2473
|
+
});
|
|
2474
|
+
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2475
|
+
normalizeGeminiDisplayName(result.model),
|
|
2476
|
+
result.usage.inputTokens,
|
|
2477
|
+
result.usage.outputTokens
|
|
2478
|
+
);
|
|
2479
|
+
return {
|
|
2480
|
+
...base,
|
|
2481
|
+
skipped: false,
|
|
2482
|
+
model: result.model,
|
|
2483
|
+
usage: result.usage,
|
|
2484
|
+
estimatedCostUsd,
|
|
2485
|
+
durationMs: Date.now() - startedAt
|
|
2486
|
+
};
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
return {
|
|
2489
|
+
...base,
|
|
2200
2490
|
skipped: false,
|
|
2201
|
-
|
|
2491
|
+
failed: true,
|
|
2492
|
+
error: formatTranslateError(error),
|
|
2493
|
+
durationMs: Date.now() - startedAt
|
|
2202
2494
|
};
|
|
2203
2495
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
model: result.model
|
|
2496
|
+
}
|
|
2497
|
+
function labelForItem(item) {
|
|
2498
|
+
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
2499
|
+
}
|
|
2500
|
+
async function translateWorklist(config, items, options = {}) {
|
|
2501
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
2502
|
+
const startedAt = Date.now();
|
|
2503
|
+
const results = new Array(items.length);
|
|
2504
|
+
const active = /* @__PURE__ */ new Set();
|
|
2505
|
+
options.onProgress?.({
|
|
2506
|
+
type: "start",
|
|
2507
|
+
total: items.length,
|
|
2508
|
+
concurrency,
|
|
2509
|
+
dryRun: Boolean(options.dryRun),
|
|
2510
|
+
model: options.model
|
|
2220
2511
|
});
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
model: result.model
|
|
2512
|
+
await runPool(items, concurrency, async (item, index) => {
|
|
2513
|
+
const label = labelForItem(item);
|
|
2514
|
+
active.add(label);
|
|
2515
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
2516
|
+
const result = await translatePage(config, item, options);
|
|
2517
|
+
results[index] = result;
|
|
2518
|
+
active.delete(label);
|
|
2519
|
+
options.onProgress?.({ type: "item-done", result });
|
|
2230
2520
|
});
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
locale: item.locale,
|
|
2235
|
-
skipped: false,
|
|
2236
|
-
model: result.model
|
|
2237
|
-
};
|
|
2521
|
+
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
2522
|
+
options.onProgress?.({ type: "done", results, totals });
|
|
2523
|
+
return results;
|
|
2238
2524
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2525
|
+
function serializeMdx(frontmatter, content) {
|
|
2526
|
+
return matter__default.default.stringify(content, frontmatter);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/export/build-static-raw-exports.ts
|
|
2530
|
+
function normalizeExtension(ext) {
|
|
2531
|
+
return ext.startsWith(".") ? ext : `.${ext}`;
|
|
2532
|
+
}
|
|
2533
|
+
function exportDirSegment(pathTemplate) {
|
|
2534
|
+
const prefix = pathPrefix(pathTemplate);
|
|
2535
|
+
return prefix.replace(/^\/+|\/+$/g, "");
|
|
2536
|
+
}
|
|
2537
|
+
function getStaticExportRoots(project, options = {}) {
|
|
2538
|
+
const { config } = project;
|
|
2539
|
+
const locales = options.locales ?? config.locales;
|
|
2540
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2541
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2542
|
+
for (const type of project.listTypes()) {
|
|
2543
|
+
if (!isRoutableType(type.config)) continue;
|
|
2544
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2545
|
+
const segment = exportDirSegment(type.config.path);
|
|
2546
|
+
roots.add(`${segment}/`);
|
|
2547
|
+
for (const locale of locales) {
|
|
2548
|
+
if (locale === config.defaultLocale) continue;
|
|
2549
|
+
roots.add(`${locale}/${segment}/`);
|
|
2550
|
+
}
|
|
2243
2551
|
}
|
|
2244
|
-
return
|
|
2552
|
+
return [...roots].sort();
|
|
2553
|
+
}
|
|
2554
|
+
function buildStaticRawExports(project, options = {}) {
|
|
2555
|
+
const { config } = project;
|
|
2556
|
+
const extension = normalizeExtension(options.extension ?? ".mdx");
|
|
2557
|
+
const locales = options.locales ?? config.locales;
|
|
2558
|
+
const excludeRedirected = options.excludeRedirected ?? true;
|
|
2559
|
+
const excludeNoindex = options.excludeNoindex ?? false;
|
|
2560
|
+
const typeFilter = options.types ? new Set(options.types) : null;
|
|
2561
|
+
const out = [];
|
|
2562
|
+
for (const type of project.listTypes()) {
|
|
2563
|
+
if (!isRoutableType(type.config)) continue;
|
|
2564
|
+
if (typeFilter && !typeFilter.has(type.id)) continue;
|
|
2565
|
+
const pathTemplate = type.config.path;
|
|
2566
|
+
const all = type.load();
|
|
2567
|
+
const enIdx = all.get(config.defaultLocale);
|
|
2568
|
+
if (!enIdx) continue;
|
|
2569
|
+
for (const locale of locales) {
|
|
2570
|
+
for (const enDoc of enIdx.bySlug.values()) {
|
|
2571
|
+
const resolved = type.resolve(enDoc.slug, locale);
|
|
2572
|
+
if (!resolved.document) continue;
|
|
2573
|
+
if (excludeRedirected && resolved.shouldRedirectTo) continue;
|
|
2574
|
+
if (excludeNoindex && resolved.document.noindex) continue;
|
|
2575
|
+
const doc = resolved.document;
|
|
2576
|
+
const slugWithExt = `${doc.slug}${extension}`;
|
|
2577
|
+
const urlPath = resolvePath(pathTemplate, slugWithExt, locale, config.defaultLocale);
|
|
2578
|
+
out.push({
|
|
2579
|
+
relativePath: urlPath.slice(1),
|
|
2580
|
+
urlPath,
|
|
2581
|
+
locale,
|
|
2582
|
+
typeId: type.id,
|
|
2583
|
+
enSlug: doc.enSlug,
|
|
2584
|
+
source: serializeMdx(doc.frontmatter, doc.content)
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return out;
|
|
2590
|
+
}
|
|
2591
|
+
function rmDirIfExists(dir) {
|
|
2592
|
+
if (fs2__default.default.existsSync(dir)) {
|
|
2593
|
+
fs2__default.default.rmSync(dir, { recursive: true, force: true });
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
function writeStaticRawExports(project, options = {}) {
|
|
2597
|
+
const outDir = path3__default.default.resolve(options.outDir ?? "public");
|
|
2598
|
+
const typeFilter = options.types;
|
|
2599
|
+
for (const root of getStaticExportRoots(project, {
|
|
2600
|
+
types: typeFilter,
|
|
2601
|
+
locales: options.locales
|
|
2602
|
+
})) {
|
|
2603
|
+
rmDirIfExists(path3__default.default.join(outDir, root));
|
|
2604
|
+
}
|
|
2605
|
+
const exports = buildStaticRawExports(project, options);
|
|
2606
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2607
|
+
for (const item of exports) {
|
|
2608
|
+
const filePath = path3__default.default.join(outDir, item.relativePath);
|
|
2609
|
+
fs2__default.default.mkdirSync(path3__default.default.dirname(filePath), { recursive: true });
|
|
2610
|
+
fs2__default.default.writeFileSync(filePath, item.source, "utf8");
|
|
2611
|
+
const key = `${item.typeId}/${item.locale}`;
|
|
2612
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2613
|
+
}
|
|
2614
|
+
for (const [key, count] of [...counts.entries()].sort()) {
|
|
2615
|
+
console.log(` ${key}: ${count}`);
|
|
2616
|
+
}
|
|
2617
|
+
return { exports, written: exports.length };
|
|
2245
2618
|
}
|
|
2246
2619
|
|
|
2247
2620
|
exports.buildAllContentRedirects = buildAllContentRedirects;
|
|
2621
|
+
exports.buildStaticRawExports = buildStaticRawExports;
|
|
2248
2622
|
exports.buildWorklist = buildWorklist;
|
|
2249
2623
|
exports.createProject = createProject;
|
|
2250
2624
|
exports.createScribe = createScribe;
|
|
2251
2625
|
exports.defineConfig = defineConfig;
|
|
2252
2626
|
exports.defineContentType = defineContentType;
|
|
2627
|
+
exports.exportDirSegment = exportDirSegment;
|
|
2253
2628
|
exports.field = field;
|
|
2254
2629
|
exports.findConfigPath = findConfigPath;
|
|
2255
2630
|
exports.generateSitemap = generateSitemap;
|
|
2256
2631
|
exports.getFieldKind = getFieldKind;
|
|
2257
2632
|
exports.getRedirectSourceSlugs = getRedirectSourceSlugs;
|
|
2258
2633
|
exports.getRelationTarget = getRelationTarget;
|
|
2634
|
+
exports.getStaticExportRoots = getStaticExportRoots;
|
|
2259
2635
|
exports.isResolvedConfig = isResolvedConfig;
|
|
2260
2636
|
exports.loadConfigSync = loadConfigSync;
|
|
2261
2637
|
exports.resolveConfig = resolveConfig;
|
|
2262
2638
|
exports.resolveLocalesFromPreset = resolveLocalesFromPreset;
|
|
2639
|
+
exports.serializeMdx = serializeMdx;
|
|
2263
2640
|
exports.translatePage = translatePage;
|
|
2264
2641
|
exports.translateWorklist = translateWorklist;
|
|
2265
2642
|
exports.unwrapSchema = unwrapSchema;
|
|
2266
2643
|
exports.validateProject = validateProject;
|
|
2644
|
+
exports.writeStaticRawExports = writeStaticRawExports;
|
|
2267
2645
|
//# sourceMappingURL=index.cjs.map
|
|
2268
2646
|
//# sourceMappingURL=index.cjs.map
|