diff --git a/package.json b/package.json index 7dbd9e69..57cd1e3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.53.0", + "version": "1.54.0", "private": true, "type": "module", "scripts": { diff --git a/scripts/split-fitness-i18n.ts b/scripts/split-fitness-i18n.ts new file mode 100644 index 00000000..aad656a4 --- /dev/null +++ b/scripts/split-fitness-i18n.ts @@ -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` 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;', ''); +writeFileSync('src/lib/i18n/fitness/en.ts', enLines.join('\n')); + +console.log('wrote src/lib/i18n/fitness/de.ts and en.ts'); diff --git a/src/lib/i18n/fitness/de.ts b/src/lib/i18n/fitness/de.ts new file mode 100644 index 00000000..a3cfce5c --- /dev/null +++ b/src/lib/i18n/fitness/de.ts @@ -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; diff --git a/src/lib/i18n/fitness/en.ts b/src/lib/i18n/fitness/en.ts new file mode 100644 index 00000000..f03d61e4 --- /dev/null +++ b/src/lib/i18n/fitness/en.ts @@ -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 Adam’s 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 — don’t 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; diff --git a/src/lib/js/fitnessBodyParts.ts b/src/lib/js/fitnessBodyParts.ts index 30610ad0..9dce2f08 100644 --- a/src/lib/js/fitnessBodyParts.ts +++ b/src/lib/js/fitnessBodyParts.ts @@ -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; diff --git a/src/lib/js/fitnessI18n.ts b/src/lib/js/fitnessI18n.ts index 01e14f25..dd444055 100644 --- a/src/lib/js/fitnessI18n.ts +++ b/src/lib/js/fitnessI18n.ts @@ -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` 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> = { en: { statistik: 'stats', verlauf: 'history', training: 'workout', aktiv: 'active', uebungen: 'exercises', erfassung: 'check-in', ernaehrung: 'nutrition' }, @@ -8,7 +24,7 @@ const slugMap: Record> = { 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>; - -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; } diff --git a/src/routes/fitness/[checkin=fitnessCheckIn]/+page.svelte b/src/routes/fitness/[checkin=fitnessCheckIn]/+page.svelte index c98575ed..ad659368 100644 --- a/src/routes/fitness/[checkin=fitnessCheckIn]/+page.svelte +++ b/src/routes/fitness/[checkin=fitnessCheckIn]/+page.svelte @@ -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})`; diff --git a/src/routes/fitness/[checkin=fitnessCheckIn]/body-parts/+page.svelte b/src/routes/fitness/[checkin=fitnessCheckIn]/body-parts/+page.svelte index c767fbfd..796c0545 100644 --- a/src/routes/fitness/[checkin=fitnessCheckIn]/body-parts/+page.svelte +++ b/src/routes/fitness/[checkin=fitnessCheckIn]/body-parts/+page.svelte @@ -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(', '); diff --git a/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte b/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte index aae18e97..54e45516 100644 --- a/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte +++ b/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte @@ -1535,7 +1535,7 @@ {#if gpsToggling}
- {t('initializing_gps', lang) ?? 'Initializing GPS…'} + {t('initializing_gps', lang)}
{/if}