/** * Köa AI-översättning (TRANSLATE_RECIPE) för ALLA publicerade recept × målspråk. * Hoppar över (recept, språk) som redan har en översättningsrad – idempotent och * resumbar, så du kan köra om utan att betala för samma jobb två gånger. * * Workern (Fönster C) MÅSTE köra med en RIKTIG AI-nyckel (inte mock) för att * jobben ska bli riktiga översättningar. Jobben sparas som utkast (draft_ai) – * publicera dem sedan med publish-translations.ts. * * Kör från repo-roten: * pnpm --filter @app/worker exec tsx scripts/translate-recipes.ts es # bara spanska (testa först!) * pnpm --filter @app/worker exec tsx scripts/translate-recipes.ts es,de # flera * pnpm --filter @app/worker exec tsx scripts/translate-recipes.ts all # alla 11 språk * * Kräver DATABASE_URL och REDIS_URL i miljön (samma som API/worker använder). */ import IORedis from "ioredis"; import { Queue } from "bullmq"; import { eq } from "drizzle-orm"; import { createDatabase, schema } from "@app/database"; import { JOB_QUEUE_NAME } from "@app/shared-types"; const LANGS = ["en", "da", "de", "es", "fi", "fr", "it", "nb", "nl", "pl", "pt"]; async function main() { if (!process.env.DATABASE_URL) throw new Error("Sätt DATABASE_URL."); const arg = (process.argv[2] ?? "all").toLowerCase(); const targets = arg === "all" ? LANGS : arg.split(",").map((s) => s.trim()).filter(Boolean); for (const l of targets) { if (!LANGS.includes(l)) throw new Error(`Okänt språk: ${l}. Giltiga: ${LANGS.join(", ")}, all`); } const { db, pool } = createDatabase(process.env.DATABASE_URL); const connection = new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379", { maxRetriesPerRequest: null, }); const queue = new Queue(JOB_QUEUE_NAME, { connection }); const recipes = await db .select({ id: schema.recipes.id }) .from(schema.recipes) .where(eq(schema.recipes.status, "published")); const existing = await db .select({ r: schema.recipeTranslations.recipeId, l: schema.recipeTranslations.languageTag }) .from(schema.recipeTranslations); const have = new Set(existing.map((e) => `${e.r}|${e.l}`)); const jobs: { name: string; data: unknown }[] = []; let skip = 0; for (const r of recipes) { for (const lang of targets) { if (have.has(`${r.id}|${lang}`)) { skip++; continue; } jobs.push({ name: "TRANSLATE_RECIPE", data: { jobType: "TRANSLATE_RECIPE", recipeId: r.id, targetLanguageTag: lang }, }); } } // Köa i klumpar (snabbare än en add per jobb). for (let i = 0; i < jobs.length; i += 500) { await queue.addBulk(jobs.slice(i, i + 500) as never); } console.log( `Köade ${jobs.length} översättningsjobb (hoppade ${skip} som redan finns). ` + `Recept: ${recipes.length}, språk: ${targets.join(", ")}.`, ); console.log( "Workern (med riktig AI-nyckel) processar dem nu. När kön tömts: kör publish-translations.ts.", ); await queue.close(); await connection.quit(); await pool.end(); } main().then( () => process.exit(0), (e) => { console.error(e); process.exit(1); }, );