diff --git a/src/lib/components/LanguageSelector.svelte b/src/lib/components/LanguageSelector.svelte index 6c8e88b..b95dadf 100644 --- a/src/lib/components/LanguageSelector.svelte +++ b/src/lib/components/LanguageSelector.svelte @@ -3,6 +3,7 @@ import { page } from '$app/stores'; import { recipeTranslationStore } from '$lib/stores/recipeTranslation'; import { languageStore } from '$lib/stores/language'; + import { convertFitnessPath } from '$lib/js/fitnessI18n'; import { onMount } from 'svelte'; let { lang = undefined }: { lang?: 'de' | 'en' } = $props(); @@ -29,6 +30,8 @@ languageStore.set('en'); } else if (path.startsWith('/rezepte') || path.startsWith('/glaube')) { languageStore.set('de'); + } else if (path.startsWith('/fitness')) { + // Language is determined by sub-route slugs; don't override store } else { // On other pages, read from localStorage if (typeof localStorage !== 'undefined') { @@ -67,6 +70,10 @@ return convertFaithPath(path, targetLang); } + if (path.startsWith('/fitness')) { + return convertFitnessPath(path, targetLang); + } + // Use translated recipe slugs from page data when available (works during SSR) const pageData = $page.data; if (targetLang === 'en' && path.startsWith('/rezepte')) { @@ -105,7 +112,8 @@ // For pages that handle their own translations inline (not recipe/faith routes), // dispatch event and stay on the page if (!path.startsWith('/rezepte') && !path.startsWith('/recipes') - && !path.startsWith('/glaube') && !path.startsWith('/faith')) { + && !path.startsWith('/glaube') && !path.startsWith('/faith') + && !path.startsWith('/fitness')) { window.dispatchEvent(new CustomEvent('languagechange', { detail: { lang } })); return; } @@ -117,6 +125,13 @@ return; } + // Handle fitness pages + if (path.startsWith('/fitness')) { + const newPath = convertFitnessPath(path, lang); + await goto(newPath); + return; + } + // If we have recipe translation data from store, use the correct short names const recipeData = $recipeTranslationStore; if (recipeData) { diff --git a/src/lib/components/fitness/ExerciseName.svelte b/src/lib/components/fitness/ExerciseName.svelte index f13f518..9a1000d 100644 --- a/src/lib/components/fitness/ExerciseName.svelte +++ b/src/lib/components/fitness/ExerciseName.svelte @@ -1,13 +1,16 @@ {#if exercise} - {exercise.name} + {exercise.name} {:else} Unknown Exercise {/if} diff --git a/src/lib/components/fitness/ExercisePicker.svelte b/src/lib/components/fitness/ExercisePicker.svelte index b7ac04c..4fda3c8 100644 --- a/src/lib/components/fitness/ExercisePicker.svelte +++ b/src/lib/components/fitness/ExercisePicker.svelte @@ -1,6 +1,10 @@ - +

{session.name}

{formatDate(session.startTime)} · {formatTime(session.startTime)} diff --git a/src/lib/components/fitness/SetTable.svelte b/src/lib/components/fitness/SetTable.svelte index df43ce3..71a89c9 100644 --- a/src/lib/components/fitness/SetTable.svelte +++ b/src/lib/components/fitness/SetTable.svelte @@ -2,6 +2,10 @@ import { Check, X } from 'lucide-svelte'; import { METRIC_LABELS } from '$lib/data/exercises'; import RestTimer from './RestTimer.svelte'; + import { page } from '$app/stores'; + import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; + + const lang = $derived(detectFitnessLang($page.url.pathname)); /** * @type {{ @@ -75,16 +79,16 @@ {#if editable && onRemove} {/if} - SET + {t('set_header', lang)} {#if previousSets} - PREV + {t('prev_header', lang)} {/if} {#each mainMetrics as metric (metric)} {METRIC_LABELS[metric]} {/each} {#if editable && hasRpe} - RPE + {t('rpe', lang)} {/if} {#if editable} diff --git a/src/lib/components/fitness/TemplateCard.svelte b/src/lib/components/fitness/TemplateCard.svelte index 997814c..1050178 100644 --- a/src/lib/components/fitness/TemplateCard.svelte +++ b/src/lib/components/fitness/TemplateCard.svelte @@ -1,6 +1,10 @@ @@ -44,11 +48,11 @@
  • {ex.sets.length} × {exercise?.name ?? ex.exerciseId}
  • {/each} {#if template.exercises.length > 4} -
  • +{template.exercises.length - 4} more
  • +
  • +{template.exercises.length - 4} {t('more', lang)}
  • {/if} {#if lastUsed} -

    Last performed: {formatDate(lastUsed)}

    +

    {t('last_performed', lang)} {formatDate(lastUsed)}

    {/if}
    diff --git a/src/lib/components/fitness/WorkoutFab.svelte b/src/lib/components/fitness/WorkoutFab.svelte index d7d6cd2..a2d46cf 100644 --- a/src/lib/components/fitness/WorkoutFab.svelte +++ b/src/lib/components/fitness/WorkoutFab.svelte @@ -2,6 +2,10 @@ import { goto } from '$app/navigation'; import { Play, Pause } from 'lucide-svelte'; import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte'; +import { page } from '$app/stores'; +import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; + +const lang = $derived(detectFitnessLang($page.url.pathname)); let { href, elapsed = '0:00', paused = false, syncStatus = 'idle', onPauseToggle, restSeconds = 0, restTotal = 0, onRestAdjust = null, onRestSkip = null } = $props(); @@ -35,7 +39,7 @@ const restProgress = $derived(restTotal > 0 ? restSeconds / restTotal : 0); {:else} - Active Workout + {t('active_workout', lang)} {/if} diff --git a/src/lib/data/exercises.ts b/src/lib/data/exercises.ts index 739170a..25eb553 100644 --- a/src/lib/data/exercises.ts +++ b/src/lib/data/exercises.ts @@ -613,7 +613,9 @@ export const exercises: Exercise[] = [ 'Stand with feet shoulder-width apart.', 'Squat down until thighs are at least parallel to the floor.', 'Drive through your heels to stand back up.' - ] + ], + imageUrl: "/fitness/squat-barbell/0.svg" + }, { id: 'front-squat-barbell', diff --git a/src/lib/js/fitnessI18n.ts b/src/lib/js/fitnessI18n.ts new file mode 100644 index 0000000..87a5ed4 --- /dev/null +++ b/src/lib/js/fitnessI18n.ts @@ -0,0 +1,224 @@ +/** Fitness route i18n — slug mappings and UI translations */ + +const slugMap: Record> = { + en: { statistik: 'stats', verlauf: 'history', training: 'workout', aktiv: 'active', uebungen: 'exercises', messen: 'measure' }, + de: { stats: 'statistik', history: 'verlauf', workout: 'training', active: 'aktiv', exercises: 'uebungen', measure: 'messen' } +}; + +const germanSlugs = new Set(Object.keys(slugMap.en)); + +/** Detect language from a fitness path by checking for any German slug */ +export function detectFitnessLang(pathname: string): 'en' | 'de' { + const segments = pathname.replace(/^\/fitness\/?/, '').split('/'); + for (const seg of segments) { + if (germanSlugs.has(seg)) return 'de'; + } + return 'en'; +} + +/** Convert a fitness path to the target language */ +export function convertFitnessPath(pathname: string, targetLang: 'en' | 'de'): string { + const map = slugMap[targetLang]; + const segments = pathname.split('/'); + return segments.map(seg => map[seg] ?? seg).join('/'); +} + +/** Get translated sub-route slugs for a given language */ +export function fitnessSlugs(lang: 'en' | 'de') { + return { + stats: lang === 'en' ? 'stats' : 'statistik', + history: lang === 'en' ? 'history' : 'verlauf', + workout: lang === 'en' ? 'workout' : 'training', + active: lang === 'en' ? 'active' : 'aktiv', + exercises: lang === 'en' ? 'exercises' : 'uebungen', + measure: lang === 'en' ? 'measure' : 'messen' + }; +} + +/** Get translated nav labels */ +export function fitnessLabels(lang: 'en' | 'de') { + return { + stats: lang === 'en' ? 'Stats' : 'Statistik', + history: lang === 'en' ? 'History' : 'Verlauf', + workout: lang === 'en' ? 'Workout' : 'Training', + exercises: lang === 'en' ? 'Exercises' : 'Übungen', + measure: lang === 'en' ? 'Measure' : 'Messen' + }; +} + +type Translations = Record>; + +const translations: Translations = { + // Common + save: { en: 'Save', de: 'Speichern' }, + saving: { en: 'Saving…', de: 'Speichern…' }, + cancel: { en: 'CANCEL', de: 'ABBRECHEN' }, + delete_: { en: 'Delete', de: 'Löschen' }, + edit: { en: 'Edit', de: 'Bearbeiten' }, + loading: { en: 'Loading…', de: 'Laden…' }, + set: { en: 'set', de: 'Satz' }, + sets: { en: 'sets', de: 'Sätze' }, + exercise: { en: 'exercise', de: 'Übung' }, + exercises_word: { en: 'exercises', de: 'Übungen' }, + + // Units + kg: { en: 'kg', de: 'kg' }, + km: { en: 'km', de: 'km' }, + min: { en: 'min', de: 'Min' }, + + // Stats page + stats_title: { en: 'Stats', de: 'Statistik' }, + workout_singular: { en: 'Workout', de: 'Training' }, + workouts_plural: { en: 'Workouts', de: 'Trainings' }, + lifted: { en: 'Lifted', de: 'Gehoben' }, + distance_covered: { en: 'Distance Covered', de: 'Zurückgelegt' }, + workouts_per_week: { en: 'Workouts per week', de: 'Trainings pro Woche' }, + no_workout_data: { en: 'No workout data to display yet.', de: 'Noch keine Trainingsdaten vorhanden.' }, + weight: { en: 'Weight', de: 'Gewicht' }, + + // History page + history_title: { en: 'History', de: 'Verlauf' }, + no_workouts_yet: { en: 'No workouts yet. Start your first workout!', de: 'Noch keine Trainings. Starte dein erstes Training!' }, + load_more: { en: 'Load more', de: 'Mehr laden' }, + + // History detail + date: { en: 'Date', de: 'Datum' }, + time: { en: 'Time', de: 'Uhrzeit' }, + duration_min: { en: 'Duration (min)', de: 'Dauer (Min)' }, + notes: { en: 'Notes', de: 'Notizen' }, + notes_placeholder: { en: 'Workout notes...', de: 'Trainingsnotizen...' }, + gps_track_stored: { en: 'GPS track stored', de: 'GPS-Track gespeichert' }, + add_set: { en: '+ ADD SET', de: '+ SATZ HINZUFÜGEN' }, + add_exercise: { en: '+ ADD EXERCISE', de: '+ ÜBUNG HINZUFÜGEN' }, + splits: { en: 'Splits', de: 'Splits' }, + pace: { en: 'PACE', de: 'TEMPO' }, + upload_gpx: { en: 'Upload GPX', de: 'GPX hochladen' }, + uploading: { en: 'Uploading...', de: 'Hochladen...' }, + personal_records: { en: 'Personal Records', de: 'Persönliche Rekorde' }, + delete_session_confirm: { en: 'Delete this workout session?', de: 'Dieses Training löschen?' }, + remove_gps_confirm: { en: 'Remove GPS track from this exercise?', de: 'GPS-Track von dieser Übung entfernen?' }, + recalc_title: { en: 'Recalculate volume, PRs, and GPS previews', de: 'Volumen, PRs und GPS-Vorschauen neu berechnen' }, + + // Workout templates page + next_in_schedule: { en: 'Next in schedule', de: 'Nächstes im Plan' }, + start_empty_workout: { en: 'START AN EMPTY WORKOUT', de: 'LEERES TRAINING STARTEN' }, + templates: { en: 'Templates', de: 'Vorlagen' }, + schedule: { en: 'Schedule', de: 'Zeitplan' }, + my_templates: { en: 'My Templates', de: 'Meine Vorlagen' }, + no_templates_yet: { en: 'No templates yet. Create one or start an empty workout.', de: 'Noch keine Vorlagen. Erstelle eine oder starte ein leeres Training.' }, + edit_template: { en: 'Edit Template', de: 'Vorlage bearbeiten' }, + new_template: { en: 'New Template', de: 'Neue Vorlage' }, + template_name_placeholder: { en: 'Template name', de: 'Vorlagenname' }, + add_set_lower: { en: '+ Add set', de: '+ Satz hinzufügen' }, + add_exercise_btn: { en: 'Add Exercise', de: 'Übung hinzufügen' }, + save_template: { en: 'Save Template', de: 'Vorlage speichern' }, + workout_schedule: { en: 'Workout Schedule', de: 'Trainingsplan' }, + schedule_hint: { en: 'Select templates and arrange their order. After completing a workout, the next one in the rotation will be suggested.', de: 'Wähle Vorlagen und ordne sie an. Nach Abschluss eines Trainings wird das nächste in der Rotation vorgeschlagen.' }, + available_templates: { en: 'Available templates', de: 'Verfügbare Vorlagen' }, + all_templates_scheduled: { en: 'All templates are in the schedule', de: 'Alle Vorlagen sind im Zeitplan' }, + save_schedule: { en: 'Save Schedule', de: 'Zeitplan speichern' }, + start_workout: { en: 'Start Workout', de: 'Training starten' }, + delete_template: { en: 'Delete', de: 'Löschen' }, + + // Active workout / completion + workout_complete: { en: 'Workout Complete', de: 'Training abgeschlossen' }, + duration: { en: 'Duration', de: 'Dauer' }, + tonnage: { en: 'Tonnage', de: 'Tonnage' }, + distance: { en: 'Distance', de: 'Distanz' }, + exercises_heading: { en: 'Exercises', de: 'Übungen' }, + volume: { en: 'volume', de: 'Volumen' }, + avg: { en: 'avg', de: 'Ø' }, + update_template: { en: 'Update Template', de: 'Vorlage aktualisieren' }, + template_updated: { en: 'Template updated', de: 'Vorlage aktualisiert' }, + template_diff_desc: { en: 'Your weights or reps differ from the template:', de: 'Gewichte oder Wiederholungen weichen von der Vorlage ab:' }, + updating: { en: 'Updating...', de: 'Aktualisieren...' }, + view_workout: { en: 'VIEW WORKOUT', de: 'TRAINING ANSEHEN' }, + workout_name_placeholder: { en: 'Workout name', de: 'Trainingsname' }, + cancel_workout: { en: 'CANCEL WORKOUT', de: 'TRAINING ABBRECHEN' }, + finish: { en: 'FINISH', de: 'BEENDEN' }, + new_set_added: { en: 'new set', de: 'neuer Satz' }, + new_sets_added: { en: 'new sets', de: 'neue Sätze' }, + + // Exercises page + exercises_title: { en: 'Exercises', de: 'Übungen' }, + search_exercises: { en: 'Search exercises…', de: 'Übungen suchen…' }, + all_body_parts: { en: 'All body parts', de: 'Alle Körperteile' }, + all_equipment: { en: 'All equipment', de: 'Alle Geräte' }, + no_exercises_match: { en: 'No exercises match your search.', de: 'Keine Übungen gefunden.' }, + + // Exercise detail + about: { en: 'ABOUT', de: 'INFO' }, + history_tab: { en: 'HISTORY', de: 'VERLAUF' }, + charts: { en: 'CHARTS', de: 'DIAGRAMME' }, + records: { en: 'RECORDS', de: 'REKORDE' }, + instructions: { en: 'Instructions', de: 'Anleitung' }, + no_history_yet: { en: 'No history for this exercise yet.', de: 'Noch kein Verlauf für diese Übung.' }, + est_1rm: { en: 'EST. 1RM', de: 'GESCH. 1RM' }, + best_set_1rm: { en: 'Best Set (Est. 1RM)', de: 'Bester Satz (Gesch. 1RM)' }, + best_set_max: { en: 'Best Set (Max Weight)', de: 'Bester Satz (Max. Gewicht)' }, + total_volume: { en: 'Total Volume', de: 'Gesamtvolumen' }, + not_enough_data: { en: 'Not enough data to display charts yet.', de: 'Noch nicht genug Daten für Diagramme.' }, + estimated_1rm: { en: 'Estimated 1RM', de: 'Geschätztes 1RM' }, + max_volume: { en: 'Max Volume', de: 'Max. Volumen' }, + max_weight: { en: 'Max Weight', de: 'Max. Gewicht' }, + rep_records: { en: 'Rep Records', de: 'Wiederholungsrekorde' }, + reps: { en: 'REPS', de: 'WDH' }, + best_performance: { en: 'BEST PERFORMANCE', de: 'BESTLEISTUNG' }, + + // Measure page + measure_title: { en: 'Measure', de: 'Messen' }, + new_measurement: { en: 'New Measurement', de: 'Neue Messung' }, + edit_measurement: { en: 'Edit Measurement', de: 'Messung bearbeiten' }, + weight_kg: { en: 'Weight (kg)', de: 'Gewicht (kg)' }, + body_fat: { en: 'Body Fat %', de: 'Körperfett %' }, + calories_kcal: { en: 'Calories (kcal)', de: 'Kalorien (kcal)' }, + body_parts_cm: { en: 'Body Parts (cm)', de: 'Körpermaße (cm)' }, + neck: { en: 'Neck', de: 'Hals' }, + shoulders: { en: 'Shoulders', de: 'Schultern' }, + chest: { en: 'Chest', de: 'Brust' }, + l_bicep: { en: 'L Bicep', de: 'L Bizeps' }, + r_bicep: { en: 'R Bicep', de: 'R Bizeps' }, + l_forearm: { en: 'L Forearm', de: 'L Unterarm' }, + r_forearm: { en: 'R Forearm', de: 'R Unterarm' }, + waist: { en: 'Waist', de: 'Taille' }, + hips: { en: 'Hips', de: 'Hüfte' }, + l_thigh: { en: 'L Thigh', de: 'L Oberschenkel' }, + r_thigh: { en: 'R Thigh', de: 'R Oberschenkel' }, + l_calf: { en: 'L Calf', de: 'L Wade' }, + r_calf: { en: 'R Calf', de: 'R Wade' }, + save_measurement: { en: 'Save Measurement', de: 'Messung speichern' }, + update_measurement: { en: 'Update Measurement', de: 'Messung aktualisieren' }, + latest: { en: 'Latest', de: 'Aktuell' }, + body_fat_short: { en: 'Body Fat', de: 'Körperfett' }, + calories: { en: 'Calories', de: 'Kalorien' }, + body_parts: { en: 'Body Parts', de: 'Körpermaße' }, + body_measurements_only: { en: 'Body measurements only', de: 'Nur Körpermaße' }, + delete_measurement_confirm: { en: 'Delete this measurement?', de: 'Diese Messung löschen?' }, + general: { en: 'General', de: 'Allgemein' }, + body_fat_pct: { en: 'Body Fat (%)', de: 'Körperfett (%)' }, + history: { en: 'History', de: 'Verlauf' }, + + // SetTable + set_header: { en: 'SET', de: 'SATZ' }, + prev_header: { en: 'PREV', de: 'VORH' }, + rpe: { en: 'RPE', de: 'RPE' }, + + // ExercisePicker + picker_title: { en: 'Add Exercise', de: 'Übung hinzufügen' }, + no_exercises_found: { en: 'No exercises found', de: 'Keine Übungen gefunden' }, + + // TemplateCard + last_performed: { en: 'Last performed:', de: 'Zuletzt durchgeführt:' }, + today: { en: 'Today', de: 'Heute' }, + yesterday: { en: 'Yesterday', de: 'Gestern' }, + days_ago: { en: 'days ago', de: 'Tagen' }, + more: { en: 'more', de: 'weitere' }, + + // WorkoutFab + active_workout: { en: 'Active Workout', de: 'Aktives Training' }, +}; + +/** Get a translated string */ +export function t(key: string, lang: 'en' | 'de'): string { + return translations[key]?.[lang] ?? translations[key]?.en ?? key; +} diff --git a/src/params/fitnessActive.ts b/src/params/fitnessActive.ts new file mode 100644 index 0000000..bb49b71 --- /dev/null +++ b/src/params/fitnessActive.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param === 'active' || param === 'aktiv'; +}; diff --git a/src/params/fitnessExercises.ts b/src/params/fitnessExercises.ts new file mode 100644 index 0000000..d25d498 --- /dev/null +++ b/src/params/fitnessExercises.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param === 'exercises' || param === 'uebungen'; +}; diff --git a/src/params/fitnessHistory.ts b/src/params/fitnessHistory.ts new file mode 100644 index 0000000..b76af35 --- /dev/null +++ b/src/params/fitnessHistory.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param === 'history' || param === 'verlauf'; +}; diff --git a/src/params/fitnessMeasure.ts b/src/params/fitnessMeasure.ts new file mode 100644 index 0000000..e22df15 --- /dev/null +++ b/src/params/fitnessMeasure.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param === 'measure' || param === 'messen'; +}; diff --git a/src/params/fitnessStats.ts b/src/params/fitnessStats.ts new file mode 100644 index 0000000..863aa83 --- /dev/null +++ b/src/params/fitnessStats.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param === 'stats' || param === 'statistik'; +}; diff --git a/src/params/fitnessWorkout.ts b/src/params/fitnessWorkout.ts new file mode 100644 index 0000000..3667fdd --- /dev/null +++ b/src/params/fitnessWorkout.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param === 'workout' || param === 'training'; +}; diff --git a/src/routes/fitness/+layout.svelte b/src/routes/fitness/+layout.svelte index 31fa5f7..77d545a 100644 --- a/src/routes/fitness/+layout.svelte +++ b/src/routes/fitness/+layout.svelte @@ -3,10 +3,12 @@ import { onMount, onDestroy } from 'svelte'; import Header from '$lib/components/Header.svelte'; import UserHeader from '$lib/components/UserHeader.svelte'; + import LanguageSelector from '$lib/components/LanguageSelector.svelte'; import { BarChart3, Clock, Dumbbell, ListChecks, Ruler } from 'lucide-svelte'; import { getWorkout } from '$lib/js/workout.svelte'; import { getWorkoutSync } from '$lib/js/workoutSync.svelte'; import WorkoutFab from '$lib/components/fitness/WorkoutFab.svelte'; + import { detectFitnessLang, fitnessSlugs, fitnessLabels } from '$lib/js/fitnessI18n'; let { data, children } = $props(); let user = $derived(data.session?.user); @@ -14,6 +16,10 @@ const workout = getWorkout(); const sync = getWorkoutSync(); + const lang = $derived(detectFitnessLang($page.url.pathname)); + const s = $derived(fitnessSlugs(lang)); + const labels = $derived(fitnessLabels(lang)); + onMount(async () => { workout.restore(); workout.onChange(() => sync.notifyChange()); @@ -30,27 +36,36 @@ return currentPath.startsWith(path); } - const isOnActivePage = $derived($page.url.pathname === '/fitness/workout/active'); + const activePath = $derived(`/fitness/${s.workout}/${s.active}`); + const isOnActivePage = $derived($page.url.pathname === activePath); /** @param {number} secs */ function formatElapsed(secs) { const m = Math.floor(secs / 60); - const s = secs % 60; - return `${m}:${s.toString().padStart(2, '0')}`; + const sec = secs % 60; + return `${m}:${sec.toString().padStart(2, '0')}`; }
    {#snippet links()} {/snippet} + {#snippet language_selector_mobile()} + + {/snippet} + + {#snippet language_selector_desktop()} + + {/snippet} + {#snippet right_side()} {/snippet} @@ -62,7 +77,7 @@ {#if workout.active && !isOnActivePage} import { goto } from '$app/navigation'; + import { page } from '$app/stores'; import { Search } from 'lucide-svelte'; import { getFilterOptions, searchExercises } from '$lib/data/exercises'; + import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; + + const lang = $derived(detectFitnessLang($page.url.pathname)); + const sl = $derived(fitnessSlugs(lang)); let { data } = $props(); @@ -18,25 +23,25 @@ })); -Exercises - Fitness +{lang === 'en' ? 'Exercises' : 'Übungen'} - Fitness
    -

    Exercises

    +

    {t('exercises_title', lang)}

    - +
    - +
    - - + +
    {:else} @@ -522,7 +527,7 @@ {@const exData = session.exercises[exIdx]}
    - GPS track stored{exData.totalDistance ? ` · ${exData.totalDistance.toFixed(2)} km` : ''} + {t('gps_track_stored', lang)}{exData.totalDistance ? ` · ${exData.totalDistance.toFixed(2)} km` : ''} @@ -544,13 +549,13 @@ />
    {/each} {:else} {#each session.exercises as ex, exIdx (ex.exerciseId + '-' + exIdx)} @@ -565,13 +570,13 @@ - + {#each mainMetrics as metric (metric)} {/each} {#if showEst1rm} - + {/if} @@ -620,12 +625,12 @@ {#if splits.length > 1} {@const avgPace = splits.reduce((a, s) => a + s.pace, 0) / splits.length}
    -

    Splits

    +

    {t('splits', lang)}

    SET{t('set_header', lang)}{METRIC_LABELS[metric]}RPEEST. 1RM{t('est_1rm', lang)}
    - + @@ -649,7 +654,7 @@ {:else if isCardio(ex.exerciseId)} {/if} @@ -658,7 +663,7 @@ {#if !editing && session.prs?.length > 0}
    -

    Personal Records

    +

    {t('personal_records', lang)}

    {#each session.prs as pr (pr.exerciseId + pr.type)} {@const exercise = getExerciseById(pr.exerciseId)} @@ -680,7 +685,7 @@ {#if !editing && session.notes}
    -

    Notes

    +

    {t('notes', lang)}

    {session.notes}

    {/if} diff --git a/src/routes/fitness/measure/+page.server.ts b/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts similarity index 100% rename from src/routes/fitness/measure/+page.server.ts rename to src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts diff --git a/src/routes/fitness/measure/+page.svelte b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte similarity index 74% rename from src/routes/fitness/measure/+page.svelte rename to src/routes/fitness/[measure=fitnessMeasure]/+page.svelte index 13cbb8c..2352745 100644 --- a/src/routes/fitness/measure/+page.svelte +++ b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte @@ -1,5 +1,9 @@ -Measure - Fitness +{lang === 'en' ? 'Measure' : 'Messen'} - Fitness
    -

    Measure

    +

    {t('measure_title', lang)}

    {#if showForm}
    { e.preventDefault(); saveMeasurement(); }}>
    -

    {editingId ? 'Edit' : 'New'} Measurement

    - +

    {editingId ? t('edit_measurement', lang) : t('new_measurement', lang)}

    +
    - +
    -

    General

    +

    {t('general', lang)}

    - +
    - +
    - +
    -

    Body Parts (cm)

    +

    {t('body_parts_cm', lang)}

    -
    -
    -
    +
    +
    +
    -
    -
    +
    +
    -
    -
    +
    +
    -
    -
    +
    +
    -
    -
    +
    +
    -
    -
    +
    +
    {/if}
    -

    Latest

    +

    {t('latest', lang)}

    - Weight + {t('weight', lang)} {latest.weight?.value ?? '—'} kg
    - Body Fat + {t('body_fat', lang)} {latest.bodyFatPercent?.value ?? '—'}%
    - Calories + {t('calories', lang)} {latest.caloricIntake?.value ?? '—'} kcal
    @@ -308,7 +312,7 @@ {#if bodyPartFields.some(f => f.value != null)}
    -

    Body Parts

    +

    {t('body_parts', lang)}

    {#each bodyPartFields.filter(f => f.value != null) as field}
    @@ -322,7 +326,7 @@ {#if measurements.length > 0}
    -

    History

    +

    {t('history', lang)}

    {#each measurements as m (m._id)}
    diff --git a/src/routes/fitness/stats/+page.server.ts b/src/routes/fitness/[stats=fitnessStats]/+page.server.ts similarity index 100% rename from src/routes/fitness/stats/+page.server.ts rename to src/routes/fitness/[stats=fitnessStats]/+page.server.ts diff --git a/src/routes/fitness/stats/+page.svelte b/src/routes/fitness/[stats=fitnessStats]/+page.svelte similarity index 89% rename from src/routes/fitness/stats/+page.svelte rename to src/routes/fitness/[stats=fitnessStats]/+page.svelte index c39d602..a7c2db3 100644 --- a/src/routes/fitness/stats/+page.svelte +++ b/src/routes/fitness/[stats=fitnessStats]/+page.svelte @@ -1,7 +1,11 @@ -Stats - Fitness +{t('stats_title', lang)} - Fitness
    -

    Stats

    +

    {t('stats_title', lang)}

    {stats.totalWorkouts ?? 0}
    -
    {(stats.totalWorkouts ?? 0) === 1 ? 'Workout' : 'Workouts'}
    +
    {(stats.totalWorkouts ?? 0) === 1 ? t('workout_singular', lang) : t('workouts_plural', lang)}
    {stats.totalTonnage ?? 0}t
    -
    Lifted
    +
    {t('lifted', lang)}
    {stats.totalCardioKm ?? 0}km
    -
    Distance Covered
    +
    {t('distance_covered', lang)}
    @@ -115,17 +119,17 @@ {:else} -

    No workout data to display yet.

    +

    {t('no_workout_data', lang)}

    {/if} {#if (stats.weightChart?.data?.length ?? 0) > 1} diff --git a/src/routes/fitness/workout/+page.server.ts b/src/routes/fitness/[workout=fitnessWorkout]/+page.server.ts similarity index 100% rename from src/routes/fitness/workout/+page.server.ts rename to src/routes/fitness/[workout=fitnessWorkout]/+page.server.ts diff --git a/src/routes/fitness/workout/+page.svelte b/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte similarity index 93% rename from src/routes/fitness/workout/+page.svelte rename to src/routes/fitness/[workout=fitnessWorkout]/+page.svelte index 45852da..821800a 100644 --- a/src/routes/fitness/workout/+page.svelte +++ b/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte @@ -1,9 +1,14 @@ -Workout - Fitness +{lang === 'en' ? 'Workout' : 'Training'} - Fitness
    {#if hasSchedule && nextTemplate}
    - Next in schedule + {t('next_in_schedule', lang)}
    -

    Templates

    +

    {t('templates', lang)}

    {#if templates.length > 0} -

    My Templates ({templates.length})

    +

    {t('my_templates', lang)} ({templates.length})

    {#each templates as template (template._id)} {:else} -

    No templates yet. Create one or start an empty workout.

    +

    {t('no_templates_yet', lang)}

    {/if}
    @@ -367,7 +372,7 @@ {@const exercise = getExerciseById(ex.exerciseId)}
  • {exercise?.name ?? ex.exerciseId} - {ex.sets.length} set{ex.sets.length !== 1 ? 's' : ''} + {ex.sets.length} {ex.sets.length !== 1 ? t('sets', lang) : t('set', lang)}
  • {/each} @@ -377,13 +382,13 @@
    @@ -398,14 +403,14 @@
    {/each}
    @@ -478,11 +483,11 @@
    KMPACE{t('pace', lang)} TIME