From 28c4d0db710f7c647e50a28085c95e730481b588 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:36:35 +0000 Subject: [PATCH] =?UTF-8?q?i18n(hemsk=C3=A4rm):=20=C3=B6vers=C3=A4tt=20rek?= =?UTF-8?q?ommendationstitlar=20+=20ingrediensnamn=20+=20h=C3=A5rdkodade?= =?UTF-8?q?=20str=C3=A4ngar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - what-to-eat översätter nu recepttitlar (resolveRecipeTitles, batch) + ⏳/saknas- ingrediensnamn (via allIngredients-id-karta + resolveIngredientNames) - index.tsx använder rec.title/item.name; hårdkodat -> t(): sökfält, Rekommenderat, Toppval, Menade du, Baka (kategori), tomma-lägen - 7 nya nycklar (wte.searchPlaceholder/recommended/topPick/didYouMean/emptySearch/ emptyHoliday, cat.baking) i alla 12 språk - KVAR (separat): whySv-prosan (kräver strukturerade skäl), säsongsval (region), meal-box-titlar Co-Authored-By: Claude --- apps/api/src/lib/contentLanguage.ts | 34 ++++++++++++++++++++++++++ apps/api/src/routes/recommendations.ts | 25 +++++++++++++++++++ apps/mobile/src/app/(tabs)/index.tsx | 21 ++++++++-------- apps/mobile/src/locales/da/common.json | 7 ++++++ apps/mobile/src/locales/de/common.json | 7 ++++++ apps/mobile/src/locales/en/common.json | 7 ++++++ apps/mobile/src/locales/es/common.json | 7 ++++++ apps/mobile/src/locales/fi/common.json | 7 ++++++ apps/mobile/src/locales/fr/common.json | 7 ++++++ apps/mobile/src/locales/it/common.json | 7 ++++++ apps/mobile/src/locales/nb/common.json | 7 ++++++ apps/mobile/src/locales/nl/common.json | 7 ++++++ apps/mobile/src/locales/pl/common.json | 7 ++++++ apps/mobile/src/locales/pt/common.json | 7 ++++++ apps/mobile/src/locales/sv/common.json | 7 ++++++ 15 files changed, 154 insertions(+), 10 deletions(-) diff --git a/apps/api/src/lib/contentLanguage.ts b/apps/api/src/lib/contentLanguage.ts index 59bff5f..04d0762 100644 --- a/apps/api/src/lib/contentLanguage.ts +++ b/apps/api/src/lib/contentLanguage.ts @@ -72,6 +72,40 @@ export async function resolveRecipeTranslation( }; } +/** + * Publicerade recepttitlar för flera recept på en gång: Map recipeId -> titel. + * För listor/rekommendationer där vi bara behöver titeln (inte hela receptet). + * Saknade id:n utelämnas (anroparen faller tillbaka på svensk titel). + */ +export async function resolveRecipeTitles( + db: Database, + recipeIds: string[], + languageTag: string, +): Promise> { + const candidates = languageCandidates(languageTag); + if (candidates.includes("sv") || recipeIds.length === 0) return new Map(); + const rows = await db + .select({ + recipeId: schema.recipeTranslations.recipeId, + languageTag: schema.recipeTranslations.languageTag, + title: schema.recipeTranslations.title, + }) + .from(schema.recipeTranslations) + .where( + and( + inArray(schema.recipeTranslations.recipeId, recipeIds), + inArray(schema.recipeTranslations.languageTag, candidates), + eq(schema.recipeTranslations.status, "published"), + ), + ); + const out = new Map(); + // Exakt språk (candidates[0]) vinner över primärt (candidates[1]). + for (const candidate of [...candidates].reverse()) { + for (const r of rows) if (r.languageTag === candidate) out.set(r.recipeId, r.title); + } + return out; +} + /** * Ingrediensnamn för användarens språk: Map ingredientId -> namn. * Saknade id:n behåller svenskt namn (fallback sker hos anroparen). diff --git a/apps/api/src/routes/recommendations.ts b/apps/api/src/routes/recommendations.ts index f219573..2c5f364 100644 --- a/apps/api/src/routes/recommendations.ts +++ b/apps/api/src/routes/recommendations.ts @@ -26,6 +26,7 @@ import { DEFAULT_TARGETS, computeDailyTargets, summarizeDay } from "@app/nutriti import { parse } from "../lib/errors.js"; import { getActiveHouseholdId, todayIso } from "../lib/helpers.js"; import { loadLocalePreferences } from "../lib/localeContext.js"; +import { resolveIngredientNames, resolveRecipeTitles } from "../lib/contentLanguage.js"; /** * "Vad ska vi äta?" (spec §18) – appens viktigaste endpoint. @@ -604,6 +605,27 @@ export async function recommendationRoutes(app: FastifyInstance) { ? await computeSearchSuggestion(app, q.search) : null; + // --- i18n: översätt titlar + ingrediensnamn till användarens språk (svensk källa = fallback) --- + const languageTag = localePrefs.languageTag; + const titleByRecipe = await resolveRecipeTitles( + app.db, + recommendations.map((r) => r.recipeId), + languageTag, + ); + const localizedById = await resolveIngredientNames( + app.db, + allIngredients + .map((i) => i.canonicalIngredientId) + .filter((id): id is string => id != null), + languageTag, + ); + const svToLocalized = new Map(); + for (const i of allIngredients) { + const loc = i.canonicalIngredientId ? localizedById.get(i.canonicalIngredientId) : undefined; + if (loc) svToLocalized.set(i.displayNameSv, loc); + } + const localizeName = (n: string): string => svToLocalized.get(n) ?? n; + return { mealType: q.mealType, context: { @@ -622,6 +644,9 @@ export async function recommendationRoutes(app: FastifyInstance) { mealBoxSuggestions, recommendations: recommendations.map((r) => ({ ...r, + title: titleByRecipe.get(r.recipeId) ?? r.titleSv, + usesExpiring: r.usesExpiring.map((u) => ({ ...u, name: localizeName(u.nameSv) })), + missingIngredients: r.missingIngredients.map(localizeName), ratingAverage: ratingByRecipe.get(r.recipeId)?.average ?? null, ratingCount: ratingByRecipe.get(r.recipeId)?.count ?? 0, })), diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 5f33cb6..b7d2c67 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -29,10 +29,11 @@ import { colors, spacing } from "@/lib/theme"; interface Recommendation { recipeId: string; titleSv: string; + title?: string; score: number; whySv: string; missingIngredients: string[]; - usesExpiring: Array<{ nameSv: string; daysLeft: number | null }>; + usesExpiring: Array<{ nameSv: string; name?: string; daysLeft: number | null }>; coveragePercent: number; ratingAverage: number | null; ratingCount: number; @@ -73,7 +74,7 @@ const CATS: ReadonlyArray<{ { key: "dinner", labelKey: "myday.mealType.dinner", mealType: "dinner", glyph: "🍽️" }, { key: "snack", labelKey: "myday.mealType.snack", mealType: "snack", glyph: "🍎" }, { key: "dessert", labelKey: "myday.mealType.dessert", mealType: "dessert", glyph: "🍰" }, - { key: "baking", labelKey: null, label: "Baka", tag: "baking", ctxMeal: "dessert", glyph: "🥐" }, + { key: "baking", labelKey: "cat.baking", label: "Baka", tag: "baking", ctxMeal: "dessert", glyph: "🥐" }, ]; const errText = (e: unknown): string | undefined => @@ -161,7 +162,7 @@ export default function WhatToEatScreen() { {/* Sök bland recept (fungerar i alla flikar, söker på namn). */} submitSearch(searchText)} @@ -187,7 +188,7 @@ export default function WhatToEatScreen() { {/* Kategori-flikar med enhetliga rutor (2 kolumner). Filtrerar in-place. */}