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.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
|
|
231
|
+
function peelOptionalWrappers(schema) {
|
|
198
232
|
let current = schema;
|
|
199
233
|
for (let i = 0; i < 8; i++) {
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
current =
|
|
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 (
|
|
210
|
-
current =
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
const elementBase =
|
|
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
|
|
256
|
-
const
|
|
294
|
+
function buildPathTrie(paths) {
|
|
295
|
+
const root = { leaf: false, children: /* @__PURE__ */ new Map() };
|
|
257
296
|
for (const path6 of paths) {
|
|
258
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
895
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|