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,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>;