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.
Files changed (57) hide show
  1. package/README.md +3 -20
  2. package/bin/scribe.js +20 -0
  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 +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(path9, typeId) {
127
- if (!path9.startsWith("/")) {
128
- throw new Error(`Content type "${typeId}": path must start with / (got "${path9}")`);
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 = (path9.match(/\{slug\}/g) ?? []).length;
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 "${path9}")`
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 path9 of paths) {
258
- const value = getAtPath(data, path9);
273
+ for (const path10 of paths) {
274
+ const value = getAtPath(data, path10);
259
275
  if (value !== void 0) {
260
- setAtPath(out, path9.filter((p) => p !== "*"), value);
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, path9) {
294
+ function getAtPath(obj, path10) {
279
295
  let current = obj;
280
- for (const segment of path9) {
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, path9.slice(path9.indexOf("*") + 1)) : void 0
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, path9, value) {
293
- if (path9.length === 0) return;
294
- if (path9.length === 1) {
295
- obj[path9[0]] = value;
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] = path9;
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 = 2;
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
- fs2__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
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, model, body_preview
613
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
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 path9 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1557
- return `${origin}${path9}`;
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
- var DEFAULT_MODEL = "gemini-2.5-pro";
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 model = input.model ?? process.env.PROSE_GEMINI_MODEL ?? DEFAULT_MODEL;
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
- return { model, raw, parsed };
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 type = config.types.find((t) => t.id === item.contentType);
2168
- if (!type) throw new Error(`Unknown content type ${item.contentType}`);
2169
- const enDoc = readEnDocument(config, type, item.enSlug);
2170
- if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
2171
- const payload = getTranslatablePayload(enDoc, type);
2172
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
2173
- const db = openStore(config, "readonly");
2174
- const existing = getTranslation(db, type.id, item.enSlug, item.locale);
2175
- db.close();
2176
- if (!options.force && existing && existing.en_hash === currentEnHash) {
2177
- return {
2178
- contentType: item.contentType,
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
- skipped: true,
2182
- reason: "fresh"
2183
- };
2184
- }
2185
- const resolvedTranslate = resolveTranslateConfig(config, type);
2186
- const prompt = buildPageTranslationPrompt({
2187
- resolved: resolvedTranslate,
2188
- targetLocale: item.locale,
2189
- enTitle: String(enDoc.frontmatter.title ?? item.enSlug),
2190
- enDescription: String(enDoc.frontmatter.description ?? ""),
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
- model: options.model
2491
+ failed: true,
2492
+ error: formatTranslateError(error),
2493
+ durationMs: Date.now() - startedAt
2202
2494
  };
2203
2495
  }
2204
- const result = await translatePageWithGemini({
2205
- prompt,
2206
- model: options.model ?? resolvedTranslate.model
2207
- });
2208
- const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2209
- const writeDb = openStore(config, "readwrite");
2210
- upsertTranslation(writeDb, {
2211
- contentType: type.id,
2212
- enSlug: item.enSlug,
2213
- locale: item.locale,
2214
- slug,
2215
- frontmatter: result.parsed.frontmatter,
2216
- body: result.parsed.body,
2217
- enHash: currentEnHash,
2218
- translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
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
- writeDb.close();
2222
- recordRevision(config, {
2223
- contentType: type.id,
2224
- enSlug: item.enSlug,
2225
- locale: item.locale,
2226
- revisionKind: "translation",
2227
- enHash: currentEnHash,
2228
- body: result.parsed.body,
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
- return {
2232
- contentType: item.contentType,
2233
- enSlug: item.enSlug,
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
- async function translateWorklist(config, items, options = {}) {
2240
- const results = [];
2241
- for (const item of items) {
2242
- results.push(await translatePage(config, item, options));
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 results;
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