i18n(hemskärm): översätt rekommendationstitlar + ingrediensnamn + hårdkodade strängar
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Map<string, string>> {
|
||||
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<string, string>();
|
||||
// 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).
|
||||
|
||||
@@ -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<string, string>();
|
||||
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,
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user