scribe-cms 0.0.12 → 0.0.14

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 (55) hide show
  1. package/README.md +21 -6
  2. package/dist/cli/index.cjs +1207 -204
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +1208 -205
  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 +969 -166
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +970 -168
  12. package/dist/index.js.map +1 -1
  13. package/dist/runtime.cjs +136 -62
  14. package/dist/runtime.cjs.map +1 -1
  15. package/dist/runtime.js +136 -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 +18 -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/studio/server.cjs +84 -47
  52. package/dist/studio/server.cjs.map +1 -1
  53. package/dist/studio/server.js +84 -47
  54. package/dist/studio/server.js.map +1 -1
  55. package/package.json +1 -1
package/dist/runtime.cjs CHANGED
@@ -138,6 +138,7 @@ 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 = raw.localeFallbacks === false ? {} : deriveLocaleFallbacks(raw.locales, defaultLocale);
141
142
  const projectRoot = path__default.default.resolve(process.cwd(), raw.rootDir);
142
143
  const contentRoot = path__default.default.resolve(projectRoot, raw.contentDir ?? "content");
143
144
  const storePath = path__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
@@ -167,6 +168,7 @@ function resolveConfig(input, baseDir) {
167
168
  defaultLocale,
168
169
  localeRouting,
169
170
  localePresets: raw.localePresets,
171
+ localeFallbacks,
170
172
  translate: raw.translate,
171
173
  types
172
174
  };
@@ -176,6 +178,25 @@ function resolveConfig(input, baseDir) {
176
178
  });
177
179
  return config;
178
180
  }
181
+ function deriveLocaleFallbacks(locales, defaultLocale) {
182
+ const byLowercase = new Map(locales.map((l) => [l.toLowerCase(), l]));
183
+ const fallbacks = {};
184
+ for (const locale of locales) {
185
+ const subtags = locale.split("-");
186
+ if (subtags.length < 2) continue;
187
+ const chain = [];
188
+ for (let end = subtags.length - 1; end >= 1; end--) {
189
+ const prefix = byLowercase.get(subtags.slice(0, end).join("-").toLowerCase());
190
+ if (prefix && prefix !== locale && prefix !== defaultLocale) {
191
+ chain.push(prefix);
192
+ }
193
+ }
194
+ if (chain.length > 0) {
195
+ fallbacks[locale] = chain;
196
+ }
197
+ }
198
+ return fallbacks;
199
+ }
179
200
  var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
180
201
  var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
181
202
  function getFieldKind(schema) {
@@ -205,20 +226,16 @@ function getRelationTarget(schema) {
205
226
  }
206
227
  return null;
207
228
  }
