From 2076bbc15b47d2aee223ed8641ce3f87b52d1013 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 02:32:31 +0000 Subject: [PATCH] =?UTF-8?q?i18n(rekommendationer):=20localisera=20?= =?UTF-8?q?=F0=9F=92=A1-raden=20+=20meal-box-text=20(12=20spr=C3=A5k)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 💡 why-texten: what-to-eat skickar nu anvĂ€ndarens languageTag till motorn; scoring anvĂ€nder buildWhy(..., ctx.languageTag) i stĂ€llet för hĂ„rdkodat buildWhySv. WHY_TEMPLATES har redan alla 12 sprĂ„k – felet var bara att sv-SE var fast. Default sv-SE = oförĂ€ndrat beteende (sĂ€kert). (Rest: utgĂ„r-ingrediensens NAMN i meningen Ă€r fortf. svenskt – motorn har bara svenska namn; noterat.) - meal-box-förslagens text: whySv-strĂ€ng -> useByUrgent-flagga + mealbox.eatBy/ready i alla 12 sprĂ„k (klienten renderar via t()). Co-Authored-By: Claude --- apps/api/src/routes/recommendations.ts | 15 ++++----------- apps/mobile/src/app/(tabs)/index.tsx | 8 ++++++-- apps/mobile/src/locales/da/common.json | 2 ++ apps/mobile/src/locales/de/common.json | 2 ++ apps/mobile/src/locales/en/common.json | 2 ++ apps/mobile/src/locales/es/common.json | 2 ++ apps/mobile/src/locales/fi/common.json | 2 ++ apps/mobile/src/locales/fr/common.json | 2 ++ apps/mobile/src/locales/it/common.json | 2 ++ apps/mobile/src/locales/nb/common.json | 2 ++ apps/mobile/src/locales/nl/common.json | 2 ++ apps/mobile/src/locales/pl/common.json | 2 ++ apps/mobile/src/locales/pt/common.json | 2 ++ apps/mobile/src/locales/sv/common.json | 2 ++ packages/recommendation-engine/src/scoring.ts | 7 ++++--- packages/recommendation-engine/src/types.ts | 2 ++ 16 files changed, 40 insertions(+), 16 deletions(-) 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 {