refactor(i18n): split fitness translations into per-locale files

The fitness UI translation table previously lived as one combined
object in fitnessI18n.ts where every entry held both languages. That
hides drift (an English string can silently disappear without TypeScript
noticing) and makes adding strings a multi-edit dance.

Split into src/lib/i18n/fitness/{de,en}.ts. de.ts is the source of
truth for the key set; en.ts uses `as const satisfies
Record<keyof typeof de, string>` so any missing English translation is
a build-time error. fitnessI18n.ts now re-exports both as a typed
table m and adds FitnessLang/FitnessKey types — the existing
t/fitnessSlugs/fitnessLabels API stays so call sites don't churn.

The strict typing immediately surfaced one real bug: t('initializing_gps')
was being called from the active workout page but the key never existed
in the dictionary, so it had been rendering the literal string
'initializing_gps' through the fallback. Added the missing key in both
locales.

Tightened BodyPartCard.labelKey and the body-parts Step JSDoc to
FitnessKey instead of plain string so card data drift catches drift at
the data site, not the call site. Two dynamic-key sites (partKeyMap
fallbacks for unmapped measurement keys) are cast pragmatically.

The 360-entry split was done by a one-shot extraction script
(scripts/split-fitness-i18n.ts) — kept for re-use against
cospendI18n.ts and calendarI18n.ts in follow-up commits.
This commit is contained in:
2026-05-01 12:15:27 +02:00
parent c521a9ec68
commit 609405da81
9 changed files with 845 additions and 473 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.53.0",
"version": "1.54.0",
"private": true,
"type": "module",
"scripts": {
+74
View File
@@ -0,0 +1,74 @@
/**
* One-shot script: extract the translations object from fitnessI18n.ts
* into per-locale files src/lib/i18n/fitness/de.ts and en.ts.
*
* Run: pnpm exec vite-node scripts/split-fitness-i18n.ts
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
const src = readFileSync('src/lib/js/fitnessI18n.ts', 'utf8');
// Slice out just the translations object body so we don't accidentally
// match function bodies or other constructs above/below.
const startMarker = 'const translations: Translations = {';
const startIdx = src.indexOf(startMarker);
if (startIdx === -1) throw new Error('translations object not found');
const endIdx = src.indexOf('\n};\n', startIdx);
if (endIdx === -1) throw new Error('translations object end not found');
const body = src.slice(startIdx + startMarker.length, endIdx);
// Match each entry. Single-line and multi-line variants both supported.
// Strings are single-quoted in the source and contain no escaped single
// quotes (the file uses unicode escapes like for apostrophes), so
// [^']* is safe.
const re =
/^\s*(\w+):\s*\{\s*\n?\s*en:\s*'([^']*)'\s*,\s*\n?\s*de:\s*'([^']*)'\s*,?\s*\n?\s*\}\s*,?/gm;
/**
* Convert a JS single-quoted string body into the actual string. The captured
* regex content is literal source text — escapes like `` are still 6 chars
* (`\`, `u`, `2`, `0`, `1`, `9`) and need decoding.
*/
function decodeJsString(raw: string): string {
const jsonReady = '"' + raw.replace(/\\'/g, "'").replace(/"/g, '\\"') + '"';
return JSON.parse(jsonReady);
}
const entries: Array<{ key: string; en: string; de: string }> = [];
let m: RegExpExecArray | null;
while ((m = re.exec(body)) !== null) {
entries.push({
key: m[1],
en: decodeJsString(m[2]),
de: decodeJsString(m[3])
});
}
console.log(`extracted ${entries.length} entries`);
mkdirSync('src/lib/i18n/fitness', { recursive: true });
// de.ts is the source of truth for the key set.
const deLines = [
'/** Generated by scripts/split-fitness-i18n.ts. */',
'/** German fitness UI strings — source of truth for the key set. */',
'',
'export const de = {'
];
for (const e of entries) deLines.push(`\t${e.key}: ${JSON.stringify(e.de)},`);
deLines.push('} as const;', '');
writeFileSync('src/lib/i18n/fitness/de.ts', deLines.join('\n'));
// en.ts uses `satisfies Record<keyof typeof de, string>` so any key missing
// from en that exists in de raises a TypeScript error.
const enLines = [
'/** Generated by scripts/split-fitness-i18n.ts. */',
"import type { de } from './de';",
'',
'export const en = {'
];
for (const e of entries) enLines.push(`\t${e.key}: ${JSON.stringify(e.en)},`);
enLines.push('} as const satisfies Record<keyof typeof de, string>;', '');
writeFileSync('src/lib/i18n/fitness/en.ts', enLines.join('\n'));
console.log('wrote src/lib/i18n/fitness/de.ts and en.ts');
+366
View File
@@ -0,0 +1,366 @@
/** Generated by scripts/split-fitness-i18n.ts. */
/** German fitness UI strings — source of truth for the key set. */
export const de = {
save: "Speichern",
saving: "Speichern…",
cancel: "ABBRECHEN",
delete_: "Löschen",
edit: "Bearbeiten",
loading: "Laden…",
set: "Satz",
sets: "Sätze",
exercise: "Übung",
exercises_word: "Übungen",
kg: "kg",
km: "km",
min: "Min",
stats_title: "Statistik",
workout_singular: "Training",
workouts_plural: "Trainings",
lifted: "Gehoben",
est_kcal: "Gesch. kcal",
burned: "Verbrannt",
kcal_set_profile: "Geschlecht & Grösse unter",
covered: "Zurückgelegt",
workouts_per_week: "Trainings pro Woche",
sex: "Geschlecht",
male: "Männlich",
female: "Weiblich",
height: "Grösse (cm)",
birth_year: "Geburtsjahr",
no_workout_data: "Noch keine Trainingsdaten vorhanden.",
weight: "Gewicht",
history_title: "Verlauf",
no_workouts_yet: "Noch keine Trainings. Starte dein erstes Training!",
load_more: "Mehr laden",
date: "Datum",
time: "Uhrzeit",
duration_min: "Dauer (Min)",
notes: "Notizen",
notes_placeholder: "Trainingsnotizen...",
gps_track_stored: "GPS-Track gespeichert",
add_set: "+ SATZ HINZUFÜGEN",
add_exercise: "+ ÜBUNG HINZUFÜGEN",
splits: "Splits",
pace: "TEMPO",
upload_gpx: "GPX hochladen",
uploading: "Hochladen...",
download_gpx: "GPX herunterladen",
elevation: "Höhenprofil",
elevation_unit: "m",
elevation_gain: "Anstieg",
elevation_loss: "Abstieg",
cadence: "Kadenz",
cadence_unit: "spm",
cadence_permission_missing: "Kadenz deaktiviert — Aktivitätserkennung in den Einstellungen erlauben",
personal_records: "Persönliche Rekorde",
delete_session_confirm: "Dieses Training löschen?",
remove_gps_confirm: "GPS-Track von dieser Übung entfernen?",
recalc_title: "Volumen, PRs und GPS-Vorschauen neu berechnen",
next_in_schedule: "Nächstes im Plan",
start_empty_workout: "leeres Training",
templates: "Vorlagen",
schedule: "Zeitplan",
my_templates: "Meine Vorlagen",
no_templates_yet: "Noch keine Vorlagen. Stöbere in der Bibliothek oder erstelle deine eigene.",
template_library: "Vorlagen-Bibliothek",
browse_library: "Bibliothek durchsuchen",
template_added: "Vorlage hinzugefügt",
edit_template: "Vorlage bearbeiten",
new_template: "Neue Vorlage",
template_name_placeholder: "Vorlagenname",
add_set_lower: "+ Satz hinzufügen",
add_exercise_btn: "Übung hinzufügen",
save_template: "Vorlage speichern",
workout_schedule: "Trainingsplan",
schedule_hint: "Wähle Vorlagen und ordne sie an. Nach Abschluss eines Trainings wird das nächste in der Rotation vorgeschlagen.",
available_templates: "Verfügbare Vorlagen",
all_templates_scheduled: "Alle Vorlagen sind im Zeitplan",
save_schedule: "Zeitplan speichern",
start_workout: "Training starten",
delete_template: "Löschen",
workout_complete: "Training abgeschlossen",
workout_saved_offline: "Offline gespeichert — wird bei Verbindung synchronisiert.",
duration: "Dauer",
tonnage: "Tonnage",
distance: "Distanz",
exercises_heading: "Übungen",
volume: "Volumen",
avg: "Ø",
update_template: "Vorlage aktualisieren",
template_updated: "Vorlage aktualisiert",
template_diff_desc: "Gewichte oder Wiederholungen weichen von der Vorlage ab:",
updating: "Aktualisieren...",
view_workout: "TRAINING ANSEHEN",
done: "FERTIG",
workout_name_placeholder: "Trainingsname",
cancel_workout: "TRAINING ABBRECHEN",
finish: "BEENDEN",
new_set_added: "neuer Satz",
new_sets_added: "neue Sätze",
exercises_title: "Übungen",
search_exercises: "Übungen suchen…",
no_exercises_match: "Keine Übungen gefunden.",
type_any: "Alle Arten",
type_weights: "Kraft",
type_stretches: "Dehnen",
stretch_pill: "Dehnung",
strength_pill: "Kraft",
cardio_pill: "Cardio",
plyo_pill: "Plyo",
about: "INFO",
history_tab: "VERLAUF",
charts: "DIAGRAMME",
records: "REKORDE",
instructions: "Anleitung",
no_history_yet: "Noch kein Verlauf für diese Übung.",
est_1rm: "GESCH. 1RM",
best_set_1rm: "Bester Satz (Gesch. 1RM)",
best_set_max: "Bester Satz (Max. Gewicht)",
total_volume: "Gesamtvolumen",
not_enough_data: "Noch nicht genug Daten für Diagramme.",
estimated_1rm: "Geschätztes 1RM",
max_volume: "Max. Volumen",
max_weight: "Max. Gewicht",
rep_records: "Wiederholungsrekorde",
reps: "WDH",
best_performance: "BESTLEISTUNG",
measure_title: "Messen",
profile: "Profil",
profile_setup_cta: "Größe & Geburtsjahr eintragen, um BMI, TDEE und Kalorienbilanz freizuschalten.",
set_up_profile: "Einrichten",
edit_profile: "Profil bearbeiten",
dismiss: "Verwerfen",
new_measurement: "Neue Messung",
edit_measurement: "Messung bearbeiten",
weight_kg: "Gewicht (kg)",
body_fat: "Körperfett %",
calories_kcal: "Kalorien (kcal)",
body_parts_cm: "Körpermasse (cm)",
neck: "Hals",
shoulders: "Schultern",
chest: "Brust",
l_bicep: "L Bizeps",
r_bicep: "R Bizeps",
l_forearm: "L Unterarm",
r_forearm: "R Unterarm",
waist: "Taille",
hips: "Hüfte",
l_thigh: "L Oberschenkel",
r_thigh: "R Oberschenkel",
l_calf: "L Wade",
r_calf: "R Wade",
biceps: "Bizeps",
forearms: "Unterarme",
thighs: "Oberschenkel",
calves: "Waden",
measure_tip_neck: "Direkt unter dem Adamsapfel, Band parallel zum Boden.",
measure_tip_shoulders: "Breiteste Stelle über die Schultern, Arme entspannt hängend.",
measure_tip_chest: "In Brustwarzenhöhe nach normalem Ausatmen, Band waagerecht.",
measure_tip_biceps: "Arm angespannt im Peak; um die dickste Stelle messen.",
measure_tip_forearms: "Breiteste Stelle unterhalb des Ellenbogens, Arm entspannt.",
measure_tip_waist: "In Nabelhöhe, locker — nicht einziehen.",
measure_tip_hips: "Um die breiteste Stelle des Gesäßes.",
measure_tip_thighs: "Mittig zwischen Leistenfalte und Knie.",
measure_tip_calves: "Breiteste Stelle, beidseitig belastet stehend.",
save_measurement: "Messung speichern",
update_measurement: "Messung aktualisieren",
measure_body_parts: "Körpermasse erfassen",
measure_body_parts_sub: "Geführter Ablauf — ein Körperteil nach dem anderen.",
last_measured: "Zuletzt gemessen",
no_measurements_yet: "Noch keine Messungen",
step_n_of_m: "Schritt {n} von {m}",
over_time: "{label} im Verlauf",
first_measurement_hint: "Erste Messung — dein Wert erscheint hier.",
running_totals: "Laufende Übersicht",
review_save: "Prüfen & speichern",
ready_to_save: "Bereit zum Speichern",
review_numbers: "Prüfe deine Werte unten.",
skip: "Auslassen",
next: "Weiter",
back: "Zurück",
review: "Prüfen",
edit_again: "Erneut bearbeiten",
exit: "Schließen",
same_both_sides: "Auf beiden Seiten gleich",
copy_l_to_r: "L → R übernehmen",
copy_l_to_r_before: "L",
copy_l_to_r_after: "R übernehmen",
kbd_nav: "Navigation",
kbd_next: "weiter",
kbd_skip: "auslassen",
kbd_wheel: "±0,1",
kbd_hint: "? drücken für Tastenkürzel",
no_body_parts_selected: "Bitte mindestens einen Wert eingeben.",
today_short: "heute",
latest: "Aktuell",
body_fat_short: "Körperfett",
calories: "Kalorien",
body_parts: "Körpermasse",
body_measurements_only: "Nur Körpermasse",
delete_measurement_confirm: "Diese Messung löschen?",
general: "Allgemein",
body_fat_pct: "Körperfett (%)",
history: "Verlauf",
past_measurements: "Frühere Messungen",
show_more: "Mehr anzeigen",
overwrite_title: "Bestehende Werte überschreiben?",
overwrite_message: "Für dieses Datum sind bereits Werte erfasst: {fields}. Überschreiben?",
overwrite_confirm: "Überschreiben",
same_as_last: "Wie zuletzt",
set_header: "SATZ",
prev_header: "VORH",
rpe: "RPE",
picker_title: "Übung hinzufügen",
no_exercises_found: "Keine Übungen gefunden",
last_performed: "Zuletzt durchgeführt:",
today: "Heute",
yesterday: "Gestern",
days_ago: "Tagen",
more: "weitere",
active_workout: "Aktives Training",
streak: "Serie",
streak_weeks: "Wochen",
streak_week: "Woche",
weekly_goal: "Wochenziel",
workouts_per_week_goal: "Trainings / Woche",
set_goal: "Ziel setzen",
goal_set: "Ziel gesetzt",
intervals: "Intervalle",
no_intervals: "Keine",
new_interval: "Neues Intervall",
edit_interval: "Intervall bearbeiten",
delete_interval: "Löschen",
delete_interval_confirm: "Diese Intervallvorlage löschen?",
add_step: "+ Schritt hinzufügen",
add_group: "+ Wiederholungsgruppe",
repeat_times: "mal",
ungroup: "Auflösen",
group_label: "Wiederholen",
step_label: "Bezeichnung",
meters: "Meter",
seconds: "Sekunden",
intervals_complete: "Intervalle abgeschlossen",
select_interval: "Intervall wählen",
custom: "Eigene",
steps_count: "Schritte",
save_interval: "Intervall speichern",
interval_name_placeholder: "Intervallname",
label_easy: "Leicht",
label_moderate: "Moderat",
label_hard: "Hart",
label_sprint: "Sprint",
label_recovery: "Erholung",
label_hill_sprints: "Bergsprints",
label_tempo: "Tempo",
label_warm_up: "Aufwärmen",
label_cool_down: "Abkühlen",
nutrition_title: "Ernährung",
breakfast: "Frühstück",
lunch: "Mittagessen",
dinner: "Abendessen",
snack: "Snack",
add_food: "Essen hinzufügen",
search_food: "Essen suchen…",
amount_grams: "Menge (g)",
meal_type: "Mahlzeit",
daily_goal: "Tagesziel",
calorie_target: "Kalorienziel (kcal)",
protein_goal: "Proteinziel",
protein_fixed: "Fest (g/Tag)",
protein_per_kg: "Pro kg Körpergewicht",
fat_percent: "Fett-Anteil",
carb_percent: "KH-Anteil",
kcal: "kcal",
protein: "Protein",
fat: "Fett",
carbs: "Kohlenhydrate",
remaining: "übrig",
over: "über",
no_entries_yet: "Noch keine Einträge. Füge Essen hinzu, um zu tracken.",
set_goal_prompt: "Setze ein Kalorienziel, um mit dem Tracking zu beginnen.",
micro_details: "Mikronährstoffe",
of_daily: "vom Tagesziel",
per_serving: "pro Portion",
log_food: "Eintragen",
delete_entry_confirm: "Diesen Eintrag löschen?",
period_tracker: "Periodentracker",
current_period: "Aktuelle Periode",
no_period_data: "Noch keine Periodendaten. Erfasse deine erste Periode.",
no_active_period: "Keine aktive Periode.",
start_period: "Periode starten",
end_period: "Periode vorbei",
period_day: "Tag",
predicted_end: "Voraussichtliches Ende",
next_period: "Nächste Periode",
cycle_length: "Zykluslänge",
period_length: "Periodenlänge",
avg_cycle: "Ø Zyklus",
avg_period: "Ø Periode",
days: "Tage",
delete_period_confirm: "Diesen Periodeneintrag löschen?",
add_past_period: "Vergangene Periode hinzufügen",
period_start: "Beginn",
period_end: "Ende",
ongoing: "laufend",
share: "Teilen",
shared_with: "Geteilt mit",
add_user: "Nutzer hinzufügen…",
no_shared: "Mit niemandem geteilt.",
shared_by: "Geteilt von",
fertile_window: "Fruchtbares Fenster",
peak_fertility: "Höchste Fruchtbarkeit",
ovulation: "Eisprung",
fertile: "Fruchtbar",
luteal_phase: "Luteal",
predicted_ovulation: "Voraussichtlicher Eisprung",
to: "bis",
overview: "Überblick",
tips: "Tipps",
similar_exercises: "Ähnliche Übungen",
primary_muscles: "Primär",
secondary_muscles: "Sekundär",
play_video: "Video abspielen",
nutrition_stats: "Ernährung",
protein_per_kg_unit: "g/kg",
calorie_balance: "Kalorienbilanz",
calorie_balance_unit: "kcal/Tag",
diet_adherence: "Einhaltung",
seven_day_avg: "7-Tage-Ø",
thirty_day: "30 Tage",
macro_split: "Makroverteilung",
no_nutrition_data: "Noch keine Ernährungsdaten. Beginne mit dem Tracking.",
target: "Ziel",
days_tracked: "Tage erfasst",
since_start: "Seit Beginn",
no_weight_data: "Gewicht eintragen",
no_calorie_goal: "Kalorienziel setzen",
muscle_balance: "Muskelbalance",
weekly_sets: "Sätze pro Woche",
custom_meals: "Eigene Mahlzeiten",
custom_meal: "Eigene Mahlzeit",
new_meal: "Neue Mahlzeit",
meal_name: "Name der Mahlzeit",
add_ingredient: "Zutat hinzufügen",
no_custom_meals: "Noch keine eigenen Mahlzeiten.",
create_meal_hint: "Erstelle wiederverwendbare Mahlzeiten zum schnellen Eintragen.",
ingredients: "Zutaten",
total: "Gesamt",
log_meal: "Mahlzeit eintragen",
delete_meal_confirm: "Diese Mahlzeit löschen?",
save_meal: "Mahlzeit speichern",
favorites: "Favoriten",
per_100g: "pro 100 g",
macros: "Makronährstoffe",
minerals: "Mineralstoffe",
vitamins: "Vitamine",
amino_acids: "Aminosäuren",
essential: "Essenziell",
non_essential: "Nicht-essenziell",
saturated_fat: "Gesättigte Fettsäuren",
fiber: "Ballaststoffe",
sugars: "Zucker",
source_db: "Quelle",
initializing_gps: "GPS wird initialisiert…",
} as const;
+366
View File
@@ -0,0 +1,366 @@
/** Generated by scripts/split-fitness-i18n.ts. */
import type { de } from './de';
export const en = {
save: "Save",
saving: "Saving…",
cancel: "CANCEL",
delete_: "Delete",
edit: "Edit",
loading: "Loading…",
set: "set",
sets: "sets",
exercise: "exercise",
exercises_word: "exercises",
kg: "kg",
km: "km",
min: "min",
stats_title: "Stats",
workout_singular: "Workout",
workouts_plural: "Workouts",
lifted: "Lifted",
est_kcal: "Est. kcal",
burned: "Burned",
kcal_set_profile: "Set sex & height in",
covered: "Covered",
workouts_per_week: "Workouts per week",
sex: "Sex",
male: "Male",
female: "Female",
height: "Height (cm)",
birth_year: "Birth Year",
no_workout_data: "No workout data to display yet.",
weight: "Weight",
history_title: "History",
no_workouts_yet: "No workouts yet. Start your first workout!",
load_more: "Load more",
date: "Date",
time: "Time",
duration_min: "Duration (min)",
notes: "Notes",
notes_placeholder: "Workout notes...",
gps_track_stored: "GPS track stored",
add_set: "+ ADD SET",
add_exercise: "+ ADD EXERCISE",
splits: "Splits",
pace: "PACE",
upload_gpx: "Upload GPX",
uploading: "Uploading...",
download_gpx: "Download GPX",
elevation: "Elevation",
elevation_unit: "m",
elevation_gain: "Gain",
elevation_loss: "Loss",
cadence: "Cadence",
cadence_unit: "spm",
cadence_permission_missing: "Cadence disabled — grant Activity Recognition in system settings",
personal_records: "Personal Records",
delete_session_confirm: "Delete this workout session?",
remove_gps_confirm: "Remove GPS track from this exercise?",
recalc_title: "Recalculate volume, PRs, and GPS previews",
next_in_schedule: "Next in schedule",
start_empty_workout: "Empty Workout",
templates: "Templates",
schedule: "Schedule",
my_templates: "My Templates",
no_templates_yet: "No templates yet. Browse the library or create your own.",
template_library: "Template Library",
browse_library: "Browse Library",
template_added: "Template added",
edit_template: "Edit Template",
new_template: "New Template",
template_name_placeholder: "Template name",
add_set_lower: "+ Add set",
add_exercise_btn: "Add Exercise",
save_template: "Save Template",
workout_schedule: "Workout Schedule",
schedule_hint: "Select templates and arrange their order. After completing a workout, the next one in the rotation will be suggested.",
available_templates: "Available templates",
all_templates_scheduled: "All templates are in the schedule",
save_schedule: "Save Schedule",
start_workout: "Start Workout",
delete_template: "Delete",
workout_complete: "Workout Complete",
workout_saved_offline: "Saved offline — will sync when back online.",
duration: "Duration",
tonnage: "Tonnage",
distance: "Distance",
exercises_heading: "Exercises",
volume: "volume",
avg: "avg",
update_template: "Update Template",
template_updated: "Template updated",
template_diff_desc: "Your weights or reps differ from the template:",
updating: "Updating...",
view_workout: "VIEW WORKOUT",
done: "DONE",
workout_name_placeholder: "Workout name",
cancel_workout: "CANCEL WORKOUT",
finish: "FINISH",
new_set_added: "new set",
new_sets_added: "new sets",
exercises_title: "Exercises",
search_exercises: "Search exercises…",
no_exercises_match: "No exercises match your search.",
type_any: "Any type",
type_weights: "Strength",
type_stretches: "Stretches",
stretch_pill: "Stretch",
strength_pill: "Strength",
cardio_pill: "Cardio",
plyo_pill: "Plyo",
about: "ABOUT",
history_tab: "HISTORY",
charts: "CHARTS",
records: "RECORDS",
instructions: "Instructions",
no_history_yet: "No history for this exercise yet.",
est_1rm: "EST. 1RM",
best_set_1rm: "Best Set (Est. 1RM)",
best_set_max: "Best Set (Max Weight)",
total_volume: "Total Volume",
not_enough_data: "Not enough data to display charts yet.",
estimated_1rm: "Estimated 1RM",
max_volume: "Max Volume",
max_weight: "Max Weight",
rep_records: "Rep Records",
reps: "REPS",
best_performance: "BEST PERFORMANCE",
measure_title: "Measure",
profile: "Profile",
profile_setup_cta: "Add height & birth year to unlock BMI, TDEE and calorie balance stats.",
set_up_profile: "Set up",
edit_profile: "Edit profile",
dismiss: "Dismiss",
new_measurement: "New Measurement",
edit_measurement: "Edit Measurement",
weight_kg: "Weight (kg)",
body_fat: "Body Fat %",
calories_kcal: "Calories (kcal)",
body_parts_cm: "Body Parts (cm)",
neck: "Neck",
shoulders: "Shoulders",
chest: "Chest",
l_bicep: "L Bicep",
r_bicep: "R Bicep",
l_forearm: "L Forearm",
r_forearm: "R Forearm",
waist: "Waist",
hips: "Hips",
l_thigh: "L Thigh",
r_thigh: "R Thigh",
l_calf: "L Calf",
r_calf: "R Calf",
biceps: "Biceps",
forearms: "Forearms",
thighs: "Thighs",
calves: "Calves",
measure_tip_neck: "Just below the Adams apple, tape parallel to the floor.",
measure_tip_shoulders: "Widest point across the deltoids, arms relaxed at your sides.",
measure_tip_chest: "At nipple line after a normal exhale, tape horizontal.",
measure_tip_biceps: "Arm flexed at the peak; tape around the thickest part.",
measure_tip_forearms: "Widest point below the elbow, arm hanging relaxed.",
measure_tip_waist: "At the navel, relaxed — dont suck in.",
measure_tip_hips: "Around the widest point of the buttocks.",
measure_tip_thighs: "Midway between hip crease and knee.",
measure_tip_calves: "Widest point, standing with weight on both feet.",
save_measurement: "Save Measurement",
update_measurement: "Update Measurement",
measure_body_parts: "Measure body parts",
measure_body_parts_sub: "Guided tape-measure flow — one part at a time.",
last_measured: "Last measured",
no_measurements_yet: "No measurements yet",
step_n_of_m: "Step {n} of {m}",
over_time: "{label} over time",
first_measurement_hint: "First measurement — your entry will appear here.",
running_totals: "Running totals",
review_save: "Review & save",
ready_to_save: "Ready to save",
review_numbers: "Review your numbers below.",
skip: "Skip",
next: "Next",
back: "Back",
review: "Review",
edit_again: "Edit again",
exit: "Exit",
same_both_sides: "Same on both sides",
copy_l_to_r: "Copy L → R",
copy_l_to_r_before: "Copy L",
copy_l_to_r_after: "R",
kbd_nav: "nav",
kbd_next: "next",
kbd_skip: "skip",
kbd_wheel: "±0.1",
kbd_hint: "Press ? for shortcuts",
no_body_parts_selected: "Enter at least one value before saving.",
today_short: "today",
latest: "Latest",
body_fat_short: "Body Fat",
calories: "Calories",
body_parts: "Body Parts",
body_measurements_only: "Body measurements only",
delete_measurement_confirm: "Delete this measurement?",
general: "General",
body_fat_pct: "Body Fat (%)",
history: "History",
past_measurements: "Past measurements",
show_more: "Show more",
overwrite_title: "Overwrite existing values?",
overwrite_message: "You already have values for this date: {fields}. Replace them?",
overwrite_confirm: "Overwrite",
same_as_last: "Same as last",
set_header: "SET",
prev_header: "PREV",
rpe: "RPE",
picker_title: "Add Exercise",
no_exercises_found: "No exercises found",
last_performed: "Last performed:",
today: "Today",
yesterday: "Yesterday",
days_ago: "days ago",
more: "more",
active_workout: "Active Workout",
streak: "Streak",
streak_weeks: "Weeks",
streak_week: "Week",
weekly_goal: "Weekly Goal",
workouts_per_week_goal: "workouts / week",
set_goal: "Set Goal",
goal_set: "Goal set",
intervals: "Intervals",
no_intervals: "None",
new_interval: "New Interval",
edit_interval: "Edit Interval",
delete_interval: "Delete",
delete_interval_confirm: "Delete this interval template?",
add_step: "+ Add Step",
add_group: "+ Add Repeat Group",
repeat_times: "times",
ungroup: "Ungroup",
group_label: "Repeat",
step_label: "Label",
meters: "meters",
seconds: "seconds",
intervals_complete: "Intervals complete",
select_interval: "Select Interval",
custom: "Custom",
steps_count: "steps",
save_interval: "Save Interval",
interval_name_placeholder: "Interval name",
label_easy: "Easy",
label_moderate: "Moderate",
label_hard: "Hard",
label_sprint: "Sprint",
label_recovery: "Recovery",
label_hill_sprints: "Hill Sprints",
label_tempo: "Tempo",
label_warm_up: "Warm Up",
label_cool_down: "Cool Down",
nutrition_title: "Nutrition",
breakfast: "Breakfast",
lunch: "Lunch",
dinner: "Dinner",
snack: "Snack",
add_food: "Add food",
search_food: "Search food…",
amount_grams: "Amount (g)",
meal_type: "Meal",
daily_goal: "Daily Goal",
calorie_target: "Calorie target (kcal)",
protein_goal: "Protein goal",
protein_fixed: "Fixed (g/day)",
protein_per_kg: "Per kg bodyweight",
fat_percent: "Fat ratio",
carb_percent: "Carbs ratio",
kcal: "kcal",
protein: "Protein",
fat: "Fat",
carbs: "Carbs",
remaining: "left",
over: "over",
no_entries_yet: "No entries yet. Add food to start tracking.",
set_goal_prompt: "Set a daily calorie goal to start tracking.",
micro_details: "Micronutrients",
of_daily: "of daily goal",
per_serving: "per serving",
log_food: "Log",
delete_entry_confirm: "Delete this food entry?",
period_tracker: "Period Tracker",
current_period: "Current Period",
no_period_data: "No period data yet. Log your first period to start tracking.",
no_active_period: "No active period.",
start_period: "Start Period",
end_period: "Period Ended",
period_day: "Day",
predicted_end: "Predicted end",
next_period: "Next period",
cycle_length: "Cycle length",
period_length: "Period length",
avg_cycle: "Avg. cycle",
avg_period: "Avg. period",
days: "days",
delete_period_confirm: "Delete this period entry?",
add_past_period: "Add Past Period",
period_start: "Start",
period_end: "End",
ongoing: "ongoing",
share: "Share",
shared_with: "Shared with",
add_user: "Add user…",
no_shared: "Not shared with anyone.",
shared_by: "Shared by",
fertile_window: "Fertile window",
peak_fertility: "Peak fertility",
ovulation: "Ovulation",
fertile: "Fertile",
luteal_phase: "Luteal",
predicted_ovulation: "Predicted ovulation",
to: "to",
overview: "Overview",
tips: "Tips",
similar_exercises: "Similar Exercises",
primary_muscles: "Primary",
secondary_muscles: "Secondary",
play_video: "Play Video",
nutrition_stats: "Nutrition",
protein_per_kg_unit: "g/kg",
calorie_balance: "Calorie Balance",
calorie_balance_unit: "kcal/day",
diet_adherence: "Adherence",
seven_day_avg: "7-day avg",
thirty_day: "30 days",
macro_split: "Macro Split",
no_nutrition_data: "No nutrition data yet. Start logging food to see stats.",
target: "Target",
days_tracked: "days tracked",
since_start: "Since start",
no_weight_data: "Log weight to enable",
no_calorie_goal: "Set calorie goal",
muscle_balance: "Muscle Balance",
weekly_sets: "Sets per week",
custom_meals: "Custom Meals",
custom_meal: "Custom Meal",
new_meal: "New Meal",
meal_name: "Meal name",
add_ingredient: "Add ingredient",
no_custom_meals: "No custom meals yet.",
create_meal_hint: "Create reusable meals for quick logging.",
ingredients: "Ingredients",
total: "Total",
log_meal: "Log Meal",
delete_meal_confirm: "Delete this custom meal?",
save_meal: "Save Meal",
favorites: "Favorites",
per_100g: "per 100 g",
macros: "Macronutrients",
minerals: "Minerals",
vitamins: "Vitamins",
amino_acids: "Amino Acids",
essential: "Essential",
non_essential: "Non-Essential",
saturated_fat: "Saturated Fat",
fiber: "Fiber",
sugars: "Sugars",
source_db: "Source",
initializing_gps: "Initializing GPS…",
} as const satisfies Record<keyof typeof de, string>;
+4 -2
View File
@@ -1,7 +1,9 @@
import type { FitnessKey } from './fitnessI18n';
export type SingleBodyPartCard = {
key: string;
slugDe: string;
labelKey: string;
labelKey: FitnessKey;
img: string | null;
paired: false;
db: string;
@@ -9,7 +11,7 @@ export type SingleBodyPartCard = {
export type PairedBodyPartCard = {
key: string;
slugDe: string;
labelKey: string;
labelKey: FitnessKey;
img: string | null;
paired: true;
dbLeft: string;
+28 -466
View File
@@ -1,4 +1,20 @@
/** Fitness route i18n — slug mappings and UI translations */
/**
* Fitness route i18n — slug mappings and UI translations.
*
* Translation tables live per-locale in `$lib/i18n/fitness/{de,en}.ts`.
* `de.ts` is the source of truth for the key set; `en.ts` uses
* `satisfies Record<keyof typeof de, string>` so any missing English
* translation surfaces as a TypeScript error at build time.
*/
import { de } from '$lib/i18n/fitness/de';
import { en } from '$lib/i18n/fitness/en';
/** All fitness translations, keyed by locale. */
export const m = { de, en } as const;
export type FitnessLang = keyof typeof m;
export type FitnessKey = keyof typeof de;
const slugMap: Record<string, Record<string, string>> = {
en: { statistik: 'stats', verlauf: 'history', training: 'workout', aktiv: 'active', uebungen: 'exercises', erfassung: 'check-in', ernaehrung: 'nutrition' },
@@ -8,7 +24,7 @@ const slugMap: Record<string, Record<string, string>> = {
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' {
export function detectFitnessLang(pathname: string): FitnessLang {
const segments = pathname.replace(/^\/fitness\/?/, '').split('/');
for (const seg of segments) {
if (germanSlugs.has(seg)) return 'de';
@@ -17,14 +33,14 @@ export function detectFitnessLang(pathname: string): 'en' | 'de' {
}
/** Convert a fitness path to the target language */
export function convertFitnessPath(pathname: string, targetLang: 'en' | 'de'): string {
export function convertFitnessPath(pathname: string, targetLang: FitnessLang): 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') {
export function fitnessSlugs(lang: FitnessLang) {
return {
stats: lang === 'en' ? 'stats' : 'statistik',
history: lang === 'en' ? 'history' : 'verlauf',
@@ -37,7 +53,7 @@ export function fitnessSlugs(lang: 'en' | 'de') {
}
/** Get translated nav labels */
export function fitnessLabels(lang: 'en' | 'de') {
export function fitnessLabels(lang: FitnessLang) {
return {
stats: lang === 'en' ? 'Stats' : 'Statistik',
history: lang === 'en' ? 'History' : 'Verlauf',
@@ -48,465 +64,11 @@ export function fitnessLabels(lang: 'en' | 'de') {
};
}
type Translations = Record<string, Record<string, string>>;
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' },
est_kcal: { en: 'Est. kcal', de: 'Gesch. kcal' },
burned: { en: 'Burned', de: 'Verbrannt' },
kcal_set_profile: { en: 'Set sex & height in', de: 'Geschlecht & Grösse unter' },
covered: { en: 'Covered', de: 'Zurückgelegt' },
workouts_per_week: { en: 'Workouts per week', de: 'Trainings pro Woche' },
sex: { en: 'Sex', de: 'Geschlecht' },
male: { en: 'Male', de: 'Männlich' },
female: { en: 'Female', de: 'Weiblich' },
height: { en: 'Height (cm)', de: 'Grösse (cm)' },
birth_year: { en: 'Birth Year', de: 'Geburtsjahr' },
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...' },
download_gpx: { en: 'Download GPX', de: 'GPX herunterladen' },
elevation: { en: 'Elevation', de: 'Höhenprofil' },
elevation_unit: { en: 'm', de: 'm' },
elevation_gain: { en: 'Gain', de: 'Anstieg' },
elevation_loss: { en: 'Loss', de: 'Abstieg' },
cadence: { en: 'Cadence', de: 'Kadenz' },
cadence_unit: { en: 'spm', de: 'spm' },
cadence_permission_missing: {
en: 'Cadence disabled — grant Activity Recognition in system settings',
de: 'Kadenz deaktiviert — Aktivitätserkennung in den Einstellungen erlauben'
},
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: 'Empty Workout', de: 'leeres Training' },
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. Browse the library or create your own.', de: 'Noch keine Vorlagen. Stöbere in der Bibliothek oder erstelle deine eigene.' },
template_library: { en: 'Template Library', de: 'Vorlagen-Bibliothek' },
browse_library: { en: 'Browse Library', de: 'Bibliothek durchsuchen' },
template_added: { en: 'Template added', de: 'Vorlage hinzugefügt' },
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' },
workout_saved_offline: { en: 'Saved offline — will sync when back online.', de: 'Offline gespeichert — wird bei Verbindung synchronisiert.' },
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' },
done: { en: 'DONE', de: 'FERTIG' },
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…' },
no_exercises_match: { en: 'No exercises match your search.', de: 'Keine Übungen gefunden.' },
type_any: { en: 'Any type', de: 'Alle Arten' },
type_weights: { en: 'Strength', de: 'Kraft' },
type_stretches: { en: 'Stretches', de: 'Dehnen' },
stretch_pill: { en: 'Stretch', de: 'Dehnung' },
strength_pill: { en: 'Strength', de: 'Kraft' },
cardio_pill: { en: 'Cardio', de: 'Cardio' },
plyo_pill: { en: 'Plyo', de: 'Plyo' },
// 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' },
profile: { en: 'Profile', de: 'Profil' },
profile_setup_cta: {
en: 'Add height & birth year to unlock BMI, TDEE and calorie balance stats.',
de: 'Größe & Geburtsjahr eintragen, um BMI, TDEE und Kalorienbilanz freizuschalten.'
},
set_up_profile: { en: 'Set up', de: 'Einrichten' },
edit_profile: { en: 'Edit profile', de: 'Profil bearbeiten' },
dismiss: { en: 'Dismiss', de: 'Verwerfen' },
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örpermasse (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' },
biceps: { en: 'Biceps', de: 'Bizeps' },
forearms: { en: 'Forearms', de: 'Unterarme' },
thighs: { en: 'Thighs', de: 'Oberschenkel' },
calves: { en: 'Calves', de: 'Waden' },
measure_tip_neck: {
en: 'Just below the Adam\u2019s apple, tape parallel to the floor.',
de: 'Direkt unter dem Adamsapfel, Band parallel zum Boden.'
},
measure_tip_shoulders: {
en: 'Widest point across the deltoids, arms relaxed at your sides.',
de: 'Breiteste Stelle \u00fcber die Schultern, Arme entspannt h\u00e4ngend.'
},
measure_tip_chest: {
en: 'At nipple line after a normal exhale, tape horizontal.',
de: 'In Brustwarzenh\u00f6he nach normalem Ausatmen, Band waagerecht.'
},
measure_tip_biceps: {
en: 'Arm flexed at the peak; tape around the thickest part.',
de: 'Arm angespannt im Peak; um die dickste Stelle messen.'
},
measure_tip_forearms: {
en: 'Widest point below the elbow, arm hanging relaxed.',
de: 'Breiteste Stelle unterhalb des Ellenbogens, Arm entspannt.'
},
measure_tip_waist: {
en: 'At the navel, relaxed \u2014 don\u2019t suck in.',
de: 'In Nabelh\u00f6he, locker \u2014 nicht einziehen.'
},
measure_tip_hips: {
en: 'Around the widest point of the buttocks.',
de: 'Um die breiteste Stelle des Ges\u00e4\u00dfes.'
},
measure_tip_thighs: {
en: 'Midway between hip crease and knee.',
de: 'Mittig zwischen Leistenfalte und Knie.'
},
measure_tip_calves: {
en: 'Widest point, standing with weight on both feet.',
de: 'Breiteste Stelle, beidseitig belastet stehend.'
},
save_measurement: { en: 'Save Measurement', de: 'Messung speichern' },
update_measurement: { en: 'Update Measurement', de: 'Messung aktualisieren' },
measure_body_parts: { en: 'Measure body parts', de: 'Körpermasse erfassen' },
measure_body_parts_sub: {
en: 'Guided tape-measure flow \u2014 one part at a time.',
de: 'Gef\u00fchrter Ablauf \u2014 ein K\u00f6rperteil nach dem anderen.'
},
last_measured: { en: 'Last measured', de: 'Zuletzt gemessen' },
no_measurements_yet: { en: 'No measurements yet', de: 'Noch keine Messungen' },
step_n_of_m: { en: 'Step {n} of {m}', de: 'Schritt {n} von {m}' },
over_time: { en: '{label} over time', de: '{label} im Verlauf' },
first_measurement_hint: {
en: 'First measurement \u2014 your entry will appear here.',
de: 'Erste Messung \u2014 dein Wert erscheint hier.'
},
running_totals: { en: 'Running totals', de: 'Laufende \u00dcbersicht' },
review_save: { en: 'Review & save', de: 'Pr\u00fcfen & speichern' },
ready_to_save: { en: 'Ready to save', de: 'Bereit zum Speichern' },
review_numbers: { en: 'Review your numbers below.', de: 'Pr\u00fcfe deine Werte unten.' },
skip: { en: 'Skip', de: 'Auslassen' },
next: { en: 'Next', de: 'Weiter' },
back: { en: 'Back', de: 'Zur\u00fcck' },
review: { en: 'Review', de: 'Pr\u00fcfen' },
edit_again: { en: 'Edit again', de: 'Erneut bearbeiten' },
exit: { en: 'Exit', de: 'Schlie\u00dfen' },
same_both_sides: { en: 'Same on both sides', de: 'Auf beiden Seiten gleich' },
copy_l_to_r: { en: 'Copy L \u2192 R', de: 'L \u2192 R \u00fcbernehmen' },
copy_l_to_r_before: { en: 'Copy L', de: 'L' },
copy_l_to_r_after: { en: 'R', de: 'R \u00fcbernehmen' },
kbd_nav: { en: 'nav', de: 'Navigation' },
kbd_next: { en: 'next', de: 'weiter' },
kbd_skip: { en: 'skip', de: 'auslassen' },
kbd_wheel: { en: '\u00b10.1', de: '\u00b10,1' },
kbd_hint: { en: 'Press ? for shortcuts', de: '? dr\u00fccken f\u00fcr Tastenk\u00fcrzel' },
no_body_parts_selected: {
en: 'Enter at least one value before saving.',
de: 'Bitte mindestens einen Wert eingeben.'
},
today_short: { en: 'today', de: 'heute' },
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örpermasse' },
body_measurements_only: { en: 'Body measurements only', de: 'Nur Körpermasse' },
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' },
past_measurements: { en: 'Past measurements', de: 'Frühere Messungen' },
show_more: { en: 'Show more', de: 'Mehr anzeigen' },
overwrite_title: { en: 'Overwrite existing values?', de: 'Bestehende Werte überschreiben?' },
overwrite_message: {
en: 'You already have values for this date: {fields}. Replace them?',
de: 'Für dieses Datum sind bereits Werte erfasst: {fields}. Überschreiben?'
},
overwrite_confirm: { en: 'Overwrite', de: 'Überschreiben' },
same_as_last: { en: 'Same as last', de: 'Wie zuletzt' },
// 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' },
// Streak / Goal
streak: { en: 'Streak', de: 'Serie' },
streak_weeks: { en: 'Weeks', de: 'Wochen' },
streak_week: { en: 'Week', de: 'Woche' },
weekly_goal: { en: 'Weekly Goal', de: 'Wochenziel' },
workouts_per_week_goal: { en: 'workouts / week', de: 'Trainings / Woche' },
set_goal: { en: 'Set Goal', de: 'Ziel setzen' },
goal_set: { en: 'Goal set', de: 'Ziel gesetzt' },
// Intervals
intervals: { en: 'Intervals', de: 'Intervalle' },
no_intervals: { en: 'None', de: 'Keine' },
new_interval: { en: 'New Interval', de: 'Neues Intervall' },
edit_interval: { en: 'Edit Interval', de: 'Intervall bearbeiten' },
delete_interval: { en: 'Delete', de: 'Löschen' },
delete_interval_confirm: { en: 'Delete this interval template?', de: 'Diese Intervallvorlage löschen?' },
add_step: { en: '+ Add Step', de: '+ Schritt hinzufügen' },
add_group: { en: '+ Add Repeat Group', de: '+ Wiederholungsgruppe' },
repeat_times: { en: 'times', de: 'mal' },
ungroup: { en: 'Ungroup', de: 'Auflösen' },
group_label: { en: 'Repeat', de: 'Wiederholen' },
step_label: { en: 'Label', de: 'Bezeichnung' },
meters: { en: 'meters', de: 'Meter' },
seconds: { en: 'seconds', de: 'Sekunden' },
intervals_complete: { en: 'Intervals complete', de: 'Intervalle abgeschlossen' },
select_interval: { en: 'Select Interval', de: 'Intervall wählen' },
custom: { en: 'Custom', de: 'Eigene' },
steps_count: { en: 'steps', de: 'Schritte' },
save_interval: { en: 'Save Interval', de: 'Intervall speichern' },
interval_name_placeholder: { en: 'Interval name', de: 'Intervallname' },
// Preset labels
label_easy: { en: 'Easy', de: 'Leicht' },
label_moderate: { en: 'Moderate', de: 'Moderat' },
label_hard: { en: 'Hard', de: 'Hart' },
label_sprint: { en: 'Sprint', de: 'Sprint' },
label_recovery: { en: 'Recovery', de: 'Erholung' },
label_hill_sprints: { en: 'Hill Sprints', de: 'Bergsprints' },
label_tempo: { en: 'Tempo', de: 'Tempo' },
label_warm_up: { en: 'Warm Up', de: 'Aufwärmen' },
label_cool_down: { en: 'Cool Down', de: 'Abkühlen' },
// Nutrition / Food log
nutrition_title: { en: 'Nutrition', de: 'Ernährung' },
breakfast: { en: 'Breakfast', de: 'Frühstück' },
lunch: { en: 'Lunch', de: 'Mittagessen' },
dinner: { en: 'Dinner', de: 'Abendessen' },
snack: { en: 'Snack', de: 'Snack' },
add_food: { en: 'Add food', de: 'Essen hinzufügen' },
search_food: { en: 'Search food…', de: 'Essen suchen…' },
amount_grams: { en: 'Amount (g)', de: 'Menge (g)' },
meal_type: { en: 'Meal', de: 'Mahlzeit' },
daily_goal: { en: 'Daily Goal', de: 'Tagesziel' },
calorie_target: { en: 'Calorie target (kcal)', de: 'Kalorienziel (kcal)' },
protein_goal: { en: 'Protein goal', de: 'Proteinziel' },
protein_fixed: { en: 'Fixed (g/day)', de: 'Fest (g/Tag)' },
protein_per_kg: { en: 'Per kg bodyweight', de: 'Pro kg Körpergewicht' },
fat_percent: { en: 'Fat ratio', de: 'Fett-Anteil' },
carb_percent: { en: 'Carbs ratio', de: 'KH-Anteil' },
kcal: { en: 'kcal', de: 'kcal' },
protein: { en: 'Protein', de: 'Protein' },
fat: { en: 'Fat', de: 'Fett' },
carbs: { en: 'Carbs', de: 'Kohlenhydrate' },
remaining: { en: 'left', de: 'übrig' },
over: { en: 'over', de: 'über' },
no_entries_yet: { en: 'No entries yet. Add food to start tracking.', de: 'Noch keine Einträge. Füge Essen hinzu, um zu tracken.' },
set_goal_prompt: { en: 'Set a daily calorie goal to start tracking.', de: 'Setze ein Kalorienziel, um mit dem Tracking zu beginnen.' },
micro_details: { en: 'Micronutrients', de: 'Mikronährstoffe' },
of_daily: { en: 'of daily goal', de: 'vom Tagesziel' },
per_serving: { en: 'per serving', de: 'pro Portion' },
log_food: { en: 'Log', de: 'Eintragen' },
delete_entry_confirm: { en: 'Delete this food entry?', de: 'Diesen Eintrag löschen?' },
// Period tracker
period_tracker: { en: 'Period Tracker', de: 'Periodentracker' },
current_period: { en: 'Current Period', de: 'Aktuelle Periode' },
no_period_data: { en: 'No period data yet. Log your first period to start tracking.', de: 'Noch keine Periodendaten. Erfasse deine erste Periode.' },
no_active_period: { en: 'No active period.', de: 'Keine aktive Periode.' },
start_period: { en: 'Start Period', de: 'Periode starten' },
end_period: { en: 'Period Ended', de: 'Periode vorbei' },
period_day: { en: 'Day', de: 'Tag' },
predicted_end: { en: 'Predicted end', de: 'Voraussichtliches Ende' },
next_period: { en: 'Next period', de: 'Nächste Periode' },
cycle_length: { en: 'Cycle length', de: 'Zykluslänge' },
period_length: { en: 'Period length', de: 'Periodenlänge' },
avg_cycle: { en: 'Avg. cycle', de: 'Ø Zyklus' },
avg_period: { en: 'Avg. period', de: 'Ø Periode' },
days: { en: 'days', de: 'Tage' },
delete_period_confirm: { en: 'Delete this period entry?', de: 'Diesen Periodeneintrag löschen?' },
add_past_period: { en: 'Add Past Period', de: 'Vergangene Periode hinzufügen' },
period_start: { en: 'Start', de: 'Beginn' },
period_end: { en: 'End', de: 'Ende' },
ongoing: { en: 'ongoing', de: 'laufend' },
share: { en: 'Share', de: 'Teilen' },
shared_with: { en: 'Shared with', de: 'Geteilt mit' },
add_user: { en: 'Add user…', de: 'Nutzer hinzufügen…' },
no_shared: { en: 'Not shared with anyone.', de: 'Mit niemandem geteilt.' },
shared_by: { en: 'Shared by', de: 'Geteilt von' },
fertile_window: { en: 'Fertile window', de: 'Fruchtbares Fenster' },
peak_fertility: { en: 'Peak fertility', de: 'Höchste Fruchtbarkeit' },
ovulation: { en: 'Ovulation', de: 'Eisprung' },
fertile: { en: 'Fertile', de: 'Fruchtbar' },
luteal_phase: { en: 'Luteal', de: 'Luteal' },
predicted_ovulation: { en: 'Predicted ovulation', de: 'Voraussichtlicher Eisprung' },
to: { en: 'to', de: 'bis' },
// Exercise detail (enriched)
overview: { en: 'Overview', de: 'Überblick' },
tips: { en: 'Tips', de: 'Tipps' },
similar_exercises: { en: 'Similar Exercises', de: 'Ähnliche Übungen' },
primary_muscles: { en: 'Primary', de: 'Primär' },
secondary_muscles: { en: 'Secondary', de: 'Sekundär' },
play_video: { en: 'Play Video', de: 'Video abspielen' },
// Nutrition stats
nutrition_stats: { en: 'Nutrition', de: 'Ernährung' },
protein_per_kg_unit: { en: 'g/kg', de: 'g/kg' },
calorie_balance: { en: 'Calorie Balance', de: 'Kalorienbilanz' },
calorie_balance_unit: { en: 'kcal/day', de: 'kcal/Tag' },
diet_adherence: { en: 'Adherence', de: 'Einhaltung' },
seven_day_avg: { en: '7-day avg', de: '7-Tage-Ø' },
thirty_day: { en: '30 days', de: '30 Tage' },
macro_split: { en: 'Macro Split', de: 'Makroverteilung' },
no_nutrition_data: { en: 'No nutrition data yet. Start logging food to see stats.', de: 'Noch keine Ernährungsdaten. Beginne mit dem Tracking.' },
target: { en: 'Target', de: 'Ziel' },
days_tracked: { en: 'days tracked', de: 'Tage erfasst' },
since_start: { en: 'Since start', de: 'Seit Beginn' },
no_weight_data: { en: 'Log weight to enable', de: 'Gewicht eintragen' },
no_calorie_goal: { en: 'Set calorie goal', de: 'Kalorienziel setzen' },
// Muscle heatmap
muscle_balance: { en: 'Muscle Balance', de: 'Muskelbalance' },
weekly_sets: { en: 'Sets per week', de: 'Sätze pro Woche' },
// Custom meals
custom_meals: { en: 'Custom Meals', de: 'Eigene Mahlzeiten' },
custom_meal: { en: 'Custom Meal', de: 'Eigene Mahlzeit' },
new_meal: { en: 'New Meal', de: 'Neue Mahlzeit' },
meal_name: { en: 'Meal name', de: 'Name der Mahlzeit' },
add_ingredient: { en: 'Add ingredient', de: 'Zutat hinzufügen' },
no_custom_meals: { en: 'No custom meals yet.', de: 'Noch keine eigenen Mahlzeiten.' },
create_meal_hint: { en: 'Create reusable meals for quick logging.', de: 'Erstelle wiederverwendbare Mahlzeiten zum schnellen Eintragen.' },
ingredients: { en: 'Ingredients', de: 'Zutaten' },
total: { en: 'Total', de: 'Gesamt' },
log_meal: { en: 'Log Meal', de: 'Mahlzeit eintragen' },
delete_meal_confirm: { en: 'Delete this custom meal?', de: 'Diese Mahlzeit löschen?' },
save_meal: { en: 'Save Meal', de: 'Mahlzeit speichern' },
// Favorites
favorites: { en: 'Favorites', de: 'Favoriten' },
// Ingredient detail
per_100g: { en: 'per 100 g', de: 'pro 100 g' },
macros: { en: 'Macronutrients', de: 'Makronährstoffe' },
minerals: { en: 'Minerals', de: 'Mineralstoffe' },
vitamins: { en: 'Vitamins', de: 'Vitamine' },
amino_acids: { en: 'Amino Acids', de: 'Aminosäuren' },
essential: { en: 'Essential', de: 'Essenziell' },
non_essential: { en: 'Non-Essential', de: 'Nicht-essenziell' },
saturated_fat: { en: 'Saturated Fat', de: 'Gesättigte Fettsäuren' },
fiber: { en: 'Fiber', de: 'Ballaststoffe' },
sugars: { en: 'Sugars', de: 'Zucker' },
source_db: { en: 'Source', de: 'Quelle' },
};
/** Get a translated string */
export function t(key: string, lang: 'en' | 'de'): string {
return translations[key]?.[lang] ?? translations[key]?.en ?? key;
/**
* Get a translated string. Prefer `m[lang].key` directly in new code — this
* helper is kept for the existing call sites and falls back to English then
* the key itself if the lookup misses.
*/
export function t(key: FitnessKey, lang: FitnessLang): string {
return m[lang][key] ?? m.en[key] ?? key;
}
@@ -15,6 +15,7 @@
import UserCog from '@lucide/svelte/icons/user-cog';
import Sparkles from '@lucide/svelte/icons/sparkles';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
/** @typedef {import('$lib/js/fitnessI18n').FitnessKey} FitnessKey */
import { toast } from '$lib/js/toast.svelte';
import { confirm } from '$lib/js/confirmDialog.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
@@ -360,7 +361,7 @@
else if (c.key === 'notes') { label = lang === 'en' ? 'Notes' : 'Notizen'; }
else if (c.key.startsWith('measurements.')) {
const part = c.key.slice('measurements.'.length);
label = t(partKeyMap[part] ?? part, lang);
label = t(/** @type {FitnessKey} */ (partKeyMap[part] ?? part), lang);
unit = ' cm';
}
return `${label} (${c.oldVal}${unit}${c.newVal}${unit})`;
@@ -14,6 +14,7 @@
import { fly, fade } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
/** @typedef {import('$lib/js/fitnessI18n').FitnessKey} FitnessKey */
import { toast } from '$lib/js/toast.svelte';
import { confirm } from '$lib/js/confirmDialog.svelte';
import DatePicker from '$lib/components/DatePicker.svelte';
@@ -26,7 +27,7 @@
const lang = $derived(detectFitnessLang(page.url.pathname));
const checkinSlug = $derived(lang === 'en' ? 'check-in' : 'erfassung');
/** @typedef {{ key: string, labelKey: string, img: string | null, paired: boolean, tipKey: string, dbSingle?: string, dbLeft?: string, dbRight?: string }} Step */
/** @typedef {{ key: FitnessKey, labelKey: FitnessKey, img: string | null, paired: boolean, tipKey: FitnessKey, dbSingle?: string, dbLeft?: string, dbRight?: string }} Step */
/** @type {Step[]} */
const steps = [
@@ -232,7 +233,7 @@
/** @param {{ key: string, oldVal: unknown, newVal: unknown }} c */
const fmtConflict = (c) => {
const part = c.key.startsWith('measurements.') ? c.key.slice('measurements.'.length) : c.key;
const label = t(partKeyMap[part] ?? part, lang);
const label = t(/** @type {FitnessKey} */ (partKeyMap[part] ?? part), lang);
return `${label} (${c.oldVal} cm → ${c.newVal} cm)`;
};
const fields = conflicts.map(fmtConflict).join(', ');
@@ -1535,7 +1535,7 @@
</button>
{#if gpsToggling}
<div class="gps-initializing">
<span class="gps-spinner"></span> {t('initializing_gps', lang) ?? 'Initializing GPS…'}
<span class="gps-spinner"></span> {t('initializing_gps', lang)}
</div>
{/if}