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.js CHANGED
@@ -127,6 +127,7 @@ function resolveConfig(input, baseDir) {
127
127
  if (localeRouting.strategy === "search-param" && !localeRouting.param) {
128
128
  throw new Error('scribe config: localeRouting search-param requires a "param" name');
129
129
  }
130
+ const localeFallbacks = raw.localeFallbacks === false ? {} : deriveLocaleFallbacks(raw.locales, defaultLocale);
130
131
  const projectRoot = path.resolve(process.cwd(), raw.rootDir);
131
132
  const contentRoot = path.resolve(projectRoot, raw.contentDir ?? "content");
132
133
  const storePath = path.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
@@ -156,6 +157,7 @@ function resolveConfig(input, baseDir) {
156
157
  defaultLocale,
157
158
  localeRouting,
158
159
  localePresets: raw.localePresets,
160
+ localeFallbacks,
159
161
  translate: raw.translate,
160
162
  types
161
163
  };
@@ -165,6 +167,25 @@ function resolveConfig(input, baseDir) {
165
167
  });
166
168
  return config;
167
169
  }
170
+ function deriveLocaleFallbacks(locales, defaultLocale) {
171
+ const byLowercase = new Map(locales.map((l) => [l.toLowerCase(), l]));
172
+ const fallbacks = {};
173
+ for (const locale of locales) {
174
+ const subtags = locale.split("-");
175
+ if (subtags.length < 2) continue;
176
+ const chain = [];
177
+ for (let end = subtags.length - 1; end >= 1; end--) {
178
+ const prefix = byLowercase.get(subtags.slice(0, end).join("-").toLowerCase());
179
+ if (prefix && prefix !== locale && prefix !== defaultLocale) {
180
+ chain.push(prefix);
181
+ }
182
+ }
183
+ if (chain.length > 0) {
184
+ fallbacks[locale] = chain;
185
+ }
186
+ }
187
+ return fallbacks;
188
+ }
168
189
  var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
169
190
  var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
170
191
  function getFieldKind(schema) {
@@ -194,20 +215,16 @@ function getRelationTarget(schema) {
194
215
  }
195
216
  return null;
196
217
  }
