feat: German translations for TheMealDB recipes + recipe detail view
- Add GermanTranslator service with 200+ ingredient and meal name mappings - Translate titles and ingredients when fetching from TheMealDB - Add click-to-view recipe detail modal on week plan meal cards - Import RecipeDetail component in WeekPlanView Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
188
backend/Services/GermanTranslator.cs
Normal file
188
backend/Services/GermanTranslator.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using MealPlanner.Models;
|
||||||
|
|
||||||
|
namespace MealPlanner.Services;
|
||||||
|
|
||||||
|
public static class GermanTranslator
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, string> MealNames = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
// Common TheMealDB meal titles
|
||||||
|
{"Beef", "Rind"}, {"Chicken", "Hähnchen"}, {"Pork", "Schwein"}, {"Lamb", "Lamm"},
|
||||||
|
{"Fish", "Fisch"}, {"Salmon", "Lachs"}, {"Tuna", "Thunfisch"}, {"Shrimp", "Garnelen"},
|
||||||
|
{"Prawn", "Garnelen"}, {"Turkey", "Truthahn"}, {"Duck", "Ente"}, {"Veal", "Kalb"},
|
||||||
|
{"Steak", "Steak"}, {"Roast", "Braten"}, {"Grilled", "Gegrilltes"}, {"Fried", "Gebratenes"},
|
||||||
|
{"Baked", "Gebackenes"}, {"Braised", "Geschmortes"}, {"Smoked", "Geräuchertes"},
|
||||||
|
{"Soup", "Suppe"}, {"Stew", "Eintopf"}, {"Salad", "Salat"}, {"Pie", "Pastete"},
|
||||||
|
{"Pasta", "Pasta"}, {"Noodles", "Nudeln"}, {"Rice", "Reis"}, {"Bread", "Brot"},
|
||||||
|
{"Cake", "Kuchen"}, {"Pancake", "Pfannkuchen"}, {"Pancakes", "Pfannkuchen"},
|
||||||
|
{"Curry", "Curry"}, {"Casserole", "Auflauf"}, {"Sandwich", "Sandwich"},
|
||||||
|
{"Wrap", "Wrap"}, {"Tacos", "Tacos"}, {"Burrito", "Burrito"},
|
||||||
|
{"Spaghetti", "Spaghetti"}, {"Lasagne", "Lasagne"}, {"Risotto", "Risotto"},
|
||||||
|
{"Pizza", "Pizza"}, {"Burger", "Burger"}, {"Meatballs", "Fleischbällchen"},
|
||||||
|
{"Teriyaki", "Teriyaki"}, {"Sweet", "Süß"}, {"Spicy", "Scharf"},
|
||||||
|
{"Creamy", "Cremig"}, {"Crispy", "Knusprig"}, {"Honey", "Honig"},
|
||||||
|
{"Garlic", "Knoblauch"}, {"Lemon", "Zitronen"}, {"Mushroom", "Pilz"},
|
||||||
|
{"Tomato", "Tomaten"}, {"Potato", "Kartoffel"}, {"Potatoes", "Kartoffeln"},
|
||||||
|
{"Cheese", "Käse"}, {"Cream", "Sahne"}, {"Butter", "Butter"},
|
||||||
|
{"Chocolate", "Schokoladen"}, {"Caramel", "Karamell"}, {"Vanilla", "Vanille"},
|
||||||
|
{"Apple", "Apfel"}, {"Orange", "Orangen"}, {"Banana", "Bananen"},
|
||||||
|
{"Beans", "Bohnen"}, {"Lentil", "Linsen"}, {"Lentils", "Linsen"},
|
||||||
|
{"Vegetable", "Gemüse"}, {"Vegetables", "Gemüse"},
|
||||||
|
{"with", "mit"}, {"and", "und"}, {"in", "in"}, {"on", "auf"},
|
||||||
|
{"Sauce", "Soße"}, {"Gravy", "Bratensaft"}, {"Dressing", "Dressing"},
|
||||||
|
{"Stuffed", "Gefüllte"}, {"Roasted", "Geröstete"}, {"Sautéed", "Sautierte"},
|
||||||
|
{"Chili", "Chili"}, {"Ginger", "Ingwer"}, {"Coconut", "Kokos"},
|
||||||
|
{"Spinach", "Spinat"}, {"Broccoli", "Brokkoli"}, {"Carrot", "Karotten"},
|
||||||
|
{"Onion", "Zwiebel"}, {"Pepper", "Pfeffer"}, {"Peppers", "Paprika"},
|
||||||
|
{"Corn", "Mais"}, {"Peas", "Erbsen"}, {"Cabbage", "Kohl"},
|
||||||
|
{"Eggplant", "Aubergine"}, {"Zucchini", "Zucchini"}, {"Celery", "Sellerie"},
|
||||||
|
{"Parsley", "Petersilie"}, {"Basil", "Basilikum"}, {"Thyme", "Thymian"},
|
||||||
|
{"Oregano", "Oregano"}, {"Rosemary", "Rosmarin"}, {"Cinnamon", "Zimt"},
|
||||||
|
{"Nutmeg", "Muskatnuss"}, {"Cumin", "Kreuzkümmel"}, {"Paprika", "Paprika"},
|
||||||
|
{"Soy", "Soja"}, {"Sesame", "Sesam"}, {"Peanut", "Erdnuss"},
|
||||||
|
{"Almond", "Mandel"}, {"Walnut", "Walnuss"},
|
||||||
|
{"Egg", "Ei"}, {"Eggs", "Eier"}, {"Milk", "Milch"}, {"Yogurt", "Joghurt"},
|
||||||
|
{"Oil", "Öl"}, {"Olive", "Oliven"}, {"Vinegar", "Essig"},
|
||||||
|
{"Sugar", "Zucker"}, {"Salt", "Salz"}, {"Flour", "Mehl"},
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> Ingredients = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
// All from above plus more specific ingredient names
|
||||||
|
{"Beef", "Rindfleisch"}, {"Chicken", "Hähnchen"}, {"Chicken Breast", "Hähnchenbrust"},
|
||||||
|
{"Chicken Thighs", "Hähnchenschenkel"}, {"Chicken Stock", "Hühnerbrühe"},
|
||||||
|
{"Minced Beef", "Rinderhackfleisch"}, {"Ground Beef", "Rinderhackfleisch"},
|
||||||
|
{"Pork", "Schweinefleisch"}, {"Bacon", "Speck"}, {"Sausage", "Wurst"},
|
||||||
|
{"Lamb", "Lammfleisch"}, {"Fish", "Fisch"}, {"Salmon", "Lachs"},
|
||||||
|
{"Tuna", "Thunfisch"}, {"Shrimp", "Garnelen"}, {"Prawns", "Garnelen"},
|
||||||
|
{"Butter", "Butter"}, {"Olive Oil", "Olivenöl"}, {"Vegetable Oil", "Pflanzenöl"},
|
||||||
|
{"Sesame Oil", "Sesamöl"}, {"Coconut Oil", "Kokosöl"},
|
||||||
|
{"Milk", "Milch"}, {"Cream", "Sahne"}, {"Sour Cream", "Saure Sahne"},
|
||||||
|
{"Heavy Cream", "Schlagsahne"}, {"Double Cream", "Sahne"},
|
||||||
|
{"Cheese", "Käse"}, {"Parmesan", "Parmesan"}, {"Cheddar", "Cheddar"},
|
||||||
|
{"Mozzarella", "Mozzarella"}, {"Feta", "Feta"}, {"Cream Cheese", "Frischkäse"},
|
||||||
|
{"Yogurt", "Joghurt"}, {"Yoghurt", "Joghurt"}, {"Egg", "Ei"}, {"Eggs", "Eier"},
|
||||||
|
{"Egg Yolks", "Eigelb"}, {"Egg Whites", "Eiweiß"},
|
||||||
|
{"Onion", "Zwiebel"}, {"Onions", "Zwiebeln"}, {"Red Onion", "Rote Zwiebel"},
|
||||||
|
{"Garlic", "Knoblauch"}, {"Garlic Clove", "Knoblauchzehe"},
|
||||||
|
{"Garlic Cloves", "Knoblauchzehen"}, {"Garlic Minced", "Knoblauch gehackt"},
|
||||||
|
{"Ginger", "Ingwer"}, {"Celery", "Sellerie"}, {"Carrot", "Karotte"},
|
||||||
|
{"Carrots", "Karotten"}, {"Potato", "Kartoffel"}, {"Potatoes", "Kartoffeln"},
|
||||||
|
{"Sweet Potato", "Süßkartoffel"}, {"Tomato", "Tomate"}, {"Tomatoes", "Tomaten"},
|
||||||
|
{"Cherry Tomatoes", "Kirschtomaten"}, {"Tomato Paste", "Tomatenmark"},
|
||||||
|
{"Tomato Puree", "Tomatenmark"}, {"Chopped Tomatoes", "Gehackte Tomaten"},
|
||||||
|
{"Tomato Sauce", "Tomatensoße"}, {"Passata", "Passata"},
|
||||||
|
{"Pepper", "Pfeffer"}, {"Bell Pepper", "Paprika"}, {"Red Pepper", "Rote Paprika"},
|
||||||
|
{"Green Pepper", "Grüne Paprika"}, {"Chilli", "Chili"}, {"Chili", "Chili"},
|
||||||
|
{"Chilli Powder", "Chilipulver"}, {"Chili Flakes", "Chiliflocken"},
|
||||||
|
{"Mushroom", "Pilze"}, {"Mushrooms", "Pilze"},
|
||||||
|
{"Spinach", "Spinat"}, {"Broccoli", "Brokkoli"}, {"Cauliflower", "Blumenkohl"},
|
||||||
|
{"Cabbage", "Kohl"}, {"Lettuce", "Salat"}, {"Cucumber", "Gurke"},
|
||||||
|
{"Zucchini", "Zucchini"}, {"Courgette", "Zucchini"}, {"Eggplant", "Aubergine"},
|
||||||
|
{"Aubergine", "Aubergine"}, {"Corn", "Mais"}, {"Peas", "Erbsen"},
|
||||||
|
{"Green Beans", "Grüne Bohnen"}, {"Kidney Beans", "Kidneybohnen"},
|
||||||
|
{"Chickpeas", "Kichererbsen"}, {"Lentils", "Linsen"},
|
||||||
|
{"Rice", "Reis"}, {"Basmati Rice", "Basmatireis"}, {"Long Grain Rice", "Langkornreis"},
|
||||||
|
{"Pasta", "Nudeln"}, {"Spaghetti", "Spaghetti"}, {"Penne", "Penne"},
|
||||||
|
{"Noodles", "Nudeln"}, {"Lasagne Sheets", "Lasagneplatten"},
|
||||||
|
{"Bread", "Brot"}, {"Breadcrumbs", "Semmelbrösel"}, {"Flour", "Mehl"},
|
||||||
|
{"Plain Flour", "Mehl"}, {"Self Raising Flour", "Mehl mit Backpulver"},
|
||||||
|
{"Cornflour", "Speisestärke"}, {"Cornstarch", "Speisestärke"},
|
||||||
|
{"Baking Powder", "Backpulver"}, {"Baking Soda", "Natron"},
|
||||||
|
{"Sugar", "Zucker"}, {"Brown Sugar", "Brauner Zucker"}, {"Caster Sugar", "Feiner Zucker"},
|
||||||
|
{"Icing Sugar", "Puderzucker"}, {"Honey", "Honig"}, {"Maple Syrup", "Ahornsirup"},
|
||||||
|
{"Salt", "Salz"}, {"Black Pepper", "Schwarzer Pfeffer"},
|
||||||
|
{"Cumin", "Kreuzkümmel"}, {"Paprika", "Paprika"}, {"Turmeric", "Kurkuma"},
|
||||||
|
{"Coriander", "Koriander"}, {"Cinnamon", "Zimt"}, {"Nutmeg", "Muskatnuss"},
|
||||||
|
{"Oregano", "Oregano"}, {"Basil", "Basilikum"}, {"Thyme", "Thymian"},
|
||||||
|
{"Rosemary", "Rosmarin"}, {"Parsley", "Petersilie"}, {"Bay Leaf", "Lorbeerblatt"},
|
||||||
|
{"Bay Leaves", "Lorbeerblätter"}, {"Mint", "Minze"}, {"Dill", "Dill"},
|
||||||
|
{"Chives", "Schnittlauch"}, {"Cilantro", "Koriander"},
|
||||||
|
{"Soy Sauce", "Sojasoße"}, {"Worcestershire Sauce", "Worcestersoße"},
|
||||||
|
{"Fish Sauce", "Fischsoße"}, {"Hot Sauce", "Scharfe Soße"},
|
||||||
|
{"Vinegar", "Essig"}, {"Balsamic Vinegar", "Balsamico-Essig"},
|
||||||
|
{"Red Wine Vinegar", "Rotweinessig"}, {"White Wine Vinegar", "Weißweinessig"},
|
||||||
|
{"Lemon", "Zitrone"}, {"Lemon Juice", "Zitronensaft"}, {"Lime", "Limette"},
|
||||||
|
{"Lime Juice", "Limettensaft"}, {"Orange", "Orange"}, {"Orange Juice", "Orangensaft"},
|
||||||
|
{"Wine", "Wein"}, {"Red Wine", "Rotwein"}, {"White Wine", "Weißwein"},
|
||||||
|
{"Beer", "Bier"}, {"Coconut Milk", "Kokosmilch"}, {"Stock", "Brühe"},
|
||||||
|
{"Beef Stock", "Rinderbrühe"}, {"Vegetable Stock", "Gemüsebrühe"},
|
||||||
|
{"Water", "Wasser"}, {"Ice", "Eis"},
|
||||||
|
{"Chocolate", "Schokolade"}, {"Dark Chocolate", "Zartbitterschokolade"},
|
||||||
|
{"Cocoa", "Kakao"}, {"Vanilla", "Vanille"}, {"Vanilla Extract", "Vanilleextrakt"},
|
||||||
|
{"Apple", "Apfel"}, {"Banana", "Banane"}, {"Strawberries", "Erdbeeren"},
|
||||||
|
{"Blueberries", "Blaubeeren"}, {"Raisins", "Rosinen"},
|
||||||
|
{"Almonds", "Mandeln"}, {"Walnuts", "Walnüsse"}, {"Peanuts", "Erdnüsse"},
|
||||||
|
{"Peanut Butter", "Erdnussbutter"}, {"Pine Nuts", "Pinienkerne"},
|
||||||
|
{"Sesame Seeds", "Sesamkörner"}, {"Sunflower Oil", "Sonnenblumenöl"},
|
||||||
|
{"Spring Onions", "Frühlingszwiebeln"}, {"Leek", "Lauch"},
|
||||||
|
{"Fennel", "Fenchel"}, {"Avocado", "Avocado"}, {"Olives", "Oliven"},
|
||||||
|
{"Capers", "Kapern"}, {"Anchovies", "Sardellen"},
|
||||||
|
{"Tortilla", "Tortilla"}, {"Pita", "Fladenbrot"}, {"Naan", "Naan"},
|
||||||
|
{"Tofu", "Tofu"}, {"Coconut", "Kokosnuss"}, {"Coconut Cream", "Kokoscreme"},
|
||||||
|
{"Curry Powder", "Currypulver"}, {"Garam Masala", "Garam Masala"},
|
||||||
|
{"Mustard", "Senf"}, {"Dijon Mustard", "Dijon-Senf"}, {"Ketchup", "Ketchup"},
|
||||||
|
{"Mayonnaise", "Mayonnaise"}, {"Sriracha", "Sriracha"},
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string TranslateTitle(string title)
|
||||||
|
{
|
||||||
|
// Word-by-word translation for meal titles
|
||||||
|
var words = title.Split(' ');
|
||||||
|
var translated = new List<string>();
|
||||||
|
int i = 0;
|
||||||
|
while (i < words.Length)
|
||||||
|
{
|
||||||
|
// Try two-word match first
|
||||||
|
if (i + 1 < words.Length)
|
||||||
|
{
|
||||||
|
var twoWord = $"{words[i]} {words[i + 1]}";
|
||||||
|
if (MealNames.TryGetValue(twoWord, out var tw))
|
||||||
|
{
|
||||||
|
translated.Add(tw);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (MealNames.TryGetValue(words[i], out var single))
|
||||||
|
translated.Add(single);
|
||||||
|
else
|
||||||
|
translated.Add(words[i]); // Keep original if no translation
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return string.Join(' ', translated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TranslateIngredient(string name)
|
||||||
|
{
|
||||||
|
// Try exact match first
|
||||||
|
if (Ingredients.TryGetValue(name, out var exact)) return exact;
|
||||||
|
|
||||||
|
// Try two-word, three-word combos from start
|
||||||
|
var words = name.Split(' ');
|
||||||
|
if (words.Length >= 3)
|
||||||
|
{
|
||||||
|
var three = $"{words[0]} {words[1]} {words[2]}";
|
||||||
|
if (Ingredients.TryGetValue(three, out var tw3)) return tw3;
|
||||||
|
}
|
||||||
|
if (words.Length >= 2)
|
||||||
|
{
|
||||||
|
var two = $"{words[0]} {words[1]}";
|
||||||
|
if (Ingredients.TryGetValue(two, out var tw2)) return tw2;
|
||||||
|
}
|
||||||
|
// Single word
|
||||||
|
if (words.Length >= 1 && Ingredients.TryGetValue(words[0], out var sw)) return sw;
|
||||||
|
|
||||||
|
return name; // Keep original
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Recipe TranslateRecipe(Recipe recipe)
|
||||||
|
{
|
||||||
|
recipe.Title = TranslateTitle(recipe.Title);
|
||||||
|
foreach (var ing in recipe.Ingredients)
|
||||||
|
{
|
||||||
|
ing.Name = TranslateIngredient(ing.Name);
|
||||||
|
}
|
||||||
|
return recipe;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ public class TheMealDbClient(HttpClient httpClient)
|
|||||||
recipe.Ingredients.Add(ingredient);
|
recipe.Ingredients.Add(ingredient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GermanTranslator.TranslateRecipe(recipe);
|
||||||
return recipe;
|
return recipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
<div class="relative w-full h-40 bg-zinc-800 overflow-hidden">
|
<div class="relative w-full h-40 bg-zinc-800 overflow-hidden cursor-pointer" @click="$emit('click')">
|
||||||
<img
|
<img
|
||||||
v-if="entry.recipe.imageUrl"
|
v-if="entry.recipe.imageUrl"
|
||||||
:src="entry.recipe.imageUrl"
|
:src="entry.recipe.imageUrl"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 px-4 py-3">
|
<div class="flex-1 px-4 py-3 cursor-pointer" @click="$emit('click')">
|
||||||
<h3 class="text-zinc-100 font-medium text-base leading-snug line-clamp-2">{{ entry.recipe.title }}</h3>
|
<h3 class="text-zinc-100 font-medium text-base leading-snug line-clamp-2">{{ entry.recipe.title }}</h3>
|
||||||
<p v-if="entry.recipe.ingredients?.length" class="mt-1 text-zinc-500 text-xs">
|
<p v-if="entry.recipe.ingredients?.length" class="mt-1 text-zinc-500 text-xs">
|
||||||
{{ entry.recipe.ingredients.length }} Zutaten
|
{{ entry.recipe.ingredients.length }} Zutaten
|
||||||
@@ -76,6 +76,7 @@ defineProps<{
|
|||||||
defineEmits<{
|
defineEmits<{
|
||||||
swap: [entry: MealPlanEntry]
|
swap: [entry: MealPlanEntry]
|
||||||
reroll: [entry: MealPlanEntry]
|
reroll: [entry: MealPlanEntry]
|
||||||
|
click: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const imgError = ref(false)
|
const imgError = ref(false)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
:day-name="getDayName(entry.date)"
|
:day-name="getDayName(entry.date)"
|
||||||
@swap="openSwapModal(entry)"
|
@swap="openSwapModal(entry)"
|
||||||
@reroll="handleReroll(entry)"
|
@reroll="handleReroll(entry)"
|
||||||
|
@click="selectedRecipe = entry.recipe"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,6 +99,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recipe detail modal -->
|
||||||
|
<RecipeDetail
|
||||||
|
v-if="selectedRecipe"
|
||||||
|
:recipe="selectedRecipe"
|
||||||
|
@close="selectedRecipe = null"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Swap modal -->
|
<!-- Swap modal -->
|
||||||
<SwapModal
|
<SwapModal
|
||||||
v-if="swapEntry && recipesStore.recipes.length"
|
v-if="swapEntry && recipesStore.recipes.length"
|
||||||
@@ -110,14 +118,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import MealCard from '../components/MealCard.vue'
|
import MealCard from '../components/MealCard.vue'
|
||||||
|
import RecipeDetail from '../components/RecipeDetail.vue'
|
||||||
import SwapModal from '../components/SwapModal.vue'
|
import SwapModal from '../components/SwapModal.vue'
|
||||||
import { useMealPlanStore, useRecipesStore } from '../stores/mealPlan'
|
import { useMealPlanStore, useRecipesStore } from '../stores/mealPlan'
|
||||||
import type { MealPlanEntry } from '../stores/mealPlan'
|
import type { MealPlanEntry, Recipe } from '../stores/mealPlan'
|
||||||
|
|
||||||
const store = useMealPlanStore()
|
const store = useMealPlanStore()
|
||||||
const recipesStore = useRecipesStore()
|
const recipesStore = useRecipesStore()
|
||||||
|
|
||||||
const swapEntry = ref<MealPlanEntry | null>(null)
|
const swapEntry = ref<MealPlanEntry | null>(null)
|
||||||
|
const selectedRecipe = ref<Recipe | null>(null)
|
||||||
|
|
||||||
const DAY_NAMES = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
|
const DAY_NAMES = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user