i18n(fitness): migrate inline ternaries across pages and components

Replace lang === 'en' string ternaries on the check-in, stats, workout,
exercises, history, and stats history detail pages, plus TemplateCard,
with t.<key> lookups against the fitness dictionary. Added new keys for
toast messages, body-part counts, body-fat label, clear/measure short
labels, "edit all fields", BF chart delta prefix, calorie balance and
adherence tooltips, actual/target legend labels, daily expenditure
prefix, height/birth/weight setup hint, exercise/workout/recent labels,
"starts with", and a {n}-template "X days ago" string.

URL slug ternaries (e.g. 'check-in' / 'erfassung') remain inline since
they encode route data, not UI text.

Bump site version to 1.56.2.
This commit is contained in:
2026-05-01 14:01:06 +02:00
parent bd9e9b397f
commit 71f7322624
14 changed files with 88 additions and 46 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.56.1",
"version": "1.56.2",
"private": true,
"type": "module",
"scripts": {
@@ -26,8 +26,8 @@
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays === 0) return t.today;
if (diffDays === 1) return t.yesterday;
if (diffDays < 7) return lang === 'en' ? `${diffDays} days ago` : `vor ${diffDays} Tagen`;
return d.toLocaleDateString(lang === 'en' ? 'en' : 'de', { month: 'short', day: 'numeric' });
if (diffDays < 7) return t.days_ago_template.replace('{n}', String(diffDays));
return d.toLocaleDateString(lang, { month: 'short', day: 'numeric' });
}
</script>
+26
View File
@@ -363,4 +363,30 @@ export const de = {
sugars: "Zucker",
source_db: "Quelle",
initializing_gps: "GPS wird initialisiert…",
// Check-in page
failed_to_load_more: "Laden fehlgeschlagen",
body_part_count_one: "1 Körperteil",
body_parts_count_other: "Körperteile",
updated_toast: "Aktualisiert",
body_fat_label: "Körperfett",
clear_action: "Leeren",
measure_short: "Messen",
edit_all_fields: "Alle Felder bearbeiten",
measurement_saved: "Messung gespeichert",
// Stats page
bf_delta_from_prefix: "Δ von",
set_height_birthyear_weight: "Größe, Geburtsjahr & Gewicht eintragen",
actual_label: "Ist",
target_label: "Ziel",
calorie_balance_tooltip: "Durchschnittlich gegessene Kalorien minus geschätzter Verbrauch (TDEE + erfasste Trainingskilokalorien) der letzten 7 Tage. Negativ = Defizit, positiv = Überschuss.",
daily_expenditure_estimate_prefix: "Geschätzter Tagesverbrauch:",
diet_adherence_tooltip: "Prozent der Tage, an denen die gegessenen Kalorien innerhalb von ±10 % deines Ziels lagen (bereinigt um verbrannte Trainings­kalorien). Nicht erfasste Tage zählen als verfehlt.",
// Misc page titles / labels
exercise_title: "Übung",
recent_label: "Aktuell",
starts_with: "beginnt mit",
days_ago_template: "vor {n} Tagen"
} as const;
+26
View File
@@ -363,4 +363,30 @@ export const en = {
sugars: "Sugars",
source_db: "Source",
initializing_gps: "Initializing GPS…",
// Check-in page
failed_to_load_more: "Failed to load more",
body_part_count_one: "1 body part",
body_parts_count_other: "body parts",
updated_toast: "Updated",
body_fat_label: "Body Fat",
clear_action: "Clear",
measure_short: "Measure",
edit_all_fields: "Edit all fields",
measurement_saved: "Measurement saved",
// Stats page
bf_delta_from_prefix: "Δ from",
set_height_birthyear_weight: "Set height, birth year & weight",
actual_label: "Actual",
target_label: "Target",
calorie_balance_tooltip: "Average daily calories eaten minus estimated expenditure (TDEE + tracked workout calories) over the last 7 days. Negative = deficit, positive = surplus.",
daily_expenditure_estimate_prefix: "Est. daily expenditure:",
diet_adherence_tooltip: "Percentage of days where calories eaten were within ±10% of your goal (adjusted for exercise calories burned). Days without tracking count as misses.",
// Misc page titles / labels
exercise_title: "Exercise",
recent_label: "Recent",
starts_with: "starts with",
days_ago_template: "{n} days ago"
} as const satisfies Record<keyof typeof de, string>;
@@ -136,10 +136,10 @@
measurements = [...measurements, ...fresh];
if (typeof body?.total === 'number') measurementsTotal = body.total;
} else {
toast.error(lang === 'en' ? 'Failed to load more' : 'Laden fehlgeschlagen');
toast.error(t.failed_to_load_more);
}
} catch {
toast.error(lang === 'en' ? 'Failed to load more' : 'Laden fehlgeschlagen');
toast.error(t.failed_to_load_more);
}
loadingMore = false;
}
@@ -179,9 +179,7 @@
? Object.values(m.measurements).filter((v) => v != null).length
: 0;
if (bpCount > 0) {
const en = bpCount === 1 ? '1 body part' : `${bpCount} body parts`;
const de = bpCount === 1 ? '1 Körperteil' : `${bpCount} Körperteile`;
parts.push(lang === 'en' ? en : de);
parts.push(bpCount === 1 ? t.body_part_count_one : `${bpCount} ${t.body_parts_count_other}`);
}
return parts.join(' · ') || t.no_measurements_yet;
}
@@ -293,7 +291,7 @@
const latestRes = await fetch('/api/fitness/measurements/latest');
if (latestRes.ok) latest = await latestRes.json();
} catch {}
toast.success(lang === 'en' ? 'Updated' : 'Aktualisiert');
toast.success(t.updated_toast);
editingId = null;
} else {
const err = await res.json().catch(() => null);
@@ -356,10 +354,10 @@
return conflicts.map((c) => {
let label = c.key;
let unit = '';
if (c.key === 'weight') { label = lang === 'en' ? 'Weight' : 'Gewicht'; unit = ' kg'; }
else if (c.key === 'bodyFatPercent') { label = lang === 'en' ? 'Body Fat' : 'Körperfett'; unit = ' %'; }
else if (c.key === 'caloricIntake') { label = lang === 'en' ? 'Calories' : 'Kalorien'; unit = ' kcal'; }
else if (c.key === 'notes') { label = lang === 'en' ? 'Notes' : 'Notizen'; }
if (c.key === 'weight') { label = t.weight; unit = ' kg'; }
else if (c.key === 'bodyFatPercent') { label = t.body_fat_label; unit = ' %'; }
else if (c.key === 'caloricIntake') { label = t.calories; unit = ' kcal'; }
else if (c.key === 'notes') { label = t.notes; }
else if (c.key.startsWith('measurements.')) {
const part = c.key.slice('measurements.'.length);
label = t[/** @type {FitnessKey} */ (partKeyMap[part] ?? part)];
@@ -408,7 +406,7 @@
measurementsTotal = measurementsTotal + 1;
}
resetForm();
toast.success(lang === 'en' ? 'Measurement saved' : 'Messung gespeichert');
toast.success(t.measurement_saved);
} else {
const err = await res.json().catch(() => null);
toast.error(err?.error ?? 'Failed to save measurement');
@@ -418,7 +416,7 @@
}
</script>
<svelte:head><title>{lang === 'en' ? 'Measure' : 'Messen'} - Bocken</title></svelte:head>
<svelte:head><title>{t.measure_short} - Bocken</title></svelte:head>
<div class="measure-page">
<h1 class="sr-only">{t.measure_title}</h1>
@@ -504,10 +502,10 @@
<Plus size={18} />
</button>
</div>
<label for="m-weight" class="metric-label">{lang === 'en' ? 'Weight' : 'Gewicht'}</label>
<label for="m-weight" class="metric-label">{t.weight}</label>
{#if formWeight}
<button type="button" class="metric-clear" onclick={() => formWeight = ''}>
<X size={12} /> {lang === 'en' ? 'Clear' : 'Leeren'}
<X size={12} /> {t.clear_action}
</button>
{/if}
</div>
@@ -535,10 +533,10 @@
<Plus size={18} />
</button>
</div>
<label for="m-bf" class="metric-label">{lang === 'en' ? 'Body Fat' : 'Körperfett'}</label>
<label for="m-bf" class="metric-label">{t.body_fat_label}</label>
{#if formBodyFat}
<button type="button" class="metric-clear" onclick={() => formBodyFat = ''}>
<X size={12} /> {lang === 'en' ? 'Clear' : 'Leeren'}
<X size={12} /> {t.clear_action}
</button>
{/if}
</div>
@@ -638,7 +636,7 @@
<div class="edit-actions">
<a class="edit-more" href={resolve('/fitness/[checkin=fitnessCheckIn]/edit/[id]', { checkin: checkinSlug, id: m._id })} aria-label={t.edit_measurement}>
<Pencil size={11} />
<span class="edit-more-label">{lang === 'en' ? 'Edit all fields' : 'Alle Felder bearbeiten'}</span>
<span class="edit-more-label">{t.edit_all_fields}</span>
<ChevronRight size={11} />
</a>
<button type="button" class="edit-btn cancel" onclick={cancelEdit} aria-label={t.cancel}>
@@ -251,7 +251,7 @@
res = await doPost(true);
}
if (res.ok) {
toast.success(lang === 'en' ? 'Measurement saved' : 'Messung gespeichert');
toast.success(t.measurement_saved);
await goto(`/fitness/${checkinSlug}`);
} else {
const err = await res.json().catch(() => null);
@@ -115,7 +115,7 @@
}));
</script>
<svelte:head><title>{lang === 'en' ? 'Exercises' : 'Übungen'} - Bocken</title></svelte:head>
<svelte:head><title>{t.exercises_title} - Bocken</title></svelte:head>
<div class="exercises-page">
<h1 class="sr-only">{t.exercises_title}</h1>
@@ -130,10 +130,10 @@
}
</script>
<svelte:head><title>{exercise?.localName ?? (lang === 'en' ? 'Exercise' : 'Übung')} - Bocken</title></svelte:head>
<svelte:head><title>{exercise?.localName ?? t.exercise_title} - Bocken</title></svelte:head>
<div class="exercise-detail">
<h1>{exercise?.localName ?? 'Exercise'}</h1>
<h1>{exercise?.localName ?? t.exercise_title}</h1>
<div class="tabs">
{#each tabs as tab}
@@ -197,7 +197,7 @@
<!-- Similar exercises -->
{#if similar.length > 0}
<div class="similar-section">
<h3>{lang === 'en' ? 'Similar Exercises' : 'Ähnliche Übungen'}</h3>
<h3>{t.similar_exercises}</h3>
<div class="similar-scroll">
{#each similar as sim}
<a class="similar-card" href={resolve('/fitness/[exercises=fitnessExercises]/[id]', { exercises: s.exercises, id: sim.id })}>
@@ -90,7 +90,7 @@
{/if}
{#if viewMonth}
<a class="month-link" href={recentHref}>
{lang === 'en' ? 'Recent' : 'Aktuell'}
{t.recent_label}
</a>
{/if}
<a class="month-link" href={prevHref}>
@@ -583,7 +583,7 @@
</script>
<svelte:head>
<title>{session?.name ?? (lang === 'en' ? 'Workout' : 'Training')} - Bocken</title>
<title>{session?.name ?? t.workout_singular} - Bocken</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
</svelte:head>
@@ -229,9 +229,7 @@
const baseline = stats.bfChart?.baseline;
const label = t.body_fat.replace(' %', '').replace(' (%)', '');
if (baseline == null) return label;
const suffix = lang === 'en'
? `Δ from ${baseline.toFixed(1)}%`
: `Δ von ${baseline.toFixed(1)}%`;
const suffix = `${t.bf_delta_from_prefix} ${baseline.toFixed(1)}%`;
return `${label} · ${suffix}`;
});
@@ -421,13 +419,9 @@
<button class="card-info-trigger" onclick={() => showBalanceInfo = !showBalanceInfo} aria-label="Info"><Info size={12} /></button>
{#if showBalanceInfo}
<div class="card-info-tooltip">
{lang === 'en'
? 'Average daily calories eaten minus estimated expenditure (TDEE + tracked workout calories) over the last 7 days. Negative = deficit, positive = surplus.'
: 'Durchschnittlich gegessene Kalorien minus geschätzter Verbrauch (TDEE + erfasste Trainingskilokalorien) der letzten 7 Tage. Negativ = Defizit, positiv = Überschuss.'}
{t.calorie_balance_tooltip}
{#if ns.avgDailyExpenditure}
{lang === 'en'
? `Est. daily expenditure: ~${ns.avgDailyExpenditure} kcal`
: `Geschätzter Tagesverbrauch: ~${ns.avgDailyExpenditure} kcal`}
{t.daily_expenditure_estimate_prefix} ~{ns.avgDailyExpenditure} kcal
{/if}
</div>
{/if}
@@ -436,7 +430,7 @@
{#if ns.avgCalorieBalance != null}
{t.seven_day_avg}
{:else if !hasDemographics || !ns.trendWeight}
{lang === 'en' ? 'Set height, birth year & weight' : 'Größe, Geburtsjahr & Gewicht eintragen'}
{t.set_height_birthyear_weight}
{:else}
{t.no_nutrition_data}
{/if}
@@ -455,9 +449,7 @@
<button class="card-info-trigger" onclick={() => showAdherenceInfo = !showAdherenceInfo} aria-label="Info"><Info size={12} /></button>
{#if showAdherenceInfo}
<div class="card-info-tooltip">
{lang === 'en'
? 'Percentage of days where calories eaten were within ±10% of your goal (adjusted for exercise calories burned). Days without tracking count as misses.'
: 'Prozent der Tage, an denen die gegessenen Kalorien innerhalb von ±10 % deines Ziels lagen (bereinigt um verbrannte Trainings\u00ADkalorien). Nicht erfasste Tage zählen als verfehlt.'}
{t.diet_adherence_tooltip}
</div>
{/if}
</div>
@@ -476,11 +468,11 @@
<div class="macro-legend">
<span class="macro-legend-item">
<svg viewBox="0 0 12 12" width="12" height="12"><path d="M3,9.5 A4,4 0 1,1 9,9.5" fill="none" stroke="var(--color-text-secondary)" stroke-width="2" stroke-linecap="round"/></svg>
{lang === 'en' ? 'Actual' : 'Ist'}
{t.actual_label}
</span>
<span class="macro-legend-item">
<svg viewBox="0 0 12 12" width="12" height="12"><path d="M6,10 L10,2 L2,2 Z" fill="var(--color-text-secondary)" stroke="var(--color-text-secondary)" stroke-width="1.5" stroke-linejoin="round"/></svg>
{lang === 'en' ? 'Target' : 'Ziel'}
{t.target_label}
</span>
</div>
{#if !ns.macroSplit}
@@ -149,7 +149,7 @@
const hasData = $derived(series.dates.length > 0);
</script>
<svelte:head><title>{t[card.labelKey]} · {lang === 'en' ? 'History' : 'Verlauf'} - Bocken</title></svelte:head>
<svelte:head><title>{t[card.labelKey]} · {t.history_title} - Bocken</title></svelte:head>
<div class="detail-page">
<header class="detail-header" style="--accent: {bodyPartAccent(card.key)}">
@@ -401,7 +401,7 @@
}
</script>
<svelte:head><title>{lang === 'en' ? 'Workout' : 'Training'} - Bocken</title></svelte:head>
<svelte:head><title>{t.workout_singular} - Bocken</title></svelte:head>
<div class="template-view">
{#if hasSchedule && nextTemplate}
@@ -431,7 +431,7 @@
<span class="next-exercises">
{nextTemplate.exercises.length} {nextTemplate.exercises.length !== 1 ? t.exercises_word : t.exercise}
{#if firstExData}
· {lang === 'en' ? 'starts with' : 'beginnt mit'} {firstExData.localName} {#if firstExWeight != null}({firstExWeight} kg){/if}
· {t.starts_with} {firstExData.localName} {#if firstExWeight != null}({firstExWeight} kg){/if}
{/if}
</span>
{/if}
@@ -976,7 +976,7 @@
</script>
<svelte:head>
<title>{workout.name || (lang === 'en' ? 'Workout' : 'Training')} - Bocken</title>
<title>{workout.name || t.workout_singular} - Bocken</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
</svelte:head>