scribe-cms 0.0.11 → 0.0.13

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 (56) hide show
  1. package/README.md +21 -6
  2. package/dist/cli/index.cjs +1258 -219
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +1259 -220
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/prompt-translate.d.ts +3 -0
  7. package/dist/cli/prompt-translate.d.ts.map +1 -1
  8. package/dist/cli/translate-progress.d.ts.map +1 -1
  9. package/dist/index.cjs +1020 -181
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +1021 -183
  12. package/dist/index.js.map +1 -1
  13. package/dist/runtime.cjs +149 -62
  14. package/dist/runtime.cjs.map +1 -1
  15. package/dist/runtime.js +149 -62
  16. package/dist/runtime.js.map +1 -1
  17. package/dist/src/config/resolve-config.d.ts.map +1 -1
  18. package/dist/src/core/introspect-schema.d.ts +7 -1
  19. package/dist/src/core/introspect-schema.d.ts.map +1 -1
  20. package/dist/src/core/types.d.ts +6 -0
  21. package/dist/src/core/types.d.ts.map +1 -1
  22. package/dist/src/create-project.d.ts.map +1 -1
  23. package/dist/src/i18n/resolve-document.d.ts +1 -1
  24. package/dist/src/i18n/resolve-document.d.ts.map +1 -1
  25. package/dist/src/index.d.ts +3 -3
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/storage/batch-jobs.d.ts +53 -0
  28. package/dist/src/storage/batch-jobs.d.ts.map +1 -0
  29. package/dist/src/storage/sqlite.d.ts.map +1 -1
  30. package/dist/src/translate/batch-worklist.d.ts +87 -0
  31. package/dist/src/translate/batch-worklist.d.ts.map +1 -0
  32. package/dist/src/translate/gemini-batch.d.ts +26 -0
  33. package/dist/src/translate/gemini-batch.d.ts.map +1 -0
  34. package/dist/src/translate/gemini-client.d.ts +23 -0
  35. package/dist/src/translate/gemini-client.d.ts.map +1 -1
  36. package/dist/src/translate/gemini-models.d.ts +9 -0
  37. package/dist/src/translate/gemini-models.d.ts.map +1 -1
  38. package/dist/src/translate/gemini-pricing.d.ts +2 -1
  39. package/dist/src/translate/gemini-pricing.d.ts.map +1 -1
  40. package/dist/src/translate/page-translator.d.ts +25 -52
  41. package/dist/src/translate/page-translator.d.ts.map +1 -1
  42. package/dist/src/translate/prompts/translation-prompt.d.ts +6 -0
  43. package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
  44. package/dist/src/translate/response-schema.d.ts +8 -1
  45. package/dist/src/translate/response-schema.d.ts.map +1 -1
  46. package/dist/src/translate/retry.d.ts +16 -0
  47. package/dist/src/translate/retry.d.ts.map +1 -0
  48. package/dist/src/translate/sanitize-mdx-jsx.d.ts.map +1 -1
  49. package/dist/src/translate/translate-core.d.ts +155 -0
  50. package/dist/src/translate/translate-core.d.ts.map +1 -0
  51. package/dist/src/validate/validate-project.d.ts.map +1 -1
  52. package/dist/studio/server.cjs +84 -47
  53. package/dist/studio/server.cjs.map +1 -1
  54. package/dist/studio/server.js +84 -47
  55. package/dist/studio/server.js.map +1 -1
  56. package/package.json +1 -1
package/dist/runtime.cjs CHANGED
@@ -138,6 +138,39 @@ function resolveConfig(input, baseDir) {
138
138
  if (localeRouting.strategy === "search-param" && !localeRouting.param) {
139
139
  throw new Error('scribe config: localeRouting search-param requires a "param" name');
140
140
  }
141
+ const localeFallbacks = {};
142
+ for (const [locale, chain] of Object.entries(raw.localeFallbacks ?? {})) {
143
+ if (!raw.locales.includes(locale)) {
144
+ throw new Error(
145
+ `scribe config: localeFallbacks key "${locale}" is not in locales [${raw.locales.join(", ")}]`
146
+ );
147
+ }
148
+ const seen = /* @__PURE__ */ new Set();
149
+ for (const fallback of chain) {
150
+ if (!raw.locales.includes(fallback)) {
151
+ throw new Error(
152
+ `scribe config: localeFallbacks["${locale}"] entry "${fallback}" is not in locales [${raw.locales.join(", ")}]`
153
+ );
154
+ }
155
+ if (fallback === locale) {
156
+ throw new Error(
157
+ `scribe config: localeFallbacks["${locale}"] must not contain its own key "${locale}"`
158
+ );
159
+ }
160
+ if (fallback === defaultLocale) {
161
+ throw new Error(
162
+ `scribe config: localeFallbacks["${locale}"] must not contain the defaultLocale "${defaultLocale}" \u2014 it is always the final fallback; remove it`
163
+ );
164
+ }
165
+ if (seen.has(fallback)) {
166
+ throw new Error(
167
+ `scribe config: localeFallbacks["${locale}"] contains duplicate entry "${fallback}"`
168
+ );
169
+ }
170
+ seen.add(fallback);
171
+ }
172
+ localeFallbacks[locale] = [...chain];
173
+ }
141
174
  const projectRoot = path__default.default.resolve(process.cwd(), raw.rootDir);
142
175
  const contentRoot = path__default.default.resolve(projectRoot, raw.contentDir ?? "content");
143
176
  const storePath = path__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
@@ -167,6 +200,7 @@ function resolveConfig(input, baseDir) {
167
200
  defaultLocale,
168
201
  localeRouting,
169
202
  localePresets: raw.localePresets,
203
+ localeFallbacks,
170
204
  translate: raw.translate,
171
205
  types
172
206
  };
@@ -205,20 +239,16 @@ function getRelationTarget(schema) {
205
239
  }
206
240
  return null;
207
241
  }
