diff --git a/apps/api/src/routes/recommendations.ts b/apps/api/src/routes/recommendations.ts index 2c5f364..67369ea 100644 --- a/apps/api/src/routes/recommendations.ts +++ b/apps/api/src/routes/recommendations.ts @@ -433,6 +433,7 @@ export async function recommendationRoutes(app: FastifyInstance) { cravingCuisine: craving?.cuisine, cravingMaxKcal: craving?.maxKcal, personalizationEnabled, + languageTag: localePrefs.languageTag, memoryItems: personalizationEnabled ? memoryItems : undefined, tasteSignals: personalizationEnabled ? tasteSignals : undefined, cookingAssumptions: personalizationEnabled ? cookingAssumptions : undefined, @@ -583,20 +584,14 @@ export async function recommendationRoutes(app: FastifyInstance) { titleSv: box.titleSv, portionsRemaining: box.portionsRemaining, recommendedUseBy: box.recommendedUseBy, - whySv: - Date.parse(box.recommendedUseBy) <= today.getTime() + 2 * 86_400_000 - ? `Matlådan bör ätas senast ${box.recommendedUseBy}. Noll matlagning, noll svinn.` - : "Färdig mat som väntar – snabbaste middagen i huset.", + useByUrgent: Date.parse(box.recommendedUseBy) <= today.getTime() + 2 * 86_400_000, })); // Betyg (stjärnor) finns på kandidaterna men följer INTE med i // ScoredRecommendation som rankAll returnerar. Återkoppla dem per recept så // mobilen kan visa "★ 4,5 · 12" i listan (annars syns aldrig satta betyg). const ratingByRecipe = new Map( - scoredCandidates.map((c) => [ - c.recipeId, - { average: c.ratingAverage, count: c.ratingCount }, - ]), + scoredCandidates.map((c) => [c.recipeId, { average: c.ratingAverage, count: c.ratingCount }]), ); // "Menade du …?" – bara när en sökning gav noll träffar (grov felstavning). @@ -614,9 +609,7 @@ export async function recommendationRoutes(app: FastifyInstance) { ); const localizedById = await resolveIngredientNames( app.db, - allIngredients - .map((i) => i.canonicalIngredientId) - .filter((id): id is string => id != null), + allIngredients.map((i) => i.canonicalIngredientId).filter((id): id is string => id != null), languageTag, ); const svToLocalized = new Map(); diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index e704f13..902648c 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -43,7 +43,7 @@ interface MealBoxSuggestion { titleSv: string; portionsRemaining: number; recommendedUseBy: string; - whySv: string; + useByUrgent: boolean; } interface WhatToEatResponse { recommendations: Recommendation[]; @@ -227,7 +227,11 @@ export default function WhatToEatScreen() { tone="success" /> - {box.whySv} + + {t(box.useByUrgent ? "mealbox.eatBy" : "mealbox.ready", { + date: box.recommendedUseBy, + })} + ))} diff --git a/apps/mobile/src/locales/da/common.json b/apps/mobile/src/locales/da/common.json index 2b2e935..6e13201 100644 --- a/apps/mobile/src/locales/da/common.json +++ b/apps/mobile/src/locales/da/common.json @@ -51,6 +51,8 @@ "common.add": "Tilføj", "common.back": "Tilbage", "common.cancel": "Annuller", + "mealbox.eatBy": "Spis denne madkasse senest den {date}. Nul madlavning, nul spild.", + "mealbox.ready": "Færdig mad der venter – husets hurtigste aftensmad.", "common.remove": "Fjern", "common.all": "Alle", "auth.tagline": "Mindre spild, mindre stress, mere tilbage i pungen", diff --git a/apps/mobile/src/locales/de/common.json b/apps/mobile/src/locales/de/common.json index 4efcdc6..e26f00d 100644 --- a/apps/mobile/src/locales/de/common.json +++ b/apps/mobile/src/locales/de/common.json @@ -51,6 +51,8 @@ "common.add": "Hinzufügen", "common.back": "Zurück", "common.cancel": "Abbrechen", + "mealbox.eatBy": "Iss diese Meal-Box spätestens am {date}. Kein Kochen, keine Verschwendung.", + "mealbox.ready": "Fertiges Essen wartet – das schnellste Abendessen im Haus.", "common.remove": "Entfernen", "common.all": "Alle", "auth.tagline": "Weniger Verschwendung, weniger Stress, mehr im Geldbeutel", diff --git a/apps/mobile/src/locales/en/common.json b/apps/mobile/src/locales/en/common.json index 23f9c43..3607622 100644 --- a/apps/mobile/src/locales/en/common.json +++ b/apps/mobile/src/locales/en/common.json @@ -51,6 +51,8 @@ "common.add": "Add", "common.back": "Back", "common.cancel": "Cancel", + "mealbox.eatBy": "Eat this meal box by {date} at the latest. Zero cooking, zero waste.", + "mealbox.ready": "Ready-made food waiting – the fastest dinner in the house.", "common.remove": "Remove", "common.all": "All", "auth.tagline": "Less waste, less stress, more left in your wallet", diff --git a/apps/mobile/src/locales/es/common.json b/apps/mobile/src/locales/es/common.json index 8b94815..64bc2c3 100644 --- a/apps/mobile/src/locales/es/common.json +++ b/apps/mobile/src/locales/es/common.json @@ -51,6 +51,8 @@ "common.add": "Añadir", "common.back": "Atrás", "common.cancel": "Cancelar", + "mealbox.eatBy": "Cómete esta tarrina como muy tarde el {date}. Cero cocina, cero desperdicio.", + "mealbox.ready": "Comida lista esperando: la cena más rápida de casa.", "common.remove": "Eliminar", "common.all": "Todos", "auth.tagline": "Menos desperdicio, menos estrés, más dinero en el bolsillo", diff --git a/apps/mobile/src/locales/fi/common.json b/apps/mobile/src/locales/fi/common.json index 72a0211..4b3f67b 100644 --- a/apps/mobile/src/locales/fi/common.json +++ b/apps/mobile/src/locales/fi/common.json @@ -51,6 +51,8 @@ "common.add": "Lisää", "common.back": "Takaisin", "common.cancel": "Peruuta", + "mealbox.eatBy": "Syö tämä ateriarasia viimeistään {date}. Ei ruoanlaittoa, ei hävikkiä.", + "mealbox.ready": "Valmista ruokaa odottamassa – talon nopein illallinen.", "common.remove": "Poista", "common.all": "Kaikki", "auth.tagline": "Vähemmän hävikkiä, vähemmän stressiä, enemmän jää lompakkoon", diff --git a/apps/mobile/src/locales/fr/common.json b/apps/mobile/src/locales/fr/common.json index d3c5719..b2ba88e 100644 --- a/apps/mobile/src/locales/fr/common.json +++ b/apps/mobile/src/locales/fr/common.json @@ -51,6 +51,8 @@ "common.add": "Ajouter", "common.back": "Retour", "common.cancel": "Annuler", + "mealbox.eatBy": "À manger avant le {date}. Zéro cuisine, zéro gaspillage.", + "mealbox.ready": "Un plat tout prêt qui attend – le dîner le plus rapide de la maison.", "common.remove": "Supprimer", "common.all": "Tous", "auth.tagline": "Moins de gaspillage, moins de stress, plus d'argent dans votre portefeuille", diff --git a/apps/mobile/src/locales/it/common.json b/apps/mobile/src/locales/it/common.json index 2dfd4f2..a294d16 100644 --- a/apps/mobile/src/locales/it/common.json +++ b/apps/mobile/src/locales/it/common.json @@ -51,6 +51,8 @@ "common.add": "Aggiungi", "common.back": "Indietro", "common.cancel": "Annulla", + "mealbox.eatBy": "Consuma questo pasto entro il {date}. Zero cucina, zero sprechi.", + "mealbox.ready": "Cibo pronto in attesa: la cena più veloce di casa.", "common.remove": "Rimuovi", "common.all": "Tutti", "auth.tagline": "Meno sprechi, meno stress, più soldi in tasca", diff --git a/apps/mobile/src/locales/nb/common.json b/apps/mobile/src/locales/nb/common.json index 1660d9d..4b4143c 100644 --- a/apps/mobile/src/locales/nb/common.json +++ b/apps/mobile/src/locales/nb/common.json @@ -51,6 +51,8 @@ "common.add": "Legg til", "common.back": "Tilbake", "common.cancel": "Avbryt", + "mealbox.eatBy": "Spis denne matboksen innen {date}. Null matlaging, null svinn.", + "mealbox.ready": "Ferdig mat som venter – husets raskeste middag.", "common.remove": "Fjern", "common.all": "Alle", "auth.tagline": "Mindre svinn, mindre stress, mer igjen i lommeboka", diff --git a/apps/mobile/src/locales/nl/common.json b/apps/mobile/src/locales/nl/common.json index f0a74a5..ae0304a 100644 --- a/apps/mobile/src/locales/nl/common.json +++ b/apps/mobile/src/locales/nl/common.json @@ -51,6 +51,8 @@ "common.add": "Toevoegen", "common.back": "Terug", "common.cancel": "Annuleren", + "mealbox.eatBy": "Eet deze maaltijdbox uiterlijk op {date}. Geen kookwerk, geen verspilling.", + "mealbox.ready": "Kant-en-klaar eten dat wacht – het snelste avondeten in huis.", "common.remove": "Verwijderen", "common.all": "Alle", "auth.tagline": "Minder verspilling, minder stress, meer over in je portemonnee", diff --git a/apps/mobile/src/locales/pl/common.json b/apps/mobile/src/locales/pl/common.json index 4f75d6f..7328236 100644 --- a/apps/mobile/src/locales/pl/common.json +++ b/apps/mobile/src/locales/pl/common.json @@ -51,6 +51,8 @@ "common.add": "Dodaj", "common.back": "Wstecz", "common.cancel": "Anuluj", + "mealbox.eatBy": "Zjedz ten pojemnik najpóźniej {date}. Zero gotowania, zero marnowania.", + "mealbox.ready": "Gotowe jedzenie czeka – najszybsza kolacja w domu.", "common.remove": "Usuń", "common.all": "Wszystkie", "auth.tagline": "Mniej marnowania, mniej stresu, więcej w portfelu", diff --git a/apps/mobile/src/locales/pt/common.json b/apps/mobile/src/locales/pt/common.json index dc216ca..4268d9d 100644 --- a/apps/mobile/src/locales/pt/common.json +++ b/apps/mobile/src/locales/pt/common.json @@ -51,6 +51,8 @@ "common.add": "Adicionar", "common.back": "Voltar", "common.cancel": "Cancelar", + "mealbox.eatBy": "Come esta refeição até {date}, o mais tardar. Zero cozinha, zero desperdício.", + "mealbox.ready": "Comida pronta à espera – o jantar mais rápido cá de casa.", "common.remove": "Remover", "common.all": "Todos", "auth.tagline": "Menos desperdício, menos stress, mais dinheiro na carteira", diff --git a/apps/mobile/src/locales/sv/common.json b/apps/mobile/src/locales/sv/common.json index 8ba71d6..3cc88ae 100644 --- a/apps/mobile/src/locales/sv/common.json +++ b/apps/mobile/src/locales/sv/common.json @@ -51,6 +51,8 @@ "common.add": "Lägg till", "common.back": "Tillbaka", "common.cancel": "Avbryt", + "mealbox.eatBy": "Matlådan bör ätas senast {date}. Noll matlagning, noll svinn.", + "mealbox.ready": "Färdig mat som väntar – snabbaste middagen i huset.", "common.remove": "Ta bort", "common.all": "Alla", "auth.tagline": "Mindre svinn, mindre stress, mer kvar i plånboken", diff --git a/packages/recommendation-engine/src/scoring.ts b/packages/recommendation-engine/src/scoring.ts index c4322f8..b6308c1 100644 --- a/packages/recommendation-engine/src/scoring.ts +++ b/packages/recommendation-engine/src/scoring.ts @@ -8,7 +8,7 @@ import type { ScoringWeights, } from "./types.js"; import { DEFAULT_WEIGHTS } from "./types.js"; -import { buildWhySv } from "./explain.js"; +import { buildWhy } from "./explain.js"; /** * Poängsätt en kandidat (0–100-skala per delkomponent, viktad summa). @@ -111,7 +111,8 @@ export function scoreCandidate( // 16. Måltids-lämplighet: nedvikta lätta övergångsrätter (frukost/dessert) vid huvudmåltid. const mainMeal = ctx.mealType === "dinner" || ctx.mealType === "lunch"; const crossover = - (candidate.mealTypes ?? []).includes("breakfast") || (candidate.mealTypes ?? []).includes("dessert"); + (candidate.mealTypes ?? []).includes("breakfast") || + (candidate.mealTypes ?? []).includes("dessert"); parts.mealFit = mainMeal && crossover ? 0.15 : 1; // 17. Diet-affinitet (spec §diet): mjuk rankning mot matvanan. Aldrig ett hårt @@ -125,7 +126,7 @@ export function scoreCandidate( titleSv: candidate.titleSv, score: Math.round(score * 10) / 10, parts, - whySv: buildWhySv(candidate, ctx, parts, provenance), + whySv: buildWhy(candidate, ctx, parts, provenance, ctx.languageTag ?? "sv-SE"), missingIngredients: candidate.coverage.missing .filter((m) => !m.optional) .map((m) => m.displayNameSv), diff --git a/packages/recommendation-engine/src/types.ts b/packages/recommendation-engine/src/types.ts index 4556af9..39dcf7c 100644 --- a/packages/recommendation-engine/src/types.ts +++ b/packages/recommendation-engine/src/types.ts @@ -82,6 +82,8 @@ export interface RecommendationContext { cookingAssumptions?: CookingAssumption[]; /** Hård grind: false = ignorera alla personliga signaler. */ personalizationEnabled: boolean; + /** Användarens språk (BCP 47) för why-texten. Default sv-SE. */ + languageTag?: string | undefined; } export interface ScoredRecommendation {