i18n(recipes): bootstrap namespace + migrate layout, NutritionSummary
Two-locale recipes dictionary lands at src/lib/i18n/recipes/{de,en}.ts
with the same satisfies-based completeness enforcement as the other
namespaces. recipesI18n.ts is the slim shim — exports m, RecipesLang,
RecipesKey, plus langFromRecipeSlug / recipeSlugFromLang helpers for
the rezepte ↔ recipes URL slug mapping.
[recipeLang]/+layout.svelte's nav-label ternary chain collapses into
t.foo lookups. NutritionSummary.svelte is the heavy hitter — 33
inline isEnglish ternaries become a single dictionary load. Most
amino-acid names use a German-stem-plus-optional-e pattern in the old
code (`Lysin{isEnglish ? 'e' : ''}`) that's now just t.lysine in the
template; less clever, much more obviously translatable.
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
<script>
|
||||
import { createNutritionCalculator } from '$lib/js/nutrition.svelte';
|
||||
import RingGraph from '$lib/components/fitness/RingGraph.svelte';
|
||||
import { m } from '$lib/js/recipesI18n';
|
||||
/** @typedef {import('$lib/js/recipesI18n').RecipesLang} RecipesLang */
|
||||
|
||||
let { flatIngredients, nutritionMappings, sectionNames, referencedNutrition, multiplier, portions, isEnglish, actions } = $props();
|
||||
const lang = $derived(/** @type {RecipesLang} */ (isEnglish ? 'en' : 'de'));
|
||||
const t = $derived(m[lang]);
|
||||
|
||||
const nutrition = createNutritionCalculator(
|
||||
() => flatIngredients,
|
||||
@@ -43,20 +47,20 @@
|
||||
});
|
||||
|
||||
const labels = $derived({
|
||||
title: isEnglish ? 'Nutrition' : 'Nährwerte',
|
||||
perPortion: isEnglish ? 'per portion' : 'pro Portion',
|
||||
protein: isEnglish ? 'Protein' : 'Eiweiß',
|
||||
fat: isEnglish ? 'Fat' : 'Fett',
|
||||
carbs: isEnglish ? 'Carbs' : 'Kohlenh.',
|
||||
fiber: isEnglish ? 'Fiber' : 'Ballaststoffe',
|
||||
sugars: isEnglish ? 'Sugars' : 'Zucker',
|
||||
saturatedFat: isEnglish ? 'Sat. Fat' : 'Ges. Fett',
|
||||
details: isEnglish ? 'Details' : 'Details',
|
||||
vitamins: isEnglish ? 'Vitamins' : 'Vitamine',
|
||||
minerals: isEnglish ? 'Minerals' : 'Mineralstoffe',
|
||||
coverage: isEnglish ? 'coverage' : 'Abdeckung',
|
||||
unmapped: isEnglish ? 'Not tracked' : 'Nicht erfasst',
|
||||
aminoAcids: isEnglish ? 'Amino Acids' : 'Aminosäuren',
|
||||
title: t.nutrition,
|
||||
perPortion: t.per_portion,
|
||||
protein: t.protein,
|
||||
fat: t.fat,
|
||||
carbs: t.carbs,
|
||||
fiber: t.fiber,
|
||||
sugars: t.sugars,
|
||||
saturatedFat: t.saturated_fat,
|
||||
details: t.details,
|
||||
vitamins: t.vitamins,
|
||||
minerals: t.minerals,
|
||||
coverage: t.coverage,
|
||||
unmapped: t.not_tracked,
|
||||
aminoAcids: t.amino_acids
|
||||
});
|
||||
|
||||
const hasAminoAcids = $derived.by(() => {
|
||||
@@ -195,33 +199,33 @@
|
||||
<div class="detail-section">
|
||||
<h4>{labels.minerals}</h4>
|
||||
<div class="detail-row"><span>Calcium</span><span>{fmt(nutrition.totalMicros.calcium / div)} mg</span></div>
|
||||
<div class="detail-row"><span>{isEnglish ? 'Iron' : 'Eisen'}</span><span>{fmt(nutrition.totalMicros.iron / div)} mg</span></div>
|
||||
<div class="detail-row"><span>{t.iron}</span><span>{fmt(nutrition.totalMicros.iron / div)} mg</span></div>
|
||||
<div class="detail-row"><span>Magnesium</span><span>{fmt(nutrition.totalMicros.magnesium / div)} mg</span></div>
|
||||
<div class="detail-row"><span>Potassium</span><span>{fmt(nutrition.totalMicros.potassium / div)} mg</span></div>
|
||||
<div class="detail-row"><span>Sodium</span><span>{fmt(nutrition.totalMicros.sodium / div)} mg</span></div>
|
||||
<div class="detail-row"><span>{isEnglish ? 'Zinc' : 'Zink'}</span><span>{fmt(nutrition.totalMicros.zinc / div)} mg</span></div>
|
||||
<div class="detail-row"><span>{t.zinc}</span><span>{fmt(nutrition.totalMicros.zinc / div)} mg</span></div>
|
||||
</div>
|
||||
{#if hasAminoAcids}
|
||||
<div class="detail-section">
|
||||
<h4>{labels.aminoAcids}</h4>
|
||||
<div class="detail-row"><span>{isEnglish ? 'Leucine' : 'Leucin'}</span><span>{fmt(nutrition.totalAminoAcids.leucine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{isEnglish ? 'Isoleucine' : 'Isoleucin'}</span><span>{fmt(nutrition.totalAminoAcids.isoleucine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Valin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.valine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Lysin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.lysine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Methionin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.methionine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Phenylalanin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.phenylalanine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Threonin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.threonine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.leucine}</span><span>{fmt(nutrition.totalAminoAcids.leucine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.isoleucine}</span><span>{fmt(nutrition.totalAminoAcids.isoleucine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.valine}</span><span>{fmt(nutrition.totalAminoAcids.valine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.lysine}</span><span>{fmt(nutrition.totalAminoAcids.lysine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.methionine}</span><span>{fmt(nutrition.totalAminoAcids.methionine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.phenylalanine}</span><span>{fmt(nutrition.totalAminoAcids.phenylalanine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.threonine}</span><span>{fmt(nutrition.totalAminoAcids.threonine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Tryptophan</span><span>{fmt(nutrition.totalAminoAcids.tryptophan / div)} g</span></div>
|
||||
<div class="detail-row"><span>Histidin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.histidine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Arginin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.arginine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Alanin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.alanine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{isEnglish ? 'Aspartic Acid' : 'Asparaginsäure'}</span><span>{fmt(nutrition.totalAminoAcids.asparticAcid / div)} g</span></div>
|
||||
<div class="detail-row"><span>{isEnglish ? 'Cysteine' : 'Cystein'}</span><span>{fmt(nutrition.totalAminoAcids.cysteine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{isEnglish ? 'Glutamic Acid' : 'Glutaminsäure'}</span><span>{fmt(nutrition.totalAminoAcids.glutamicAcid / div)} g</span></div>
|
||||
<div class="detail-row"><span>Glycin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.glycine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Prolin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.proline / div)} g</span></div>
|
||||
<div class="detail-row"><span>Serin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.serine / div)} g</span></div>
|
||||
<div class="detail-row"><span>Tyrosin{isEnglish ? 'e' : ''}</span><span>{fmt(nutrition.totalAminoAcids.tyrosine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.histidine}</span><span>{fmt(nutrition.totalAminoAcids.histidine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.arginine}</span><span>{fmt(nutrition.totalAminoAcids.arginine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.alanine}</span><span>{fmt(nutrition.totalAminoAcids.alanine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.aspartic_acid}</span><span>{fmt(nutrition.totalAminoAcids.asparticAcid / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.cysteine}</span><span>{fmt(nutrition.totalAminoAcids.cysteine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.glutamic_acid}</span><span>{fmt(nutrition.totalAminoAcids.glutamicAcid / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.glycine}</span><span>{fmt(nutrition.totalAminoAcids.glycine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.proline}</span><span>{fmt(nutrition.totalAminoAcids.proline / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.serine}</span><span>{fmt(nutrition.totalAminoAcids.serine / div)} g</span></div>
|
||||
<div class="detail-row"><span>{t.tyrosine}</span><span>{fmt(nutrition.totalAminoAcids.tyrosine / div)} g</span></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/** German recipes UI strings — source of truth for the key set. */
|
||||
|
||||
export const de = {
|
||||
// Layout / nav
|
||||
all_recipes: 'Alle Rezepte',
|
||||
favorites: 'Favoriten',
|
||||
season_nav: 'Saison',
|
||||
category_nav: 'Kategorie',
|
||||
icon_nav: 'Icon',
|
||||
tags_nav: 'Tags',
|
||||
|
||||
// Nutrition summary
|
||||
nutrition: 'Nährwerte',
|
||||
per_portion: 'pro Portion',
|
||||
protein: 'Eiweiß',
|
||||
fat: 'Fett',
|
||||
carbs: 'Kohlenh.',
|
||||
fiber: 'Ballaststoffe',
|
||||
sugars: 'Zucker',
|
||||
saturated_fat: 'Ges. Fett',
|
||||
details: 'Details',
|
||||
vitamins: 'Vitamine',
|
||||
minerals: 'Mineralstoffe',
|
||||
coverage: 'Abdeckung',
|
||||
not_tracked: 'Nicht erfasst',
|
||||
amino_acids: 'Aminosäuren',
|
||||
iron: 'Eisen',
|
||||
zinc: 'Zink',
|
||||
leucine: 'Leucin',
|
||||
isoleucine: 'Isoleucin',
|
||||
valine: 'Valin',
|
||||
lysine: 'Lysin',
|
||||
methionine: 'Methionin',
|
||||
phenylalanine: 'Phenylalanin',
|
||||
threonine: 'Threonin',
|
||||
histidine: 'Histidin',
|
||||
arginine: 'Arginin',
|
||||
alanine: 'Alanin',
|
||||
aspartic_acid: 'Asparaginsäure',
|
||||
cysteine: 'Cystein',
|
||||
glutamic_acid: 'Glutaminsäure',
|
||||
glycine: 'Glycin',
|
||||
proline: 'Prolin',
|
||||
serine: 'Serin',
|
||||
tyrosine: 'Tyrosin'
|
||||
} as const;
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { de } from './de';
|
||||
|
||||
export const en = {
|
||||
// Layout / nav
|
||||
all_recipes: 'All Recipes',
|
||||
favorites: 'Favorites',
|
||||
season_nav: 'Season',
|
||||
category_nav: 'Category',
|
||||
icon_nav: 'Icon',
|
||||
tags_nav: 'Tags',
|
||||
|
||||
// Nutrition summary
|
||||
nutrition: 'Nutrition',
|
||||
per_portion: 'per portion',
|
||||
protein: 'Protein',
|
||||
fat: 'Fat',
|
||||
carbs: 'Carbs',
|
||||
fiber: 'Fiber',
|
||||
sugars: 'Sugars',
|
||||
saturated_fat: 'Sat. Fat',
|
||||
details: 'Details',
|
||||
vitamins: 'Vitamins',
|
||||
minerals: 'Minerals',
|
||||
coverage: 'coverage',
|
||||
not_tracked: 'Not tracked',
|
||||
amino_acids: 'Amino Acids',
|
||||
iron: 'Iron',
|
||||
zinc: 'Zinc',
|
||||
leucine: 'Leucine',
|
||||
isoleucine: 'Isoleucine',
|
||||
valine: 'Valine',
|
||||
lysine: 'Lysine',
|
||||
methionine: 'Methionine',
|
||||
phenylalanine: 'Phenylalanine',
|
||||
threonine: 'Threonine',
|
||||
histidine: 'Histidine',
|
||||
arginine: 'Arginine',
|
||||
alanine: 'Alanine',
|
||||
aspartic_acid: 'Aspartic Acid',
|
||||
cysteine: 'Cysteine',
|
||||
glutamic_acid: 'Glutamic Acid',
|
||||
glycine: 'Glycine',
|
||||
proline: 'Proline',
|
||||
serine: 'Serine',
|
||||
tyrosine: 'Tyrosine'
|
||||
} as const satisfies Record<keyof typeof de, string>;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Recipes route i18n.
|
||||
*
|
||||
* Translation tables live per-locale in `$lib/i18n/recipes/{de,en}.ts`.
|
||||
* `de.ts` is the source of truth for the key set; `en.ts` uses
|
||||
* `satisfies Record<keyof typeof de, string>` so missing translations
|
||||
* fail the build.
|
||||
*
|
||||
* Recipes routes get `lang` from the layout server load (data.lang),
|
||||
* derived from the [recipeLang=recipeLang] param matcher: rezepte→de,
|
||||
* recipes→en. Use `langFromRecipeSlug(params.recipeLang)` if you need it
|
||||
* from the slug directly.
|
||||
*/
|
||||
|
||||
import { de } from '$lib/i18n/recipes/de';
|
||||
import { en } from '$lib/i18n/recipes/en';
|
||||
|
||||
export const m = { de, en } as const;
|
||||
|
||||
export type RecipesLang = keyof typeof m;
|
||||
export type RecipesKey = keyof typeof de;
|
||||
|
||||
/** Map a `[recipeLang]` slug to the locale code. */
|
||||
export function langFromRecipeSlug(recipeLang: string | null | undefined): RecipesLang {
|
||||
return recipeLang === 'recipes' ? 'en' : 'de';
|
||||
}
|
||||
|
||||
/** Reverse: locale → URL slug. */
|
||||
export function recipeSlugFromLang(lang: RecipesLang): 'recipes' | 'rezepte' {
|
||||
return lang === 'en' ? 'recipes' : 'rezepte';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a translated string. Prefer `m[lang].key` directly in new code —
|
||||
* this helper is kept for incremental migration.
|
||||
*/
|
||||
export function t(key: RecipesKey, lang: RecipesLang): string {
|
||||
return m[lang][key] ?? m.en[key] ?? key;
|
||||
}
|
||||
@@ -56,14 +56,18 @@ let { data, children } = $props();
|
||||
|
||||
let user = $derived(data.session?.user);
|
||||
|
||||
const isEnglish = $derived(data.lang === 'en');
|
||||
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: isEnglish ? 'All Recipes' : 'Alle Rezepte',
|
||||
favorites: isEnglish ? 'Favorites' : 'Favoriten',
|
||||
inSeason: isEnglish ? 'Season' : 'Saison',
|
||||
category: isEnglish ? 'Category' : 'Kategorie',
|
||||
icon: 'Icon',
|
||||
keywords: 'Tags'
|
||||
allRecipes: t.all_recipes,
|
||||
favorites: t.favorites,
|
||||
inSeason: t.season_nav,
|
||||
category: t.category_nav,
|
||||
icon: t.icon_nav,
|
||||
keywords: t.tags_nav
|
||||
});
|
||||
|
||||
/** @param {string} path */
|
||||
|
||||
Reference in New Issue
Block a user