208
- function unwrapSchema(schema) {
242
+ function peelOptionalWrappers(schema) {
209
243
  let current = schema;
210
244
  for (let i = 0; i < 8; i++) {
211
- const anySchema = current;
212
- if (typeof anySchema.unwrap === "function") {
213
- current = anySchema.unwrap();
214
- continue;
215
- }
216
- if (typeof anySchema.removeDefault === "function") {
217
- current = anySchema.removeDefault();
245
+ const type = current._def?.type;
246
+ if (type === "optional" || type === "nullable") {
247
+ current = current.unwrap();
218
248
  continue;
219
249
  }
220
- if (anySchema._def?.innerType) {
221
- current = anySchema._def.innerType;
250
+ if (type === "default") {
251
+ current = current.removeDefault();
222
252
  continue;
223
253
  }
224
254
  break;
@@ -227,6 +257,12 @@ function unwrapSchema(schema) {
227
257
  }
228
258
 
229
259
  // src/core/introspect-schema.ts
260
+ function getArrayElement(schema) {
261
+ if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
262
+ return schema.element;
263
+ }
264
+ return null;
265
+ }
230
266
  function introspectSchema(schema, prefix = []) {
231
267
  const relation = getRelationTarget(schema);
232
268
  if (relation && prefix.length > 0) {
@@ -240,7 +276,10 @@ function introspectSchema(schema, prefix = []) {
240
276
  }
241
277
  ];
242
278
  }
243
- const base = unwrapSchema(schema);
279
+ if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
280
+ return [{ path: prefix, kind: "translatable" }];
281
+ }
282
+ const base = peelOptionalWrappers(schema);
244
283
  if (base instanceof Object && "shape" in base) {
245
284
  const shape = base.shape;
246
285
  const fields = [];
@@ -249,9 +288,9 @@ function introspectSchema(schema, prefix = []) {
249
288
  }
250
289
  return fields;
251
290
  }
252
- if (base instanceof Object && "element" in base) {
253
- const element = base.element;
254
- const elementBase = unwrapSchema(element);
291
+ const element = getArrayElement(base);
292
+ if (element) {
293
+ const elementBase = peelOptionalWrappers(element);
255
294
  if (elementBase instanceof Object && "shape" in elementBase) {
256
295
  const shape = elementBase.shape;
257
296
  const fields = [];
@@ -263,15 +302,48 @@ function introspectSchema(schema, prefix = []) {
263
302
  }
264
303
  return [{ path: prefix, kind: getFieldKind(schema) }];
265
304
  }
266
- function extractByPaths(data, paths) {
267
- const out = {};
305
+ function buildPathTrie(paths) {
306
+ const root = { leaf: false, children: /* @__PURE__ */ new Map() };
268
307
  for (const path6 of paths) {
269
- const value = getAtPath(data, path6);
308
+ let node = root;
309
+ for (const segment of path6) {
310
+ let child = node.children.get(segment);
311
+ if (!child) {
312
+ child = { leaf: false, children: /* @__PURE__ */ new Map() };
313
+ node.children.set(segment, child);
314
+ }
315
+ node = child;
316
+ }
317
+ node.leaf = true;
318
+ }
319
+ return root;
320
+ }
321
+ function extractNode(data, trie) {
322
+ if (trie.leaf) return data;
323
+ const star = trie.children.get("*");
324
+ if (star) {
325
+ if (!Array.isArray(data)) return void 0;
326
+ return data.map(
327
+ (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
328
+ );
329
+ }
330
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
331
+ const record = data;
332
+ const out = {};
333
+ let any = false;
334
+ for (const [key, child] of trie.children) {
335
+ const value = extractNode(record[key], child);
270
336
  if (value !== void 0) {
271
- setAtPath(out, path6.filter((p) => p !== "*"), value);
337
+ out[key] = value;
338
+ any = true;
272
339
  }
273
340
  }
274
- return out;
341
+ return any ? out : void 0;
342
+ }
343
+ function extractByPaths(data, paths) {
344
+ if (paths.length === 0) return {};
345
+ const extracted = extractNode(data, buildPathTrie(paths));
346
+ return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
275
347
  }
276
348
  function pickTranslatable(data, schema) {
277
349
  const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
@@ -286,32 +358,6 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
286
358
  const translatable = pickTranslatable(localeData, schema);
287
359
  return deepMerge(structural, translatable);
288
360
  }
289
- function getAtPath(obj, path6) {
290
- let current = obj;
291
- for (const segment of path6) {
292
- if (segment === "*") {
293
- if (!Array.isArray(current)) return void 0;
294
- return current.map(
295
- (item) => typeof item === "object" && item !== null ? getAtPath(item, path6.slice(path6.indexOf("*") + 1)) : void 0
296
- );
297
- }
298
- if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
299
- current = current[segment];
300
- }
301
- return current;
302
- }
303
- function setAtPath(obj, path6, value) {
304
- if (path6.length === 0) return;
305
- if (path6.length === 1) {
306
- obj[path6[0]] = value;
307
- return;
308
- }
309
- const [head, ...rest] = path6;
310
- if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
311
- obj[head] = {};
312
- }
313
- setAtPath(obj[head], rest, value);
314
- }
315
361
  function deepMerge(base, overlay) {
316
362
  const out = { ...base };
317
363
  for (const [key, value] of Object.entries(overlay)) {
@@ -462,7 +508,7 @@ function seoFieldsFromEn(enDoc) {
462
508
  canonicalPathOverride: enDoc.canonicalPathOverride
463
509
  };
464
510
  }
465
- var SCHEMA_VERSION = 4;
511
+ var SCHEMA_VERSION = 5;
466
512
  var MIGRATIONS = [
467
513
  `CREATE TABLE IF NOT EXISTS meta (
468
514
  key TEXT PRIMARY KEY,
@@ -510,7 +556,30 @@ var MIGRATIONS = [
510
556
  UNIQUE (content_type, en_slug, en_hash)
511
557
  )`,
512
558
  `CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
513
- ON en_snapshots(content_type, en_slug, created_at DESC)`
559
+ ON en_snapshots(content_type, en_slug, created_at DESC)`,
560
+ `CREATE TABLE IF NOT EXISTS translation_batch_jobs (
561
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
562
+ job_name TEXT NOT NULL UNIQUE,
563
+ model TEXT NOT NULL,
564
+ display_model TEXT NOT NULL,
565
+ created_at TEXT NOT NULL,
566
+ state TEXT NOT NULL,
567
+ completed_at TEXT
568
+ )`,
569
+ `CREATE TABLE IF NOT EXISTS translation_batch_items (
570
+ job_id INTEGER NOT NULL,
571
+ request_index INTEGER NOT NULL,
572
+ content_type TEXT NOT NULL,
573
+ en_slug TEXT NOT NULL,
574
+ locale TEXT NOT NULL,
575
+ en_hash TEXT NOT NULL,
576
+ snapshot_id INTEGER NOT NULL,
577
+ status TEXT NOT NULL,
578
+ error TEXT,
579
+ PRIMARY KEY (job_id, request_index)
580
+ )`,
581
+ `CREATE INDEX IF NOT EXISTS idx_batch_items_status
582
+ ON translation_batch_items(status)`
514
583
  ];
515
584
  function resolveStorePath(config) {
516
585
  return config.storePath;
@@ -533,6 +602,8 @@ function addColumnIfMissing(db, table, column, ddlType) {
533
602
  }
534
603
  }
535
604
  function readSchemaVersion(db) {
605
+ const metaTable = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'meta'`).get();
606
+ if (!metaTable) return 0;
536
607
  const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
537
608
  return row ? Number.parseInt(row.value, 10) || 0 : 0;
538
609
  }
@@ -593,7 +664,8 @@ function sanitizeMdxJsxAttributeQuotes(body) {
593
664
  if (i >= body.length || body[i] === ">" || body[i] === "/") break;
594
665
  const attrStart = i;
595
666
  while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
596
- body.slice(attrStart, i);
667
+ const attrName = body.slice(attrStart, i);
668
+ if (!attrName) break;
597
669
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
598
670
  if (body[i] !== "=") {
599
671
  tagOut += body.slice(attrStart, i);
@@ -602,10 +674,12 @@ function sanitizeMdxJsxAttributeQuotes(body) {
602
674
  tagOut += body.slice(attrStart, i);
603
675
  tagOut += "=";
604
676
  i += 1;
677
+ const wsStart = i;
605
678
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
679
+ tagOut += body.slice(wsStart, i);
606
680
  const quote = body[i];
607
681
  if (quote !== '"' && quote !== "'") {
608
- continue;
682
+ break;
609
683
  }
610
684
  if (quote === "'") {
611
685
  const valStart2 = i + 1;
@@ -888,7 +962,7 @@ function createContentLoader(config, type) {
888
962
  }
889
963
 
890
964
  // src/i18n/resolve-document.ts
891
- function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
965
+ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting, fallbackLocales = []) {
892
966
  const urlBuilder = createUrlBuilder({
893
967
  locales: [defaultLocale, locale],
894
968
  defaultLocale,
@@ -902,15 +976,28 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
902
976
  for (const [docLocale, docIdx] of allDocs) {
903
977
  const found = docIdx.bySlug.get(slug);
904
978
  if (!found) continue;
905
- const correctSlug = getSlugForLocale(found, docLocale, locale, allDocs, defaultLocale);
906
- if (correctSlug && correctSlug !== slug) {
979
+ const enSlug = docLocale === defaultLocale ? found.slug : found.enSlug;
980
+ let target;
981
+ let targetLocale = locale;
982
+ for (const candidateLocale of [locale, ...fallbackLocales]) {
983
+ const cand = candidateLocale === defaultLocale ? allDocs.get(candidateLocale)?.bySlug.get(enSlug) : allDocs.get(candidateLocale)?.byEnSlug.get(enSlug);
984
+ if (cand) {
985
+ target = cand;
986
+ targetLocale = candidateLocale;
987
+ break;
988
+ }
989
+ }
990
+ if (target) {
991
+ if (target.slug === slug) {
992
+ return { document: target, actualLocale: targetLocale };
993
+ }
907
994
  if (!type.path) {
908
995
  return { document: null, actualLocale: locale };
909
996
  }
910
997
  return {
911
998
  document: null,
912
999
  actualLocale: locale,
913
- shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
1000
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, target.slug, locale)
914
1001
  };
915
1002
  }
916
1003
  if (docLocale === defaultLocale) {
@@ -941,13 +1028,6 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
941
1028
  }
942
1029
  return { document: null, actualLocale: locale };
943
1030
  }
944
- function getSlugForLocale(document, sourceLocale, targetLocale, allDocs, defaultLocale) {
945
- if (sourceLocale === targetLocale) return document.slug;
946
- const englishSlug = sourceLocale === defaultLocale ? document.slug : document.enSlug;
947
- if (!englishSlug) return null;
948
- if (targetLocale === defaultLocale) return englishSlug;
949
- return allDocs.get(targetLocale)?.byEnSlug.get(englishSlug)?.slug ?? null;
950
- }
951
1031
 
952
1032
  // src/create-project.ts
953
1033
  function comparatorFor(orderBy) {
@@ -1000,7 +1080,8 @@ function buildRuntime(config, type, getRuntime) {
1000
1080
  config.defaultLocale,
1001
1081
  load(),
1002
1082
  type,
1003
- config.localeRouting
1083
+ config.localeRouting,
1084
+ config.localeFallbacks[locale] ?? []
1004
1085
  );
1005
1086
  if (result.document && type.path) {
1006
1087
  return {
@@ -1022,8 +1103,14 @@ function buildRuntime(config, type, getRuntime) {
1022
1103
  const params = [];
1023
1104
  for (const locale of options.locales ?? config.locales) {
1024
1105
  const localeIdx = all.get(locale);
1106
+ const fallbacks = config.localeFallbacks[locale] ?? [];
1025
1107
  for (const doc of enIdx.bySlug.values()) {
1026
- const slug = locale === config.defaultLocale ? doc.slug : localeIdx?.byEnSlug.get(doc.slug)?.slug ?? doc.slug;
1108
+ let slug;
1109
+ if (locale === config.defaultLocale) {
1110
+ slug = doc.slug;
1111
+ } else {
1112
+ slug = localeIdx?.byEnSlug.get(doc.slug)?.slug ?? fallbacks.map((fb) => all.get(fb)?.byEnSlug.get(doc.slug)?.slug).find((s) => s !== void 0) ?? doc.slug;
1113
+ }
1027
1114
  params.push({ locale, slug });
1028
1115
  }
1029
1116
  }