197
- function unwrapSchema(schema) {
218
+ function peelOptionalWrappers(schema) {
198
219
  let current = schema;
199
220
  for (let i = 0; i < 8; i++) {
200
- const anySchema = current;
201
- if (typeof anySchema.unwrap === "function") {
202
- current = anySchema.unwrap();
221
+ const type = current._def?.type;
222
+ if (type === "optional" || type === "nullable") {
223
+ current = current.unwrap();
203
224
  continue;
204
225
  }
205
- if (typeof anySchema.removeDefault === "function") {
206
- current = anySchema.removeDefault();
207
- continue;
208
- }
209
- if (anySchema._def?.innerType) {
210
- current = anySchema._def.innerType;
226
+ if (type === "default") {
227
+ current = current.removeDefault();
211
228
  continue;
212
229
  }
213
230
  break;
@@ -216,6 +233,12 @@ function unwrapSchema(schema) {
216
233
  }
217
234
 
218
235
  // src/core/introspect-schema.ts
236
+ function getArrayElement(schema) {
237
+ if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
238
+ return schema.element;
239
+ }
240
+ return null;
241
+ }
219
242
  function introspectSchema(schema, prefix = []) {
220
243
  const relation = getRelationTarget(schema);
221
244
  if (relation && prefix.length > 0) {
@@ -229,7 +252,10 @@ function introspectSchema(schema, prefix = []) {
229
252
  }
230
253
  ];
231
254
  }
232
- const base = unwrapSchema(schema);
255
+ if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
256
+ return [{ path: prefix, kind: "translatable" }];
257
+ }
258
+ const base = peelOptionalWrappers(schema);
233
259
  if (base instanceof Object && "shape" in base) {
234
260
  const shape = base.shape;
235
261
  const fields = [];
@@ -238,9 +264,9 @@ function introspectSchema(schema, prefix = []) {
238
264
  }
239
265
  return fields;
240
266
  }
241
- if (base instanceof Object && "element" in base) {
242
- const element = base.element;
243
- const elementBase = unwrapSchema(element);
267
+ const element = getArrayElement(base);
268
+ if (element) {
269
+ const elementBase = peelOptionalWrappers(element);
244
270
  if (elementBase instanceof Object && "shape" in elementBase) {
245
271
  const shape = elementBase.shape;
246
272
  const fields = [];
@@ -252,15 +278,48 @@ function introspectSchema(schema, prefix = []) {
252
278
  }
253
279
  return [{ path: prefix, kind: getFieldKind(schema) }];
254
280
  }
255
- function extractByPaths(data, paths) {
256
- const out = {};
281
+ function buildPathTrie(paths) {
282
+ const root = { leaf: false, children: /* @__PURE__ */ new Map() };
257
283
  for (const path6 of paths) {
258
- const value = getAtPath(data, path6);
284
+ let node = root;
285
+ for (const segment of path6) {
286
+ let child = node.children.get(segment);
287
+ if (!child) {
288
+ child = { leaf: false, children: /* @__PURE__ */ new Map() };
289
+ node.children.set(segment, child);
290
+ }
291
+ node = child;
292
+ }
293
+ node.leaf = true;
294
+ }
295
+ return root;
296
+ }
297
+ function extractNode(data, trie) {
298
+ if (trie.leaf) return data;
299
+ const star = trie.children.get("*");
300
+ if (star) {
301
+ if (!Array.isArray(data)) return void 0;
302
+ return data.map(
303
+ (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
304
+ );
305
+ }
306
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
307
+ const record = data;
308
+ const out = {};
309
+ let any = false;
310
+ for (const [key, child] of trie.children) {
311
+ const value = extractNode(record[key], child);
259
312
  if (value !== void 0) {
260
- setAtPath(out, path6.filter((p) => p !== "*"), value);
313
+ out[key] = value;
314
+ any = true;
261
315
  }
262
316
  }
263
- return out;
317
+ return any ? out : void 0;
318
+ }
319
+ function extractByPaths(data, paths) {
320
+ if (paths.length === 0) return {};
321
+ const extracted = extractNode(data, buildPathTrie(paths));
322
+ return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
264
323
  }
265
324
  function pickTranslatable(data, schema) {
266
325
  const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
@@ -275,32 +334,6 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
275
334
  const translatable = pickTranslatable(localeData, schema);
276
335
  return deepMerge(structural, translatable);
277
336
  }
278
- function getAtPath(obj, path6) {
279
- let current = obj;
280
- for (const segment of path6) {
281
- if (segment === "*") {
282
- if (!Array.isArray(current)) return void 0;
283
- return current.map(
284
- (item) => typeof item === "object" && item !== null ? getAtPath(item, path6.slice(path6.indexOf("*") + 1)) : void 0
285
- );
286
- }
287
- if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
288
- current = current[segment];
289
- }
290
- return current;
291
- }
292
- function setAtPath(obj, path6, value) {
293
- if (path6.length === 0) return;
294
- if (path6.length === 1) {
295
- obj[path6[0]] = value;
296
- return;
297
- }
298
- const [head, ...rest] = path6;
299
- if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
300
- obj[head] = {};
301
- }
302
- setAtPath(obj[head], rest, value);
303
- }
304
337
  function deepMerge(base, overlay) {
305
338
  const out = { ...base };
306
339
  for (const [key, value] of Object.entries(overlay)) {
@@ -451,7 +484,7 @@ function seoFieldsFromEn(enDoc) {
451
484
  canonicalPathOverride: enDoc.canonicalPathOverride
452
485
  };
453
486
  }
454
- var SCHEMA_VERSION = 4;
487
+ var SCHEMA_VERSION = 5;
455
488
  var MIGRATIONS = [
456
489
  `CREATE TABLE IF NOT EXISTS meta (
457
490
  key TEXT PRIMARY KEY,
@@ -499,7 +532,30 @@ var MIGRATIONS = [
499
532
  UNIQUE (content_type, en_slug, en_hash)
500
533
  )`,
501
534
  `CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
502
- ON en_snapshots(content_type, en_slug, created_at DESC)`
535
+ ON en_snapshots(content_type, en_slug, created_at DESC)`,
536
+ `CREATE TABLE IF NOT EXISTS translation_batch_jobs (
537
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
538
+ job_name TEXT NOT NULL UNIQUE,
539
+ model TEXT NOT NULL,
540
+ display_model TEXT NOT NULL,
541
+ created_at TEXT NOT NULL,
542
+ state TEXT NOT NULL,
543
+ completed_at TEXT
544
+ )`,
545
+ `CREATE TABLE IF NOT EXISTS translation_batch_items (
546
+ job_id INTEGER NOT NULL,
547
+ request_index INTEGER NOT NULL,
548
+ content_type TEXT NOT NULL,
549
+ en_slug TEXT NOT NULL,
550
+ locale TEXT NOT NULL,
551
+ en_hash TEXT NOT NULL,
552
+ snapshot_id INTEGER NOT NULL,
553
+ status TEXT NOT NULL,
554
+ error TEXT,
555
+ PRIMARY KEY (job_id, request_index)
556
+ )`,
557
+ `CREATE INDEX IF NOT EXISTS idx_batch_items_status
558
+ ON translation_batch_items(status)`
503
559
  ];
504
560
  function resolveStorePath(config) {
505
561
  return config.storePath;
@@ -522,6 +578,8 @@ function addColumnIfMissing(db, table, column, ddlType) {
522
578
  }
523
579
  }
524
580
  function readSchemaVersion(db) {
581
+ const metaTable = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'meta'`).get();
582
+ if (!metaTable) return 0;
525
583
  const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
526
584
  return row ? Number.parseInt(row.value, 10) || 0 : 0;
527
585
  }
@@ -582,7 +640,8 @@ function sanitizeMdxJsxAttributeQuotes(body) {
582
640
  if (i >= body.length || body[i] === ">" || body[i] === "/") break;
583
641
  const attrStart = i;
584
642
  while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
585
- body.slice(attrStart, i);
643
+ const attrName = body.slice(attrStart, i);
644
+ if (!attrName) break;
586
645
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
587
646
  if (body[i] !== "=") {
588
647
  tagOut += body.slice(attrStart, i);
@@ -591,10 +650,12 @@ function sanitizeMdxJsxAttributeQuotes(body) {
591
650
  tagOut += body.slice(attrStart, i);
592
651
  tagOut += "=";
593
652
  i += 1;
653
+ const wsStart = i;
594
654
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
655
+ tagOut += body.slice(wsStart, i);
595
656
  const quote = body[i];
596
657
  if (quote !== '"' && quote !== "'") {
597
- continue;
658
+ break;
598
659
  }
599
660
  if (quote === "'") {
600
661
  const valStart2 = i + 1;
@@ -877,7 +938,7 @@ function createContentLoader(config, type) {
877
938
  }
878
939
 
879
940
  // src/i18n/resolve-document.ts
880
- function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
941
+ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting, fallbackLocales = []) {
881
942
  const urlBuilder = createUrlBuilder({
882
943
  locales: [defaultLocale, locale],
883
944
  defaultLocale,
@@ -891,15 +952,28 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
891
952
  for (const [docLocale, docIdx] of allDocs) {
892
953
  const found = docIdx.bySlug.get(slug);
893
954
  if (!found) continue;
894
- const correctSlug = getSlugForLocale(found, docLocale, locale, allDocs, defaultLocale);
895
- if (correctSlug && correctSlug !== slug) {
955
+ const enSlug = docLocale === defaultLocale ? found.slug : found.enSlug;
956
+ let target;
957
+ let targetLocale = locale;
958
+ for (const candidateLocale of [locale, ...fallbackLocales]) {
959
+ const cand = candidateLocale === defaultLocale ? allDocs.get(candidateLocale)?.bySlug.get(enSlug) : allDocs.get(candidateLocale)?.byEnSlug.get(enSlug);
960
+ if (cand) {
961
+ target = cand;
962
+ targetLocale = candidateLocale;
963
+ break;
964
+ }
965
+ }
966
+ if (target) {
967
+ if (target.slug === slug) {
968
+ return { document: target, actualLocale: targetLocale };
969
+ }
896
970
  if (!type.path) {
897
971
  return { document: null, actualLocale: locale };
898
972
  }
899
973
  return {
900
974
  document: null,
901
975
  actualLocale: locale,
902
- shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
976
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, target.slug, locale)
903
977
  };
904
978
  }
905
979
  if (docLocale === defaultLocale) {
@@ -930,13 +1004,6 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
930
1004
  }
931
1005
  return { document: null, actualLocale: locale };
932
1006
  }
933
- function getSlugForLocale(document, sourceLocale, targetLocale, allDocs, defaultLocale) {
934
- if (sourceLocale === targetLocale) return document.slug;
935
- const englishSlug = sourceLocale === defaultLocale ? document.slug : document.enSlug;
936
- if (!englishSlug) return null;
937
- if (targetLocale === defaultLocale) return englishSlug;
938
- return allDocs.get(targetLocale)?.byEnSlug.get(englishSlug)?.slug ?? null;
939
- }
940
1007
 
941
1008
  // src/create-project.ts
942
1009
  function comparatorFor(orderBy) {
@@ -989,7 +1056,8 @@ function buildRuntime(config, type, getRuntime) {
989
1056
  config.defaultLocale,
990
1057
  load(),
991
1058
  type,
992
- config.localeRouting
1059
+ config.localeRouting,
1060
+ config.localeFallbacks[locale] ?? []
993
1061
  );
994
1062
  if (result.document && type.path) {
995
1063
  return {
@@ -1011,8 +1079,14 @@ function buildRuntime(config, type, getRuntime) {
1011
1079
  const params = [];
1012
1080
  for (const locale of options.locales ?? config.locales) {
1013
1081
  const localeIdx = all.get(locale);
1082
+ const fallbacks = config.localeFallbacks[locale] ?? [];
1014
1083
  for (const doc of enIdx.bySlug.values()) {
1015
- const slug = locale === config.defaultLocale ? doc.slug : localeIdx?.byEnSlug.get(doc.slug)?.slug ?? doc.slug;
1084
+ let slug;
1085
+ if (locale === config.defaultLocale) {
1086
+ slug = doc.slug;
1087
+ } else {
1088
+ 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;
1089
+ }
1016
1090
  params.push({ locale, slug });
1017
1091
  }
1018
1092
  }