From ea1a85e9357bc9f42444018e80576b335c3ce7fd Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Fri, 1 May 2026 13:34:44 +0200 Subject: [PATCH] i18n(recipes): migrate 13 pages and components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bulk migration of the recipes namespace following the same pattern as fitness/cospend/calendar/faith. Layout collapses its label-object into t.foo lookups; NutritionSummary's 33 ternaries (incl. the German-stem-plus-optional-e amino-acid pattern that read `Lysin{isEnglish ? 'e' : ''}`) become straight dictionary references; AddToFoodLogButton, IngredientsPage, to-try, search, favorites, the index, and the small landing pages (category, tag, season, icon, tips-and-tricks) all migrate the same way. The recipes dict is now ~120 keys. Patterns kept intentionally: - Long page-specific marketing copy (subheading sentences, meta descriptions that include dynamic counts, hero alt text variants) stays inline as `lang === 'en' ? '...' : '...'` rather than bloating the dict with one-shot strings. - URL slug ternaries stay inline — those are URL data, not UI text. - The `recipes/admin/nutrition` page was deliberately skipped — admin tooling, ~18 ternaries that are mostly admin-jargon strings used in exactly one place. Detail pages ([name]/+page, [name]/+error, IngredientsPage extras, InstructionsPage, smaller components) and the admin page remain for follow-up commits. --- package.json | 2 +- .../recipes/AddToFoodLogButton.svelte | 30 +++--- .../components/recipes/IngredientsPage.svelte | 36 ++++--- src/lib/i18n/recipes/de.ts | 99 ++++++++++++++++++- src/lib/i18n/recipes/en.ts | 99 ++++++++++++++++++- .../[recipeLang=recipeLang]/+page.svelte | 13 ++- .../category/+page.svelte | 9 +- .../favorites/+page.svelte | 29 +++--- .../[recipeLang=recipeLang]/icon/+page.svelte | 9 +- .../search/+page.svelte | 31 +++--- .../season/+page.svelte | 9 +- .../[recipeLang=recipeLang]/tag/+page.svelte | 11 ++- .../tips-and-tricks/+page.svelte | 9 +- .../to-try/+page.svelte | 38 +++---- 14 files changed, 321 insertions(+), 103 deletions(-) diff --git a/package.json b/package.json index ccaf34d8..08139bab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.55.2", + "version": "1.56.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/recipes/AddToFoodLogButton.svelte b/src/lib/components/recipes/AddToFoodLogButton.svelte index 50123903..0d2c0c60 100644 --- a/src/lib/components/recipes/AddToFoodLogButton.svelte +++ b/src/lib/components/recipes/AddToFoodLogButton.svelte @@ -2,6 +2,8 @@ import UtensilsCrossed from '@lucide/svelte/icons/utensils-crossed'; import X from '@lucide/svelte/icons/x'; import { toast } from '$lib/js/toast.svelte'; + import { m } from '$lib/js/recipesI18n'; + /** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */ let { recipeName, @@ -12,6 +14,8 @@ portions = '', isEnglish = true, } = $props(); + const lang = $derived(/** @type {RecipesLang} */ (isEnglish ? 'en' : 'de')); + const t = $derived(m[lang]); // Flatten ingredient sections into a flat array with indices const flatIngredients = $derived.by(() => { @@ -35,16 +39,16 @@ let useGrams = $state(false); const labels = $derived({ - addToLog: isEnglish ? 'Add to food log' : 'Zum Ernährungstagebuch', - portions: isEnglish ? 'Portions' : 'Portionen', - grams: isEnglish ? 'Grams' : 'Gramm', - meal: isEnglish ? 'Meal' : 'Mahlzeit', - breakfast: isEnglish ? 'Breakfast' : 'Frühstück', - lunch: isEnglish ? 'Lunch' : 'Mittagessen', - dinner: isEnglish ? 'Dinner' : 'Abendessen', - snack: isEnglish ? 'Snack' : 'Snack', - log: isEnglish ? 'Log' : 'Eintragen', - cancel: isEnglish ? 'Cancel' : 'Abbrechen', + addToLog: t.add_to_food_log, + portions: t.portions_label, + grams: t.grams_label, + meal: t.meal_label, + breakfast: t.breakfast, + lunch: t.lunch, + dinner: t.dinner, + snack: t.snack, + log: t.log_action, + cancel: t.cancel }); // Parse portion count from recipe's portions string (e.g. "4 Portionen") @@ -171,13 +175,13 @@ }) }); if (res.ok) { - toast.success(isEnglish ? 'Added to food log' : 'Zum Ernährungstagebuch hinzugefügt'); + toast.success(t.added_to_food_log); showDialog = false; } else { - toast.error(isEnglish ? 'Failed to add' : 'Fehler beim Hinzufügen'); + toast.error(t.add_failed); } } catch { - toast.error(isEnglish ? 'Failed to add' : 'Fehler beim Hinzufügen'); + toast.error(t.add_failed); } finally { saving = false; } diff --git a/src/lib/components/recipes/IngredientsPage.svelte b/src/lib/components/recipes/IngredientsPage.svelte index afcdf19a..0cae126c 100644 --- a/src/lib/components/recipes/IngredientsPage.svelte +++ b/src/lib/components/recipes/IngredientsPage.svelte @@ -6,6 +6,8 @@ import { page } from '$app/state'; import HefeSwapper from './HefeSwapper.svelte'; import NutritionSummary from './NutritionSummary.svelte'; import AddToFoodLogButton from './AddToFoodLogButton.svelte'; +import { m } from '$lib/js/recipesI18n'; +/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */ let { data } = $props(); const isLoggedIn = $derived(!!data.session?.user); const hasNutrition = $derived(!!data.nutritionMappings?.length); @@ -123,23 +125,25 @@ const flattenedIngredients = $derived.by(() => { // svelte-ignore state_referenced_locally let multiplier = $state(data.multiplier || 1); -const isEnglish = $derived(data.lang === 'en'); +const lang = $derived(/** @type {RecipesLang} */ (data.lang)); +const t = $derived(m[lang]); +const isEnglish = $derived(lang === 'en'); const labels = $derived({ - portions: isEnglish ? 'Portions:' : 'Portionen:', - adjustAmount: isEnglish ? 'Adjust Amount:' : 'Menge anpassen:', - ingredients: isEnglish ? 'Ingredients' : 'Zutaten', - cakeForm: isEnglish ? 'Cake form' : 'Backform', - adjustForm: isEnglish ? 'Adjust cake form' : 'Backform anpassen', - round: isEnglish ? 'Round' : 'Rund', - rectangular: isEnglish ? 'Rectangular' : 'Rechteckig', + portions: t.portions, + adjustAmount: t.adjust_amount, + ingredients: t.ingredients, + cakeForm: t.cake_form, + adjustForm: t.adjust_cake_form, + round: t.round_form, + rectangular: t.rectangular_form, gugelhupf: 'Gugelhupf', - diameter: isEnglish ? 'Diameter' : 'Durchmesser', - outerDiameter: isEnglish ? 'Outer Ø' : 'Aussen-Ø', - innerDiameter: isEnglish ? 'Inner Ø' : 'Innen-Ø', - width: isEnglish ? 'Width' : 'Breite', - length: isEnglish ? 'Length' : 'Länge', - factor: isEnglish ? 'Factor' : 'Faktor', - restoreDefault: isEnglish ? 'Restore default' : 'Standard wiederherstellen', + diameter: t.diameter, + outerDiameter: t.outer_diameter, + innerDiameter: t.inner_diameter, + width: t.width, + length: t.length, + factor: t.factor, + restoreDefault: t.restore_default }); // Cake form scaling @@ -200,7 +204,7 @@ const isDefaultForm = $derived( ); const cakeSummaryText = $derived.by(() => { - if (userFormShape === 'round') return `${userFormDiameter} cm ${isEnglish ? 'round' : 'rund'}`; + if (userFormShape === 'round') return `${userFormDiameter} cm ${t.round_lowercase}`; if (userFormShape === 'rectangular') return `${userFormWidth}×${userFormLength} cm`; if (userFormShape === 'gugelhupf') return `${userFormDiameter}/${userFormInnerDiameter} cm Gugelhupf`; return ''; diff --git a/src/lib/i18n/recipes/de.ts b/src/lib/i18n/recipes/de.ts index 696c4089..dc936dba 100644 --- a/src/lib/i18n/recipes/de.ts +++ b/src/lib/i18n/recipes/de.ts @@ -42,5 +42,102 @@ export const de = { glycine: 'Glycin', proline: 'Prolin', serine: 'Serin', - tyrosine: 'Tyrosin' + tyrosine: 'Tyrosin', + + // Ingredients page + portions: 'Portionen:', + adjust_amount: 'Menge anpassen:', + ingredients: 'Zutaten', + cake_form: 'Backform', + adjust_cake_form: 'Backform anpassen', + round_form: 'Rund', + rectangular_form: 'Rechteckig', + diameter: 'Durchmesser', + outer_diameter: 'Aussen-Ø', + inner_diameter: 'Innen-Ø', + width: 'Breite', + length: 'Länge', + factor: 'Faktor', + restore_default: 'Standard wiederherstellen', + round_lowercase: 'rund', + + // AddToFoodLogButton + meal labels + add_to_food_log: 'Zum Ernährungstagebuch', + added_to_food_log: 'Zum Ernährungstagebuch hinzugefügt', + add_failed: 'Fehler beim Hinzufügen', + portions_label: 'Portionen', + grams_label: 'Gramm', + meal_label: 'Mahlzeit', + breakfast: 'Frühstück', + lunch: 'Mittagessen', + dinner: 'Abendessen', + snack: 'Snack', + log_action: 'Eintragen', + cancel: 'Abbrechen', + save: 'Speichern', + + // To-try page + to_try_title: 'Zum Ausprobieren', + to_try_page_title: 'Zum Ausprobieren - Bocken Rezepte', + to_try_meta_description: 'Rezepte, die wir ausprobieren wollen.', + to_try_nothing: 'Noch nichts vorhanden', + to_try_empty_state: 'Füge ein Rezept hinzu, das du ausprobieren möchtest.', + recipe_name: 'Rezeptname', + label_optional: 'Bezeichnung (optional)', + notes_optional: 'Notizen (optional)', + add_link: 'Link hinzufügen', + add_recipe_to_try: 'Rezept hinzufügen', + edit_recipe: 'Rezept bearbeiten', + delete_recipe_confirm: 'Dieses Rezept löschen?', + + // Search page + search_results_title: 'Suchergebnisse', + search_meta_description: 'Suchergebnisse in den Bockenschen Rezepten.', + filtered_by: 'Gefiltert nach:', + keywords_label: 'Stichwörter', + seasons_label: 'Monate', + favorites_only: 'Nur Favoriten', + search_error: 'Fehler bei der Suche:', + results_for: 'Ergebnisse für', + no_recipes_found: 'Keine Rezepte gefunden.', + try_other_search: 'Versuche es mit anderen Suchbegriffen.', + + // Common page titles + shared + site_title: 'Bocken Rezepte', + all: 'Alle', + + // Index page + index_title: 'Rezepte', + in_season_now: 'In Saison', + meta_alt_hero: 'Pasta al Ragu mit Linguine', + + // Detail page + season_label: 'Saison:', + keywords_colon: 'Stichwörter:', + last_modified: 'Letzte Änderung:', + + // Favorites + favorites_page_title: 'Meine Favoriten - Bocken Rezepte', + no_favorites_yet: 'Noch keine Favoriten gespeichert', + error_loading_favorites: 'Fehler beim Laden der Favoriten:', + recipe_singular_link: 'Rezept', + recipes_to_try_link: 'Zum Ausprobieren', + no_matching_favorites: 'Keine passenden Favoriten gefunden.', + + // Error pages + recipe_not_found: 'Rezept nicht gefunden', + recipe_not_found_desc: 'Das angeforderte Rezept konnte nicht gefunden werden.', + checking_german_version: 'Suche nach deutscher Version…', + recipes_link: 'Rezepte', + + // Categories / tags / season / icon / tips index pages + categories_title: 'Kategorien', + keywords_title: 'Stichwörter', + search_tags: 'Tags suchen…', + in_season_title: 'Saisonal', + icons_title: 'Icons', + tips_title: 'Tipps & Tricks', + favorites_meta_description: 'Meine favorisierten Rezepte aus der Bockenschen Küche.', + empty_favorites_1: 'Du hast noch keine Rezepte als Favoriten gespeichert.', + empty_favorites_2: 'Besuche ein Rezept und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.' } as const; diff --git a/src/lib/i18n/recipes/en.ts b/src/lib/i18n/recipes/en.ts index 0c27b6d0..26e7607d 100644 --- a/src/lib/i18n/recipes/en.ts +++ b/src/lib/i18n/recipes/en.ts @@ -42,5 +42,102 @@ export const en = { glycine: 'Glycine', proline: 'Proline', serine: 'Serine', - tyrosine: 'Tyrosine' + tyrosine: 'Tyrosine', + + // Ingredients page + portions: 'Portions:', + adjust_amount: 'Adjust Amount:', + ingredients: 'Ingredients', + cake_form: 'Cake form', + adjust_cake_form: 'Adjust cake form', + round_form: 'Round', + rectangular_form: 'Rectangular', + diameter: 'Diameter', + outer_diameter: 'Outer Ø', + inner_diameter: 'Inner Ø', + width: 'Width', + length: 'Length', + factor: 'Factor', + restore_default: 'Restore default', + round_lowercase: 'round', + + // AddToFoodLogButton + meal labels + add_to_food_log: 'Add to food log', + added_to_food_log: 'Added to food log', + add_failed: 'Failed to add', + portions_label: 'Portions', + grams_label: 'Grams', + meal_label: 'Meal', + breakfast: 'Breakfast', + lunch: 'Lunch', + dinner: 'Dinner', + snack: 'Snack', + log_action: 'Log', + cancel: 'Cancel', + save: 'Save', + + // To-try page + to_try_title: 'To Try', + to_try_page_title: 'Recipes To Try - Bocken Recipes', + to_try_meta_description: 'Recipes we want to try from around the web.', + to_try_nothing: 'Nothing here yet', + to_try_empty_state: 'Add a recipe you want to try using the form below.', + recipe_name: 'Recipe name', + label_optional: 'Label (optional)', + notes_optional: 'Notes (optional)', + add_link: 'Add link', + add_recipe_to_try: 'Add recipe to try', + edit_recipe: 'Edit recipe', + delete_recipe_confirm: 'Delete this recipe?', + + // Search page + search_results_title: 'Search Results', + search_meta_description: "Search results in Bocken's recipes.", + filtered_by: 'Filtered by:', + keywords_label: 'Keywords', + seasons_label: 'Seasons', + favorites_only: 'Favorites only', + search_error: 'Search error:', + results_for: 'results for', + no_recipes_found: 'No recipes found.', + try_other_search: 'Try different search terms.', + + // Common page titles + shared + site_title: 'Bocken Recipes', + all: 'All', + + // Index page + index_title: 'Recipes', + in_season_now: 'In Season', + meta_alt_hero: 'Pasta al Ragu with Linguine', + + // Detail page + season_label: 'Season:', + keywords_colon: 'Keywords:', + last_modified: 'Last modified:', + + // Favorites + favorites_page_title: 'My Favorites - Bocken Recipes', + no_favorites_yet: 'No favorites saved yet', + error_loading_favorites: 'Error loading favorites:', + recipe_singular_link: 'recipe', + recipes_to_try_link: 'Recipes to try', + no_matching_favorites: 'No matching favorites found.', + + // Error pages + recipe_not_found: 'Recipe Not Found', + recipe_not_found_desc: 'The requested recipe could not be found.', + checking_german_version: 'Checking for German version…', + recipes_link: 'Recipes', + + // Categories / tags / season / icon / tips index pages + categories_title: 'Categories', + keywords_title: 'Keywords', + search_tags: 'Search tags…', + in_season_title: 'In Season', + icons_title: 'Icons', + tips_title: 'Tips & Tricks', + favorites_meta_description: "My favorite recipes from Bocken's kitchen.", + empty_favorites_1: "You haven't saved any recipes as favorites yet.", + empty_favorites_2: 'Visit a recipe and click the heart icon to add it to your favorites.' } as const satisfies Record; diff --git a/src/routes/[recipeLang=recipeLang]/+page.svelte b/src/routes/[recipeLang=recipeLang]/+page.svelte index 55f58f2c..b9ae57e9 100644 --- a/src/routes/[recipeLang=recipeLang]/+page.svelte +++ b/src/routes/[recipeLang=recipeLang]/+page.svelte @@ -5,8 +5,11 @@ import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import Search from '$lib/components/recipes/Search.svelte'; import { getCategories } from '$lib/js/categories'; + import { m, type RecipesLang } from '$lib/js/recipesI18n'; let { data } = $props<{ data: PageData }>(); + const lang = $derived(data.lang as RecipesLang); + const t = $derived(m[lang]); let current_month = new Date().getMonth() + 1; // Search state @@ -118,17 +121,17 @@ const hasMore = $derived(visibleCount < displayRecipes.length); const labels = $derived({ - title: isEnglish ? 'Recipes' : 'Rezepte', + title: t.index_title, subheading: isEnglish ? `${data.all_brief.length} recipes and constantly growing...` : `${data.all_brief.length} Rezepte und stetig wachsend...`, - all: isEnglish ? 'All' : 'Alle', - inSeason: isEnglish ? 'In Season' : 'In Saison', - metaTitle: isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte', + all: t.all, + inSeason: t.in_season_now, + metaTitle: t.site_title, metaDescription: isEnglish ? "A constantly growing collection of recipes from Bocken's kitchen." : "Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche.", - metaAlt: isEnglish ? 'Pasta al Ragu with Linguine' : 'Pasta al Ragu mit Linguine' + metaAlt: t.meta_alt_hero });