i18n(innehåll): komplett ingrediensöversättning (362×11) + receptöversättnings-verktyg
Ingredienser: fyllde luckan från 101 till 362 för da/de/es/fi/fr/it/nb/nl/pl/pt (engelska hade redan alla 362). Nu 3982 översättningar, 11 språk kompletta – en spansk/tysk/fransk användare ser ingrediensnamn på sitt språk, inte svenska. Författat av språkexperter per språk, verifierat mot 362-listan + kvalitetskoll. Recept: 407 recept har 0 översättningar (svensk källa visas). Lade till två körbara skript för AI-pipelinen (kräver din Gemini-nyckel i workern): - scripts/translate-recipes.ts [språk|all] – köar TRANSLATE_RECIPE för alla publicerade recept × språk, idempotent (hoppar redan gjorda). - scripts/publish-translations.ts [språk|all] – publicerar de utkast som klarat den inbyggda verifieringen (numbersPreserved/stegantal/confidence); övriga stannar som utkast för granskning. Verifierat mot testdatabas: ingredient_translations 3982 rader/11 språk kompletta; enqueue köar 405 jobb; processor skapar draft_ai+ok; publish publicerar; steg- översättningar följer med och serveras.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Publicera AI-receptöversättningar som klarat den inbyggda verifieringen
|
||||
* (verification.ok = true). Endast PUBLICERADE översättningar visas för
|
||||
* användare. Utkast som föll på verifieringen lämnas kvar för manuell
|
||||
* granskning i admin (så inget felöversatt går live automatiskt).
|
||||
*
|
||||
* Kör EFTER att translate-recipes.ts-kön har tömts:
|
||||
* pnpm --filter @app/worker exec tsx scripts/publish-translations.ts es
|
||||
* pnpm --filter @app/worker exec tsx scripts/publish-translations.ts all
|
||||
*
|
||||
* Kräver DATABASE_URL i miljön.
|
||||
*/
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { createDatabase, schema } from "@app/database";
|
||||
|
||||
async function main() {
|
||||
if (!process.env.DATABASE_URL) throw new Error("Sätt DATABASE_URL.");
|
||||
const arg = (process.argv[2] ?? "all").toLowerCase();
|
||||
const { db, pool } = createDatabase(process.env.DATABASE_URL);
|
||||
|
||||
const cond = [
|
||||
eq(schema.recipeTranslations.status, "draft_ai"),
|
||||
sql`${schema.recipeTranslations.verification}->>'ok' = 'true'`,
|
||||
];
|
||||
if (arg !== "all") cond.push(eq(schema.recipeTranslations.languageTag, arg));
|
||||
|
||||
const published = await db
|
||||
.update(schema.recipeTranslations)
|
||||
.set({ status: "published", updatedAt: new Date() })
|
||||
.where(and(...cond))
|
||||
.returning({ id: schema.recipeTranslations.id });
|
||||
|
||||
const [remaining] = await db
|
||||
.select({ n: sql<number>`count(*)::int` })
|
||||
.from(schema.recipeTranslations)
|
||||
.where(eq(schema.recipeTranslations.status, "draft_ai"));
|
||||
|
||||
console.log(`Publicerade ${published.length} verifierade översättningar (${arg}).`);
|
||||
console.log(
|
||||
`Kvar som utkast (föll på verifieringen – granska i admin vid behov): ${remaining?.n ?? 0}.`,
|
||||
);
|
||||
await pool.end();
|
||||
}
|
||||
main().then(
|
||||
() => process.exit(0),
|
||||
(e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user