106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
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<string> {
|
||
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<number, { instruction: string; tip: string | null }>;
|
||
}
|
||
|
||
/**
|
||
* 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<ResolvedRecipeText | null> {
|
||
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<Map<string, string>> {
|
||
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<string, string>();
|
||
for (const candidate of [...candidates].reverse()) {
|
||
for (const r of rows) if (r.languageTag === candidate) out.set(r.ingredientId, r.name);
|
||
}
|
||
return out;
|
||
}
|