fitness: add German translations for all 77 exercises

Add per-exercise de property with translated name and instructions.
Add shared term translation map for bodyPart, equipment, target, and
muscle names. Add localizeExercise() and translateTerm() helpers.
Update all display components to use localized fields (localName,
localBodyPart, localEquipment, etc.) and pass lang to search/lookup.
This commit is contained in:
2026-03-23 07:44:31 +01:00
parent 80479c0312
commit ee8cc8ec20
10 changed files with 828 additions and 131 deletions
@@ -5,12 +5,13 @@
let { exerciseId } = $props();
const exercise = $derived(getExerciseById(exerciseId));
const sl = $derived(fitnessSlugs(detectFitnessLang($page.url.pathname)));
const lang = $derived(detectFitnessLang($page.url.pathname));
const exercise = $derived(getExerciseById(exerciseId, lang));
const sl = $derived(fitnessSlugs(lang));
</script>
{#if exercise}
<a href="/fitness/{sl.exercises}/{exerciseId}" class="exercise-link">{exercise.name}</a>
<a href="/fitness/{sl.exercises}/{exerciseId}" class="exercise-link">{exercise.localName}</a>
{:else}
<span class="exercise-unknown">Unknown Exercise</span>
{/if}
@@ -1,5 +1,5 @@
<script>
import { exercises, getFilterOptions, searchExercises } from '$lib/data/exercises';
import { getFilterOptions, searchExercises, translateTerm } from '$lib/data/exercises';
import { Search, X } from 'lucide-svelte';
import { page } from '$app/stores';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
@@ -23,7 +23,8 @@
const filtered = $derived(searchExercises({
search: query || undefined,
bodyPart: bodyPartFilter || undefined,
equipment: equipmentFilter || undefined
equipment: equipmentFilter || undefined,
lang
}));
/** @param {string} id */
@@ -58,13 +59,13 @@
<select bind:value={bodyPartFilter}>
<option value="">{t('all_body_parts', lang)}</option>
{#each filterOptions.bodyParts as bp (bp)}
<option value={bp}>{bp.charAt(0).toUpperCase() + bp.slice(1)}</option>
{@const label = translateTerm(bp, lang)}<option value={bp}>{label.charAt(0).toUpperCase() + label.slice(1)}</option>
{/each}
</select>
<select bind:value={equipmentFilter}>
<option value="">{t('all_equipment', lang)}</option>
{#each filterOptions.equipment as eq (eq)}
<option value={eq}>{eq.charAt(0).toUpperCase() + eq.slice(1)}</option>
{@const label = translateTerm(eq, lang)}<option value={eq}>{label.charAt(0).toUpperCase() + label.slice(1)}</option>
{/each}
</select>
</div>
@@ -73,8 +74,8 @@
{#each filtered as exercise (exercise.id)}
<li>
<button class="exercise-item" onclick={() => select(exercise.id)}>
<span class="ex-name">{exercise.name}</span>
<span class="ex-meta">{exercise.bodyPart} · {exercise.equipment}</span>
<span class="ex-name">{exercise.localName}</span>
<span class="ex-meta">{exercise.localBodyPart} · {exercise.localEquipment}</span>
</button>
</li>
{/each}
@@ -4,7 +4,8 @@
import { Clock, Weight, Trophy, Route, Gauge } from 'lucide-svelte';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
const sl = $derived(fitnessSlugs(detectFitnessLang($page.url.pathname)));
const lang = $derived(detectFitnessLang($page.url.pathname));
const sl = $derived(fitnessSlugs(lang));
/**
* @type {{
@@ -59,7 +60,7 @@
* @param {string} exerciseId
*/
function bestSetLabel(sets, exerciseId) {
const exercise = getExerciseById(exerciseId);
const exercise = getExerciseById(exerciseId, lang);
const metrics = getExerciseMetrics(exercise);
const isCardio = metrics.includes('distance');
@@ -116,7 +117,7 @@
/** Check if this session has any cardio exercise with GPS data */
const hasGpsCardio = $derived(session.exercises.some(ex => {
const exercise = getExerciseById(ex.exerciseId);
const exercise = getExerciseById(ex.exerciseId, lang);
return exercise?.bodyPart === 'cardio' && ex.totalDistance;
}));
@@ -125,7 +126,7 @@
let dist = 0;
let dur = 0;
for (const ex of session.exercises) {
const exercise = getExerciseById(ex.exerciseId);
const exercise = getExerciseById(ex.exerciseId, lang);
if (exercise?.bodyPart !== 'cardio') continue;
if (ex.totalDistance) {
dist += ex.totalDistance;
@@ -166,10 +167,10 @@
<div class="exercise-list">
{#each session.exercises as ex (ex.exerciseId)}
{@const exercise = getExerciseById(ex.exerciseId)}
{@const exercise = getExerciseById(ex.exerciseId, lang)}
{@const label = bestSetLabel(ex.sets, ex.exerciseId)}
<div class="exercise-row">
<span class="ex-sets">{ex.sets.length} &times; {exercise?.name ?? ex.exerciseId}</span>
<span class="ex-sets">{ex.sets.length} &times; {exercise?.localName ?? ex.exerciseId}</span>
{#if label}
<span class="ex-best">{label}</span>
{/if}
@@ -44,8 +44,8 @@
</div>
<ul class="exercise-preview">
{#each template.exercises.slice(0, 4) as ex}
{@const exercise = getExerciseById(ex.exerciseId)}
<li>{ex.sets.length} &times; {exercise?.name ?? ex.exerciseId}</li>
{@const exercise = getExerciseById(ex.exerciseId, lang)}
<li>{ex.sets.length} &times; {exercise?.localName ?? ex.exerciseId}</li>
{/each}
{#if template.exercises.length > 4}
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>
+775 -83
View File
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { Search } from 'lucide-svelte';
import { getFilterOptions, searchExercises } from '$lib/data/exercises';
import { getFilterOptions, searchExercises, translateTerm } from '$lib/data/exercises';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
@@ -19,7 +19,8 @@
const filtered = $derived(searchExercises({
search: query || undefined,
bodyPart: bodyPartFilter || undefined,
equipment: equipmentFilter || undefined
equipment: equipmentFilter || undefined,
lang
}));
</script>
@@ -37,13 +38,13 @@
<select bind:value={bodyPartFilter}>
<option value="">{t('all_body_parts', lang)}</option>
{#each filterOptions.bodyParts as bp}
<option value={bp}>{bp.charAt(0).toUpperCase() + bp.slice(1)}</option>
{@const label = translateTerm(bp, lang)}<option value={bp}>{label.charAt(0).toUpperCase() + label.slice(1)}</option>
{/each}
</select>
<select bind:value={equipmentFilter}>
<option value="">{t('all_equipment', lang)}</option>
{#each filterOptions.equipment as eq}
<option value={eq}>{eq.charAt(0).toUpperCase() + eq.slice(1)}</option>
{@const label = translateTerm(eq, lang)}<option value={eq}>{label.charAt(0).toUpperCase() + label.slice(1)}</option>
{/each}
</select>
</div>
@@ -53,8 +54,8 @@
<li>
<a href="/fitness/{sl.exercises}/{exercise.id}" class="exercise-row">
<div class="exercise-info">
<span class="exercise-name">{exercise.name}</span>
<span class="exercise-meta">{exercise.bodyPart} · {exercise.equipment}</span>
<span class="exercise-name">{exercise.localName}</span>
<span class="exercise-meta">{exercise.localBodyPart} · {exercise.localEquipment}</span>
</div>
</a>
</li>
@@ -1,6 +1,6 @@
<script>
import { page } from '$app/stores';
import { getExerciseById } from '$lib/data/exercises';
import { getExerciseById, localizeExercise } from '$lib/data/exercises';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
@@ -31,7 +31,8 @@
let activeTab = $state('about');
const exercise = $derived(data.exercise?.exercise ?? getExerciseById(data.exercise?.id));
const rawExercise = $derived(data.exercise?.exercise ?? getExerciseById(data.exercise?.id));
const exercise = $derived(rawExercise ? localizeExercise(rawExercise, lang) : undefined);
// History API returns { history: [{ sessionId, sessionName, date, sets }], total }
const history = $derived(data.history?.history ?? []);
// Stats API returns { charts: { est1rmOverTime, ... }, personalRecords: { ... }, records }
@@ -162,10 +163,10 @@
}
</script>
<svelte:head><title>{exercise?.name ?? (lang === 'en' ? 'Exercise' : 'Übung')} - Fitness</title></svelte:head>
<svelte:head><title>{exercise?.localName ?? (lang === 'en' ? 'Exercise' : 'Übung')} - Fitness</title></svelte:head>
<div class="exercise-detail">
<h1>{exercise?.name ?? 'Exercise'}</h1>
<h1>{exercise?.localName ?? 'Exercise'}</h1>
<div class="tabs">
{#each tabs as tab}
@@ -182,20 +183,20 @@
{#if activeTab === 'about'}
<div class="tab-content">
{#if exercise?.imageUrl}
<img src={exercise.imageUrl} alt={exercise.name} class="exercise-image" />
<img src={exercise.imageUrl} alt={exercise.localName} class="exercise-image" />
{/if}
<div class="tags">
<span class="tag body-part">{exercise?.bodyPart}</span>
<span class="tag equipment">{exercise?.equipment}</span>
<span class="tag target">{exercise?.target}</span>
<span class="tag body-part">{exercise?.localBodyPart}</span>
<span class="tag equipment">{exercise?.localEquipment}</span>
<span class="tag target">{exercise?.localTarget}</span>
</div>
{#if exercise?.secondaryMuscles?.length}
<p class="secondary">Also works: {exercise.secondaryMuscles.join(', ')}</p>
{#if exercise?.localSecondaryMuscles?.length}
<p class="secondary">{lang === 'en' ? 'Also works' : 'Trainiert auch'}: {exercise.localSecondaryMuscles.join(', ')}</p>
{/if}
{#if exercise?.instructions?.length}
{#if exercise?.localInstructions?.length}
<h3>{t('instructions', lang)}</h3>
<ol class="instructions">
{#each exercise.instructions as step}
{#each exercise.localInstructions as step}
<li>{step}</li>
{/each}
</ol>
@@ -99,12 +99,12 @@
/** @param {string} exerciseId */
function addExerciseToEdit(exerciseId) {
const exercise = getExerciseById(exerciseId);
const exercise = getExerciseById(exerciseId, lang);
editData.exercises = [
...editData.exercises,
{
exerciseId,
name: exercise?.name ?? exerciseId,
name: exercise?.localName ?? exerciseId,
restTime: 120,
sets: [{ completed: true }]
}
@@ -666,10 +666,10 @@
<h2>{t('personal_records', lang)}</h2>
<div class="pr-list">
{#each session.prs as pr (pr.exerciseId + pr.type)}
{@const exercise = getExerciseById(pr.exerciseId)}
{@const exercise = getExerciseById(pr.exerciseId, lang)}
<div class="pr-item">
<Trophy size={14} class="pr-icon" />
<span class="pr-exercise">{exercise?.name ?? pr.exerciseId}</span>
<span class="pr-exercise">{exercise?.localName ?? pr.exerciseId}</span>
<span class="pr-type">
{#if pr.type === 'est1rm'}Est. 1RM
{:else if pr.type === 'maxWeight'}Max Weight
@@ -369,9 +369,9 @@
<div class="modal-body">
<ul class="template-exercises">
{#each selectedTemplate.exercises as ex (ex.exerciseId)}
{@const exercise = getExerciseById(ex.exerciseId)}
{@const exercise = getExerciseById(ex.exerciseId, lang)}
<li>
<span class="tex-name">{exercise?.name ?? ex.exerciseId}</span>
<span class="tex-name">{exercise?.localName ?? ex.exerciseId}</span>
<span class="tex-sets">{ex.sets.length} {ex.sets.length !== 1 ? t('sets', lang) : t('set', lang)}</span>
</li>
{/each}
@@ -415,12 +415,12 @@
/>
{#each editorExercises as ex, exIdx (exIdx)}
{@const exercise = getExerciseById(ex.exerciseId)}
{@const exercise = getExerciseById(ex.exerciseId, lang)}
{@const exMetrics = getExerciseMetrics(exercise).filter((/** @type {string} */ m) => m !== 'rpe')}
{@const hasRpe = getExerciseMetrics(exercise).includes('rpe')}
<div class="editor-exercise">
<div class="editor-ex-header">
<span class="editor-ex-name">{exercise?.name ?? ex.exerciseId}</span>
<span class="editor-ex-name">{exercise?.localName ?? ex.exerciseId}</span>
<div class="editor-ex-actions">
<button class="move-exercise" disabled={exIdx === 0} onclick={() => editorMoveExercise(exIdx, -1)} aria-label="Move up">
<ChevronUp size={14} />
@@ -103,7 +103,7 @@
const prs = [];
const exerciseSummaries = local.exercises.map((/** @type {any} */ ex) => {
const exercise = getExerciseById(ex.exerciseId);
const exercise = getExerciseById(ex.exerciseId, lang);
const metrics = getExerciseMetrics(exercise);
const isCardio = metrics.includes('distance');
const isBilateral = exercise?.bilateral ?? false;
@@ -223,7 +223,7 @@
const tmplEx = template.exercises?.find((/** @type {any} */ e) => e.exerciseId === actual.exerciseId);
if (!tmplEx) continue;
const exercise = getExerciseById(actual.exerciseId);
const exercise = getExerciseById(actual.exerciseId, lang);
const metrics = getExerciseMetrics(exercise);
if (metrics.includes('distance')) continue; // skip cardio
@@ -247,7 +247,7 @@
if (changed) {
diffs.push({
exerciseId: actual.exerciseId,
name: exercise?.name ?? actual.exerciseId,
name: exercise?.localName ?? actual.exerciseId,
oldSets: tmplSets,
newSets: completedSets.map((/** @type {any} */ s) => ({
reps: s.reps ?? undefined,
@@ -374,7 +374,7 @@
<div class="pr-list">
{#each completionData.prs as pr}
<div class="pr-item">
<span class="pr-exercise">{getExerciseById(pr.exerciseId)?.name ?? pr.exerciseId}</span>
<span class="pr-exercise">{getExerciseById(pr.exerciseId, lang)?.localName ?? pr.exerciseId}</span>
<span class="pr-detail">{pr.type}: <strong>{pr.value}</strong></span>
</div>
{/each}
@@ -387,7 +387,7 @@
{#each completionData.exerciseSummaries as ex}
<div class="ex-summary">
<div class="ex-summary-header">
<span class="ex-summary-name">{getExerciseById(ex.exerciseId)?.name ?? ex.exerciseId}</span>
<span class="ex-summary-name">{getExerciseById(ex.exerciseId, lang)?.localName ?? ex.exerciseId}</span>
<span class="ex-summary-sets">{ex.sets} {ex.sets !== 1 ? t('sets', lang) : t('set', lang)}</span>
</div>
<div class="ex-summary-stats">