Files
Cibello-app/apps/api/src/lib/contentLanguage.ts
T
2026-08-05 19:21:11 +07:00

106 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}