Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
import { and, eq, inArray } from "drizzle-orm";
import { schema, type Database } from "@app/database";
import { loadLocalePreferences } from "./localeContext.js";
/**
* Innehållsspråk (i18n-spec §1114): 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;
}