i18n(rekommendationer): localisera 💡-raden + meal-box-text (12 språk)

- 💡 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 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-21 02:32:31 +00:00
parent 6d36ec7459
commit 2076bbc15b
16 changed files with 40 additions and 16 deletions
@@ -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 (0100-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),
@@ -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 {