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/index.js
CHANGED
|
@@ -244,39 +244,7 @@ function resolveConfig(input, baseDir) {
|
|
|
244
244
|
if (localeRouting.strategy === "search-param" && !localeRouting.param) {
|
|
245
245
|
throw new Error('scribe config: localeRouting search-param requires a "param" name');
|
|
246
246
|
}
|
|
247
|
-
const localeFallbacks = {};
|
|
248
|
-
for (const [locale, chain] of Object.entries(raw.localeFallbacks ?? {})) {
|
|
249
|
-
if (!raw.locales.includes(locale)) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`scribe config: localeFallbacks key "${locale}" is not in locales [${raw.locales.join(", ")}]`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
const seen = /* @__PURE__ */ new Set();
|
|
255
|
-
for (const fallback of chain) {
|
|
256
|
-
if (!raw.locales.includes(fallback)) {
|
|
257
|
-
throw new Error(
|
|
258
|
-
`scribe config: localeFallbacks["${locale}"] entry "${fallback}" is not in locales [${raw.locales.join(", ")}]`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
if (fallback === locale) {
|
|
262
|
-
throw new Error(
|
|
263
|
-
`scribe config: localeFallbacks["${locale}"] must not contain its own key "${locale}"`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
if (fallback === defaultLocale) {
|
|
267
|
-
throw new Error(
|
|
268
|
-
`scribe config: localeFallbacks["${locale}"] must not contain the defaultLocale "${defaultLocale}" \u2014 it is always the final fallback; remove it`
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
if (seen.has(fallback)) {
|
|
272
|
-
throw new Error(
|
|
273
|
-
`scribe config: localeFallbacks["${locale}"] contains duplicate entry "${fallback}"`
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
seen.add(fallback);
|
|
277
|
-
}
|
|
278
|
-
localeFallbacks[locale] = [...chain];
|
|
279
|
-
}
|
|
247
|
+
const localeFallbacks = raw.localeFallbacks === false ? {} : deriveLocaleFallbacks(raw.locales, defaultLocale);
|
|
280
248
|
const projectRoot = path3.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
281
249
|
const contentRoot = path3.resolve(projectRoot, raw.contentDir ?? "content");
|
|
282
250
|
const storePath = path3.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
@@ -316,6 +284,25 @@ function resolveConfig(input, baseDir) {
|
|
|
316
284
|
});
|
|
317
285
|
return config;
|
|
318
286
|
}
|
|
287
|
+
function deriveLocaleFallbacks(locales, defaultLocale) {
|
|
288
|
+
const byLowercase = new Map(locales.map((l) => [l.toLowerCase(), l]));
|
|
289
|
+
const fallbacks = {};
|
|
290
|
+
for (const locale of locales) {
|
|
291
|
+
const subtags = locale.split("-");
|
|
292
|
+
if (subtags.length < 2) continue;
|
|
293
|
+
const chain = [];
|
|
294
|
+
for (let end = subtags.length - 1; end >= 1; end--) {
|
|
295
|
+
const prefix = byLowercase.get(subtags.slice(0, end).join("-").toLowerCase());
|
|
296
|
+
if (prefix && prefix !== locale && prefix !== defaultLocale) {
|
|
297
|
+
chain.push(prefix);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (chain.length > 0) {
|
|
301
|
+
fallbacks[locale] = chain;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return fallbacks;
|
|
305
|
+
}
|
|
319
306
|
|
|
320
307
|
// src/core/introspect-schema.ts
|
|
321
308
|
function getArrayElement(schema) {
|
|
@@ -2840,6 +2827,13 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
|
|
|
2840
2827
|
}
|
|
2841
2828
|
return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
|
|
2842
2829
|
}
|
|
2830
|
+
function buildRetryContextSection(previousError) {
|
|
2831
|
+
return [
|
|
2832
|
+
"",
|
|
2833
|
+
"## Previous attempt",
|
|
2834
|
+
`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).`
|
|
2835
|
+
];
|
|
2836
|
+
}
|
|
2843
2837
|
function buildPageTranslationPrompt(input) {
|
|
2844
2838
|
const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
|
|
2845
2839
|
const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
|
|
@@ -2870,7 +2864,10 @@ function buildPageTranslationPrompt(input) {
|
|
|
2870
2864
|
buildOutputFormatLine(
|
|
2871
2865
|
Object.keys(input.translatableFrontmatter).length > 0,
|
|
2872
2866
|
input.slugStrategy
|
|
2873
|
-
)
|
|
2867
|
+
),
|
|
2868
|
+
// Retry context is locale-specific suffix material: it MUST stay after the
|
|
2869
|
+
// EN body so the cacheable prefix is byte-identical with and without it.
|
|
2870
|
+
...input.previousError ? buildRetryContextSection(input.previousError) : []
|
|
2874
2871
|
];
|
|
2875
2872
|
return [...prefix, ...suffix].join("\n");
|
|
2876
2873
|
}
|
|
@@ -3085,7 +3082,8 @@ function prepareTranslation(config, item, options, startedAt) {
|
|
|
3085
3082
|
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
3086
3083
|
translatableFrontmatter: payload.frontmatter,
|
|
3087
3084
|
enBody: payload.body,
|
|
3088
|
-
slugStrategy: type.slugStrategy
|
|
3085
|
+
slugStrategy: type.slugStrategy,
|
|
3086
|
+
previousError: item.previousError
|
|
3089
3087
|
});
|
|
3090
3088
|
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
3091
3089
|
return {
|
|
@@ -3497,6 +3495,82 @@ async function runPool(items, concurrency, worker) {
|
|
|
3497
3495
|
function labelForItem(item) {
|
|
3498
3496
|
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
3499
3497
|
}
|
|
3498
|
+
function buildRetryWorklist(failed, originalByKey) {
|
|
3499
|
+
return failed.map((result) => {
|
|
3500
|
+
const key = translationItemKey(result);
|
|
3501
|
+
const original = originalByKey.get(key);
|
|
3502
|
+
return {
|
|
3503
|
+
contentType: result.contentType,
|
|
3504
|
+
enSlug: result.enSlug,
|
|
3505
|
+
locale: result.locale,
|
|
3506
|
+
reason: original?.reason ?? "missing",
|
|
3507
|
+
currentEnHash: original?.currentEnHash ?? "",
|
|
3508
|
+
storedEnHash: original?.storedEnHash,
|
|
3509
|
+
previousError: result.error ?? "Translation failed"
|
|
3510
|
+
};
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
async function runRetryRound(config, retryItems, options) {
|
|
3514
|
+
const startedAt = Date.now();
|
|
3515
|
+
const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
|
|
3516
|
+
if (options.mode === "direct") {
|
|
3517
|
+
const active = /* @__PURE__ */ new Set();
|
|
3518
|
+
await runPool(retryItems, options.concurrency, async (item) => {
|
|
3519
|
+
const label = labelForItem(item);
|
|
3520
|
+
active.add(label);
|
|
3521
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
3522
|
+
const result = await translateOne(item);
|
|
3523
|
+
active.delete(label);
|
|
3524
|
+
options.onResult(result);
|
|
3525
|
+
});
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
const entries = [];
|
|
3529
|
+
for (const item of retryItems) {
|
|
3530
|
+
const itemStartedAt = Date.now();
|
|
3531
|
+
try {
|
|
3532
|
+
const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
|
|
3533
|
+
if (outcome.status === "done") {
|
|
3534
|
+
options.onResult(outcome.result);
|
|
3535
|
+
} else {
|
|
3536
|
+
entries.push({
|
|
3537
|
+
apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
|
|
3538
|
+
prompt: outcome.prepared.prompt,
|
|
3539
|
+
prepared: outcome.prepared
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
options.onResult(failedResultForItem(item, error, itemStartedAt));
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
const plans = planBatchJobs(entries);
|
|
3547
|
+
const submitted = await Promise.all(
|
|
3548
|
+
plans.map(async (plan, planIndex) => {
|
|
3549
|
+
try {
|
|
3550
|
+
return await submitBatchJobPlan(config, {
|
|
3551
|
+
plan,
|
|
3552
|
+
displayModel: normalizeGeminiDisplayName(plan.apiModel),
|
|
3553
|
+
jobIndex: planIndex,
|
|
3554
|
+
jobCount: plans.length,
|
|
3555
|
+
onProgress: options.onProgress
|
|
3556
|
+
});
|
|
3557
|
+
} catch (error) {
|
|
3558
|
+
for (const entry of plan.entries) {
|
|
3559
|
+
options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
|
|
3560
|
+
}
|
|
3561
|
+
return void 0;
|
|
3562
|
+
}
|
|
3563
|
+
})
|
|
3564
|
+
);
|
|
3565
|
+
const jobsToPoll = submitted.filter((row) => row !== void 0);
|
|
3566
|
+
if (jobsToPoll.length > 0) {
|
|
3567
|
+
await pollAndIngestBatchJobs(config, jobsToPoll, {
|
|
3568
|
+
jobCount: jobsToPoll.length,
|
|
3569
|
+
onProgress: options.onProgress,
|
|
3570
|
+
onResult: options.onResult
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3500
3574
|
async function translatePage(config, item, options = {}) {
|
|
3501
3575
|
const startedAt = Date.now();
|
|
3502
3576
|
let outcome;
|
|
@@ -3669,9 +3743,40 @@ async function translateWorklist(config, items, options = {}) {
|
|
|
3669
3743
|
});
|
|
3670
3744
|
}
|
|
3671
3745
|
}
|
|
3672
|
-
const
|
|
3746
|
+
const mainResults = collector.assemble();
|
|
3747
|
+
const failed = mainResults.filter((result) => result.failed);
|
|
3748
|
+
if (failed.length === 0) {
|
|
3749
|
+
const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
|
|
3750
|
+
options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
|
|
3751
|
+
return mainResults;
|
|
3752
|
+
}
|
|
3753
|
+
options.onProgress?.({ type: "retry-start", failed });
|
|
3754
|
+
const originalByKey = new Map(
|
|
3755
|
+
items.map((item) => [translationItemKey(item), item])
|
|
3756
|
+
);
|
|
3757
|
+
const retryItems = buildRetryWorklist(failed, originalByKey);
|
|
3758
|
+
const retryByKey = /* @__PURE__ */ new Map();
|
|
3759
|
+
await runRetryRound(config, retryItems, {
|
|
3760
|
+
model: options.model,
|
|
3761
|
+
force: options.force,
|
|
3762
|
+
concurrency,
|
|
3763
|
+
mode,
|
|
3764
|
+
onProgress: options.onProgress,
|
|
3765
|
+
onResult: (result) => {
|
|
3766
|
+
retryByKey.set(translationItemKey(result), result);
|
|
3767
|
+
options.onProgress?.({ type: "item-done", result });
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
const results = mainResults.map((result) => {
|
|
3771
|
+
if (!result.failed) return result;
|
|
3772
|
+
return retryByKey.get(translationItemKey(result)) ?? result;
|
|
3773
|
+
});
|
|
3774
|
+
const retriedTranslated = failed.filter((original) => {
|
|
3775
|
+
const retried = retryByKey.get(translationItemKey(original));
|
|
3776
|
+
return retried !== void 0 && !retried.failed && !retried.skipped;
|
|
3777
|
+
}).length;
|
|
3673
3778
|
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
3674
|
-
options.onProgress?.({ type: "done", results, totals });
|
|
3779
|
+
options.onProgress?.({ type: "done", results, totals, retriedTranslated });
|
|
3675
3780
|
return results;
|
|
3676
3781
|
}
|
|
3677
3782
|
function countPendingItems(config, jobs) {
|