scribe-cms 0.0.13 → 0.0.15
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 +1 -1
- package/dist/cli/index.cjs +170 -41
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +170 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/translate-progress.d.ts.map +1 -1
- package/dist/index.cjs +142 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +142 -37
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +20 -33
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +20 -33
- package/dist/runtime.js.map +1 -1
- package/dist/src/config/resolve-config.d.ts.map +1 -1
- package/dist/src/translate/page-translator.d.ts +28 -0
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/translate/prompts/translation-prompt.d.ts +2 -6
- package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
- package/dist/src/translate/translate-core.d.ts +9 -0
- package/dist/src/translate/translate-core.d.ts.map +1 -1
- package/dist/src/translate/worklist.d.ts +6 -0
- package/dist/src/translate/worklist.d.ts.map +1 -1
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ export default defineConfig({
|
|
|
40
40
|
// store: ".scribe/store.sqlite" (default)
|
|
41
41
|
locales: ["en", "fr"],
|
|
42
42
|
// defaultLocale: "en" (default)
|
|
43
|
-
// localeFallbacks:
|
|
43
|
+
// localeFallbacks: true (default: regional variants fall back to their base language, e.g. pt-BR to pt; set false to disable)
|
|
44
44
|
types: [
|
|
45
45
|
defineContentType({
|
|
46
46
|
id: "blog",
|
package/dist/cli/index.cjs
CHANGED
|
@@ -360,39 +360,7 @@ function resolveConfig(input, baseDir) {
|
|
|
360
360
|
if (localeRouting.strategy === "search-param" && !localeRouting.param) {
|
|
361
361
|
throw new Error('scribe config: localeRouting search-param requires a "param" name');
|
|
362
362
|
}
|
|
363
|
-
const localeFallbacks = {};
|
|
364
|
-
for (const [locale, chain] of Object.entries(raw.localeFallbacks ?? {})) {
|
|
365
|
-
if (!raw.locales.includes(locale)) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
`scribe config: localeFallbacks key "${locale}" is not in locales [${raw.locales.join(", ")}]`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
const seen = /* @__PURE__ */ new Set();
|
|
371
|
-
for (const fallback of chain) {
|
|
372
|
-
if (!raw.locales.includes(fallback)) {
|
|
373
|
-
throw new Error(
|
|
374
|
-
`scribe config: localeFallbacks["${locale}"] entry "${fallback}" is not in locales [${raw.locales.join(", ")}]`
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
if (fallback === locale) {
|
|
378
|
-
throw new Error(
|
|
379
|
-
`scribe config: localeFallbacks["${locale}"] must not contain its own key "${locale}"`
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
if (fallback === defaultLocale) {
|
|
383
|
-
throw new Error(
|
|
384
|
-
`scribe config: localeFallbacks["${locale}"] must not contain the defaultLocale "${defaultLocale}" \u2014 it is always the final fallback; remove it`
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
if (seen.has(fallback)) {
|
|
388
|
-
throw new Error(
|
|
389
|
-
`scribe config: localeFallbacks["${locale}"] contains duplicate entry "${fallback}"`
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
seen.add(fallback);
|
|
393
|
-
}
|
|
394
|
-
localeFallbacks[locale] = [...chain];
|
|
395
|
-
}
|
|
363
|
+
const localeFallbacks = raw.localeFallbacks === false ? {} : deriveLocaleFallbacks(raw.locales, defaultLocale);
|
|
396
364
|
const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
397
365
|
const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
|
|
398
366
|
const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
@@ -432,6 +400,25 @@ function resolveConfig(input, baseDir) {
|
|
|
432
400
|
});
|
|
433
401
|
return config;
|
|
434
402
|
}
|
|
403
|
+
function deriveLocaleFallbacks(locales, defaultLocale) {
|
|
404
|
+
const byLowercase = new Map(locales.map((l) => [l.toLowerCase(), l]));
|
|
405
|
+
const fallbacks = {};
|
|
406
|
+
for (const locale of locales) {
|
|
407
|
+
const subtags = locale.split("-");
|
|
408
|
+
if (subtags.length < 2) continue;
|
|
409
|
+
const chain = [];
|
|
410
|
+
for (let end = subtags.length - 1; end >= 1; end--) {
|
|
411
|
+
const prefix = byLowercase.get(subtags.slice(0, end).join("-").toLowerCase());
|
|
412
|
+
if (prefix && prefix !== locale && prefix !== defaultLocale) {
|
|
413
|
+
chain.push(prefix);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (chain.length > 0) {
|
|
417
|
+
fallbacks[locale] = chain;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return fallbacks;
|
|
421
|
+
}
|
|
435
422
|
|
|
436
423
|
// src/create-project.ts
|
|
437
424
|
init_cjs_shims();
|
|
@@ -2567,6 +2554,13 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
|
|
|
2567
2554
|
}
|
|
2568
2555
|
return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
|
|
2569
2556
|
}
|
|
2557
|
+
function buildRetryContextSection(previousError) {
|
|
2558
|
+
return [
|
|
2559
|
+
"",
|
|
2560
|
+
"## Previous attempt",
|
|
2561
|
+
`A previous attempt at this translation was rejected with the following validation errors: ${previousError}. Produce a corrected translation that fixes these issues while re-checking every schema constraint (required fields, array minimums, maximum lengths).`
|
|
2562
|
+
];
|
|
2563
|
+
}
|
|
2570
2564
|
function buildPageTranslationPrompt(input) {
|
|
2571
2565
|
const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
|
|
2572
2566
|
const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
|
|
@@ -2597,7 +2591,10 @@ function buildPageTranslationPrompt(input) {
|
|
|
2597
2591
|
buildOutputFormatLine(
|
|
2598
2592
|
Object.keys(input.translatableFrontmatter).length > 0,
|
|
2599
2593
|
input.slugStrategy
|
|
2600
|
-
)
|
|
2594
|
+
),
|
|
2595
|
+
// Retry context is locale-specific suffix material: it MUST stay after the
|
|
2596
|
+
// EN body so the cacheable prefix is byte-identical with and without it.
|
|
2597
|
+
...input.previousError ? buildRetryContextSection(input.previousError) : []
|
|
2601
2598
|
];
|
|
2602
2599
|
return [...prefix, ...suffix].join("\n");
|
|
2603
2600
|
}
|
|
@@ -2817,7 +2814,8 @@ function prepareTranslation(config, item, options, startedAt) {
|
|
|
2817
2814
|
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2818
2815
|
translatableFrontmatter: payload.frontmatter,
|
|
2819
2816
|
enBody: payload.body,
|
|
2820
|
-
slugStrategy: type.slugStrategy
|
|
2817
|
+
slugStrategy: type.slugStrategy,
|
|
2818
|
+
previousError: item.previousError
|
|
2821
2819
|
});
|
|
2822
2820
|
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2823
2821
|
return {
|
|
@@ -3229,6 +3227,82 @@ async function runPool(items, concurrency, worker) {
|
|
|
3229
3227
|
function labelForItem(item) {
|
|
3230
3228
|
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
3231
3229
|
}
|
|
3230
|
+
function buildRetryWorklist(failed, originalByKey) {
|
|
3231
|
+
return failed.map((result) => {
|
|
3232
|
+
const key = translationItemKey(result);
|
|
3233
|
+
const original = originalByKey.get(key);
|
|
3234
|
+
return {
|
|
3235
|
+
contentType: result.contentType,
|
|
3236
|
+
enSlug: result.enSlug,
|
|
3237
|
+
locale: result.locale,
|
|
3238
|
+
reason: original?.reason ?? "missing",
|
|
3239
|
+
currentEnHash: original?.currentEnHash ?? "",
|
|
3240
|
+
storedEnHash: original?.storedEnHash,
|
|
3241
|
+
previousError: result.error ?? "Translation failed"
|
|
3242
|
+
};
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
async function runRetryRound(config, retryItems, options) {
|
|
3246
|
+
const startedAt = Date.now();
|
|
3247
|
+
const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
|
|
3248
|
+
if (options.mode === "direct") {
|
|
3249
|
+
const active = /* @__PURE__ */ new Set();
|
|
3250
|
+
await runPool(retryItems, options.concurrency, async (item) => {
|
|
3251
|
+
const label = labelForItem(item);
|
|
3252
|
+
active.add(label);
|
|
3253
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
3254
|
+
const result = await translateOne(item);
|
|
3255
|
+
active.delete(label);
|
|
3256
|
+
options.onResult(result);
|
|
3257
|
+
});
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
const entries = [];
|
|
3261
|
+
for (const item of retryItems) {
|
|
3262
|
+
const itemStartedAt = Date.now();
|
|
3263
|
+
try {
|
|
3264
|
+
const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
|
|
3265
|
+
if (outcome.status === "done") {
|
|
3266
|
+
options.onResult(outcome.result);
|
|
3267
|
+
} else {
|
|
3268
|
+
entries.push({
|
|
3269
|
+
apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
|
|
3270
|
+
prompt: outcome.prepared.prompt,
|
|
3271
|
+
prepared: outcome.prepared
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
} catch (error) {
|
|
3275
|
+
options.onResult(failedResultForItem(item, error, itemStartedAt));
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
const plans = planBatchJobs(entries);
|
|
3279
|
+
const submitted = await Promise.all(
|
|
3280
|
+
plans.map(async (plan, planIndex) => {
|
|
3281
|
+
try {
|
|
3282
|
+
return await submitBatchJobPlan(config, {
|
|
3283
|
+
plan,
|
|
3284
|
+
displayModel: normalizeGeminiDisplayName(plan.apiModel),
|
|
3285
|
+
jobIndex: planIndex,
|
|
3286
|
+
jobCount: plans.length,
|
|
3287
|
+
onProgress: options.onProgress
|
|
3288
|
+
});
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
for (const entry of plan.entries) {
|
|
3291
|
+
options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
|
|
3292
|
+
}
|
|
3293
|
+
return void 0;
|
|
3294
|
+
}
|
|
3295
|
+
})
|
|
3296
|
+
);
|
|
3297
|
+
const jobsToPoll = submitted.filter((row) => row !== void 0);
|
|
3298
|
+
if (jobsToPoll.length > 0) {
|
|
3299
|
+
await pollAndIngestBatchJobs(config, jobsToPoll, {
|
|
3300
|
+
jobCount: jobsToPoll.length,
|
|
3301
|
+
onProgress: options.onProgress,
|
|
3302
|
+
onResult: options.onResult
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3232
3306
|
async function translatePage(config, item, options = {}) {
|
|
3233
3307
|
const startedAt = Date.now();
|
|
3234
3308
|
let outcome;
|
|
@@ -3401,9 +3475,40 @@ async function translateWorklist(config, items, options = {}) {
|
|
|
3401
3475
|
});
|
|
3402
3476
|
}
|
|
3403
3477
|
}
|
|
3404
|
-
const
|
|
3478
|
+
const mainResults = collector.assemble();
|
|
3479
|
+
const failed = mainResults.filter((result) => result.failed);
|
|
3480
|
+
if (failed.length === 0) {
|
|
3481
|
+
const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
|
|
3482
|
+
options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
|
|
3483
|
+
return mainResults;
|
|
3484
|
+
}
|
|
3485
|
+
options.onProgress?.({ type: "retry-start", failed });
|
|
3486
|
+
const originalByKey = new Map(
|
|
3487
|
+
items.map((item) => [translationItemKey(item), item])
|
|
3488
|
+
);
|
|
3489
|
+
const retryItems = buildRetryWorklist(failed, originalByKey);
|
|
3490
|
+
const retryByKey = /* @__PURE__ */ new Map();
|
|
3491
|
+
await runRetryRound(config, retryItems, {
|
|
3492
|
+
model: options.model,
|
|
3493
|
+
force: options.force,
|
|
3494
|
+
concurrency,
|
|
3495
|
+
mode,
|
|
3496
|
+
onProgress: options.onProgress,
|
|
3497
|
+
onResult: (result) => {
|
|
3498
|
+
retryByKey.set(translationItemKey(result), result);
|
|
3499
|
+
options.onProgress?.({ type: "item-done", result });
|
|
3500
|
+
}
|
|
3501
|
+
});
|
|
3502
|
+
const results = mainResults.map((result) => {
|
|
3503
|
+
if (!result.failed) return result;
|
|
3504
|
+
return retryByKey.get(translationItemKey(result)) ?? result;
|
|
3505
|
+
});
|
|
3506
|
+
const retriedTranslated = failed.filter((original) => {
|
|
3507
|
+
const retried = retryByKey.get(translationItemKey(original));
|
|
3508
|
+
return retried !== void 0 && !retried.failed && !retried.skipped;
|
|
3509
|
+
}).length;
|
|
3405
3510
|
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
3406
|
-
options.onProgress?.({ type: "done", results, totals });
|
|
3511
|
+
options.onProgress?.({ type: "done", results, totals, retriedTranslated });
|
|
3407
3512
|
return results;
|
|
3408
3513
|
}
|
|
3409
3514
|
function countPendingItems(config, jobs) {
|
|
@@ -4225,10 +4330,11 @@ function detailForResult(result) {
|
|
|
4225
4330
|
if (result.error) parts.push(red(result.error));
|
|
4226
4331
|
return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
|
|
4227
4332
|
}
|
|
4228
|
-
function formatSummaryLine(totals, dryRun) {
|
|
4333
|
+
function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
|
|
4229
4334
|
const action = dryRun ? "Would translate" : "Translated";
|
|
4335
|
+
const base = totals.translated - retriedTranslated;
|
|
4230
4336
|
const parts = [
|
|
4231
|
-
`${action} ${totals.translated}`,
|
|
4337
|
+
retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
|
|
4232
4338
|
`skipped ${totals.skipped}`
|
|
4233
4339
|
];
|
|
4234
4340
|
if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
|
|
@@ -4277,8 +4383,18 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4277
4383
|
}
|
|
4278
4384
|
return;
|
|
4279
4385
|
}
|
|
4386
|
+
if (event.type === "retry-start") {
|
|
4387
|
+
for (const result of event.failed) {
|
|
4388
|
+
console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4389
|
+
}
|
|
4390
|
+
console.log(`Retrying ${event.failed.length} failed items with validation context...`);
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4280
4393
|
if (event.type === "done") {
|
|
4281
|
-
console.log(formatSummaryLine(event.totals, dryRun));
|
|
4394
|
+
console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
|
|
4395
|
+
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4396
|
+
console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4397
|
+
}
|
|
4282
4398
|
}
|
|
4283
4399
|
},
|
|
4284
4400
|
finish() {
|
|
@@ -4445,12 +4561,25 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4445
4561
|
render();
|
|
4446
4562
|
break;
|
|
4447
4563
|
}
|
|
4564
|
+
case "retry-start": {
|
|
4565
|
+
total += event.failed.length;
|
|
4566
|
+
printPersistent([
|
|
4567
|
+
...event.failed.map(
|
|
4568
|
+
(result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
|
|
4569
|
+
),
|
|
4570
|
+
cyan(`Retrying ${event.failed.length} failed items with validation context...`)
|
|
4571
|
+
]);
|
|
4572
|
+
render();
|
|
4573
|
+
break;
|
|
4574
|
+
}
|
|
4448
4575
|
case "done":
|
|
4449
4576
|
showCursor();
|
|
4450
4577
|
if (renderedLines > 0) {
|
|
4451
4578
|
process.stdout.write(`\x1B[${renderedLines}A`);
|
|
4452
4579
|
}
|
|
4453
|
-
process.stdout.write(
|
|
4580
|
+
process.stdout.write(
|
|
4581
|
+
"\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
|
|
4582
|
+
);
|
|
4454
4583
|
renderedLines = 0;
|
|
4455
4584
|
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4456
4585
|
process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
|