scribe-cms 0.0.12 → 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 (55) hide show
  1. package/README.md +21 -6
  2. package/dist/cli/index.cjs +1220 -204
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +1221 -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 +982 -166
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +983 -168
  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 +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,39 @@ 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 = {};
131
+ for (const [locale, chain] of Object.entries(raw.localeFallbacks ?? {})) {
132
+ if (!raw.locales.includes(locale)) {
133
+ throw new Error(
134
+ `scribe config: localeFallbacks key "${locale}" is not in locales [${raw.locales.join(", ")}]`
135
+ );
136
+ }
137
+ const seen = /* @__PURE__ */ new Set();
138
+ for (const fallback of chain) {
139
+ if (!raw.locales.includes(fallback)) {
140
+ throw new Error(
141
+ `scribe config: localeFallbacks["${locale}"] entry "${fallback}" is not in locales [${raw.locales.join(", ")}]`
142
+ );
143
+ }
144
+ if (fallback === locale) {
145
+ throw new Error(
146
+ `scribe config: localeFallbacks["${locale}"] must not contain its own key "${locale}"`
147
+ );
148
+ }
149
+ if (fallback === defaultLocale) {
150
+ throw new Error(
151
+ `scribe config: localeFallbacks["${locale}"] must not contain the defaultLocale "${defaultLocale}" \u2014 it is always the final fallback; remove it`
152
+ );
153
+ }
154
+ if (seen.has(fallback)) {
155
+ throw new Error(
156
+ `scribe config: localeFallbacks["${locale}"] contains duplicate entry "${fallback}"`
157
+ );
158
+ }
159
+ seen.add(fallback);
160
+ }
161
+ localeFallbacks[locale] = [...chain];
162
+ }
130
163
  const projectRoot = path.resolve(process.cwd(), raw.rootDir);
131
164
  const contentRoot = path.resolve(projectRoot, raw.contentDir ?? "content");
132
165
  const storePath = path.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
@@ -156,6 +189,7 @@ function resolveConfig(input, baseDir) {
156
189
  defaultLocale,
157
190
  localeRouting,
158
191
  localePresets: raw.localePresets,
192
+ localeFallbacks,
159
193
  translate: raw.translate,
160
194
  types
161
195
  };
@@ -194,20 +228,16 @@ function getRelationTarget(schema) {
194
228
  }
195
229
  return null;
196
230
  }
