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/dist/cli/index.js
CHANGED
|
@@ -343,39 +343,7 @@ function resolveConfig(input, baseDir) {
|
|
|
343
343
|
if (localeRouting.strategy === "search-param" && !localeRouting.param) {
|
|
344
344
|
throw new Error('scribe config: localeRouting search-param requires a "param" name');
|
|
345
345
|
}
|
|
346
|
-
const localeFallbacks = {};
|
|
347
|
-
for (const [locale, chain] of Object.entries(raw.localeFallbacks ?? {})) {
|
|
348
|
-
if (!raw.locales.includes(locale)) {
|
|
349
|
-
throw new Error(
|
|
350
|
-
`scribe config: localeFallbacks key "${locale}" is not in locales [${raw.locales.join(", ")}]`
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
const seen = /* @__PURE__ */ new Set();
|
|
354
|
-
for (const fallback of chain) {
|
|
355
|
-
if (!raw.locales.includes(fallback)) {
|
|
356
|
-
throw new Error(
|
|
357
|
-
`scribe config: localeFallbacks["${locale}"] entry "${fallback}" is not in locales [${raw.locales.join(", ")}]`
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
if (fallback === locale) {
|
|
361
|
-
throw new Error(
|
|
362
|
-
`scribe config: localeFallbacks["${locale}"] must not contain its own key "${locale}"`
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
if (fallback === defaultLocale) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
`scribe config: localeFallbacks["${locale}"] must not contain the defaultLocale "${defaultLocale}" \u2014 it is always the final fallback; remove it`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
if (seen.has(fallback)) {
|
|
371
|
-
throw new Error(
|
|
372
|
-
`scribe config: localeFallbacks["${locale}"] contains duplicate entry "${fallback}"`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
seen.add(fallback);
|
|
376
|
-
}
|
|
377
|
-
localeFallbacks[locale] = [...chain];
|
|
378
|
-
}
|
|
346
|
+
const localeFallbacks = raw.localeFallbacks === false ? {} : deriveLocaleFallbacks(raw.locales, defaultLocale);
|
|
379
347
|
const projectRoot = path4.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
380
348
|
const contentRoot = path4.resolve(projectRoot, raw.contentDir ?? "content");
|
|
381
349
|
const storePath = path4.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
@@ -415,6 +383,25 @@ function resolveConfig(input, baseDir) {
|
|
|
415
383
|
});
|
|
416
384
|
return config;
|
|
417
385
|
}
|
|
386
|
+
function deriveLocaleFallbacks(locales, defaultLocale) {
|
|
387
|
+
const byLowercase = new Map(locales.map((l) => [l.toLowerCase(), l]));
|
|
388
|
+
const fallbacks = {};
|
|
389
|
+
for (const locale of locales) {
|
|
390
|
+
const subtags = locale.split("-");
|
|
391
|
+
if (subtags.length < 2) continue;
|
|
392
|
+
const chain = [];
|
|
393
|
+
for (let end = subtags.length - 1; end >= 1; end--) {
|
|
394
|
+
const prefix = byLowercase.get(subtags.slice(0, end).join("-").toLowerCase());
|
|
395
|
+
if (prefix && prefix !== locale && prefix !== defaultLocale) {
|
|
396
|
+
chain.push(prefix);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (chain.length > 0) {
|
|
400
|
+
fallbacks[locale] = chain;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return fallbacks;
|
|
404
|
+
}
|
|
418
405
|
|
|
419
406
|
// src/create-project.ts
|
|
420
407
|
init_esm_shims();
|
|
@@ -2550,6 +2537,13 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
|
|
|
2550
2537
|
}
|
|
2551
2538
|
return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
|
|
2552
2539
|
}
|
|
2540
|
+
function buildRetryContextSection(previousError) {
|
|
2541
|
+
return [
|
|
2542
|
+
"",
|
|
2543
|
+
"## Previous attempt",
|
|
2544
|
+
`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).`
|
|
2545
|
+
];
|
|
2546
|
+
}
|
|
2553
2547
|
function buildPageTranslationPrompt(input) {
|
|
2554
2548
|
const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
|
|
2555
2549
|
const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
|
|
@@ -2580,7 +2574,10 @@ function buildPageTranslationPrompt(input) {
|
|
|
2580
2574
|
buildOutputFormatLine(
|
|
2581
2575
|
Object.keys(input.translatableFrontmatter).length > 0,
|
|
2582
2576
|
input.slugStrategy
|
|
2583
|
-
)
|
|
2577
|
+
),
|
|
2578
|
+
// Retry context is locale-specific suffix material: it MUST stay after the
|
|
2579
|
+
// EN body so the cacheable prefix is byte-identical with and without it.
|
|
2580
|
+
...input.previousError ? buildRetryContextSection(input.previousError) : []
|
|
2584
2581
|
];
|
|
2585
2582
|
return [...prefix, ...suffix].join("\n");
|
|
2586
2583
|
}
|
|
@@ -2800,7 +2797,8 @@ function prepareTranslation(config, item, options, startedAt) {
|
|
|
2800
2797
|
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2801
2798
|
translatableFrontmatter: payload.frontmatter,
|
|
2802
2799
|
enBody: payload.body,
|
|
2803
|
-
slugStrategy: type.slugStrategy
|
|
2800
|
+
slugStrategy: type.slugStrategy,
|
|
2801
|
+
previousError: item.previousError
|
|
2804
2802
|
});
|
|
2805
2803
|
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2806
2804
|
return {
|
|
@@ -3212,6 +3210,82 @@ async function runPool(items, concurrency, worker) {
|
|
|
3212
3210
|
function labelForItem(item) {
|
|
3213
3211
|
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
3214
3212
|
}
|
|
3213
|
+
function buildRetryWorklist(failed, originalByKey) {
|
|
3214
|
+
return failed.map((result) => {
|
|
3215
|
+
const key = translationItemKey(result);
|
|
3216
|
+
const original = originalByKey.get(key);
|
|
3217
|
+
return {
|
|
3218
|
+
contentType: result.contentType,
|
|
3219
|
+
enSlug: result.enSlug,
|
|
3220
|
+
locale: result.locale,
|
|
3221
|
+
reason: original?.reason ?? "missing",
|
|
3222
|
+
currentEnHash: original?.currentEnHash ?? "",
|
|
3223
|
+
storedEnHash: original?.storedEnHash,
|
|
3224
|
+
previousError: result.error ?? "Translation failed"
|
|
3225
|
+
};
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
async function runRetryRound(config, retryItems, options) {
|
|
3229
|
+
const startedAt = Date.now();
|
|
3230
|
+
const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
|
|
3231
|
+
if (options.mode === "direct") {
|
|
3232
|
+
const active = /* @__PURE__ */ new Set();
|
|
3233
|
+
await runPool(retryItems, options.concurrency, async (item) => {
|
|
3234
|
+
const label = labelForItem(item);
|
|
3235
|
+
active.add(label);
|
|
3236
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
3237
|
+
const result = await translateOne(item);
|
|
3238
|
+
active.delete(label);
|
|
3239
|
+
options.onResult(result);
|
|
3240
|
+
});
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
const entries = [];
|
|
3244
|
+
for (const item of retryItems) {
|
|
3245
|
+
const itemStartedAt = Date.now();
|
|
3246
|
+
try {
|
|
3247
|
+
const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
|
|
3248
|
+
if (outcome.status === "done") {
|
|
3249
|
+
options.onResult(outcome.result);
|
|
3250
|
+
} else {
|
|
3251
|
+
entries.push({
|
|
3252
|
+
apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
|
|
3253
|
+
prompt: outcome.prepared.prompt,
|
|
3254
|
+
prepared: outcome.prepared
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
} catch (error) {
|
|
3258
|
+
options.onResult(failedResultForItem(item, error, itemStartedAt));
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
const plans = planBatchJobs(entries);
|
|
3262
|
+
const submitted = await Promise.all(
|
|
3263
|
+
plans.map(async (plan, planIndex) => {
|
|
3264
|
+
try {
|
|
3265
|
+
return await submitBatchJobPlan(config, {
|
|
3266
|
+
plan,
|
|
3267
|
+
displayModel: normalizeGeminiDisplayName(plan.apiModel),
|
|
3268
|
+
jobIndex: planIndex,
|
|
3269
|
+
jobCount: plans.length,
|
|
3270
|
+
onProgress: options.onProgress
|
|
3271
|
+
});
|
|
3272
|
+
} catch (error) {
|
|
3273
|
+
for (const entry of plan.entries) {
|
|
3274
|
+
options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
|
|
3275
|
+
}
|
|
3276
|
+
return void 0;
|
|
3277
|
+
}
|
|
3278
|
+
})
|
|
3279
|
+
);
|
|
3280
|
+
const jobsToPoll = submitted.filter((row) => row !== void 0);
|
|
3281
|
+
if (jobsToPoll.length > 0) {
|
|
3282
|
+
await pollAndIngestBatchJobs(config, jobsToPoll, {
|
|
3283
|
+
jobCount: jobsToPoll.length,
|
|
3284
|
+
onProgress: options.onProgress,
|
|
3285
|
+
onResult: options.onResult
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3215
3289
|
async function translatePage(config, item, options = {}) {
|
|
3216
3290
|
const startedAt = Date.now();
|
|
3217
3291
|
let outcome;
|
|
@@ -3384,9 +3458,40 @@ async function translateWorklist(config, items, options = {}) {
|
|
|
3384
3458
|
});
|
|
3385
3459
|
}
|
|
3386
3460
|
}
|
|
3387
|
-
const
|
|
3461
|
+
const mainResults = collector.assemble();
|
|
3462
|
+
const failed = mainResults.filter((result) => result.failed);
|
|
3463
|
+
if (failed.length === 0) {
|
|
3464
|
+
const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
|
|
3465
|
+
options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
|
|
3466
|
+
return mainResults;
|
|
3467
|
+
}
|
|
3468
|
+
options.onProgress?.({ type: "retry-start", failed });
|
|
3469
|
+
const originalByKey = new Map(
|
|
3470
|
+
items.map((item) => [translationItemKey(item), item])
|
|
3471
|
+
);
|
|
3472
|
+
const retryItems = buildRetryWorklist(failed, originalByKey);
|
|
3473
|
+
const retryByKey = /* @__PURE__ */ new Map();
|
|
3474
|
+
await runRetryRound(config, retryItems, {
|
|
3475
|
+
model: options.model,
|
|
3476
|
+
force: options.force,
|
|
3477
|
+
concurrency,
|
|
3478
|
+
mode,
|
|
3479
|
+
onProgress: options.onProgress,
|
|
3480
|
+
onResult: (result) => {
|
|
3481
|
+
retryByKey.set(translationItemKey(result), result);
|
|
3482
|
+
options.onProgress?.({ type: "item-done", result });
|
|
3483
|
+
}
|
|
3484
|
+
});
|
|
3485
|
+
const results = mainResults.map((result) => {
|
|
3486
|
+
if (!result.failed) return result;
|
|
3487
|
+
return retryByKey.get(translationItemKey(result)) ?? result;
|
|
3488
|
+
});
|
|
3489
|
+
const retriedTranslated = failed.filter((original) => {
|
|
3490
|
+
const retried = retryByKey.get(translationItemKey(original));
|
|
3491
|
+
return retried !== void 0 && !retried.failed && !retried.skipped;
|
|
3492
|
+
}).length;
|
|
3388
3493
|
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
3389
|
-
options.onProgress?.({ type: "done", results, totals });
|
|
3494
|
+
options.onProgress?.({ type: "done", results, totals, retriedTranslated });
|
|
3390
3495
|
return results;
|
|
3391
3496
|
}
|
|
3392
3497
|
function countPendingItems(config, jobs) {
|
|
@@ -4208,10 +4313,11 @@ function detailForResult(result) {
|
|
|
4208
4313
|
if (result.error) parts.push(red(result.error));
|
|
4209
4314
|
return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
|
|
4210
4315
|
}
|
|
4211
|
-
function formatSummaryLine(totals, dryRun) {
|
|
4316
|
+
function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
|
|
4212
4317
|
const action = dryRun ? "Would translate" : "Translated";
|
|
4318
|
+
const base = totals.translated - retriedTranslated;
|
|
4213
4319
|
const parts = [
|
|
4214
|
-
`${action} ${totals.translated}`,
|
|
4320
|
+
retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
|
|
4215
4321
|
`skipped ${totals.skipped}`
|
|
4216
4322
|
];
|
|
4217
4323
|
if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
|
|
@@ -4260,8 +4366,18 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4260
4366
|
}
|
|
4261
4367
|
return;
|
|
4262
4368
|
}
|
|
4369
|
+
if (event.type === "retry-start") {
|
|
4370
|
+
for (const result of event.failed) {
|
|
4371
|
+
console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4372
|
+
}
|
|
4373
|
+
console.log(`Retrying ${event.failed.length} failed items with validation context...`);
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4263
4376
|
if (event.type === "done") {
|
|
4264
|
-
console.log(formatSummaryLine(event.totals, dryRun));
|
|
4377
|
+
console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
|
|
4378
|
+
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4379
|
+
console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4380
|
+
}
|
|
4265
4381
|
}
|
|
4266
4382
|
},
|
|
4267
4383
|
finish() {
|
|
@@ -4428,12 +4544,25 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4428
4544
|
render();
|
|
4429
4545
|
break;
|
|
4430
4546
|
}
|
|
4547
|
+
case "retry-start": {
|
|
4548
|
+
total += event.failed.length;
|
|
4549
|
+
printPersistent([
|
|
4550
|
+
...event.failed.map(
|
|
4551
|
+
(result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
|
|
4552
|
+
),
|
|
4553
|
+
cyan(`Retrying ${event.failed.length} failed items with validation context...`)
|
|
4554
|
+
]);
|
|
4555
|
+
render();
|
|
4556
|
+
break;
|
|
4557
|
+
}
|
|
4431
4558
|
case "done":
|
|
4432
4559
|
showCursor();
|
|
4433
4560
|
if (renderedLines > 0) {
|
|
4434
4561
|
process.stdout.write(`\x1B[${renderedLines}A`);
|
|
4435
4562
|
}
|
|
4436
|
-
process.stdout.write(
|
|
4563
|
+
process.stdout.write(
|
|
4564
|
+
"\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
|
|
4565
|
+
);
|
|
4437
4566
|
renderedLines = 0;
|
|
4438
4567
|
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4439
4568
|
process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
|