i18n(recipes): finish remaining ternaries across components and pages

Migrate FavoritesFilter, IconFilter, TagFilter, FilterPanel, HefeSwapper
and the offline-shell, season/[month], icon/[icon], favorites, search,
tips-and-tricks, and index pages to use the recipes i18n dictionary.
Add corresponding keys for filter toggles, filter placeholders, yeast
toggle title, recipes-growing suffix, search "for" preposition, and
favorites count labels. Strip unused isEnglish derivations from layout,
tag, and category landing pages.

Bump site version to 1.56.1.
This commit is contained in:
2026-05-01 13:54:41 +02:00
parent ea1a85e935
commit bd9e9b397f
30 changed files with 295 additions and 118 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.56.0",
"version": "1.56.1",
"private": true,
"type": "module",
"scripts": {
@@ -1,5 +1,7 @@
<script>
import TagChip from '$lib/components/recipes/TagChip.svelte';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let {
categories = [],
@@ -9,9 +11,9 @@
useAndLogic = true
} = $props();
const isEnglish = $derived(lang === 'en');
const label = $derived(isEnglish ? 'Category' : 'Kategorie');
const selectLabel = $derived(isEnglish ? 'Select category...' : 'Kategorie auswählen...');
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const label = $derived(t.category_nav);
const selectLabel = $derived(t.select_category_placeholder);
// Convert selected to array for OR mode, keep as single value for AND mode
const selectedArray = $derived(
@@ -74,7 +74,12 @@ const t: Record<string, Record<string, string>> = {
moveReferenceDownAria: 'Referenz nach unten verschieben',
removeReferenceAria: 'Referenz entfernen',
moveListUpAria: 'Liste nach oben verschieben',
moveListDownAria: 'Liste nach unten verschieben'
moveListDownAria: 'Liste nach unten verschieben',
notSet: 'Nicht gesetzt',
duration: 'Dauer',
temperature: 'Temperatur',
mode: 'Modus',
customModePlaceholder: 'oder eigenen Modus eingeben…'
},
en: {
preparation: 'Preparation:',
@@ -109,7 +114,12 @@ const t: Record<string, Record<string, string>> = {
moveReferenceDownAria: 'Move reference down',
removeReferenceAria: 'Remove reference',
moveListUpAria: 'Move list up',
moveListDownAria: 'Move list down'
moveListDownAria: 'Move list down',
notSet: 'Not set',
duration: 'Duration',
temperature: 'Temperature',
mode: 'Mode',
customModePlaceholder: 'or enter custom mode…'
}
};
@@ -1017,7 +1027,7 @@ h3{
{#if add_info.baking.mode}<span class="chip mode">{add_info.baking.mode}</span>{/if}
</span>
{:else if !bakingExpanded}
<span class="baking-summary muted">{lang === 'de' ? 'Nicht gesetzt' : 'Not set'}</span>
<span class="baking-summary muted">{t[lang].notSet}</span>
{/if}
<svg class="chevron" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path d="M7 10l5 5 5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1027,7 +1037,7 @@ h3{
{#if bakingExpanded}
<div id="baking-fields-{lang}" class="baking-form">
<div class="baking-field">
<label for="baking-length-{lang}">{lang === 'de' ? 'Dauer' : 'Duration'}</label>
<label for="baking-length-{lang}">{t[lang].duration}</label>
<div class="input-wrap">
<input
id="baking-length-{lang}"
@@ -1041,7 +1051,7 @@ h3{
</div>
</div>
<div class="baking-field">
<label for="baking-temp-{lang}">{lang === 'de' ? 'Temperatur' : 'Temperature'}</label>
<label for="baking-temp-{lang}">{t[lang].temperature}</label>
<div class="input-wrap">
<input
id="baking-temp-{lang}"
@@ -1055,7 +1065,7 @@ h3{
</div>
</div>
<div class="baking-field mode-field">
<span class="mode-label">{lang === 'de' ? 'Modus' : 'Mode'}</span>
<span class="mode-label">{t[lang].mode}</span>
<div class="mode-chips">
{#each BAKING_MODES[lang] as mode}
<button
@@ -1070,7 +1080,7 @@ h3{
type="text"
class="mode-custom"
bind:value={add_info.baking.mode}
placeholder={lang === 'de' ? 'oder eigenen Modus eingeben…' : 'or enter custom mode…'}
placeholder={t[lang].customModePlaceholder}
autocomplete="off"
/>
</div>
@@ -1,5 +1,7 @@
<script>
import Toggle from '$lib/components/Toggle.svelte';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let {
enabled = false,
@@ -8,8 +10,8 @@
lang = 'de'
} = $props();
const isEnglish = $derived(lang === 'en');
const label = $derived(isEnglish ? 'Favorites' : 'Favoriten');
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const label = $derived(t.favorites);
// svelte-ignore state_referenced_locally
let checked = $state(enabled);
@@ -5,6 +5,8 @@
import SeasonFilter from './SeasonFilter.svelte';
import FavoritesFilter from './FavoritesFilter.svelte';
import LogicModeToggle from './LogicModeToggle.svelte';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let {
availableCategories = [],
@@ -27,6 +29,7 @@
onLogicModeToggle = () => {}
} = $props();
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const isEnglish = $derived(lang === 'en');
const months = $derived(isEnglish
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
@@ -132,7 +135,7 @@
<div class="filter-wrapper">
<button class="toggle-button" onclick={toggleFilters} type="button">
<span>{filtersOpen ? (isEnglish ? 'Hide Filters' : 'Filter ausblenden') : (isEnglish ? 'Show Filters' : 'Filter einblenden')}</span>
<span>{filtersOpen ? t.hide_filters : t.show_filters}</span>
<span class="arrow" class:open={filtersOpen}>▼</span>
</button>
@@ -3,12 +3,13 @@
import { enhance } from '$app/forms';
import { page } from '$app/state';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let { item, multiplier = 1, yeastId = 0, lang = 'de' } = $props();
const isEnglish = $derived(lang === 'en');
const toggleTitle = $derived(isEnglish
? 'Switch between fresh yeast and dry yeast'
: 'Zwischen Frischhefe und Trockenhefe wechseln');
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const toggleTitle = $derived(t.yeast_toggle_title);
// Get all current URL parameters to preserve state
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : page.url.searchParams);
+4 -2
View File
@@ -1,5 +1,7 @@
<script>
import TagChip from '$lib/components/recipes/TagChip.svelte';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let {
availableIcons = [],
@@ -9,9 +11,9 @@
useAndLogic = true
} = $props();
const isEnglish = $derived(lang === 'en');
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const label = 'Icon';
const selectLabel = $derived(isEnglish ? 'Select icon...' : 'Icon auswählen...');
const selectLabel = $derived(t.select_icon_placeholder);
// Convert selected to array for OR mode, keep as single value for AND mode
const selectedArray = $derived(
@@ -5,6 +5,8 @@ import Croissant from '@lucide/svelte/icons/croissant';
import Flame from '@lucide/svelte/icons/flame';
import CookingPot from '@lucide/svelte/icons/cooking-pot';
import UtensilsCrossed from '@lucide/svelte/icons/utensils-crossed';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let { data } = $props();
// svelte-ignore state_referenced_locally
@@ -99,16 +101,18 @@ const flattenedInstructions = $derived.by(() => {
return flattenInstructionReferences(data.instructions, lang);
});
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({
preparation: isEnglish ? 'Preparation:' : 'Vorbereitung:',
bulkFermentation: isEnglish ? 'Bulk Fermentation:' : 'Stockgare:',
finalProof: isEnglish ? 'Final Proof:' : 'Stückgare:',
baking: isEnglish ? 'Baking:' : 'Backen:',
cooking: isEnglish ? 'Cooking:' : 'Kochen:',
onThePlate: isEnglish ? 'On the Plate:' : 'Auf dem Teller:',
instructions: isEnglish ? 'Instructions' : 'Zubereitung',
at: isEnglish ? 'at' : 'bei'
preparation: t.preparation_section,
bulkFermentation: t.bulk_fermentation,
finalProof: t.final_proof,
baking: t.baking_section,
cooking: t.cooking_section,
onThePlate: t.on_the_plate,
instructions: t.instructions_label,
at: t.at_temp
});
</script>
<style>
@@ -1,4 +1,6 @@
<script>
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let {
useAndLogic = true,
@@ -6,10 +8,10 @@
lang = 'de'
} = $props();
const isEnglish = $derived(lang === 'en');
const label = $derived(isEnglish ? 'Filter Mode' : 'Filter-Modus');
const andLabel = $derived(isEnglish ? 'AND' : 'UND');
const orLabel = $derived(isEnglish ? 'OR' : 'ODER');
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const label = $derived(t.filter_mode);
const andLabel = $derived(t.and_label);
const orLabel = $derived(t.or_label);
// svelte-ignore state_referenced_locally
let checked = $state(useAndLogic);
+6 -3
View File
@@ -3,6 +3,8 @@
import { browser } from '$app/environment';
import FilterPanel from './FilterPanel.svelte';
import { getCategories } from '$lib/js/categories';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
// Filter props for different contexts
let {
@@ -20,12 +22,13 @@
// Generate categories internally based on language
const categories = $derived(getCategories(lang));
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const isEnglish = $derived(lang === 'en');
const searchResultsUrl = $derived(isEnglish ? '/recipes/search' : '/rezepte/search');
const labels = $derived({
placeholder: isEnglish ? 'Search...' : 'Suche...',
searchTitle: isEnglish ? 'Search' : 'Suchen',
clearTitle: isEnglish ? 'Clear search' : 'Sucheintrag löschen'
placeholder: t.search_placeholder_short,
searchTitle: t.search_title,
clearTitle: t.clear_search_title
});
let searchQuery = $state('');
@@ -1,5 +1,7 @@
<script>
import TagChip from '$lib/components/recipes/TagChip.svelte';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let {
selectedSeasons = [],
@@ -8,9 +10,9 @@
months = []
} = $props();
const isEnglish = $derived(lang === 'en');
const label = $derived(isEnglish ? 'Season' : 'Saison');
const selectLabel = $derived(isEnglish ? 'Select season...' : 'Saison auswählen...');
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const label = $derived(t.season_nav);
const selectLabel = $derived(t.select_season_placeholder);
let inputValue = $state('');
let dropdownOpen = $state(false);
+4 -2
View File
@@ -1,5 +1,7 @@
<script>
import TagChip from '$lib/components/recipes/TagChip.svelte';
import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
let {
availableTags = [],
@@ -8,9 +10,9 @@
lang = 'de'
} = $props();
const isEnglish = $derived(lang === 'en');
const t = $derived(m[/** @type {RecipesLang} */ (lang)]);
const label = 'Tags';
const addTagLabel = $derived(isEnglish ? 'Type or select tag...' : 'Tag eingeben oder auswählen...');
const addTagLabel = $derived(t.add_tag_placeholder);
// Filter out already selected tags
const unselectedTags = $derived(availableTags.filter(t => !selectedTags.includes(t)));
+4 -2
View File
@@ -1,5 +1,7 @@
<script>
import { m } from '$lib/js/recipesI18n';
let { item, ondelete, onedit, isEnglish = false } = $props();
const t = $derived(isEnglish ? m.en : m.de);
/** @param {string} url */
function getDomain(url) {
@@ -142,8 +144,8 @@
<div class="card">
<div class="accent"></div>
<button class="card-btn edit-btn" onclick={() => onedit(item)} aria-label={isEnglish ? 'Edit' : 'Bearbeiten'}></button>
<button class="card-btn delete-btn" onclick={() => ondelete(item._id)} aria-label={isEnglish ? 'Delete' : 'Löschen'}></button>
<button class="card-btn edit-btn" onclick={() => onedit(item)} aria-label={t.edit}></button>
<button class="card-btn delete-btn" onclick={() => ondelete(item._id)} aria-label={t.delete}></button>
<div class="body">
<p class="name">{item.name}</p>
{#if item.links?.length}
+76 -1
View File
@@ -139,5 +139,80 @@ export const de = {
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.'
empty_favorites_2: 'Besuche ein Rezept und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.',
// Filters
filter_mode: 'Filter-Modus',
and_label: 'UND',
or_label: 'ODER',
select_category_placeholder: 'Kategorie auswählen…',
select_season_placeholder: 'Saison auswählen…',
// Search component
search_placeholder_short: 'Suche…',
search_title: 'Suchen',
clear_search_title: 'Sucheintrag löschen',
// Tag / Category landing
recipes_with_keyword: 'Rezepte mit Stichwort',
recipes_in_category: 'Rezepte in Kategorie',
// Card actions
edit: 'Bearbeiten',
delete: 'Löschen',
// Administration page
administration_title: 'Administration',
untranslated_recipes: 'Unübersetzte Rezepte',
alt_text_generator: 'Alt-Text Generator',
image_colors: 'Bildfarben',
nutrition_mappings: 'Nährwert-Zuordnungen',
// Recipe detail page (long site title variant)
site_title_long: "Bocken'sche Rezepte",
// InstructionsPage section labels
preparation_section: 'Vorbereitung:',
bulk_fermentation: 'Stockgare:',
final_proof: 'Stückgare:',
baking_section: 'Backen:',
cooking_section: 'Kochen:',
on_the_plate: 'Auf dem Teller:',
instructions_label: 'Zubereitung',
at_temp: 'bei',
// CreateStepList baking
not_set: 'Nicht gesetzt',
duration: 'Dauer',
temperature: 'Temperatur',
mode_label: 'Modus',
custom_mode_placeholder: 'oder eigenen Modus eingeben…',
// Administration page descriptions
administration_description: 'Rezepte und Inhalte verwalten',
untranslated_description: 'Rezepte ansehen und verwalten, die übersetzt werden müssen',
alt_text_description: 'Alternativtext für Rezeptbilder mit KI generieren',
image_colors_description: 'Dominante Farben aus Rezeptbildern für Ladeplatzhalter extrahieren',
nutrition_mappings_description: 'Kalorien- und Nährwertdaten für alle Rezepte generieren oder aktualisieren',
// Smaller filters / pages
loading_offline: 'Lade Offline-Inhalte…',
hide_filters: 'Filter ausblenden',
show_filters: 'Filter einblenden',
select_icon_placeholder: 'Icon auswählen…',
add_tag_placeholder: 'Tag eingeben oder auswählen…',
// Index / tips / yeast
recipes_growing_suffix: 'Rezepte und stetig wachsend…',
recipes_collection_meta: 'Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche.',
tips_description: "Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche.",
yeast_toggle_title: 'Zwischen Frischhefe und Trockenhefe wechseln',
// Search results pageTitle
search_results_for_word: 'für',
// Favorites count label
favorites_count_label: 'favorisierte Rezepte',
favorite_recipe_singular: 'favorite recipe',
favorite_recipes_plural: 'favorite recipes'
} as const;
+76 -1
View File
@@ -139,5 +139,80 @@ export const en = {
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.'
empty_favorites_2: 'Visit a recipe and click the heart icon to add it to your favorites.',
// Filters
filter_mode: 'Filter Mode',
and_label: 'AND',
or_label: 'OR',
select_category_placeholder: 'Select category…',
select_season_placeholder: 'Select season…',
// Search component
search_placeholder_short: 'Search…',
search_title: 'Search',
clear_search_title: 'Clear search',
// Tag / Category landing
recipes_with_keyword: 'Recipes with Keyword',
recipes_in_category: 'Recipes in Category',
// Card actions
edit: 'Edit',
delete: 'Delete',
// Administration page
administration_title: 'Administration',
untranslated_recipes: 'Untranslated Recipes',
alt_text_generator: 'Alt-Text Generator',
image_colors: 'Image Colors',
nutrition_mappings: 'Nutrition Mappings',
// Recipe detail page
site_title_long: 'Bocken Recipes',
// InstructionsPage section labels
preparation_section: 'Preparation:',
bulk_fermentation: 'Bulk Fermentation:',
final_proof: 'Final Proof:',
baking_section: 'Baking:',
cooking_section: 'Cooking:',
on_the_plate: 'On the Plate:',
instructions_label: 'Instructions',
at_temp: 'at',
// CreateStepList baking
not_set: 'Not set',
duration: 'Duration',
temperature: 'Temperature',
mode_label: 'Mode',
custom_mode_placeholder: 'or enter custom mode…',
// Administration page descriptions
administration_description: 'Manage recipes and content',
untranslated_description: 'View and manage recipes that need translation',
alt_text_description: 'Generate alternative text for recipe images using AI',
image_colors_description: 'Extract dominant colors from recipe images for loading placeholders',
nutrition_mappings_description: 'Generate or regenerate calorie and nutrition data for all recipes',
// Smaller filters / pages
loading_offline: 'Loading offline content…',
hide_filters: 'Hide Filters',
show_filters: 'Show Filters',
select_icon_placeholder: 'Select icon…',
add_tag_placeholder: 'Type or select tag…',
// Index / tips / yeast
recipes_growing_suffix: 'recipes and constantly growing…',
recipes_collection_meta: "A constantly growing collection of recipes from Bocken's kitchen.",
tips_description: "A constantly growing collection of recipes from Bocken's kitchen.",
yeast_toggle_title: 'Switch between fresh yeast and dry yeast',
// Search results pageTitle
search_results_for_word: 'for',
// Favorites count label
favorites_count_label: 'favorite recipes',
favorite_recipe_singular: 'favorite recipe',
favorite_recipes_plural: 'favorite recipes'
} as const satisfies Record<keyof typeof de, string>;
@@ -60,7 +60,6 @@ import { m } from '$lib/js/recipesI18n';
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
const lang = $derived(/** @type {RecipesLang} */ (data.lang));
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
const labels = $derived({
allRecipes: t.all_recipes,
favorites: t.favorites,
@@ -23,7 +23,7 @@
hasActiveSearch = ids.size < data.all_brief.length;
}
const isEnglish = $derived(data.lang === 'en');
const isEnglish = $derived(lang === 'en');
const categories = $derived(getCategories(data.lang));
// Pick a seasonal hero recipe (changes daily) — only recipes with hashed images
@@ -122,15 +122,11 @@
const labels = $derived({
title: t.index_title,
subheading: isEnglish
? `${data.all_brief.length} recipes and constantly growing...`
: `${data.all_brief.length} Rezepte und stetig wachsend...`,
subheading: `${data.all_brief.length} ${t.recipes_growing_suffix}`,
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.",
metaDescription: t.recipes_collection_meta,
metaAlt: t.meta_alt_hero
});
</script>
@@ -5,6 +5,7 @@
import { onMount } from 'svelte';
import ErrorView from '$lib/components/ErrorView.svelte';
import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings';
import { m } from '$lib/js/recipesI18n';
let status = $derived(page.status);
let error = $derived(page.error as any);
@@ -37,23 +38,22 @@
status === 404 && isEnglishRoute && germanRecipeExists && !checkingGermanRecipe
);
const t = $derived(m[isEnglish ? 'en' : 'de']);
let title = $derived(
status === 404 ? (isEnglish ? 'Recipe Not Found' : 'Rezept nicht gefunden')
: getErrorTitle(status, isEnglish)
status === 404 ? t.recipe_not_found : getErrorTitle(status, isEnglish)
);
let description = $derived(
showGermanFallback
? 'This recipe has not been translated to English yet, but the German version is available.'
: status === 404
? (isEnglish ? 'The requested recipe could not be found.' : 'Das angeforderte Rezept konnte nicht gefunden werden.')
? t.recipe_not_found_desc
: getErrorDescription(status, isEnglish)
);
let details = $derived(
checkingGermanRecipe
? (isEnglish ? 'Checking for German version…' : 'Suche nach deutscher Version…')
: error?.details
checkingGermanRecipe ? t.checking_german_version : error?.details
);
let recipesHref = $derived(resolve('/[recipeLang=recipeLang]', { recipeLang: isEnglishRoute ? 'recipes' : 'rezepte' }));
@@ -75,7 +75,7 @@
{#snippet actions()}
{#if status === 401}
<button class="link link-primary" onclick={login}>{pick(errorLabels.login, isEnglish)}</button>
<a class="link" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
<a class="link" href={recipesHref}>{t.recipes_link}</a>
{:else if showGermanFallback}
<button class="link link-primary" onclick={viewGermanRecipe}>View German recipe</button>
{#if user}
@@ -84,9 +84,9 @@
<a class="link" href={recipesHref}>Recipes</a>
{:else if status === 500}
<button class="link link-primary" onclick={goBack}>{pick(errorLabels.tryAgain, isEnglish)}</button>
<a class="link" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
<a class="link" href={recipesHref}>{t.recipes_link}</a>
{:else}
<a class="link link-primary" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
<a class="link link-primary" href={recipesHref}>{t.recipes_link}</a>
<button class="link" onclick={goBack}>{pick(errorLabels.goBack, isEnglish)}</button>
{/if}
{/snippet}
@@ -14,6 +14,7 @@
import FavoriteButton from '$lib/components/FavoriteButton.svelte';
import { onDestroy } from 'svelte';
import { recipeTranslationStore } from '$lib/stores/recipeTranslation.svelte';
import { m, type RecipesLang } from '$lib/js/recipesI18n';
let { data } = $props<{ data: PageData }>();
@@ -32,7 +33,9 @@
recipeTranslationStore.set(null);
});
const isEnglish = $derived(data.lang === 'en');
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
// Use mediapath from images array (includes hash for cache busting)
// Fallback to short_name.webp for backward compatibility
@@ -100,10 +103,10 @@
const formatted_display_date = $derived(display_date.toLocaleDateString(isEnglish ? 'en-US' : 'de-DE', options));
const labels = $derived({
season: isEnglish ? 'Season:' : 'Saison:',
keywords: isEnglish ? 'Keywords:' : 'Stichwörter:',
lastModified: isEnglish ? 'Last modified:' : 'Letzte Änderung:',
title: isEnglish ? "Bocken Recipes" : "Bocken'sche Rezepte"
season: t.season_label,
keywords: t.keywords_colon,
lastModified: t.last_modified,
title: t.site_title_long
});
</script>
<style>
@@ -1,50 +1,39 @@
<script lang="ts">
import type { PageData } from './$types';
import { m, type RecipesLang } from '$lib/js/recipesI18n';
let { data } = $props<{ data: PageData }>();
const t = $derived(m[data.lang as RecipesLang]);
// svelte-ignore state_referenced_locally
const isEnglish = data.lang === 'en';
const pageTitle = isEnglish ? 'Administration' : 'Administration';
const pageDescription = isEnglish
? 'Manage recipes and content'
: 'Rezepte und Inhalte verwalten';
const pageTitle = $derived(t.administration_title);
const pageDescription = $derived(t.administration_description);
// svelte-ignore state_referenced_locally
const links = [
const links = $derived([
{
title: isEnglish ? 'Untranslated Recipes' : 'Unübersetzte Rezepte',
description: isEnglish
? 'View and manage recipes that need translation'
: 'Rezepte ansehen und verwalten, die übersetzt werden müssen',
title: t.untranslated_recipes,
description: t.untranslated_description,
href: `/${data.recipeLang}/admin/untranslated`,
icon: '🌐'
},
{
title: isEnglish ? 'Alt-Text Generator' : 'Alt-Text Generator',
description: isEnglish
? 'Generate alternative text for recipe images using AI'
: 'Alternativtext für Rezeptbilder mit KI generieren',
title: t.alt_text_generator,
description: t.alt_text_description,
href: `/${data.recipeLang}/admin/alt-text-generator`,
icon: '🖼️'
},
{
title: isEnglish ? 'Image Colors' : 'Bildfarben',
description: isEnglish
? 'Extract dominant colors from recipe images for loading placeholders'
: 'Dominante Farben aus Rezeptbildern für Ladeplatzhalter extrahieren',
title: t.image_colors,
description: t.image_colors_description,
href: `/${data.recipeLang}/admin/image-colors`,
icon: '🎨'
},
{
title: isEnglish ? 'Nutrition Mappings' : 'Nährwert-Zuordnungen',
description: isEnglish
? 'Generate or regenerate calorie and nutrition data for all recipes'
: 'Kalorien- und Nährwertdaten für alle Rezepte generieren oder aktualisieren',
title: t.nutrition_mappings,
description: t.nutrition_mappings_description,
href: `/${data.recipeLang}/admin/nutrition`,
icon: '🥗'
}
];
]);
</script>
<style>
@@ -8,7 +8,6 @@
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
const labels = $derived({
title: t.categories_title,
siteTitle: t.site_title
@@ -11,9 +11,11 @@
let { data } = $props<{ data: PageData }>();
let current_month = new Date().getMonth() + 1;
const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const label = $derived(t.recipes_in_category);
const siteTitle = $derived(t.site_title);
let matchedRecipeIds = $state(new Set<string>());
let hasActiveSearch = $state(false);
@@ -10,14 +10,16 @@
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
const countLabel = $derived(
lang === 'en'
? (data.favorites.length === 1 ? t.favorite_recipe_singular : t.favorite_recipes_plural)
: t.favorites_count_label
);
const labels = $derived({
title: t.favorites,
pageTitle: t.favorites_page_title,
metaDescription: t.favorites_meta_description,
count: isEnglish
? `${data.favorites.length} favorite recipe${data.favorites.length !== 1 ? 's' : ''}`
: `${data.favorites.length} favorisierte Rezepte`,
count: `${data.favorites.length} ${countLabel}`,
noFavorites: t.no_favorites_yet,
errorLoading: t.error_loading_favorites,
emptyState1: t.empty_favorites_1,
@@ -103,7 +105,7 @@
</div>
{:else if data.favorites.length > 0}
<div class="empty-state">
<p>{isEnglish ? 'No matching favorites found.' : 'Keine passenden Favoriten gefunden.'}</p>
<p>{t.no_matching_favorites}</p>
</div>
{:else}
<div class="empty-state">
@@ -6,8 +6,10 @@
let { data } = $props<{ data: PageData }>();
import { rand_array } from '$lib/js/randomize';
const isEnglish = $derived(data.lang === 'en');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const siteTitle = $derived(t.site_title);
// Search state
let matchedRecipeIds = $state(new Set());
@@ -2,8 +2,10 @@
import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { m, type RecipesLang } from '$lib/js/recipesI18n';
let { data } = $props();
const t = $derived(m[data.lang as RecipesLang]);
// This page serves as an "app shell" that gets cached by the service worker.
// When a user directly navigates to a recipe page while offline and that exact
@@ -36,7 +38,7 @@ onMount(() => {
<div class="offline-shell">
<div class="loading-spinner"></div>
<p>{data.lang === 'en' ? 'Loading offline content...' : 'Lade Offline-Inhalte...'}</p>
<p>{t.loading_offline}</p>
</div>
<style>
@@ -9,12 +9,9 @@
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
const labels = $derived({
title: t.search_results_title,
pageTitle: isEnglish
? `Search Results${data.query ? ` for "${data.query}"` : ''} - Bocken Recipes`
: `Suchergebnisse${data.query ? ` für "${data.query}"` : ''} - Bocken Rezepte`,
pageTitle: `${t.search_results_title}${data.query ? ` ${t.search_results_for_word} "${data.query}"` : ''} - ${t.site_title}`,
metaDescription: t.search_meta_description,
filteredBy: t.filtered_by,
category: t.category_nav,
@@ -5,11 +5,14 @@
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
let { data } = $props<{ data: PageData }>();
const isEnglish = $derived(data.lang === 'en');
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
const months = $derived(isEnglish
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
const siteTitle = $derived(t.site_title);
const currentMonth = $derived(months[data.month - 1]);
import { rand_array } from '$lib/js/randomize';
@@ -8,7 +8,6 @@
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
const labels = $derived({
title: t.keywords_title,
siteTitle: t.site_title,
@@ -11,9 +11,11 @@
let { data } = $props<{ data: PageData }>();
let current_month = new Date().getMonth() + 1;
const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const label = $derived(t.recipes_with_keyword);
const siteTitle = $derived(t.site_title);
let matchedRecipeIds = $state(new Set<string>());
let hasActiveSearch = $state(false);
@@ -8,13 +8,10 @@
import { m, type RecipesLang } from '$lib/js/recipesI18n';
const lang = $derived(data.lang as RecipesLang);
const t = $derived(m[lang]);
const isEnglish = $derived(lang === 'en');
const labels = $derived({
title: t.tips_title,
siteTitle: t.site_title,
description: isEnglish
? "A constantly growing collection of recipes from Bocken's kitchen."
: 'Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche.'
description: t.tips_description
});
</script>
<style>