197
- function unwrapSchema(schema) {
231
+ function peelOptionalWrappers(schema) {
198
232
  let current = schema;
199
233
  for (let i = 0; i < 8; i++) {
200
- const anySchema = current;
201
- if (typeof anySchema.unwrap === "function") {
202
- current = anySchema.unwrap();
203
- continue;
204
- }
205
- if (typeof anySchema.removeDefault === "function") {
206
- current = anySchema.removeDefault();
234
+ const type = current._def?.type;
235
+ if (type === "optional" || type === "nullable") {
236
+ current = current.unwrap();
207
237
  continue;
208
238
  }
209
- if (anySchema._def?.innerType) {
210
- current = anySchema._def.innerType;
239
+ if (type === "default") {
240
+ current = current.removeDefault();
211
241
  continue;
212
242
  }
213
243
  break;
@@ -216,6 +246,12 @@ function unwrapSchema(schema) {
216
246
  }
217
247
 
218
248
  // src/core/introspect-schema.ts
249
+ function getArrayElement(schema) {
250
+ if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
251
+ return schema.element;
252
+ }
253
+ return null;
254
+ }
219
255
  function introspectSchema(schema, prefix = []) {
220
256
  const relation = getRelationTarget(schema);
221
257
  if (relation && prefix.length > 0) {
@@ -229,7 +265,10 @@ function introspectSchema(schema, prefix = []) {
229
265
  }
230
266
  ];
231
267
  }
232
- const base = unwrapSchema(schema);
268
+ if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
269
+ return [{ path: prefix, kind: "translatable" }];
270
+ }
271
+ const base = peelOptionalWrappers(schema);
233
272
  if (base instanceof Object && "shape" in base) {
234
273
  const shape = base.shape;
235
274
  const fields = [];
@@ -238,9 +277,9 @@ function introspectSchema(schema, prefix = []) {
238
277
  }
239
278
  return fields;
240
279
  }
241
- if (base instanceof Object && "element" in base) {
242
- const element = base.element;
243
- const elementBase = unwrapSchema(element);
280
+ const element = getArrayElement(base);
281
+ if (element) {
282
+ const elementBase = peelOptionalWrappers(element);
244
283
  if (elementBase instanceof Object && "shape" in elementBase) {
245
284
  const shape = elementBase.shape;
246
285
  const fields = [];
@@ -252,15 +291,48 @@ function introspectSchema(schema, prefix = []) {
252
291
  }
253
292
  return [{ path: prefix, kind: getFieldKind(schema) }];
254
293
  }
255
- function extractByPaths(data, paths) {
256
- const out = {};
294
+ function buildPathTrie(paths) {
295
+ const root = { leaf: false, children: /* @__PURE__ */ new Map() };
257
296
  for (const path6 of paths) {
258
- const value = getAtPath(data, path6);
297
+ let node = root;
298
+ for (const segment of path6) {
299
+ let child = node.children.get(segment);
300
+ if (!child) {
301
+ child = { leaf: false, children: /* @__PURE__ */ new Map() };
302
+ node.children.set(segment, child);
303
+ }
304
+ node = child;
305
+ }
306
+ node.leaf = true;
307
+ }
308
+ return root;
309
+ }
310
+ function extractNode(data, trie) {
311
+ if (trie.leaf) return data;
312
+ const star = trie.children.get("*");
313
+ if (star) {
314
+ if (!Array.isArray(data)) return void 0;
315
+ return data.map(
316
+ (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
317
+ );
318
+ }
319
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
320
+ const record = data;
321
+ const out = {};
322
+ let any = false;
323
+ for (const [key, child] of trie.children) {
324
+ const value = extractNode(record[key], child);
259
325
  if (value !== void 0) {
260
- setAtPath(out, path6.filter((p) => p !== "*"), value);
326
+ out[key] = value;
327
+ any = true;
261
328
  }
262
329
  }
263
- return out;
330
+ return any ? out : void 0;
331
+ }
332
+ function extractByPaths(data, paths) {
333
+ if (paths.length === 0) return {};
334
+ const extracted = extractNode(data, buildPathTrie(paths));
335
+ return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
264
336
  }
265
337
  function pickTranslatable(data, schema) {
266
338
  const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
@@ -275,32 +347,6 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
275
347
  const translatable = pickTranslatable(localeData, schema);
276
348
  return deepMerge(structural, translatable);
277
349
  }
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
350
  function deepMerge(base, overlay) {
305
351
  const out = { ...base };
306
352
  for (const [key, value] of Object.entries(overlay)) {
@@ -451,7 +497,7 @@ function seoFieldsFromEn(enDoc) {
451
497
  canonicalPathOverride: enDoc.canonicalPathOverride
452
498
  };
453
499
  }
454
- var SCHEMA_VERSION = 4;
500
+ var SCHEMA_VERSION = 5;
455
501
  var MIGRATIONS = [
456
502
  `CREATE TABLE IF NOT EXISTS meta (
457
503
  key TEXT PRIMARY KEY,
@@ -499,7 +545,30 @@ var MIGRATIONS = [
499
545
  UNIQUE (content_type, en_slug, en_hash)
500
546
  )`,
501
547
  `CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
502
- ON en_snapshots(content_type, en_slug, created_at DESC)`
548
+ ON en_snapshots(content_type, en_slug, created_at DESC)`,
549
+ `CREATE TABLE IF NOT EXISTS translation_batch_jobs (
550
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
551
+ job_name TEXT NOT NULL UNIQUE,
552
+ model TEXT NOT NULL,
553
+ display_model TEXT NOT NULL,
554
+ created_at TEXT NOT NULL,
555
+ state TEXT NOT NULL,
556
+ completed_at TEXT
557
+ )`,
558
+ `CREATE TABLE IF NOT EXISTS translation_batch_items (
559
+ job_id INTEGER NOT NULL,
560
+ request_index INTEGER NOT NULL,
561
+ content_type TEXT NOT NULL,
562
+ en_slug TEXT NOT NULL,
563
+ locale TEXT NOT NULL,
564
+ en_hash TEXT NOT NULL,
565
+ snapshot_id INTEGER NOT NULL,
566
+ status TEXT NOT NULL,
567
+ error TEXT,
568
+ PRIMARY KEY (job_id, request_index)
569
+ )`,
570
+ `CREATE INDEX IF NOT EXISTS idx_batch_items_status
571
+ ON translation_batch_items(status)`
503
572
  ];
504
573
  function resolveStorePath(config) {
505
574
  return config.storePath;
@@ -522,6 +591,8 @@ function addColumnIfMissing(db, table, column, ddlType) {
522
591
  }
523
592
  }
524
593
  function readSchemaVersion(db) {
594
+ const metaTable = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'meta'`).get();
595
+ if (!metaTable) return 0;
525
596
  const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
526
597
  return row ? Number.parseInt(row.value, 10) || 0 : 0;
527
598
  }
@@ -582,7 +653,8 @@ function sanitizeMdxJsxAttributeQuotes(body) {
582
653
  if (i >= body.length || body[i] === ">" || body[i] === "/") break;
583
654
  const attrStart = i;
584
655
  while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
585
- body.slice(attrStart, i);
656
+ const attrName = body.slice(attrStart, i);
657
+ if (!attrName) break;
586
658
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
587
659
  if (body[i] !== "=") {
588
660
  tagOut += body.slice(attrStart, i);
@@ -591,10 +663,12 @@ function sanitizeMdxJsxAttributeQuotes(body) {
591
663
  tagOut += body.slice(attrStart, i);
592
664
  tagOut += "=";
593
665
  i += 1;
666
+ const wsStart = i;
594
667
  while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
668
+ tagOut += body.slice(wsStart, i);
595
669
  const quote = body[i];
596
670
  if (quote !== '"' && quote !== "'") {
597
- continue;
671
+ break;
598
672
  }
599
673
  if (quote === "'") {
600
674
  const valStart2 = i + 1;
@@ -877,7 +951,7 @@ function createContentLoader(config, type) {
877
951
  }
878
952
 
879
953
  // src/i18n/resolve-document.ts
880
- function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting) {
954
+ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, localeRouting, fallbackLocales = []) {
881
955
  const urlBuilder = createUrlBuilder({
882
956
  locales: [defaultLocale, locale],
883
957
  defaultLocale,
@@ -891,15 +965,28 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
891
965
  for (const [docLocale, docIdx] of allDocs) {
892
966
  const found = docIdx.bySlug.get(slug);
893
967
  if (!found) continue;
894
- const correctSlug = getSlugForLocale(found, docLocale, locale, allDocs, defaultLocale);
895
- if (correctSlug && correctSlug !== slug) {
968
+ const enSlug = docLocale === defaultLocale ? found.slug : found.enSlug;
969
+ let target;
970
+ let targetLocale = locale;
971
+ for (const candidateLocale of [locale, ...fallbackLocales]) {
972
+ const cand = candidateLocale === defaultLocale ? allDocs.get(candidateLocale)?.bySlug.get(enSlug) : allDocs.get(candidateLocale)?.byEnSlug.get(enSlug);
973
+ if (cand) {
974
+ target = cand;
975
+ targetLocale = candidateLocale;
976
+ break;
977
+ }
978
+ }
979
+ if (target) {
980
+ if (target.slug === slug) {
981
+ return { document: target, actualLocale: targetLocale };
982
+ }
896
983
  if (!type.path) {
897
984
  return { document: null, actualLocale: locale };
898
985
  }
899
986
  return {
900
987
  document: null,
901
988
  actualLocale: locale,
902
- shouldRedirectTo: urlBuilder.resolvePath(type.path, correctSlug, locale)
989
+ shouldRedirectTo: urlBuilder.resolvePath(type.path, target.slug, locale)
903
990
  };
904
991
  }
905
992
  if (docLocale === defaultLocale) {
@@ -930,13 +1017,6 @@ function resolveLocalizedDocument(slug, locale, defaultLocale, allDocs, type, lo
930
1017
  }
931
1018
  return { document: null, actualLocale: locale };
932
1019
  }
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
1020
 
941
1021
  // src/create-project.ts
942
1022
  function comparatorFor(orderBy) {
@@ -989,7 +1069,8 @@ function buildRuntime(config, type, getRuntime) {
989
1069
  config.defaultLocale,
990
1070
  load(),
991
1071
  type,
992
- config.localeRouting
1072
+ config.localeRouting,
1073
+ config.localeFallbacks[locale] ?? []
993
1074
  );
994
1075
  if (result.document && type.path) {
995
1076
  return {
@@ -1011,8 +1092,14 @@ function buildRuntime(config, type, getRuntime) {
1011
1092
  const params = [];
1012
1093
  for (const locale of options.locales ?? config.locales) {
1013
1094
  const localeIdx = all.get(locale);
1095
+ const fallbacks = config.localeFallbacks[locale] ?? [];
1014
1096
  for (const doc of enIdx.bySlug.values()) {
1015
- const slug = locale === config.defaultLocale ? doc.slug : localeIdx?.byEnSlug.get(doc.slug)?.slug ?? doc.slug;
1097
+ let slug;
1098
+ if (locale === config.defaultLocale) {
1099
+ slug = doc.slug;
1100
+ } else {
1101
+ 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;
1102
+ }
1016
1103
  params.push({ locale, slug });
1017
1104
  }
1018
1105
  }