208
- function unwrapSchema(schema) {
229
+ function peelOptionalWrappers(schema) {
209
230
  let current = schema;
210
231
  for (let i = 0; i < 8; i++) {
211
- const anySchema = current;
212
- if (typeof anySchema.unwrap === "function") {
213
- current = anySchema.unwrap();
232
+ const type = current._def?.type;
233
+ if (type === "optional" || type === "nullable") {
234
+ current = current.unwrap();
214
235
  continue;
215
236
  }
216
- if (typeof anySchema.removeDefault === "function") {
217
- current = anySchema.removeDefault();
218
- continue;
219
- }
220
- if (anySchema._def?.innerType) {
221
- current = anySchema._def.innerType;
237
+ if (type === "default") {
238
+ current = current.removeDefault();
222
239
  continue;
223
240
  }
224
241
  break;
@@ -227,6 +244,12 @@ function unwrapSchema(schema) {
227
244
  }
228
245
 
229
246
  // src/core/introspect-schema.ts
247
+ function getArrayElement(schema) {
248
+ if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
249
+ return schema.element;
250
+ }
251
+ return null;
252
+ }
230
253
  function introspectSchema(schema, prefix = []) {
231
254
  const relation = getRelationTarget(schema);
232
255
  if (relation && prefix.length > 0) {
@@ -240,7 +263,10 @@ function introspectSchema(schema, prefix = []) {
240
263
  }
241
264
  ];
242
265
  }
243
- const base = unwrapSchema(schema);
266
+ if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
267
+ return [{ path: prefix, kind: "translatable" }];
268
+ }
269
+ const base = peelOptionalWrappers(schema);
244
270
  if (base instanceof Object && "shape" in base) {
245
271
  const shape = base.shape;
246
272
  const fields = [];
@@ -249,9 +275,9 @@ function introspectSchema(schema, prefix = []) {
249
275
  }
250
276
  return fields;
251
277
  }
252
- if (base instanceof Object && "element" in base) {
253
- const element = base.element;
254
- const elementBase = unwrapSchema(element);
278
+ const element = getArrayElement(base);
279
+ if (element) {
280
+ const elementBase = peelOptionalWrappers(element);
255
281
  if (elementBase instanceof Object && "shape" in elementBase) {
256
282
  const shape = elementBase.shape;
257
283
  const fields = [];
@@ -263,15 +289,48 @@ function introspectSchema(schema, prefix = []) {
263
289
  }
264
290
  return [{ path: prefix, kind: getFieldKind(schema) }];
265
291
  }
266
- function extractByPaths(data, paths) {
267
- const out = {};
292
+ function buildPathTrie(paths) {
293
+ const root = { leaf: false, children: /* @__PURE__ */ new Map() };
268
294
  for (const path6 of paths) {
269
- const value = getAtPath(data, path6);
295
+ let node = root;
296
+ for (const segment of path6) {
297
+ let child = node.children.get(segment);
298
+ if (!child) {
299
+ child = { leaf: false, children: /* @__PURE__ */ new Map() };
300
+ node.children.set(segment, child);
301
+ }
302
+ node = child;
303
+ }
304
+ node.leaf = true;
305
+ }
306
+ return root;
307
+ }
308
+ function extractNode(data, trie) {
309
+ if (trie.leaf) return data;
310
+ const star = trie.children.get("*");
311
+ if (star) {
312
+ if (!Array.isArray(data)) return void 0;
313
+ return data.map(
314
+ (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
315
+ );
316
+ }
317
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
318
+ const record = data;
319
+ const out = {};
320
+ let any = false;
321
+ for (const [key, child] of trie.children) {
322
+ const value = extractNode(record[key], child);
270
323
  if (value !== void 0) {
271
- setAtPath(out, path6.filter((p) => p !== "*"), value);
324
+ out[key] = value;
325
+ any = true;
272
326
  }
273
327
  }
274
- return out;
328
+ return any ? out : void 0;
329
+ }
330
+ function extractByPaths(data, paths) {
331
+ if (paths.length === 0) return {};
332
+ const extracted = extractNode(data, buildPathTrie(paths));
333
+ return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
275
334
  }
276
335
  function pickTranslatable(data, schema) {
277
336
  const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
@@ -286,32 +345,6 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
286
345
  const translatable = pickTranslatable(localeData, schema);
287
346
  return deepMerge(structural, translatable);
288
347
  }
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
348
  function deepMerge(base, overlay) {
316
349
  const out = { ...base };
317
350
  for (const [key, value] of Object.entries(overlay)) {
@@ -462,7 +495,7 @@ function seoFieldsFromEn(enDoc) {
462
495
  canonicalPathOverride: enDoc.canonicalPathOverride
463
496
  };
464
497
  }
465
- var SCHEMA_VERSION = 4;
498
+ var SCHEMA_VERSION = 5;
466
499
  var MIGRATIONS = [
467
500
  `CREATE TABLE IF NOT EXISTS meta (
468
501
  key TEXT PRIMARY KEY,
@@ -510,7 +543,30 @@ var MIGRATIONS = [
510
543
  UNIQUE (content_type, en_slug, en_hash)
511
544
  )`,
512
545
  `CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
513
- ON en_snapshots(content_type, en_slug, created_at DESC)`
546
+ ON en_snapshots(content_type, en_slug, created_at DESC)`,
547
+ `CREATE TABLE IF NOT EXISTS translation_batch_jobs (
548
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
549
+ job_name TEXT NOT NULL UNIQUE,
550
+ model TEXT NOT NULL,
551
+ display_model TEXT NOT NULL,
552
+ created_at TEXT NOT NULL,
553
+ state TEXT NOT NULL,
554
+ completed_at TEXT
555
+ )`,
556
+ `CREATE TABLE IF NOT EXISTS translation_batch_items (
557
+ job_id INTEGER NOT NULL,
558
+ request_index INTEGER NOT NULL,
559
+ content_type TEXT NOT NULL,
560
+ en_slug TEXT NOT NULL,
561
+ locale TEXT NOT NULL,
562
+ en_hash TEXT NOT NULL,
563
+ snapshot_id INTEGER NOT NULL,
564
+ status TEXT NOT NULL,
565
+ error TEXT,
566
+ PRIMARY KEY (job_id, request_index)
567
+ )`,
568
+ `CREATE INDEX IF NOT EXISTS idx_batch_items_status
569
+ ON translation_batch_items(status)`
514
570
  ];
515
571
  function resolveStorePath(config) {
516
572
  return config.storePath;
@@ -533,6 +589,8 @@ function addColumnIfMissing(db, table, column, ddlType) {
533
589
  }
534
590
  }
535
591
  function readSchemaVersion(db) {
592
+ const metaTable = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'meta'`).get();
593
+ if (!metaTable) return 0;
536
594
  const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
537
595
  return row ? Number.parseInt(row.value, 10) || 0 : 0;
538
596
  }
@@ -593,7 +651,8 @@ function sanitizeMdxJsxAttributeQuotes(body) {
593
651
  if (i >= body.length || body[i] === ">" || body[i] === "/") break;
594
652
  const attrStart = i;
595
653
  while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
596
- body.slice(attrStart, i);
654
+ const attrName = body.slice(attrStart, i);
655
+ if (!attrName) break;
597
656
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
598
657
  if (body[i] !== "=") {
599
658
  tagOut += body.slice(attrStart, i);
@@ -602,10 +661,12 @@ function sanitizeMdxJsxAttributeQuotes(body) {
602
661
  tagOut += body.slice(attrStart, i);
603
662
  tagOut += "=";
604
663
  i += 1;
664
+ const wsStart = i;
605
665
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
666
+ tagOut += body.slice(wsStart, i);
606
667
  const quote = body[i];
607
668
  if (quote !== '"' && quote !== "'") {
608
- continue;
669
+ break;
609
670
  }
610
671
  if (quote === "'") {
611
672
  const valStart2 = i + 1;
@@ -888,7 +949,7 @@ function createContentLoader(config, type) {
888
949
  }
889
950
 
890
951
  // src/i18n/resolve-document.ts
891
- function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
952
+ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting, fallbackLocales = []) {
892
953
  const urlBuilder = createUrlBuilder({
893
954
  locales: [defaultLocale, locale],
894
955
  defaultLocale,
@@ -902,15 +963,28 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
902
963
  for (const [docLocale, docIdx] of allDocs) {
903
964
  const found = docIdx.bySlug.get(slug);
904
965
  if (!found) continue;
905
- const correctSlug = getSlugForLocale(found, docLocale, locale, allDocs, defaultLocale);
906
- if (correctSlug && correctSlug !== slug) {
966
+ const enSlug = docLocale === defaultLocale ? found.slug : found.enSlug;
967
+ let target;
968
+ let targetLocale = locale;
969
+ for (const candidateLocale of [locale, ...fallbackLocales]) {
970
+ const cand = candidateLocale === defaultLocale ? allDocs.get(candidateLocale)?.bySlug.get(enSlug) : allDocs.get(candidateLocale)?.byEnSlug.get(enSlug);
971
+ if (cand) {
972
+ target = cand;
973
+ targetLocale = candidateLocale;
974
+ break;
975
+ }
976
+ }
977
+ if (target) {
978
+ if (target.slug === slug) {
979
+ return { document: target, actualLocale: targetLocale };
980
+ }
907
981
  if (!type.path) {
908
982
  return { document: null, actualLocale: locale };
909
983
  }
910
984
  return {
911
985
  document: null,
912
986
  actualLocale: locale,
913
- shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
987
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, target.slug, locale)
914
988
  };
915
989
  }
916
990
  if (docLocale === defaultLocale) {
@@ -941,13 +1015,6 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
941
1015
  }
942
1016
  return { document: null, actualLocale: locale };
943
1017
  }
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
1018
 
952
1019
  // src/create-project.ts
953
1020
  function comparatorFor(orderBy) {
@@ -1000,7 +1067,8 @@ function buildRuntime(config, type, getRuntime) {
1000
1067
  config.defaultLocale,
1001
1068
  load(),
1002
1069
  type,
1003
- config.localeRouting
1070
+ config.localeRouting,
1071
+ config.localeFallbacks[locale] ?? []
1004
1072
  );
1005
1073
  if (result.document && type.path) {
1006
1074
  return {
@@ -1022,8 +1090,14 @@ function buildRuntime(config, type, getRuntime) {
1022
1090
  const params = [];
1023
1091
  for (const locale of options.locales ?? config.locales) {
1024
1092
  const localeIdx = all.get(locale);
1093
+ const fallbacks = config.localeFallbacks[locale] ?? [];
1025
1094
  for (const doc of enIdx.bySlug.values()) {
1026
- const slug = locale === config.defaultLocale ? doc.slug : localeIdx?.byEnSlug.get(doc.slug)?.slug ?? doc.slug;
1095
+ let slug;
1096
+ if (locale === config.defaultLocale) {
1097
+ slug = doc.slug;
1098
+ } else {
1099
+ 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;
1100
+ }
1027
1101
  params.push({ locale, slug });
1028
1102
  }
1029
1103
  }