52bd6e74bd
- retry-failed-translations.ts: kör om de receptöversättningar som föll på verifieringen (skriver ut vilka + varför, köar TRANSLATE_RECIPE på nytt). Idempotent; kör tills listan är tom, publicera sedan. - Kok-läget (cooking/[id].tsx) visade svensk titel + svenska ingrediensnamn fast detalj-API:t redan levererar översatta fält. Nu title/displayName med svensk fallback (stegen var redan översatta). - "(valfritt)" var hårdkodat i kok-läget + recept-detaljen -> ny nyckel recipe.optional i alla 12 språk. Vakt + typecheck gröna. Co-Authored-By: Claude <noreply@anthropic.com>
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
/**
|
||
* Kör om de receptöversättningar som föll på den deterministiska verifieringen
|
||
* (verification.ok = false) och därför ligger kvar som svenska. Skriver ut vilka
|
||
* + varför, och köar TRANSLATE_RECIPE på nytt. Workern översätter om (upsert med
|
||
* ny verifiering) → kör sedan publish-translations.ts all för att publicera de
|
||
* som nu klarar. Idempotent: kör om tills listan är tom.
|
||
*
|
||
* pnpm --filter @app/worker exec tsx scripts/retry-failed-translations.ts # kör om
|
||
* pnpm --filter @app/worker exec tsx scripts/retry-failed-translations.ts --dry-run # visa bara
|
||
*
|
||
* Fönster C (workern) måste vara igång med riktig AI-nyckel.
|
||
* Kräver DATABASE_URL + REDIS_URL (laddas från .env).
|
||
*/
|
||
import { config as loadDotenv } from "dotenv";
|
||
import { existsSync } from "node:fs";
|
||
import path from "node:path";
|
||
import IORedis from "ioredis";
|
||
import { Queue } from "bullmq";
|
||
import { and, eq, sql } from "drizzle-orm";
|
||
import { createDatabase, schema } from "@app/database";
|
||
import { JOB_QUEUE_NAME } from "@app/shared-types";
|
||
|
||
for (const candidate of [".env", "../.env", "../../.env"]) {
|
||
const p = path.resolve(process.cwd(), candidate);
|
||
if (existsSync(p)) {
|
||
loadDotenv({ path: p });
|
||
break;
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
if (!process.env.DATABASE_URL) throw new Error("Sätt DATABASE_URL.");
|
||
const dryRun = process.argv.includes("--dry-run");
|
||
const { db, pool } = createDatabase(process.env.DATABASE_URL);
|
||
|
||
const failed = await db
|
||
.select({
|
||
recipeId: schema.recipeTranslations.recipeId,
|
||
lang: schema.recipeTranslations.languageTag,
|
||
title: schema.recipes.titleSv,
|
||
verification: schema.recipeTranslations.verification,
|
||
})
|
||
.from(schema.recipeTranslations)
|
||
.innerJoin(schema.recipes, eq(schema.recipes.id, schema.recipeTranslations.recipeId))
|
||
.where(
|
||
and(
|
||
eq(schema.recipeTranslations.status, "draft_ai"),
|
||
sql`${schema.recipeTranslations.verification}->>'ok' = 'false'`,
|
||
),
|
||
)
|
||
.orderBy(schema.recipes.titleSv);
|
||
|
||
if (failed.length === 0) {
|
||
console.log("✓ Inga misslyckade översättningar – allt är verifierat och klart.");
|
||
await pool.end();
|
||
return;
|
||
}
|
||
|
||
console.log(`${failed.length} översättning(ar) föll på verifieringen:\n`);
|
||
for (const f of failed) {
|
||
const notes = f.verification?.notes?.length ? f.verification.notes.join("; ") : "okänd orsak";
|
||
console.log(` [${f.lang}] ${f.title}`);
|
||
console.log(` → ${notes}`);
|
||
}
|
||
|
||
if (dryRun) {
|
||
console.log("\n(dry-run: köade inget. Kör utan --dry-run för att översätta om.)");
|
||
await pool.end();
|
||
return;
|
||
}
|
||
|
||
const connection = new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379", {
|
||
maxRetriesPerRequest: null,
|
||
});
|
||
const queue = new Queue(JOB_QUEUE_NAME, { connection });
|
||
await queue.addBulk(
|
||
failed.map((f) => ({
|
||
name: "TRANSLATE_RECIPE",
|
||
data: { jobType: "TRANSLATE_RECIPE", recipeId: f.recipeId, targetLanguageTag: f.lang },
|
||
})) as never,
|
||
);
|
||
console.log(`\nKöade ${failed.length} omförsök. Workern (Fönster C) översätter om nu.`);
|
||
console.log(
|
||
"När kön tömts: pnpm --filter @app/worker exec tsx scripts/publish-translations.ts all",
|
||
);
|
||
console.log("Kör det här scriptet igen för att se om något fortfarande faller (då kan det vara receptet självt som behöver en manuell titt).");
|
||
await queue.close();
|
||
await connection.quit();
|
||
await pool.end();
|
||
}
|
||
main().then(
|
||
() => process.exit(0),
|
||
(e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
},
|
||
);
|