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.
- package/README.md +21 -6
- package/dist/cli/index.cjs +1258 -219
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1259 -220
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt-translate.d.ts +3 -0
- package/dist/cli/prompt-translate.d.ts.map +1 -1
- package/dist/cli/translate-progress.d.ts.map +1 -1
- package/dist/index.cjs +1020 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1021 -183
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +149 -62
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +149 -62
- package/dist/runtime.js.map +1 -1
- package/dist/src/config/resolve-config.d.ts.map +1 -1
- package/dist/src/core/introspect-schema.d.ts +7 -1
- package/dist/src/core/introspect-schema.d.ts.map +1 -1
- package/dist/src/core/types.d.ts +6 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/create-project.d.ts.map +1 -1
- package/dist/src/i18n/resolve-document.d.ts +1 -1
- package/dist/src/i18n/resolve-document.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/storage/batch-jobs.d.ts +53 -0
- package/dist/src/storage/batch-jobs.d.ts.map +1 -0
- package/dist/src/storage/sqlite.d.ts.map +1 -1
- package/dist/src/translate/batch-worklist.d.ts +87 -0
- package/dist/src/translate/batch-worklist.d.ts.map +1 -0
- package/dist/src/translate/gemini-batch.d.ts +26 -0
- package/dist/src/translate/gemini-batch.d.ts.map +1 -0
- package/dist/src/translate/gemini-client.d.ts +23 -0
- package/dist/src/translate/gemini-client.d.ts.map +1 -1
- package/dist/src/translate/gemini-models.d.ts +9 -0
- package/dist/src/translate/gemini-models.d.ts.map +1 -1
- package/dist/src/translate/gemini-pricing.d.ts +2 -1
- package/dist/src/translate/gemini-pricing.d.ts.map +1 -1
- package/dist/src/translate/page-translator.d.ts +25 -52
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/translate/prompts/translation-prompt.d.ts +6 -0
- package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
- package/dist/src/translate/response-schema.d.ts +8 -1
- package/dist/src/translate/response-schema.d.ts.map +1 -1
- package/dist/src/translate/retry.d.ts +16 -0
- package/dist/src/translate/retry.d.ts.map +1 -0
- package/dist/src/translate/sanitize-mdx-jsx.d.ts.map +1 -1
- package/dist/src/translate/translate-core.d.ts +155 -0
- package/dist/src/translate/translate-core.d.ts.map +1 -0
- package/dist/src/validate/validate-project.d.ts.map +1 -1
- package/dist/studio/server.cjs +84 -47
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.js +84 -47
- package/dist/studio/server.js.map +1 -1
- 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
|
|
242
|
+
function peelOptionalWrappers(schema) {
|
|
209
243
|
let current = schema;
|
|
210
244
|
for (let i = 0; i < 8; i++) {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
current =
|
|
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 (
|
|
221
|
-
current =
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
const elementBase =
|
|
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
|
|
267
|
-
const
|
|
305
|
+
function buildPathTrie(paths) {
|
|
306
|
+
const root = { leaf: false, children: /* @__PURE__ */ new Map() };
|
|
268
307
|
for (const path6 of paths) {
|
|
269
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
906
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|