import { and, eq, inArray } from "drizzle-orm"; import { schema, type Database } from "@app/database"; import { loadLocalePreferences } from "./localeContext.js"; /** * Innehållsspråk (i18n-spec §11–14): svensk källtext är sanningen, publicerade * översättningar är vyer. Upplösning: exakt tagg ("en-US") → primärt språk * ("en") → svensk källa. Endast status=published serveras till användare. */ /** "en-US" -> ["en-US", "en"], "sv" -> ["sv"]. */ export function languageCandidates(languageTag: string): string[] { const primary = languageTag.split("-")[0] ?? languageTag; return primary === languageTag ? [languageTag] : [languageTag, primary]; } export async function userLanguageTag(db: Database, userId: string): Promise { const prefs = await loadLocalePreferences(db, userId); return prefs.languageTag; } export interface ResolvedRecipeText { language: string; title: string; description: string | null; storageGuidance: string | null; /** stepNumber -> { instruction, tip } */ steps: Map; } /** * Hämtar publicerad receptöversättning för användarens språk, eller null om * svensk källa ska användas. En DB-runda för texterna + en för stegen. */ export async function resolveRecipeTranslation( db: Database, recipeId: string, languageTag: string, ): Promise { const candidates = languageCandidates(languageTag); if (candidates.includes("sv")) return null; const rows = await db .select() .from(schema.recipeTranslations) .where( and( eq(schema.recipeTranslations.recipeId, recipeId), inArray(schema.recipeTranslations.languageTag, candidates), eq(schema.recipeTranslations.status, "published"), ), ); if (rows.length === 0) return null; const best = candidates.map((c) => rows.find((r) => r.languageTag === c)).find(Boolean) ?? rows[0]!; const stepRows = await db .select() .from(schema.recipeStepTranslations) .where( and( eq(schema.recipeStepTranslations.recipeId, recipeId), eq(schema.recipeStepTranslations.languageTag, best.languageTag), ), ); return { language: best.languageTag, title: best.title, description: best.description, storageGuidance: best.storageGuidance, steps: new Map(stepRows.map((s) => [s.stepNumber, { instruction: s.instruction, tip: s.tip }])), }; } /** * Ingrediensnamn för användarens språk: Map ingredientId -> namn. * Saknade id:n behåller svenskt namn (fallback sker hos anroparen). */ export async function resolveIngredientNames( db: Database, ingredientIds: string[], languageTag: string, ): Promise> { const candidates = languageCandidates(languageTag); if (candidates.includes("sv") || ingredientIds.length === 0) return new Map(); const rows = await db .select({ ingredientId: schema.ingredientTranslations.ingredientId, languageTag: schema.ingredientTranslations.languageTag, name: schema.ingredientTranslations.name, }) .from(schema.ingredientTranslations) .where( and( inArray(schema.ingredientTranslations.ingredientId, ingredientIds), inArray(schema.ingredientTranslations.languageTag, candidates), eq(schema.ingredientTranslations.status, "published"), ), ); const out = new Map(); for (const candidate of [...candidates].reverse()) { for (const r of rows) if (r.languageTag === candidate) out.set(r.ingredientId, r.name); } return out; }