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 69b3ac2aa4
commit 9a27e50495
10 changed files with 828 additions and 131 deletions

View File

@@ -5,12 +5,13 @@
let { exerciseId } = $props(); let { exerciseId } = $props();
const exercise = $derived(getExerciseById(exerciseId)); const lang = $derived(detectFitnessLang($page.url.pathname));
const sl = $derived(fitnessSlugs(detectFitnessLang($page.url.pathname))); const exercise = $derived(getExerciseById(exerciseId, lang));
const sl = $derived(fitnessSlugs(lang));
</script> </script>
{#if exercise} {#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} {:else}
<span class="exercise-unknown">Unknown Exercise</span> <span class="exercise-unknown">Unknown Exercise</span>
{/if} {/if}

View File

@@ -1,5 +1,5 @@
<script> <script>
import { exercises, getFilterOptions, searchExercises } from '$lib/data/exercises'; import { getFilterOptions, searchExercises, translateTerm } from '$lib/data/exercises';
import { Search, X } from 'lucide-svelte'; import { Search, X } from 'lucide-svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
@@ -23,7 +23,8 @@
const filtered = $derived(searchExercises({ const filtered = $derived(searchExercises({
search: query || undefined, search: query || undefined,
bodyPart: bodyPartFilter || undefined, bodyPart: bodyPartFilter || undefined,
equipment: equipmentFilter || undefined equipment: equipmentFilter || undefined,
lang
})); }));
/** @param {string} id */ /** @param {string} id */
@@ -58,13 +59,13 @@
<select bind:value={bodyPartFilter}> <select bind:value={bodyPartFilter}>
<option value="">{t('all_body_parts', lang)}</option> <option value="">{t('all_body_parts', lang)}</option>
{#each filterOptions.bodyParts as bp (bp)} {#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} {/each}
</select> </select>
<select bind:value={equipmentFilter}> <select bind:value={equipmentFilter}>
<option value="">{t('all_equipment', lang)}</option> <option value="">{t('all_equipment', lang)}</option>
{#each filterOptions.equipment as eq (eq)} {#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} {/each}
</select> </select>
</div> </div>
@@ -73,8 +74,8 @@
{#each filtered as exercise (exercise.id)} {#each filtered as exercise (exercise.id)}
<li> <li>
<button class="exercise-item" onclick={() => select(exercise.id)}> <button class="exercise-item" onclick={() => select(exercise.id)}>
<span class="ex-name">{exercise.name}</span> <span class="ex-name">{exercise.localName}</span>
<span class="ex-meta">{exercise.bodyPart} · {exercise.equipment}</span> <span class="ex-meta">{exercise.localBodyPart} · {exercise.localEquipment}</span>
</button> </button>
</li> </li>
{/each} {/each}

View File

@@ -4,7 +4,8 @@
import { Clock, Weight, Trophy, Route, Gauge } from 'lucide-svelte'; import { Clock, Weight, Trophy, Route, Gauge } from 'lucide-svelte';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n'; 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 {{ * @type {{
@@ -59,7 +60,7 @@
* @param {string} exerciseId * @param {string} exerciseId
*/ */
function bestSetLabel(sets, exerciseId) { function bestSetLabel(sets, exerciseId) {
const exercise = getExerciseById(exerciseId); const exercise = getExerciseById(exerciseId, lang);
const metrics = getExerciseMetrics(exercise); const metrics = getExerciseMetrics(exercise);
const isCardio = metrics.includes('distance'); const isCardio = metrics.includes('distance');
@@ -116,7 +117,7 @@
/** Check if this session has any cardio exercise with GPS data */ /** Check if this session has any cardio exercise with GPS data */
const hasGpsCardio = $derived(session.exercises.some(ex => { const hasGpsCardio = $derived(session.exercises.some(ex => {
const exercise = getExerciseById(ex.exerciseId); const exercise = getExerciseById(ex.exerciseId, lang);
return exercise?.bodyPart === 'cardio' && ex.totalDistance; return exercise?.bodyPart === 'cardio' && ex.totalDistance;
})); }));
@@ -125,7 +126,7 @@
let dist = 0; let dist = 0;
let dur = 0; let dur = 0;
for (const ex of session.exercises) { for (const ex of session.exercises) {
const exercise = getExerciseById(ex.exerciseId); const exercise = getExerciseById(ex.exerciseId, lang);
if (exercise?.bodyPart !== 'cardio') continue; if (exercise?.bodyPart !== 'cardio') continue;
if (ex.totalDistance) { if (ex.totalDistance) {
dist += ex.totalDistance; dist += ex.totalDistance;
@@ -166,10 +167,10 @@
<div class="exercise-list"> <div class="exercise-list">
{#each session.exercises as ex (ex.exerciseId)} {#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)} {@const label = bestSetLabel(ex.sets, ex.exerciseId)}
<div class="exercise-row"> <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} {#if label}
<span class="ex-best">{label}</span> <span class="ex-best">{label}</span>
{/if} {/if}

View File

@@ -44,8 +44,8 @@
</div> </div>
<ul class="exercise-preview"> <ul class="exercise-preview">
{#each template.exercises.slice(0, 4) as ex} {#each template.exercises.slice(0, 4) as ex}
{@const exercise = getExerciseById(ex.exerciseId)} {@const exercise = getExerciseById(ex.exerciseId, lang)}
<li>{ex.sets.length} &times; {exercise?.name ?? ex.exerciseId}</li> <li>{ex.sets.length} &times; {exercise?.localName ?? ex.exerciseId}</li>
{/each} {/each}
{#if template.exercises.length > 4} {#if template.exercises.length > 4}
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li> <li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Search } from 'lucide-svelte'; 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'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname)); const lang = $derived(detectFitnessLang($page.url.pathname));
@@ -19,7 +19,8 @@
const filtered = $derived(searchExercises({ const filtered = $derived(searchExercises({
search: query || undefined, search: query || undefined,
bodyPart: bodyPartFilter || undefined, bodyPart: bodyPartFilter || undefined,
equipment: equipmentFilter || undefined equipment: equipmentFilter || undefined,
lang
})); }));
</script> </script>
@@ -37,13 +38,13 @@
<select bind:value={bodyPartFilter}> <select bind:value={bodyPartFilter}>
<option value="">{t('all_body_parts', lang)}</option> <option value="">{t('all_body_parts', lang)}</option>
{#each filterOptions.bodyParts as bp} {#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} {/each}
</select> </select>
<select bind:value={equipmentFilter}> <select bind:value={equipmentFilter}>
<option value="">{t('all_equipment', lang)}</option> <option value="">{t('all_equipment', lang)}</option>
{#each filterOptions.equipment as eq} {#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} {/each}
</select> </select>
</div> </div>
@@ -53,8 +54,8 @@
<li> <li>
<a href="/fitness/{sl.exercises}/{exercise.id}" class="exercise-row"> <a href="/fitness/{sl.exercises}/{exercise.id}" class="exercise-row">
<div class="exercise-info"> <div class="exercise-info">
<span class="exercise-name">{exercise.name}</span> <span class="exercise-name">{exercise.localName}</span>
<span class="exercise-meta">{exercise.bodyPart} · {exercise.equipment}</span> <span class="exercise-meta">{exercise.localBodyPart} · {exercise.localEquipment}</span>
</div> </div>
</a> </a>
</li> </li>

View File

@@ -1,6 +1,6 @@
<script> <script>
import { page } from '$app/stores'; 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'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname)); const lang = $derived(detectFitnessLang($page.url.pathname));
@@ -31,7 +31,8 @@
let activeTab = $state('about'); 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 } // History API returns { history: [{ sessionId, sessionName, date, sets }], total }
const history = $derived(data.history?.history ?? []); const history = $derived(data.history?.history ?? []);
// Stats API returns { charts: { est1rmOverTime, ... }, personalRecords: { ... }, records } // Stats API returns { charts: { est1rmOverTime, ... }, personalRecords: { ... }, records }
@@ -162,10 +163,10 @@
} }
</script> </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"> <div class="exercise-detail">
<h1>{exercise?.name ?? 'Exercise'}</h1> <h1>{exercise?.localName ?? 'Exercise'}</h1>
<div class="tabs"> <div class="tabs">
{#each tabs as tab} {#each tabs as tab}
@@ -182,20 +183,20 @@
{#if activeTab === 'about'} {#if activeTab === 'about'}
<div class="tab-content"> <div class="tab-content">
{#if exercise?.imageUrl} {#if exercise?.imageUrl}
<img src={exercise.imageUrl} alt={exercise.name} class="exercise-image" /> <img src={exercise.imageUrl} alt={exercise.localName} class="exercise-image" />
{/if} {/if}
<div class="tags"> <div class="tags">
<span class="tag body-part">{exercise?.bodyPart}</span> <span class="tag body-part">{exercise?.localBodyPart}</span>
<span class="tag equipment">{exercise?.equipment}</span> <span class="tag equipment">{exercise?.localEquipment}</span>
<span class="tag target">{exercise?.target}</span> <span class="tag target">{exercise?.localTarget}</span>
</div> </div>
{#if exercise?.secondaryMuscles?.length} {#if exercise?.localSecondaryMuscles?.length}
<p class="secondary">Also works: {exercise.secondaryMuscles.join(', ')}</p> <p class="secondary">{lang === 'en' ? 'Also works' : 'Trainiert auch'}: {exercise.localSecondaryMuscles.join(', ')}</p>
{/if} {/if}
{#if exercise?.instructions?.length} {#if exercise?.localInstructions?.length}
<h3>{t('instructions', lang)}</h3> <h3>{t('instructions', lang)}</h3>
<ol class="instructions"> <ol class="instructions">
{#each exercise.instructions as step} {#each exercise.localInstructions as step}
<li>{step}</li> <li>{step}</li>
{/each} {/each}
</ol> </ol>

View File

@@ -99,12 +99,12 @@
/** @param {string} exerciseId */ /** @param {string} exerciseId */
function addExerciseToEdit(exerciseId) { function addExerciseToEdit(exerciseId) {
const exercise = getExerciseById(exerciseId); const exercise = getExerciseById(exerciseId, lang);
editData.exercises = [ editData.exercises = [
...editData.exercises, ...editData.exercises,
{ {
exerciseId, exerciseId,
name: exercise?.name ?? exerciseId, name: exercise?.localName ?? exerciseId,
restTime: 120, restTime: 120,
sets: [{ completed: true }] sets: [{ completed: true }]
} }
@@ -666,10 +666,10 @@
<h2>{t('personal_records', lang)}</h2> <h2>{t('personal_records', lang)}</h2>
<div class="pr-list"> <div class="pr-list">
{#each session.prs as pr (pr.exerciseId + pr.type)} {#each session.prs as pr (pr.exerciseId + pr.type)}
{@const exercise = getExerciseById(pr.exerciseId)} {@const exercise = getExerciseById(pr.exerciseId, lang)}
<div class="pr-item"> <div class="pr-item">
<Trophy size={14} class="pr-icon" /> <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"> <span class="pr-type">
{#if pr.type === 'est1rm'}Est. 1RM {#if pr.type === 'est1rm'}Est. 1RM
{:else if pr.type === 'maxWeight'}Max Weight {:else if pr.type === 'maxWeight'}Max Weight

View File

@@ -369,9 +369,9 @@
<div class="modal-body"> <div class="modal-body">
<ul class="template-exercises"> <ul class="template-exercises">
{#each selectedTemplate.exercises as ex (ex.exerciseId)} {#each selectedTemplate.exercises as ex (ex.exerciseId)}
{@const exercise = getExerciseById(ex.exerciseId)} {@const exercise = getExerciseById(ex.exerciseId, lang)}
<li> <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> <span class="tex-sets">{ex.sets.length} {ex.sets.length !== 1 ? t('sets', lang) : t('set', lang)}</span>
</li> </li>
{/each} {/each}
@@ -415,12 +415,12 @@
/> />
{#each editorExercises as ex, exIdx (exIdx)} {#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 exMetrics = getExerciseMetrics(exercise).filter((/** @type {string} */ m) => m !== 'rpe')}
{@const hasRpe = getExerciseMetrics(exercise).includes('rpe')} {@const hasRpe = getExerciseMetrics(exercise).includes('rpe')}
<div class="editor-exercise"> <div class="editor-exercise">
<div class="editor-ex-header"> <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"> <div class="editor-ex-actions">
<button class="move-exercise" disabled={exIdx === 0} onclick={() => editorMoveExercise(exIdx, -1)} aria-label="Move up"> <button class="move-exercise" disabled={exIdx === 0} onclick={() => editorMoveExercise(exIdx, -1)} aria-label="Move up">
<ChevronUp size={14} /> <ChevronUp size={14} />

View File

@@ -103,7 +103,7 @@
const prs = []; const prs = [];
const exerciseSummaries = local.exercises.map((/** @type {any} */ ex) => { const exerciseSummaries = local.exercises.map((/** @type {any} */ ex) => {
const exercise = getExerciseById(ex.exerciseId); const exercise = getExerciseById(ex.exerciseId, lang);
const metrics = getExerciseMetrics(exercise); const metrics = getExerciseMetrics(exercise);
const isCardio = metrics.includes('distance'); const isCardio = metrics.includes('distance');
const isBilateral = exercise?.bilateral ?? false; const isBilateral = exercise?.bilateral ?? false;
@@ -223,7 +223,7 @@
const tmplEx = template.exercises?.find((/** @type {any} */ e) => e.exerciseId === actual.exerciseId); const tmplEx = template.exercises?.find((/** @type {any} */ e) => e.exerciseId === actual.exerciseId);
if (!tmplEx) continue; if (!tmplEx) continue;
const exercise = getExerciseById(actual.exerciseId); const exercise = getExerciseById(actual.exerciseId, lang);
const metrics = getExerciseMetrics(exercise); const metrics = getExerciseMetrics(exercise);
if (metrics.includes('distance')) continue; // skip cardio if (metrics.includes('distance')) continue; // skip cardio
@@ -247,7 +247,7 @@
if (changed) { if (changed) {
diffs.push({ diffs.push({
exerciseId: actual.exerciseId, exerciseId: actual.exerciseId,
name: exercise?.name ?? actual.exerciseId, name: exercise?.localName ?? actual.exerciseId,
oldSets: tmplSets, oldSets: tmplSets,
newSets: completedSets.map((/** @type {any} */ s) => ({ newSets: completedSets.map((/** @type {any} */ s) => ({
reps: s.reps ?? undefined, reps: s.reps ?? undefined,
@@ -374,7 +374,7 @@
<div class="pr-list"> <div class="pr-list">
{#each completionData.prs as pr} {#each completionData.prs as pr}
<div class="pr-item"> <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> <span class="pr-detail">{pr.type}: <strong>{pr.value}</strong></span>
</div> </div>
{/each} {/each}
@@ -387,7 +387,7 @@
{#each completionData.exerciseSummaries as ex} {#each completionData.exerciseSummaries as ex}
<div class="ex-summary"> <div class="ex-summary">
<div class="ex-summary-header"> <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> <span class="ex-summary-sets">{ex.sets} {ex.sets !== 1 ? t('sets', lang) : t('set', lang)}</span>
</div> </div>
<div class="ex-summary-stats"> <div class="ex-summary-stats">