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.
Files changed (58) hide show
  1. package/README.md +2 -3
  2. package/bin/scribe.js +2 -11
  3. package/dist/cli/index.cjs +1284 -171
  4. package/dist/cli/index.cjs.map +1 -1
  5. package/dist/cli/index.js +1284 -171
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/cli/prompt-translate.d.ts +16 -0
  8. package/dist/cli/prompt-translate.d.ts.map +1 -0
  9. package/dist/cli/translate-progress.d.ts +11 -0
  10. package/dist/cli/translate-progress.d.ts.map +1 -0
  11. package/dist/index.cjs +479 -101
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.js +475 -102
  14. package/dist/index.js.map +1 -1
  15. package/dist/runtime.cjs +12 -2
  16. package/dist/runtime.cjs.map +1 -1
  17. package/dist/runtime.js +12 -2
  18. package/dist/runtime.js.map +1 -1
  19. package/dist/src/core/field.d.ts +2 -0
  20. package/dist/src/core/field.d.ts.map +1 -1
  21. package/dist/src/core/types.d.ts +1 -1
  22. package/dist/src/core/types.d.ts.map +1 -1
  23. package/dist/src/export/build-static-raw-exports.d.ts +37 -0
  24. package/dist/src/export/build-static-raw-exports.d.ts.map +1 -0
  25. package/dist/src/export/write-static-raw-exports.d.ts +12 -0
  26. package/dist/src/export/write-static-raw-exports.d.ts.map +1 -0
  27. package/dist/src/history/record-revision.d.ts +2 -0
  28. package/dist/src/history/record-revision.d.ts.map +1 -1
  29. package/dist/src/index.d.ts +7 -0
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/storage/sqlite.d.ts.map +1 -1
  32. package/dist/src/storage/translations.d.ts +6 -0
  33. package/dist/src/storage/translations.d.ts.map +1 -1
  34. package/dist/src/translate/gemini-client.d.ts +7 -0
  35. package/dist/src/translate/gemini-client.d.ts.map +1 -1
  36. package/dist/src/translate/gemini-models.d.ts +8 -0
  37. package/dist/src/translate/gemini-models.d.ts.map +1 -0
  38. package/dist/src/translate/gemini-pricing.d.ts +11 -0
  39. package/dist/src/translate/gemini-pricing.d.ts.map +1 -0
  40. package/dist/src/translate/page-translator.d.ts +38 -1
  41. package/dist/src/translate/page-translator.d.ts.map +1 -1
  42. package/dist/src/translate/prompts/translation-prompt.d.ts +1 -2
  43. package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
  44. package/dist/src/translate/response-schema.d.ts +7 -0
  45. package/dist/src/translate/response-schema.d.ts.map +1 -0
  46. package/dist/src/translate/validate-translation.d.ts +14 -0
  47. package/dist/src/translate/validate-translation.d.ts.map +1 -0
  48. package/dist/src/translate/worklist.d.ts +3 -0
  49. package/dist/src/translate/worklist.d.ts.map +1 -1
  50. package/dist/src/validate/validate-project.d.ts.map +1 -1
  51. package/dist/studio/server.cjs +611 -194
  52. package/dist/studio/server.cjs.map +1 -1
  53. package/dist/studio/server.d.ts +1 -1
  54. package/dist/studio/server.d.ts.map +1 -1
  55. package/dist/studio/server.js +611 -194
  56. package/dist/studio/server.js.map +1 -1
  57. package/package.json +16 -5
  58. 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(path9, typeId) {
116
- if (!path9.startsWith("/")) {
117
- throw new Error(`Content type "${typeId}": path must start with / (got "${path9}")`);
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 = (path9.match(/\{slug\}/g) ?? []).length;
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 "${path9}")`
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 path9 of paths) {
247
- const value = getAtPath(data, path9);
262
+ for (const path10 of paths) {
263
+ const value = getAtPath(data, path10);
248
264
  if (value !== void 0) {
249
- setAtPath(out, path9.filter((p) => p !== "*"), value);
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, path9) {
283
+ function getAtPath(obj, path10) {
268
284
  let current = obj;
269
- for (const segment of path9) {
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, path9.slice(path9.indexOf("*") + 1)) : void 0
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, path9, value) {
282
- if (path9.length === 0) return;
283
- if (path9.length === 1) {
284
- obj[path9[0]] = value;
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] = path9;
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 = 2;
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
- fs2.mkdirSync(path3.dirname(storePath), { recursive: true });
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, model, body_preview
602
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
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 path9 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1546
- return `${origin}${path9}`;
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
- var DEFAULT_MODEL = "gemini-2.5-pro";
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 model = input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL;
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
- return { model, raw, parsed };
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 type = config.types.find((t) => t.id === item.contentType);
2157
- if (!type) throw new Error(`Unknown content type ${item.contentType}`);
2158
- const enDoc = readEnDocument(config, type, item.enSlug);
2159
- if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
2160
- const payload = getTranslatablePayload(enDoc, type);
2161
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
2162
- const db = openStore(config, "readonly");
2163
- const existing = getTranslation(db, type.id, item.enSlug, item.locale);
2164
- db.close();
2165
- if (!options.force && existing && existing.en_hash === currentEnHash) {
2166
- return {
2167
- contentType: item.contentType,
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
- skipped: true,
2171
- reason: "fresh"
2172
- };
2173
- }
2174
- const resolvedTranslate = resolveTranslateConfig(config, type);
2175
- const prompt = buildPageTranslationPrompt({
2176
- resolved: resolvedTranslate,
2177
- targetLocale: item.locale,
2178
- enTitle: String(enDoc.frontmatter.title ?? item.enSlug),
2179
- enDescription: String(enDoc.frontmatter.description ?? ""),
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
- model: options.model
2480
+ failed: true,
2481
+ error: formatTranslateError(error),
2482
+ durationMs: Date.now() - startedAt
2191
2483
  };
2192
2484
  }
2193
- const result = await translatePageWithGemini({
2194
- prompt,
2195
- model: options.model ?? resolvedTranslate.model
2196
- });
2197
- const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2198
- const writeDb = openStore(config, "readwrite");
2199
- upsertTranslation(writeDb, {
2200
- contentType: type.id,
2201
- enSlug: item.enSlug,
2202
- locale: item.locale,
2203
- slug,
2204
- frontmatter: result.parsed.frontmatter,
2205
- body: result.parsed.body,
2206
- enHash: currentEnHash,
2207
- translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
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
- writeDb.close();
2211
- recordRevision(config, {
2212
- contentType: type.id,
2213
- enSlug: item.enSlug,
2214
- locale: item.locale,
2215
- revisionKind: "translation",
2216
- enHash: currentEnHash,
2217
- body: result.parsed.body,
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
- return {
2221
- contentType: item.contentType,
2222
- enSlug: item.enSlug,
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
- async function translateWorklist(config, items, options = {}) {
2229
- const results = [];
2230
- for (const item of items) {
2231
- results.push(await translatePage(config, item, options));
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 results;
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