diff --git a/tabatago-swift/TabataGo/Models/UserProfile.swift b/tabatago-swift/TabataGo/Models/UserProfile.swift index dc1d2a7..a345471 100644 --- a/tabatago-swift/TabataGo/Models/UserProfile.swift +++ b/tabatago-swift/TabataGo/Models/UserProfile.swift @@ -6,11 +6,11 @@ import Foundation enum FitnessLevel: String, Codable, CaseIterable { case beginner, intermediate, advanced - var label: String { + var label: LocalizedStringResource { switch self { - case .beginner: "Beginner" - case .intermediate: "Intermediate" - case .advanced: "Advanced" + case .beginner: L10n.level.beginner + case .intermediate: L10n.level.intermediate + case .advanced: L10n.level.advanced } } } @@ -19,12 +19,12 @@ enum FitnessGoal: String, Codable, CaseIterable { case weightLoss = "weight-loss" case cardio, strength, wellness - var label: String { + var label: LocalizedStringResource { switch self { - case .weightLoss: "Weight Loss" - case .cardio: "Cardio" - case .strength: "Strength" - case .wellness: "Wellness" + case .weightLoss: L10n.goal.weightLoss + case .cardio: L10n.goal.cardio + case .strength: L10n.goal.strength + case .wellness: L10n.goal.wellness } } } diff --git a/tabatago-swift/TabataGo/Resources/Localizable.xcstrings b/tabatago-swift/TabataGo/Resources/Localizable.xcstrings index 71a2437..9b71cdf 100644 --- a/tabatago-swift/TabataGo/Resources/Localizable.xcstrings +++ b/tabatago-swift/TabataGo/Resources/Localizable.xcstrings @@ -167,6 +167,35 @@ } } }, + "action.retry" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erneut versuchen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réessayer" + } + } + } + }, "action.save" : { "extractionState" : "manual", "localizations" : { @@ -370,6 +399,35 @@ } } }, + "activity.calories" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalorien" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calories" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calorías" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calories" + } + } + } + }, "activity.currentStreak" : { "extractionState" : "manual", "localizations" : { @@ -399,6 +457,35 @@ } } }, + "activity.days" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tage" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "days" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "días" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "jours" + } + } + } + }, "activity.history" : { "extractionState" : "manual", "localizations" : { @@ -515,6 +602,35 @@ } } }, + "activity.today" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heute" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aujourd'hui" + } + } + } + }, "activity.workouts" : { "extractionState" : "manual", "localizations" : { @@ -545,38 +661,236 @@ } }, "All" : { - - }, - "Any challenges?" : { - - }, - "Apple Health" : { - - }, - "Back to Home" : { - - }, - "Best Streak" : { - - }, - "BEST VALUE" : { - - }, - "Block %@ of %@" : { + "extractionState" : "manual", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle" + } + }, "en" : { "stringUnit" : { - "state" : "new", - "value" : "Block %1$@ of %2$@" + "state" : "translated", + "value" : "All" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tous" + } + } + } + }, + "Any challenges?" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herausforderungen?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Any challenges?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Algún obstáculo?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Des obstacles ?" + } + } + } + }, + "Apple Health" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + } + } + }, + "Back to Home" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zurück zur Startseite" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Back to Home" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volver al inicio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retour à l'accueil" + } + } + } + }, + "Best Streak" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beste Serie" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Best Streak" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mejor racha" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meilleure série" + } + } + } + }, + "BEST VALUE" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "BESTES ANGEBOT" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "BEST VALUE" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "MEJOR OFERTA" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MEILLEURE OFFRE" } } } }, "Cancel anytime. Prices in your local currency." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jederzeit kündbar. Preise in Ihrer Landeswährung." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel anytime. Prices in your local currency." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancela en cualquier momento. Precios en tu moneda local." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annulez à tout moment. Prix dans votre devise locale." + } + } + } }, "Complete your first Tabata to see your activity here." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schließe dein erstes Tabata ab, um deine Aktivität hier zu sehen." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Complete your first Tabata to see your activity here." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Completa tu primer Tabata para ver tu actividad aquí." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Complétez votre premier Tabata pour voir votre activité ici." + } + } + } }, "complete.avgHeartRate" : { "extractionState" : "manual", @@ -665,6 +979,35 @@ } } }, + "complete.completion" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vollendung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Completion" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compleción" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Complétion" + } + } + } + }, "complete.duration" : { "extractionState" : "manual", "localizations" : { @@ -781,6 +1124,35 @@ } } }, + "complete.saving" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Speichern…" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saving…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardando…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sauvegarde…" + } + } + } + }, "complete.shareWorkout" : { "extractionState" : "manual", "localizations" : { @@ -840,22 +1212,123 @@ } }, "Current Streak" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuelle Serie" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current Streak" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Racha actual" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Série actuelle" + } + } + } }, "days" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tage" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "days" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "días" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "jours" + } + } + } }, "Explore" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erkunden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Explore" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Explorar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Explorer" + } + } + } }, "Failed to load programs" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programme konnten nicht geladen werden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to load programs" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudieron cargar los programas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de charger les programmes" + } + } + } }, "For privacy concerns, contact us at privacy@tabatago.app" : { - }, - "FREE" : { - }, "goal.cardio" : { "extractionState" : "manual", @@ -1118,14 +1591,95 @@ } } }, + "health.today" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heute" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aujourd'hui" + } + } + } + }, "Hey, %@! 👋" : { }, "High-intensity Tabata workouts,\ndesigned for real results." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hochintensive Tabata-Workouts,\nfür echte Ergebnisse entwickelt." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "High-intensity Tabata workouts,\ndesigned for real results." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrenamientos Tabata de alta intensidad,\ndiseñados para resultados reales." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Des entraînements Tabata haute intensité,\nconçus pour de vrais résultats." + } + } + } }, "History" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verlauf" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "History" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historial" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historique" + } + } + } }, "home.allTime" : { "extractionState" : "manual", @@ -1156,6 +1710,35 @@ } } }, + "home.browseSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bestimmte Muskelgruppen gezielt trainieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Target specific muscle groups" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apunta a grupos musculares específicos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cibler des groupes musculaires spécifiques" + } + } + } + }, "home.browseTitle" : { "extractionState" : "manual", "localizations" : { @@ -1185,6 +1768,35 @@ } } }, + "home.failedToLoad" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programme konnten nicht geladen werden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to load programs" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudieron cargar los programas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de charger les programmes" + } + } + } + }, "home.featuredSubtitle" : { "extractionState" : "manual", "localizations" : { @@ -1243,6 +1855,35 @@ } } }, + "home.free" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kostenlos" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Free" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gratis" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gratuit" + } + } + } + }, "home.streak" : { "extractionState" : "manual", "localizations" : { @@ -1302,10 +1943,33 @@ } }, "Joined" : { - - }, - "Joined %@" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beigetreten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Joined" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registrado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inscrit(e)" + } + } + } }, "level.advanced" : { "extractionState" : "manual", @@ -1511,10 +2175,62 @@ } }, "Name" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nom" + } + } + } }, "No workouts yet" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noch keine Workouts" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No workouts yet" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aún no hay entrenamientos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune séance pour le moment" + } + } + } }, "onboarding.allSet" : { "extractionState" : "manual", @@ -1545,6 +2261,122 @@ } } }, + "onboarding.allSetSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein personalisierter Tabata-Plan ist bereit." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your personalised Tabata plan is ready." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu plan Tabata personalizado está listo." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre plan Tabata personnalisé est prêt." + } + } + } + }, + "onboarding.anyChallenges" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herausforderungen?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Any challenges?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Algún obstáculo?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Des obstacles ?" + } + } + } + }, + "onboarding.challengesSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optional — hilft uns, Tipps zu personalisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optional — helps us personalise tips" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opcional — nos ayuda a personalizar consejos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optionnel — nous aide à personnaliser vos conseils" + } + } + } + }, + "onboarding.enterName" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Namen eingeben" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter your name" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingresa tu nombre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrez votre prénom" + } + } + } + }, "onboarding.fitnessLevel" : { "extractionState" : "manual", "localizations" : { @@ -1574,6 +2406,180 @@ } } }, + "onboarding.fitnessLevelSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wir empfehlen die richtigen Workouts." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We'll recommend the right workouts." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te recomendaremos los entrenamientos correctos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous recommanderons les bonnes séances." + } + } + } + }, + "onboarding.getStarted" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loslegen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Get Started" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Empezar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Commencer" + } + } + } + }, + "onboarding.goal.cardioDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kardiovaskuläre Ausdauer verbessern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Improve cardiovascular endurance" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mejorar la resistencia cardiovascular" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Améliorer l'endurance cardiovasculaire" + } + } + } + }, + "onboarding.goal.strengthDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muskeln aufbauen und Kraft steigern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Build muscle and increase power" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarrollar músculo y aumentar la fuerza" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Développer les muscles et augmenter la puissance" + } + } + } + }, + "onboarding.goal.weightLossDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalorien verbrennen und Körperfett reduzieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Burn calories and reduce body fat" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quemar calorías y reducir grasa corporal" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brûler des calories et réduire la graisse" + } + } + } + }, + "onboarding.goal.wellnessDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allgemeine Gesundheit und Energie verbessern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Improve overall health and energy" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mejorar la salud general y la energía" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Améliorer la santé globale et l'énergie" + } + } + } + }, "onboarding.howOften" : { "extractionState" : "manual", "localizations" : { @@ -1603,6 +2609,122 @@ } } }, + "onboarding.howOftenSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei realistisch — Beständigkeit schlägt Intensität." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Be realistic — consistency beats intensity." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sé realista — la constancia supera a la intensidad." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Soyez réaliste — la régularité prime sur l'intensité." + } + } + } + }, + "onboarding.level.advancedDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erfahrener Athlet auf der Suche nach maximaler Herausforderung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Experienced athlete seeking maximum challenge" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atleta experimentado que busca el máximo desafío" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Athlète confirmé cherchant le défi maximum" + } + } + } + }, + "onboarding.level.beginnerDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neu beim HIIT oder nach einer Pause" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New to HIIT or returning after a break" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuevo en HIIT o volviendo tras un descanso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nouveau au HIIT ou après une pause" + } + } + } + }, + "onboarding.level.intermediateDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Regelmäßige Sportler, bereit für mehr Intensität" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Regular exerciser, ready for more intensity" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deportista regular, listo para más intensidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sportif régulier, prêt pour plus d'intensité" + } + } + } + }, "onboarding.mainGoal" : { "extractionState" : "manual", "localizations" : { @@ -1632,6 +2754,209 @@ } } }, + "onboarding.mainGoalSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das hilft uns, dein Programm zu kuratieren." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This helps us curate your program." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto nos ayuda a personalizar tu programa." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cela nous aide à personnaliser votre programme." + } + } + } + }, + "onboarding.perWeek" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "pro Woche" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "per week" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "por semana" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "par semaine" + } + } + } + }, + "onboarding.pill4MinWorkouts" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "4-Min-Workouts" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "4-Min Workouts" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "4 min de entrenamiento" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "4 min chrono" + } + } + } + }, + "onboarding.pillNoEquipment" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohne Geräte" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Equipment" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin equipo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sans matériel" + } + } + } + }, + "onboarding.pillVoiceGuided" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprachgeführt" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voice-Guided" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con guía de voz" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guidé par la voix" + } + } + } + }, + "onboarding.startFirstWorkout" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erstes Workout starten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start My First Workout" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iniciar mi primer entrenamiento" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Démarrer mon premier entraînement" + } + } + } + }, + "onboarding.tabataDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hochintensive Tabata-Workouts,\nfür echte Ergebnisse entwickelt." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "High-intensity Tabata workouts,\ndesigned for real results." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrenamientos Tabata de alta intensidad,\ndiseñados para resultados reales." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Des entraînements Tabata haute intensité,\nconçus pour de vrais résultats." + } + } + } + }, "onboarding.whatIsYourName" : { "extractionState" : "manual", "localizations" : { @@ -1661,8 +2986,92 @@ } } }, + "onboarding.whatIsYourNameSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wir personalisieren dein Erlebnis." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We'll personalise your experience." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Personalizaremos tu experiencia." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous personnaliserons votre expérience." + } + } + } + }, "Optional — helps us personalise tips" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optional — hilft uns, Tipps zu personalisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optional — helps us personalise tips" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opcional — nos ayuda a personalizar consejos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optionnel — nous aide à personnaliser vos conseils" + } + } + } + }, + "paywall.bestValue" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "BESTES ANGEBOT" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "BEST VALUE" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "MEJOR OFERTA" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MEILLEURE OFFRE" + } + } + } }, "paywall.cancelAnytime" : { "extractionState" : "manual", @@ -1693,6 +3102,93 @@ } } }, + "paywall.error" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fehler" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur" + } + } + } + }, + "paywall.healthkitSync" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "HealthKit-Sync" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "HealthKit Sync" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync HealthKit" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync HealthKit" + } + } + } + }, + "paywall.healthkitSyncDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jedes Workout wird in Apple Health gespeichert" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every workout saved to Apple Health" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cada entrenamiento guardado en Apple Health" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chaque séance sauvegardée dans Apple Health" + } + } + } + }, "paywall.premiumActive" : { "extractionState" : "manual", "localizations" : { @@ -1722,6 +3218,122 @@ } } }, + "paywall.processing" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verarbeitung…" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Processing…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Procesando…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En cours…" + } + } + } + }, + "paywall.progressSync" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fortschritts-Sync" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Progress Sync" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync de progreso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync progression" + } + } + } + }, + "paywall.progressSyncDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Verlauf wird in der Cloud gesichert" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your history backed up to the cloud" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu historial guardado en la nube" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre historique sauvegardé dans le cloud" + } + } + } + }, + "paywall.somethingWrong" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etwas ist schiefgelaufen." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Something went wrong." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algo salió mal." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une erreur s'est produite." + } + } + } + }, "paywall.startPremium" : { "extractionState" : "manual", "localizations" : { @@ -1809,6 +3421,64 @@ } } }, + "paywall.unlimitedWorkouts" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbegrenzte Workouts" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlimited Workouts" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrenamientos ilimitados" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Séances illimitées" + } + } + } + }, + "paywall.unlimitedWorkoutsDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zugriff auf alle Körperzonen und Schwierigkeitsstufen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Access all body zones & difficulty levels" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acceso a todas las zonas y niveles de dificultad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accès à toutes les zones et niveaux de difficulté" + } + } + } + }, "paywall.upgradePrompt" : { "extractionState" : "manual", "localizations" : { @@ -1838,8 +3508,92 @@ } } }, + "paywall.voiceCoaching" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprachcoaching" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voice Coaching" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrenador de voz" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coach vocal" + } + } + } + }, + "paywall.voiceCoachingDesc" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio-Anleitung durch jede Phase" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio guidance through every phase" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guía de audio durante cada fase" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guidage audio tout au long de chaque phase" + } + } + } + }, "per week" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "pro Woche" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "per week" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "por semana" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "par semaine" + } + } + } }, "player.blockOf" : { "extractionState" : "manual", @@ -2160,20 +3914,1266 @@ } } }, + "policy.analytics" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Analytik" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Analytics" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Analítica" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Analytique" + } + } + } + }, + "policy.appleHealth" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health" + } + } + } + }, + "policy.changesToTerms" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Änderungen der Bedingungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Changes to Terms" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambios en los términos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifications des conditions" + } + } + } + }, + "policy.contact" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontakt" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contact" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contacto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contact" + } + } + } + }, + "policy.dataStorage" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datenspeicherung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data Storage" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almacenamiento de datos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stockage des données" + } + } + } + }, + "policy.dataWeCollect" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesammelte Daten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data We Collect" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos que recopilamos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Données collectées" + } + } + } + }, + "policy.healthDisclaimer" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesundheitshinweis" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Health Disclaimer" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviso de salud" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avertissement santé" + } + } + } + }, + "policy.limitationOfLiability" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haftungsbeschränkung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitation of Liability" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitación de responsabilidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitation de responsabilité" + } + } + } + }, + "policy.privacyTitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datenschutzerklärung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Policy" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Política de privacidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Politique de confidentialité" + } + } + } + }, + "policy.purchases" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Käufe" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Purchases" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compras" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Achats" + } + } + } + }, + "policy.subscription" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Subscription" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suscripción" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement" + } + } + } + }, + "policy.termsTitle" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nutzungsbedingungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terms of Service" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Términos de servicio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conditions d'utilisation" + } + } + } + }, + "policy.useOfApp" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App-Nutzung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Use of the App" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uso de la app" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilisation de l'app" + } + } + } + }, "Premium subscriptions are billed monthly or yearly through the App Store. Subscriptions automatically renew unless cancelled at least 24 hours before the renewal date." : { + }, + "profile.athlete" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Athlet" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Athlete" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atleta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Athlète" + } + } + } + }, + "profile.fitnessProfile" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fitnessprofil" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fitness Profile" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfil físico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil sportif" + } + } + } + }, + "profile.goal" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ziel" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Goal" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objetivo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objectif" + } + } + } + }, + "profile.joinedFmt" : { + "comment" : "printf: %@ = formatted date. e.g. 'Joined Jan 15, 2026'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beigetreten %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Joined %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unido %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inscrit %@" + } + } + } + }, + "profile.level" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stufe" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Level" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niveau" + } + } + } + }, + "profile.monthly" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Monatlich" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Monthly" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensual" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensuel" + } + } + } + }, + "profile.subscription" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Subscription" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suscripción" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement" + } + } + } + }, + "profile.weeklyGoal" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wochenziel" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weekly Goal" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objetivo semanal" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objectif hebdomadaire" + } + } + } + }, + "profile.weeklyGoalFmt" : { + "comment" : "printf: %d = frequency. e.g. '3x / week'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dx / Woche" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dx / week" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dx / semana" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dx / semaine" + } + } + } + }, + "profile.yearly" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jährlich" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yearly" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anual" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuel" + } + } + } + }, + "programDetail.blockFmt" : { + "comment" : "printf: %d = block number. e.g. 'Block 1'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Block %d" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Block %d" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloque %d" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloc %d" + } + } + } + }, + "programDetail.blockOfFmt" : { + "comment" : "printf: %d = current block, %d = total. e.g. 'Block 2 of 3'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Block %d von %d" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Block %d of %d" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloque %d de %d" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloc %d sur %d" + } + } + } + }, + "programDetail.blockSubtitleFmt" : { + "comment" : "printf: %d rounds, %d work seconds, %d rest seconds. e.g. '8 rounds · 20s work / 10s rest'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d Runden · %ds Arbeit / %ds Pause" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d rounds · %ds work / %ds rest" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d rondas · %ds trabajo / %ds descanso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d rounds · %ds travail / %ds repos" + } + } + } + }, + "programDetail.coolDown" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abkühlen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cool Down" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enfriamiento" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Récupération" + } + } + } + }, + "programDetail.kcalFmt" : { + "comment" : "printf: %d = number of kilocalories. e.g. '180 kcal'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d kcal" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d kcal" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d kcal" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d kcal" + } + } + } + }, + "programDetail.minFmt" : { + "comment" : "printf: %d = number of minutes. e.g. '12 min'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d Min" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d min" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d min" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d min" + } + } + } + }, + "programDetail.roundsFmt" : { + "comment" : "printf: %d = number of rounds. e.g. '8 rounds'", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d Runden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d rounds" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d rondas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d rounds" + } + } + } + }, + "programDetail.startWorkout" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Training starten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start Workout" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iniciar entrenamiento" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Démarrer l'entraînement" + } + } + } + }, + "programDetail.unlockPremium" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Premium freischalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlock Premium" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquear Premium" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Débloquer Premium" + } + } + } + }, + "programDetail.warmUp" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aufwärmen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warm Up" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calentamiento" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Échauffement" + } + } + } }, "Programs for %@ are coming soon." : { + }, + "programs.all" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tous" + } + } + } + }, + "programs.allLevels" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Stufen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All Levels" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos los niveles" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tous les niveaux" + } + } + } + }, + "programs.filterZone" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Körperzone" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Body Zone" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zona corporal" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zone corporelle" + } + } + } + }, + "programs.noneFound" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Programme gefunden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Programs Found" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se encontraron programas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucun programme trouvé" + } + } + } + }, + "programs.searchPrompt" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Workouts suchen…" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Search workouts…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buscar entrenamientos…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rechercher des séances…" + } + } + } }, "Resting HR" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruheherzfrequenz" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resting HR" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "FC reposo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "FC repos" + } + } + } }, "Restore Purchases" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Käufe wiederherstellen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restore Purchases" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restaurar compras" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restaurer les achats" + } + } + } }, "Saved to Apple Health" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In Apple Health gespeichert" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saved to Apple Health" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardado en Apple Health" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sauvegardé dans Apple Health" + } + } + } + }, + "settings.about" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Über" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "About" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acerca de" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "À propos" + } + } + } + }, + "settings.account" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konto" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuenta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compte" + } + } + } }, "settings.audio" : { "extractionState" : "manual", @@ -2204,6 +5204,35 @@ } } }, + "settings.confirmReset" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zurücksetzen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecer" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réinitialiser" + } + } + } + }, "settings.dailyReminder" : { "extractionState" : "manual", "localizations" : { @@ -2291,6 +5320,64 @@ } } }, + "settings.joined" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beigetreten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Joined" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inscription" + } + } + } + }, + "settings.manageHealth" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesundheitsberechtigungen verwalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manage Health Permissions" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestionar permisos de salud" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gérer les autorisations santé" + } + } + } + }, "settings.music" : { "extractionState" : "manual", "localizations" : { @@ -2320,6 +5407,64 @@ } } }, + "settings.name" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nom" + } + } + } + }, + "settings.notSet" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicht festgelegt" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Not set" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No establecido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non défini" + } + } + } + }, "settings.reminders" : { "extractionState" : "manual", "localizations" : { @@ -2349,6 +5494,35 @@ } } }, + "settings.reminderTime" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erinnerungszeit" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reminder Time" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hora del recordatorio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heure du rappel" + } + } + } + }, "settings.resetProgress" : { "extractionState" : "manual", "localizations" : { @@ -2378,6 +5552,64 @@ } } }, + "settings.resetProgressConfirm" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fortschritt zurücksetzen?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset All Progress?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Restablecer el progreso?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réinitialiser la progression ?" + } + } + } + }, + "settings.resetProgressMessage" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dies löscht deinen Trainingsverlauf und deine Serie dauerhaft. Dies kann nicht rückgängig gemacht werden." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This will permanently delete your workout history and streak. This cannot be undone." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto eliminará permanentemente tu historial de entrenamientos y tu racha. No se puede deshacer." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cela supprimera définitivement votre historique d'entraînements et votre série. Cette action est irréversible." + } + } + } + }, "settings.soundEffects" : { "extractionState" : "manual", "localizations" : { @@ -2436,6 +5668,35 @@ } } }, + "settings.version" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + } + } + }, "settings.voiceCoaching" : { "extractionState" : "manual", "localizations" : { @@ -2585,7 +5846,33 @@ } }, "TabataGo" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo" + } + } + } }, "TabataGo collects minimal data to provide you with a great workout experience. We collect your name, fitness preferences, and workout history locally on your device." : { @@ -2600,25 +5887,207 @@ }, "TabataGo Premium" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo Premium" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo Premium" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo Premium" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TabataGo Premium" + } + } + } }, "This Week" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Woche" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This Week" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta semana" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cette semaine" + } + } + } }, "This will permanently delete your workout history and streak. This cannot be undone." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dadurch werden dein Workout-Verlauf und deine Serie dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This will permanently delete your workout history and streak. This cannot be undone." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto eliminará permanentemente tu historial de entrenamientos y tu racha. Esta acción no se puede deshacer." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cela supprimera définitivement votre historique de séances et votre série. Cette action est irréversible." + } + } + } }, "Today" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heute" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aujourd'hui" + } + } + } }, "Try changing your filters." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versuche, deine Filter zu ändern." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Try changing your filters." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intenta cambiar tus filtros." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Essayez de modifier vos filtres." + } + } + } }, "Unlock every workout, every week." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entsperre jedes Workout, jede Woche." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlock every workout, every week." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquea cada entrenamiento, cada semana." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Débloquez chaque séance, chaque semaine." + } + } + } }, "Version" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + } + } }, "We may update these terms at any time. Continued use of the app after changes constitutes acceptance of the new terms." : { @@ -2630,22 +6099,155 @@ }, "Workout Complete!" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Workout abgeschlossen!" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Workout Complete!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Entrenamiento completado!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entraînement terminé !" + } + } + } }, "You're all set, %@!" : { }, "You're all set!" : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alles bereit!" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You're all set!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Todo listo!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tout est prêt !" + } + } + } }, "Your personalised Tabata plan is ready." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein personalisierter Tabata-Plan ist bereit." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your personalised Tabata plan is ready." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu plan Tabata personalizado está listo." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre plan Tabata personnalisé est prêt." + } + } + } }, "Your progress will not be saved." : { - + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Fortschritt wird nicht gespeichert." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your progress will not be saved." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu progreso no se guardará." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre progression ne sera pas sauvegardée." + } + } + } }, "Your workout history, profile, and settings are stored locally using SwiftData. If you enable cloud sync, data is securely stored in Supabase with industry-standard encryption." : { + }, + "zone.default.description" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gezielte Workouts" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Targeted workouts" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrenamientos dirigidos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entraînements ciblés" + } + } + } }, "zone.full" : { "extractionState" : "manual", @@ -2676,6 +6278,35 @@ } } }, + "zone.full.description" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ganzkörpertraining von Kopf bis Fuß" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total body burn, head to toe" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quema total del cuerpo, de pies a cabeza" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brûlure totale du corps, de la tête aux pieds" + } + } + } + }, "zone.lower" : { "extractionState" : "manual", "localizations" : { @@ -2705,6 +6336,35 @@ } } }, + "zone.lower.description" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beine, Gesäß und Körpermitte" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Legs, glutes & core stability" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Piernas, glúteos y estabilidad del core" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jambes, fessiers et gainage" + } + } + } + }, "zone.upper" : { "extractionState" : "manual", "localizations" : { @@ -2733,6 +6393,35 @@ } } } + }, + "zone.upper.description" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arme, Brust, Schultern und Rücken" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arms, chest, shoulders & back" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brazos, pecho, hombros y espalda" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bras, poitrine, épaules et dos" + } + } + } } }, "version" : "1.0" diff --git a/tabatago-swift/TabataGo/Theme/Theme.swift b/tabatago-swift/TabataGo/Theme/Theme.swift index 60c3ff8..8524fe6 100644 --- a/tabatago-swift/TabataGo/Theme/Theme.swift +++ b/tabatago-swift/TabataGo/Theme/Theme.swift @@ -136,7 +136,7 @@ extension View { // ─── Stat Badge ─────────────────────────────────────────────────── struct StatBadge: View { - let label: String + let label: LocalizedStringResource let value: String var color: Color = .primary var icon: String? = nil diff --git a/tabatago-swift/TabataGo/Utilities/Strings.swift b/tabatago-swift/TabataGo/Utilities/Strings.swift index 6a74038..8750a73 100644 --- a/tabatago-swift/TabataGo/Utilities/Strings.swift +++ b/tabatago-swift/TabataGo/Utilities/Strings.swift @@ -4,16 +4,17 @@ import Foundation /// Usage: Text(L10n.action.start) or String(localized: L10n.player.phase.work) enum L10n { enum action { - static let back = LocalizedStringResource("action.back") - static let cancel = LocalizedStringResource("action.cancel") - static let `continue` = LocalizedStringResource("action.continue") - static let done = LocalizedStringResource("action.done") - static let save = LocalizedStringResource("action.save") - static let share = LocalizedStringResource("action.share") - static let start = LocalizedStringResource("action.start") - static let startWorkout = LocalizedStringResource("action.startWorkout") - static let startTraining = LocalizedStringResource("action.startTraining") - static let unlockPremium = LocalizedStringResource("action.unlockPremium") + static let back = LocalizedStringResource("action.back") + static let cancel = LocalizedStringResource("action.cancel") + static let `continue` = LocalizedStringResource("action.continue") + static let done = LocalizedStringResource("action.done") + static let retry = LocalizedStringResource("action.retry") + static let save = LocalizedStringResource("action.save") + static let share = LocalizedStringResource("action.share") + static let start = LocalizedStringResource("action.start") + static let startWorkout = LocalizedStringResource("action.startWorkout") + static let startTraining = LocalizedStringResource("action.startTraining") + static let unlockPremium = LocalizedStringResource("action.unlockPremium") static let restorePurchases = LocalizedStringResource("action.restorePurchases") } enum tab { @@ -26,14 +27,21 @@ enum L10n { static let featuredTitle = LocalizedStringResource("home.featuredTitle") static let featuredSubtitle = LocalizedStringResource("home.featuredSubtitle") static let browseTitle = LocalizedStringResource("home.browseTitle") + static let browseSubtitle = LocalizedStringResource("home.browseSubtitle") static let streak = LocalizedStringResource("home.streak") static let thisWeek = LocalizedStringResource("home.thisWeek") static let allTime = LocalizedStringResource("home.allTime") + static let free = LocalizedStringResource("home.free") + static let failedToLoad = LocalizedStringResource("home.failedToLoad") } enum zone { - static let upper = LocalizedStringResource("zone.upper") - static let lower = LocalizedStringResource("zone.lower") - static let full = LocalizedStringResource("zone.full") + static let upper = LocalizedStringResource("zone.upper") + static let lower = LocalizedStringResource("zone.lower") + static let full = LocalizedStringResource("zone.full") + static let upperDescription = LocalizedStringResource("zone.upper.description") + static let lowerDescription = LocalizedStringResource("zone.lower.description") + static let fullDescription = LocalizedStringResource("zone.full.description") + static let defaultDescription = LocalizedStringResource("zone.default.description") static func label(for zone: String) -> LocalizedStringResource { switch zone.lowercased() { @@ -43,12 +51,45 @@ enum L10n { default: return LocalizedStringResource(stringLiteral: zone.capitalized) } } + static func description(for zone: String) -> LocalizedStringResource { + switch zone.lowercased() { + case "upper-body": return upperDescription + case "lower-body": return lowerDescription + case "full-body": return fullDescription + default: return defaultDescription + } + } } enum level { static let beginner = LocalizedStringResource("level.beginner") static let intermediate = LocalizedStringResource("level.intermediate") static let advanced = LocalizedStringResource("level.advanced") } + enum programs { + static let filterZone = LocalizedStringResource("programs.filterZone") + static let all = LocalizedStringResource("programs.all") + static let allLevels = LocalizedStringResource("programs.allLevels") + static let noneFound = LocalizedStringResource("programs.noneFound") + static let searchPrompt = LocalizedStringResource("programs.searchPrompt") + } + enum programDetail { + static let warmUp = LocalizedStringResource("programDetail.warmUp") + static let coolDown = LocalizedStringResource("programDetail.coolDown") + static let startWorkout = LocalizedStringResource("programDetail.startWorkout") + static let unlockPremium = LocalizedStringResource("programDetail.unlockPremium") + /// printf: %d = block number + static let blockFmt = LocalizedStringResource("programDetail.blockFmt") + /// printf: %d block, %d of total + static let blockOfFmt = LocalizedStringResource("programDetail.blockOfFmt") + /// printf: %d rounds, %d workTime, %d restTime + static let blockSubtitleFmt = LocalizedStringResource("programDetail.blockSubtitleFmt") + /// printf: %d rounds + static let roundsFmt = LocalizedStringResource("programDetail.roundsFmt") + /// printf: %d minutes + static let minFmt = LocalizedStringResource("programDetail.minFmt") + /// printf: %d kcal + static let kcalFmt = LocalizedStringResource("programDetail.kcalFmt") + } enum player { enum phase { static let getReady = LocalizedStringResource("player.phase.getReady") @@ -77,6 +118,7 @@ enum L10n { } enum complete { static let title = LocalizedStringResource("complete.title") + static let saving = LocalizedStringResource("complete.saving") static let saveToHealth = LocalizedStringResource("complete.saveToHealth") static let savedToHealth = LocalizedStringResource("complete.savedToHealth") static let backToHome = LocalizedStringResource("complete.backToHome") @@ -84,23 +126,68 @@ enum L10n { static let calories = LocalizedStringResource("complete.calories") static let rounds = LocalizedStringResource("complete.rounds") static let avgHeartRate = LocalizedStringResource("complete.avgHeartRate") + static let completion = LocalizedStringResource("complete.completion") static let shareWorkout = LocalizedStringResource("complete.shareWorkout") } enum activity { - static let currentStreak = LocalizedStringResource("activity.currentStreak") - static let bestStreak = LocalizedStringResource("activity.bestStreak") - static let history = LocalizedStringResource("activity.history") - static let workouts = LocalizedStringResource("activity.workouts") - static let minutes = LocalizedStringResource("activity.minutes") - static let noWorkouts = LocalizedStringResource("activity.noWorkouts") - static let noWorkoutsMessage = LocalizedStringResource("activity.noWorkoutsMessage") + static let currentStreak = LocalizedStringResource("activity.currentStreak") + static let bestStreak = LocalizedStringResource("activity.bestStreak") + static let history = LocalizedStringResource("activity.history") + static let workouts = LocalizedStringResource("activity.workouts") + static let minutes = LocalizedStringResource("activity.minutes") + static let calories = LocalizedStringResource("activity.calories") + static let days = LocalizedStringResource("activity.days") + static let today = LocalizedStringResource("activity.today") + static let noWorkouts = LocalizedStringResource("activity.noWorkouts") + static let noWorkoutsMessage = LocalizedStringResource("activity.noWorkoutsMessage") + } + enum profile { + static let subscription = LocalizedStringResource("profile.subscription") + static let fitnessProfile = LocalizedStringResource("profile.fitnessProfile") + static let level = LocalizedStringResource("profile.level") + static let goal = LocalizedStringResource("profile.goal") + static let weeklyGoal = LocalizedStringResource("profile.weeklyGoal") + static let yearly = LocalizedStringResource("profile.yearly") + static let monthly = LocalizedStringResource("profile.monthly") + static let athlete = LocalizedStringResource("profile.athlete") + /// printf: %@ = formatted date. e.g. "Joined Jan 15, 2026" + static let joinedFmt = LocalizedStringResource("profile.joinedFmt") + /// printf: %d = frequency. e.g. "3x / week" + static let weeklyGoalFmt = LocalizedStringResource("profile.weeklyGoalFmt") } enum onboarding { - static let whatIsYourName = LocalizedStringResource("onboarding.whatIsYourName") - static let fitnessLevel = LocalizedStringResource("onboarding.fitnessLevel") - static let mainGoal = LocalizedStringResource("onboarding.mainGoal") - static let howOften = LocalizedStringResource("onboarding.howOften") - static let allSet = LocalizedStringResource("onboarding.allSet") + static let whatIsYourName = LocalizedStringResource("onboarding.whatIsYourName") + static let whatIsYourNameSubtitle = LocalizedStringResource("onboarding.whatIsYourNameSubtitle") + static let fitnessLevel = LocalizedStringResource("onboarding.fitnessLevel") + static let fitnessLevelSubtitle = LocalizedStringResource("onboarding.fitnessLevelSubtitle") + static let mainGoal = LocalizedStringResource("onboarding.mainGoal") + static let mainGoalSubtitle = LocalizedStringResource("onboarding.mainGoalSubtitle") + static let howOften = LocalizedStringResource("onboarding.howOften") + static let howOftenSubtitle = LocalizedStringResource("onboarding.howOftenSubtitle") + static let allSet = LocalizedStringResource("onboarding.allSet") + static let allSetSubtitle = LocalizedStringResource("onboarding.allSetSubtitle") + static let getStarted = LocalizedStringResource("onboarding.getStarted") + static let startFirstWorkout = LocalizedStringResource("onboarding.startFirstWorkout") + static let enterName = LocalizedStringResource("onboarding.enterName") + static let perWeek = LocalizedStringResource("onboarding.perWeek") + static let anyChallenges = LocalizedStringResource("onboarding.anyChallenges") + static let challengesSubtitle = LocalizedStringResource("onboarding.challengesSubtitle") + static let pill4MinWorkouts = LocalizedStringResource("onboarding.pill4MinWorkouts") + static let pillNoEquipment = LocalizedStringResource("onboarding.pillNoEquipment") + static let pillVoiceGuided = LocalizedStringResource("onboarding.pillVoiceGuided") + static let tabataDesc = LocalizedStringResource("onboarding.tabataDesc") + + enum levelDesc { + static let beginner = LocalizedStringResource("onboarding.level.beginnerDesc") + static let intermediate = LocalizedStringResource("onboarding.level.intermediateDesc") + static let advanced = LocalizedStringResource("onboarding.level.advancedDesc") + } + enum goalDesc { + static let weightLoss = LocalizedStringResource("onboarding.goal.weightLossDesc") + static let cardio = LocalizedStringResource("onboarding.goal.cardioDesc") + static let strength = LocalizedStringResource("onboarding.goal.strengthDesc") + static let wellness = LocalizedStringResource("onboarding.goal.wellnessDesc") + } } enum goal { static let weightLoss = LocalizedStringResource("goal.weightLoss") @@ -109,24 +196,62 @@ enum L10n { static let wellness = LocalizedStringResource("goal.wellness") } enum settings { - static let title = LocalizedStringResource("settings.title") - static let audio = LocalizedStringResource("settings.audio") - static let soundEffects = LocalizedStringResource("settings.soundEffects") - static let voiceCoaching = LocalizedStringResource("settings.voiceCoaching") - static let music = LocalizedStringResource("settings.music") - static let haptics = LocalizedStringResource("settings.haptics") - static let hapticFeedback = LocalizedStringResource("settings.hapticFeedback") - static let reminders = LocalizedStringResource("settings.reminders") - static let dailyReminder = LocalizedStringResource("settings.dailyReminder") - static let resetProgress = LocalizedStringResource("settings.resetProgress") + static let title = LocalizedStringResource("settings.title") + static let audio = LocalizedStringResource("settings.audio") + static let soundEffects = LocalizedStringResource("settings.soundEffects") + static let voiceCoaching = LocalizedStringResource("settings.voiceCoaching") + static let music = LocalizedStringResource("settings.music") + static let haptics = LocalizedStringResource("settings.haptics") + static let hapticFeedback = LocalizedStringResource("settings.hapticFeedback") + static let reminders = LocalizedStringResource("settings.reminders") + static let dailyReminder = LocalizedStringResource("settings.dailyReminder") + static let reminderTime = LocalizedStringResource("settings.reminderTime") + static let manageHealth = LocalizedStringResource("settings.manageHealth") + static let account = LocalizedStringResource("settings.account") + static let notSet = LocalizedStringResource("settings.notSet") + static let about = LocalizedStringResource("settings.about") + static let resetProgress = LocalizedStringResource("settings.resetProgress") + static let resetProgressConfirm = LocalizedStringResource("settings.resetProgressConfirm") + static let confirmReset = LocalizedStringResource("settings.confirmReset") + static let name = LocalizedStringResource("settings.name") + static let joined = LocalizedStringResource("settings.joined") + static let version = LocalizedStringResource("settings.version") + static let resetProgressMessage = LocalizedStringResource("settings.resetProgressMessage") + } + enum policy { + static let privacyTitle = LocalizedStringResource("policy.privacyTitle") + static let termsTitle = LocalizedStringResource("policy.termsTitle") + static let dataWeCollect = LocalizedStringResource("policy.dataWeCollect") + static let appleHealth = LocalizedStringResource("policy.appleHealth") + static let analytics = LocalizedStringResource("policy.analytics") + static let purchases = LocalizedStringResource("policy.purchases") + static let dataStorage = LocalizedStringResource("policy.dataStorage") + static let contact = LocalizedStringResource("policy.contact") + static let useOfApp = LocalizedStringResource("policy.useOfApp") + static let subscription = LocalizedStringResource("policy.subscription") + static let healthDisclaimer = LocalizedStringResource("policy.healthDisclaimer") + static let limitationOfLiability = LocalizedStringResource("policy.limitationOfLiability") + static let changesToTerms = LocalizedStringResource("policy.changesToTerms") } enum paywall { - static let title = LocalizedStringResource("paywall.title") - static let subtitle = LocalizedStringResource("paywall.subtitle") - static let startPremium = LocalizedStringResource("paywall.startPremium") - static let premiumActive = LocalizedStringResource("paywall.premiumActive") - static let upgradePrompt = LocalizedStringResource("paywall.upgradePrompt") - static let cancelAnytime = LocalizedStringResource("paywall.cancelAnytime") + static let title = LocalizedStringResource("paywall.title") + static let subtitle = LocalizedStringResource("paywall.subtitle") + static let startPremium = LocalizedStringResource("paywall.startPremium") + static let premiumActive = LocalizedStringResource("paywall.premiumActive") + static let upgradePrompt = LocalizedStringResource("paywall.upgradePrompt") + static let cancelAnytime = LocalizedStringResource("paywall.cancelAnytime") + static let processing = LocalizedStringResource("paywall.processing") + static let bestValue = LocalizedStringResource("paywall.bestValue") + static let unlimitedWorkouts = LocalizedStringResource("paywall.unlimitedWorkouts") + static let unlimitedWorkoutsDesc = LocalizedStringResource("paywall.unlimitedWorkoutsDesc") + static let healthkitSync = LocalizedStringResource("paywall.healthkitSync") + static let healthkitSyncDesc = LocalizedStringResource("paywall.healthkitSyncDesc") + static let progressSync = LocalizedStringResource("paywall.progressSync") + static let progressSyncDesc = LocalizedStringResource("paywall.progressSyncDesc") + static let voiceCoaching = LocalizedStringResource("paywall.voiceCoaching") + static let voiceCoachingDesc = LocalizedStringResource("paywall.voiceCoachingDesc") + static let error = LocalizedStringResource("paywall.error") + static let somethingWrong = LocalizedStringResource("paywall.somethingWrong") } enum health { static let appleHealth = LocalizedStringResource("health.appleHealth") @@ -134,5 +259,6 @@ enum L10n { static let exercise = LocalizedStringResource("health.exercise") static let stand = LocalizedStringResource("health.stand") static let restingHR = LocalizedStringResource("health.restingHR") + static let today = LocalizedStringResource("health.today") } } diff --git a/tabatago-swift/TabataGo/Views/Complete/CompletionView.swift b/tabatago-swift/TabataGo/Views/Complete/CompletionView.swift index 8fa0976..7a4dbe8 100644 --- a/tabatago-swift/TabataGo/Views/Complete/CompletionView.swift +++ b/tabatago-swift/TabataGo/Views/Complete/CompletionView.swift @@ -33,7 +33,7 @@ struct CompletionView: View { .symbolEffect(.bounce, value: confettiTrigger) .padding(.top, 32) - Text("Workout Complete!") + Text(L10n.complete.title) .font(.system(size: 32, weight: .black, design: .rounded)) .foregroundStyle(.primary) @@ -46,33 +46,33 @@ struct CompletionView: View { if let session { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { CompletionStat( - label: "Duration", + label: L10n.complete.duration, value: formatDuration(session.durationSeconds), icon: "clock.fill", color: Theme.rest ) CompletionStat( - label: "Calories", + label: L10n.complete.calories, value: "\(Int(session.caloriesBurned)) kcal", icon: "flame.fill", color: Theme.brand ) CompletionStat( - label: "Rounds", + label: L10n.complete.rounds, value: "\(session.roundsCompleted) / \(session.totalRounds)", icon: "repeat", color: Theme.success ) if let hr = session.averageHeartRate { CompletionStat( - label: "Avg Heart Rate", + label: L10n.complete.avgHeartRate, value: "\(Int(hr)) bpm", icon: "heart.fill", color: .red ) } else { CompletionStat( - label: "Completion", + label: L10n.complete.completion, value: "\(Int(session.completionRate * 100))%", icon: "checkmark.circle.fill", color: Theme.success @@ -90,7 +90,7 @@ struct CompletionView: View { HStack { Image(systemName: "heart.text.square.fill") .foregroundStyle(.red) - Text(isSavingToHealth ? "Saving..." : "Save to Apple Health") + Text(isSavingToHealth ? L10n.complete.saving : L10n.complete.saveToHealth) .fontWeight(.semibold) Spacer() if isSavingToHealth { @@ -110,7 +110,7 @@ struct CompletionView: View { HStack { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Theme.success) - Text("Saved to Apple Health") + Text(L10n.complete.savedToHealth) .fontWeight(.semibold) .foregroundStyle(.primary) } @@ -124,7 +124,7 @@ struct CompletionView: View { Button { showShareSheet = true } label: { - Label("Share Workout", systemImage: "square.and.arrow.up") + Label(String(localized: L10n.complete.shareWorkout), systemImage: "square.and.arrow.up") .font(.headline) .frame(maxWidth: .infinity) .padding() @@ -136,7 +136,7 @@ struct CompletionView: View { Button { onDone() } label: { - Text("Back to Home") + Text(L10n.complete.backToHome) .font(.headline.weight(.bold)) .foregroundStyle(.white) .frame(maxWidth: .infinity) @@ -164,7 +164,6 @@ struct CompletionView: View { private func saveToHealth() async { guard let session else { return } isSavingToHealth = true - // Extract Sendable values on @MainActor before crossing into HealthKitService actor. let saveData = HealthKitService.WorkoutSaveData( startedAt: session.startedAt, completedAt: session.completedAt, @@ -202,7 +201,7 @@ struct CompletionView: View { } struct CompletionStat: View { - let label: String + let label: LocalizedStringResource let value: String let icon: String let color: Color diff --git a/tabatago-swift/TabataGo/Views/Onboarding/OnboardingView.swift b/tabatago-swift/TabataGo/Views/Onboarding/OnboardingView.swift index aa8fe38..1327120 100644 --- a/tabatago-swift/TabataGo/Views/Onboarding/OnboardingView.swift +++ b/tabatago-swift/TabataGo/Views/Onboarding/OnboardingView.swift @@ -86,11 +86,11 @@ struct OnboardingView: View { } } - private var buttonLabel: String { + private var buttonLabel: LocalizedStringResource { switch step { - case .welcome: return "Get Started" - case .ready: return "Start My First Workout" - default: return "Continue" + case .welcome: return L10n.onboarding.getStarted + case .ready: return L10n.onboarding.startFirstWorkout + default: return L10n.action.continue } } @@ -129,10 +129,10 @@ private struct WelcomeStep: View { @State private var showPills = false @State private var pillStates = [false, false, false] - private let pills = [ - ("bolt.fill", "4-Min Workouts"), - ("house.fill", "No Equipment"), - ("mic.fill", "Voice-Guided"), + private let pills: [(String, LocalizedStringResource)] = [ + ("bolt.fill", L10n.onboarding.pill4MinWorkouts), + ("house.fill", L10n.onboarding.pillNoEquipment), + ("mic.fill", L10n.onboarding.pillVoiceGuided), ] var body: some View { @@ -150,7 +150,7 @@ private struct WelcomeStep: View { .font(.system(size: 44, weight: .black, design: .rounded)) .foregroundStyle(.primary) - Text("High-intensity Tabata workouts,\ndesigned for real results.") + Text(L10n.onboarding.tabataDesc) .font(.title3) .foregroundStyle(.secondary) .multilineTextAlignment(.center) @@ -197,10 +197,10 @@ private struct NameStep: View { VStack(spacing: 32) { Spacer() - OnboardingHeader(title: "What's your name?", subtitle: "We'll personalise your experience.") + OnboardingHeader(title: L10n.onboarding.whatIsYourName, subtitle: L10n.onboarding.whatIsYourNameSubtitle) VStack(spacing: 16) { - TextField("Enter your name", text: $name) + TextField(String(localized: L10n.onboarding.enterName), text: $name) .font(.title2) .multilineTextAlignment(.center) .padding() @@ -238,13 +238,13 @@ private struct LevelStep: View { var body: some View { VStack(spacing: 32) { Spacer() - OnboardingHeader(title: "What's your fitness level?", subtitle: "We'll recommend the right workouts.") + OnboardingHeader(title: L10n.onboarding.fitnessLevel, subtitle: L10n.onboarding.fitnessLevelSubtitle) VStack(spacing: 12) { ForEach(Array(FitnessLevel.allCases.enumerated()), id: \.element) { i, level in SelectionCard( label: level.label, - subtitle: levelDescription(level), + subtitle: String(localized: levelDescription(level)), icon: levelIcon(level), isSelected: selection == level, color: Theme.levelColor(level.rawValue) @@ -263,19 +263,19 @@ private struct LevelStep: View { .onAppear { appeared = true } } - private func levelDescription(_ level: FitnessLevel) -> String { + private func levelDescription(_ level: FitnessLevel) -> LocalizedStringResource { switch level { - case .beginner: return "New to HIIT or returning after a break" - case .intermediate: return "Regular exerciser, ready for more intensity" - case .advanced: return "Experienced athlete seeking maximum challenge" + case .beginner: return L10n.onboarding.levelDesc.beginner + case .intermediate: return L10n.onboarding.levelDesc.intermediate + case .advanced: return L10n.onboarding.levelDesc.advanced } } private func levelIcon(_ level: FitnessLevel) -> String { switch level { - case .beginner: return "figure.walk" + case .beginner: return "figure.walk" case .intermediate: return "figure.run" - case .advanced: return "figure.highintensity.intervaltraining" + case .advanced: return "figure.highintensity.intervaltraining" } } } @@ -287,13 +287,13 @@ private struct GoalStep: View { var body: some View { VStack(spacing: 32) { Spacer() - OnboardingHeader(title: "What's your main goal?", subtitle: "This helps us curate your program.") + OnboardingHeader(title: L10n.onboarding.mainGoal, subtitle: L10n.onboarding.mainGoalSubtitle) VStack(spacing: 12) { ForEach(Array(FitnessGoal.allCases.enumerated()), id: \.element) { i, goal in SelectionCard( label: goal.label, - subtitle: goalDescription(goal), + subtitle: String(localized: goalDescription(goal)), icon: goalIcon(goal), isSelected: selection == goal, color: Theme.brand @@ -312,21 +312,21 @@ private struct GoalStep: View { .onAppear { appeared = true } } - private func goalDescription(_ goal: FitnessGoal) -> String { + private func goalDescription(_ goal: FitnessGoal) -> LocalizedStringResource { switch goal { - case .weightLoss: return "Burn calories and reduce body fat" - case .cardio: return "Improve cardiovascular endurance" - case .strength: return "Build muscle and increase power" - case .wellness: return "Improve overall health and energy" + case .weightLoss: return L10n.onboarding.goalDesc.weightLoss + case .cardio: return L10n.onboarding.goalDesc.cardio + case .strength: return L10n.onboarding.goalDesc.strength + case .wellness: return L10n.onboarding.goalDesc.wellness } } private func goalIcon(_ goal: FitnessGoal) -> String { switch goal { case .weightLoss: return "scalemass" - case .cardio: return "heart.fill" - case .strength: return "dumbbell.fill" - case .wellness: return "leaf.fill" + case .cardio: return "heart.fill" + case .strength: return "dumbbell.fill" + case .wellness: return "leaf.fill" } } } @@ -341,7 +341,7 @@ private struct FrequencyStep: View { VStack(spacing: 28) { Spacer(minLength: 20) - OnboardingHeader(title: "How often can you train?", subtitle: "Be realistic — consistency beats intensity.") + OnboardingHeader(title: L10n.onboarding.howOften, subtitle: L10n.onboarding.howOftenSubtitle) // Frequency picker HStack(spacing: 12) { @@ -352,7 +352,7 @@ private struct FrequencyStep: View { VStack(spacing: 6) { Text("\(n)x") .font(.system(size: 28, weight: .black, design: .rounded)) - Text("per week") + Text(L10n.onboarding.perWeek) .font(.caption) } .foregroundStyle(frequency == n ? .white : .primary) @@ -374,10 +374,10 @@ private struct FrequencyStep: View { // Barriers VStack(alignment: .leading, spacing: 14) { - Text("Any challenges?") + Text(L10n.onboarding.anyChallenges) .font(.headline) .padding(.horizontal, 24) - Text("Optional — helps us personalise tips") + Text(L10n.onboarding.challengesSubtitle) .font(.subheadline) .foregroundStyle(.secondary) .padding(.horizontal, 24) @@ -441,7 +441,7 @@ private struct ReadyStep: View { VStack(spacing: 14) { if name.trimmingCharacters(in: .whitespaces).isEmpty { - Text("You're all set!") + Text(L10n.onboarding.allSet) .font(.system(size: 34, weight: .black, design: .rounded)) .foregroundStyle(.primary) .multilineTextAlignment(.center) @@ -453,7 +453,7 @@ private struct ReadyStep: View { .multilineTextAlignment(.center) } - Text("Your personalised Tabata plan is ready.") + Text(L10n.onboarding.allSetSubtitle) .font(.title3) .foregroundStyle(.secondary) } @@ -472,8 +472,8 @@ private struct ReadyStep: View { // ─── Reusable components ────────────────────────────────────────── struct OnboardingHeader: View { - let title: String - let subtitle: String + let title: LocalizedStringResource + let subtitle: LocalizedStringResource var body: some View { VStack(spacing: 10) { @@ -491,7 +491,7 @@ struct OnboardingHeader: View { } struct SelectionCard: View { - let label: String + let label: LocalizedStringResource let subtitle: String let icon: String let isSelected: Bool @@ -544,7 +544,7 @@ struct SelectionCard: View { } struct PrimaryButton: View { - let label: String + let label: LocalizedStringResource let action: () -> Void var body: some View { diff --git a/tabatago-swift/TabataGo/Views/Paywall/PaywallView.swift b/tabatago-swift/TabataGo/Views/Paywall/PaywallView.swift index f26948a..867be05 100644 --- a/tabatago-swift/TabataGo/Views/Paywall/PaywallView.swift +++ b/tabatago-swift/TabataGo/Views/Paywall/PaywallView.swift @@ -37,20 +37,28 @@ struct PaywallView: View { ) .symbolEffect(.bounce, value: vm.isPurchasing) - Text("TabataGo Premium") + Text(L10n.paywall.title) .font(.system(size: 32, weight: .black, design: .rounded)) .foregroundStyle(.primary) - Text("Unlock every workout, every week.") + Text(L10n.paywall.subtitle) .font(.subheadline) .foregroundStyle(.secondary) } // ── Features ─────────────────────────────────── VStack(spacing: 12) { - FeatureRow(icon: "bolt.fill", color: Theme.brand, title: "Unlimited Workouts", subtitle: "Access all body zones & difficulty levels") - FeatureRow(icon: "heart.fill", color: .red, title: "HealthKit Sync", subtitle: "Every workout saved to Apple Health") - FeatureRow(icon: "icloud.fill", color: Theme.rest, title: "Progress Sync", subtitle: "Your history backed up to the cloud") - FeatureRow(icon: "waveform", color: Theme.success, title: "Voice Coaching", subtitle: "Audio guidance through every phase") + FeatureRow(icon: "bolt.fill", color: Theme.brand, + title: L10n.paywall.unlimitedWorkouts, + subtitle: L10n.paywall.unlimitedWorkoutsDesc) + FeatureRow(icon: "heart.fill", color: .red, + title: L10n.paywall.healthkitSync, + subtitle: L10n.paywall.healthkitSyncDesc) + FeatureRow(icon: "icloud.fill", color: Theme.rest, + title: L10n.paywall.progressSync, + subtitle: L10n.paywall.progressSyncDesc) + FeatureRow(icon: "waveform", color: Theme.success, + title: L10n.paywall.voiceCoaching, + subtitle: L10n.paywall.voiceCoachingDesc) } .padding(.horizontal) @@ -78,7 +86,7 @@ struct PaywallView: View { } label: { HStack { if vm.isPurchasing { ProgressView().tint(.white) } - Text(vm.isPurchasing ? "Processing..." : "Start Premium") + Text(vm.isPurchasing ? L10n.paywall.processing : L10n.paywall.startPremium) .font(.headline.weight(.bold)) } .foregroundStyle(.white) @@ -96,12 +104,12 @@ struct PaywallView: View { Button { Task { await vm.restorePurchases() } } label: { - Text("Restore Purchases") + Text(L10n.action.restorePurchases) .font(.subheadline) .foregroundStyle(.secondary) } - Text("Cancel anytime. Prices in your local currency.") + Text(L10n.paywall.cancelAnytime) .font(.caption) .foregroundStyle(.tertiary) .multilineTextAlignment(.center) @@ -116,10 +124,10 @@ struct PaywallView: View { if succeeded { dismiss() } } .onAppear { AnalyticsService.shared.paywallViewed(source: "paywall_sheet") } - .alert("Error", isPresented: $vm.showError) { - Button("OK") {} + .alert(String(localized: L10n.paywall.error), isPresented: $vm.showError) { + Button(String(localized: L10n.action.done)) {} } message: { - Text(vm.errorMessage ?? "Something went wrong.") + Text(vm.errorMessage ?? String(localized: L10n.paywall.somethingWrong)) } } } @@ -127,8 +135,8 @@ struct PaywallView: View { struct FeatureRow: View { let icon: String let color: Color - let title: String - let subtitle: String + let title: LocalizedStringResource + let subtitle: LocalizedStringResource var body: some View { HStack(spacing: 14) { @@ -168,7 +176,7 @@ struct PackageCard: View { Text(package.storeProduct.localizedTitle) .font(.headline.weight(.semibold)) if isYearly { - Text("BEST VALUE") + Text(L10n.paywall.bestValue) .font(.caption2.weight(.bold)) .foregroundStyle(.white) .padding(.horizontal, 8) diff --git a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift index de9e60e..242d2c5 100644 --- a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift +++ b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift @@ -122,14 +122,14 @@ struct PlayerView: View { CompletionView(session: vm.completedSession, program: program, onDone: { dismiss() }) .navigationBarBackButtonHidden() } - .alert("End Workout?", isPresented: $vm.showExitConfirmation) { - Button("End Workout", role: .destructive) { + .alert(String(localized: L10n.player.endWorkout), isPresented: $vm.showExitConfirmation) { + Button(String(localized: L10n.player.endWorkout), role: .destructive) { vm.abandonWorkout() dismiss() } - Button("Keep Going", role: .cancel) {} + Button(String(localized: L10n.player.keepGoing), role: .cancel) {} } message: { - Text("Your progress will not be saved.") + Text(L10n.player.endWorkoutMessage) } } // NavigationStack } @@ -182,7 +182,7 @@ struct PlayerTopBar: View { .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) .lineLimit(1) - Text("Block \(block) of \(totalBlocks)") + Text(String(format: String(localized: L10n.programDetail.blockOfFmt), block, totalBlocks)) .font(.caption) .foregroundStyle(.white.opacity(0.7)) } diff --git a/tabatago-swift/TabataGo/Views/Programs/BodyZoneView.swift b/tabatago-swift/TabataGo/Views/Programs/BodyZoneView.swift index c3f89ba..6be350c 100644 --- a/tabatago-swift/TabataGo/Views/Programs/BodyZoneView.swift +++ b/tabatago-swift/TabataGo/Views/Programs/BodyZoneView.swift @@ -8,9 +8,9 @@ struct BodyZoneView: View { private var zoneTitle: String { switch zone { - case "upper-body": return "Upper Body" - case "lower-body": return "Lower Body" - case "full-body": return "Full Body" + case "upper-body": return String(localized: L10n.zone.upper) + case "lower-body": return String(localized: L10n.zone.lower) + case "full-body": return String(localized: L10n.zone.full) default: return zone.replacingOccurrences(of: "-", with: " ").capitalized } } @@ -29,13 +29,13 @@ struct BodyZoneView: View { Image(systemName: "exclamationmark.triangle") .font(.title2) .foregroundStyle(.secondary) - Text("Failed to load programs") + Text(L10n.home.failedToLoad) .font(.subheadline.weight(.semibold)) Text(error) .font(.caption) .foregroundStyle(.secondary) .multilineTextAlignment(.center) - Button("Retry") { Task { await vm.refresh() } } + Button(String(localized: L10n.action.retry)) { Task { await vm.refresh() } } .buttonStyle(.bordered) .padding(.top, 4) } @@ -43,7 +43,7 @@ struct BodyZoneView: View { .listRowBackground(Color.clear) } else if programs.isEmpty { ContentUnavailableView( - "No Programs Yet", + String(localized: L10n.programs.noneFound), systemImage: "dumbbell", description: Text("Programs for \(zoneTitle) are coming soon.") ) diff --git a/tabatago-swift/TabataGo/Views/Programs/ProgramDetailView.swift b/tabatago-swift/TabataGo/Views/Programs/ProgramDetailView.swift index a3bb415..bdb3108 100644 --- a/tabatago-swift/TabataGo/Views/Programs/ProgramDetailView.swift +++ b/tabatago-swift/TabataGo/Views/Programs/ProgramDetailView.swift @@ -32,9 +32,9 @@ struct ProgramDetailView: View { .font(.system(size: 28, weight: .black, design: .rounded)) .foregroundStyle(.white) HStack(spacing: 16) { - Label("\(program.estimatedDuration) min", systemImage: "clock.fill") - Label("\(program.estimatedCalories) kcal", systemImage: "flame.fill") - Label("\(program.totalRounds) rounds", systemImage: "repeat") + Label(String(format: String(localized: L10n.programDetail.minFmt), program.estimatedDuration), systemImage: "clock.fill") + Label(String(format: String(localized: L10n.programDetail.kcalFmt), program.estimatedCalories), systemImage: "flame.fill") + Label(String(format: String(localized: L10n.programDetail.roundsFmt), program.totalRounds), systemImage: "repeat") } .font(.subheadline.weight(.medium)) .foregroundStyle(.white.opacity(0.85)) @@ -53,7 +53,7 @@ struct ProgramDetailView: View { // ── Warmup ───────────────────────────────── if !program.warmup.movements.isEmpty { - ExerciseSection(title: "Warm Up", icon: "figure.cooldown", color: Theme.prep) { + ExerciseSection(title: String(localized: L10n.programDetail.warmUp), icon: "figure.cooldown", color: Theme.prep) { ForEach(program.warmup.movements, id: \.name) { move in ExerciseRow(name: move.nameEn, duration: "\(move.duration)s", color: Theme.prep) } @@ -63,10 +63,10 @@ struct ProgramDetailView: View { // ── Tabata Blocks ────────────────────────── ForEach(Array(program.blocks.enumerated()), id: \.offset) { i, block in ExerciseSection( - title: "Block \(i + 1)", + title: String(format: String(localized: L10n.programDetail.blockFmt), i + 1), icon: "bolt.fill", color: Theme.brand, - subtitle: "\(block.rounds) rounds · \(block.workTime)s work / \(block.restTime)s rest" + subtitle: String(format: String(localized: L10n.programDetail.blockSubtitleFmt), block.rounds, block.workTime, block.restTime) ) { ExerciseRow( name: block.exercise1.nameEn, @@ -86,7 +86,7 @@ struct ProgramDetailView: View { // ── Cooldown ─────────────────────────────── if !program.cooldown.movements.isEmpty { - ExerciseSection(title: "Cool Down", icon: "snowflake", color: Theme.rest) { + ExerciseSection(title: String(localized: L10n.programDetail.coolDown), icon: "snowflake", color: Theme.rest) { ForEach(program.cooldown.movements, id: \.name) { move in ExerciseRow(name: move.nameEn, duration: "\(move.duration)s", color: Theme.rest) } @@ -120,7 +120,7 @@ struct ProgramDetailView: View { if !canAccess { Image(systemName: "lock.fill") } - Text(canAccess ? "Start Workout" : "Unlock Premium") + Text(canAccess ? String(localized: L10n.programDetail.startWorkout) : String(localized: L10n.programDetail.unlockPremium)) .font(.headline.weight(.bold)) } .foregroundStyle(.white) diff --git a/tabatago-swift/TabataGo/Views/Settings/PolicyViews.swift b/tabatago-swift/TabataGo/Views/Settings/PolicyViews.swift index 4946324..4bb08b7 100644 --- a/tabatago-swift/TabataGo/Views/Settings/PolicyViews.swift +++ b/tabatago-swift/TabataGo/Views/Settings/PolicyViews.swift @@ -5,22 +5,22 @@ struct PrivacyPolicyView: View { ScrollView { VStack(alignment: .leading, spacing: 20) { Group { - PolicySection(title: "Data We Collect") { + PolicySection(title: L10n.policy.dataWeCollect) { Text("TabataGo collects minimal data to provide you with a great workout experience. We collect your name, fitness preferences, and workout history locally on your device.") } - PolicySection(title: "Apple Health") { + PolicySection(title: L10n.policy.appleHealth) { Text("When you grant permission, TabataGo saves your Tabata workouts to Apple Health, including calories burned, heart rate, and workout duration. This data stays on your device and is governed by Apple's privacy policies.") } - PolicySection(title: "Analytics") { + PolicySection(title: L10n.policy.analytics) { Text("We use PostHog to collect anonymised usage analytics to improve the app. No personally identifiable information is sent. You can opt out in your device privacy settings.") } - PolicySection(title: "Purchases") { + PolicySection(title: L10n.policy.purchases) { Text("Subscription purchases are handled by Apple's App Store and RevenueCat. We do not store your payment information.") } - PolicySection(title: "Data Storage") { + PolicySection(title: L10n.policy.dataStorage) { Text("Your workout history, profile, and settings are stored locally using SwiftData. If you enable cloud sync, data is securely stored in Supabase with industry-standard encryption.") } - PolicySection(title: "Contact") { + PolicySection(title: L10n.policy.contact) { Text("For privacy concerns, contact us at privacy@tabatago.app") } } @@ -28,7 +28,7 @@ struct PrivacyPolicyView: View { } .padding(.vertical) } - .navigationTitle("Privacy Policy") + .navigationTitle(String(localized: L10n.policy.privacyTitle)) .navigationBarTitleDisplayMode(.large) } } @@ -38,19 +38,19 @@ struct TermsOfServiceView: View { ScrollView { VStack(alignment: .leading, spacing: 20) { Group { - PolicySection(title: "Use of the App") { + PolicySection(title: L10n.policy.useOfApp) { Text("TabataGo is designed for fitness purposes. By using the app, you agree to use it responsibly and consult a healthcare professional before starting any new exercise program.") } - PolicySection(title: "Subscription") { + PolicySection(title: L10n.policy.subscription) { Text("Premium subscriptions are billed monthly or yearly through the App Store. Subscriptions automatically renew unless cancelled at least 24 hours before the renewal date.") } - PolicySection(title: "Health Disclaimer") { + PolicySection(title: L10n.policy.healthDisclaimer) { Text("TabataGo is not a medical device. The app does not provide medical advice. Always consult a doctor before beginning a new exercise program, especially if you have pre-existing health conditions.") } - PolicySection(title: "Limitation of Liability") { + PolicySection(title: L10n.policy.limitationOfLiability) { Text("TabataGo is provided 'as is'. We are not liable for any injuries or health issues arising from the use of our workout programs.") } - PolicySection(title: "Changes to Terms") { + PolicySection(title: L10n.policy.changesToTerms) { Text("We may update these terms at any time. Continued use of the app after changes constitutes acceptance of the new terms.") } } @@ -58,13 +58,13 @@ struct TermsOfServiceView: View { } .padding(.vertical) } - .navigationTitle("Terms of Service") + .navigationTitle(String(localized: L10n.policy.termsTitle)) .navigationBarTitleDisplayMode(.large) } } struct PolicySection: View { - let title: String + let title: LocalizedStringResource @ViewBuilder let content: () -> Content var body: some View { diff --git a/tabatago-swift/TabataGo/Views/Settings/SettingsView.swift b/tabatago-swift/TabataGo/Views/Settings/SettingsView.swift index 825ec12..ced3263 100644 --- a/tabatago-swift/TabataGo/Views/Settings/SettingsView.swift +++ b/tabatago-swift/TabataGo/Views/Settings/SettingsView.swift @@ -12,19 +12,19 @@ struct SettingsView: View { var body: some View { Form { // ── Audio ────────────────────────────────────────────── - Section("Audio") { + Section(String(localized: L10n.settings.audio)) { if let profile { - Toggle("Sound Effects", isOn: Binding( + Toggle(String(localized: L10n.settings.soundEffects), isOn: Binding( get: { profile.soundEffectsEnabled }, set: { profile.soundEffectsEnabled = $0; save() } )) - Toggle("Voice Coaching", isOn: Binding( + Toggle(String(localized: L10n.settings.voiceCoaching), isOn: Binding( get: { profile.voiceCoachingEnabled }, set: { profile.voiceCoachingEnabled = $0; save() } )) - Toggle("Music", isOn: Binding( + Toggle(String(localized: L10n.settings.music), isOn: Binding( get: { profile.musicEnabled }, set: { profile.musicEnabled = $0; save() } )) @@ -45,9 +45,9 @@ struct SettingsView: View { } // ── Haptics ──────────────────────────────────────────── - Section("Haptics") { + Section(String(localized: L10n.settings.haptics)) { if let profile { - Toggle("Haptic Feedback", isOn: Binding( + Toggle(String(localized: L10n.settings.hapticFeedback), isOn: Binding( get: { profile.hapticsEnabled }, set: { profile.hapticsEnabled = $0; save() } )) @@ -55,16 +55,16 @@ struct SettingsView: View { } // ── Reminders ───────────────────────────────────────── - Section("Reminders") { + Section(String(localized: L10n.settings.reminders)) { if let profile { - Toggle("Daily Reminder", isOn: Binding( + Toggle(String(localized: L10n.settings.dailyReminder), isOn: Binding( get: { profile.remindersEnabled }, set: { profile.remindersEnabled = $0; save() } )) if profile.remindersEnabled { DatePicker( - "Reminder Time", + String(localized: L10n.settings.reminderTime), selection: Binding( get: { var c = DateComponents() @@ -86,12 +86,12 @@ struct SettingsView: View { } // ── HealthKit ───────────────────────────────────────── - Section("Apple Health") { + Section(String(localized: L10n.health.appleHealth)) { Button { Task { try? await HealthKitService.shared.requestAuthorization() } } label: { HStack { - Label("Manage Health Permissions", systemImage: "heart.text.square") + Label(String(localized: L10n.settings.manageHealth), systemImage: "heart.text.square") Spacer() Image(systemName: "arrow.up.right") .font(.caption) @@ -102,16 +102,18 @@ struct SettingsView: View { } // ── Account ──────────────────────────────────────────── - Section("Account") { + Section(String(localized: L10n.settings.account)) { if let profile { HStack { - Text("Name") + Text(String(localized: L10n.settings.name)) Spacer() - Text(profile.name.isEmpty ? "Not set" : profile.name) + Text(profile.name.isEmpty + ? String(localized: L10n.settings.notSet) + : profile.name) .foregroundStyle(.secondary) } HStack { - Text("Joined") + Text(String(localized: L10n.settings.joined)) Spacer() Text(profile.joinDate.formatted(date: .abbreviated, time: .omitted)) .foregroundStyle(.secondary) @@ -121,30 +123,30 @@ struct SettingsView: View { Button(role: .destructive) { showingResetAlert = true } label: { - Label("Reset All Progress", systemImage: "trash") + Label(String(localized: L10n.settings.resetProgress), systemImage: "trash") .foregroundStyle(.red) } } // ── About ────────────────────────────────────────────── - Section("About") { + Section(String(localized: L10n.settings.about)) { HStack { - Text("Version") + Text(String(localized: L10n.settings.version)) Spacer() Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0") .foregroundStyle(.secondary) } - NavigationLink("Privacy Policy") { PrivacyPolicyView() } - NavigationLink("Terms of Service") { TermsOfServiceView() } + NavigationLink(String(localized: L10n.policy.privacyTitle)) { PrivacyPolicyView() } + NavigationLink(String(localized: L10n.policy.termsTitle)) { TermsOfServiceView() } } } - .navigationTitle("Settings") + .navigationTitle(String(localized: L10n.settings.title)) .navigationBarTitleDisplayMode(.large) - .alert("Reset All Progress?", isPresented: $showingResetAlert) { - Button("Reset", role: .destructive) { resetProgress() } - Button("Cancel", role: .cancel) {} + .alert(String(localized: L10n.settings.resetProgressConfirm), isPresented: $showingResetAlert) { + Button(String(localized: L10n.settings.confirmReset), role: .destructive) { resetProgress() } + Button(String(localized: L10n.action.cancel), role: .cancel) {} } message: { - Text("This will permanently delete your workout history and streak. This cannot be undone.") + Text(L10n.settings.resetProgressMessage) } } diff --git a/tabatago-swift/TabataGo/Views/Tabs/ActivityTab.swift b/tabatago-swift/TabataGo/Views/Tabs/ActivityTab.swift index a3b73be..8e528ee 100644 --- a/tabatago-swift/TabataGo/Views/Tabs/ActivityTab.swift +++ b/tabatago-swift/TabataGo/Views/Tabs/ActivityTab.swift @@ -30,14 +30,14 @@ struct ActivityTab: View { // ── Weekly Summary ───────────────────────────── VStack(alignment: .leading, spacing: 12) { - Text("This Week") + Text(L10n.home.thisWeek) .font(.title3.weight(.bold)) .padding(.horizontal) HStack(spacing: 12) { - StatBadge(label: "Workouts", value: "\(weeklyCount)", color: Theme.brand, icon: "flame.fill") - StatBadge(label: "Minutes", value: "\(weeklyMinutes)", color: Theme.rest, icon: "clock.fill") - StatBadge(label: "Calories", value: "\(Int(weeklyCalories))", color: Theme.success, icon: "bolt.fill") + StatBadge(label: L10n.activity.workouts, value: "\(weeklyCount)", color: Theme.brand, icon: "flame.fill") + StatBadge(label: L10n.activity.minutes, value: "\(weeklyMinutes)", color: Theme.rest, icon: "clock.fill") + StatBadge(label: L10n.activity.calories, value: "\(Int(weeklyCalories))", color: Theme.success, icon: "bolt.fill") } .padding(.horizontal) } @@ -45,7 +45,7 @@ struct ActivityTab: View { // ── Workout History ──────────────────────────── if !sessions.isEmpty { VStack(alignment: .leading, spacing: 12) { - Text("History") + Text(L10n.activity.history) .font(.title3.weight(.bold)) .padding(.horizontal) @@ -65,7 +65,7 @@ struct ActivityTab: View { } .padding(.top, 8) } - .navigationTitle("Activity") + .navigationTitle(String(localized: L10n.tab.activity)) .navigationBarTitleDisplayMode(.large) .task { await healthVM.refresh() } .refreshable { await healthVM.refresh() } @@ -97,11 +97,11 @@ struct StreakBanner: View { .font(.system(size: 52, weight: .black, design: .rounded)) .monospacedDigit() .foregroundStyle(Theme.brand) - Text("days") + Text(L10n.activity.days) .font(.headline) .foregroundStyle(.secondary) } - Text("Current Streak") + Text(L10n.activity.currentStreak) .font(.subheadline) .foregroundStyle(.secondary) } @@ -116,11 +116,11 @@ struct StreakBanner: View { .font(.system(size: 52, weight: .black, design: .rounded)) .monospacedDigit() .foregroundStyle(Theme.success) - Text("days") + Text(L10n.activity.days) .font(.headline) .foregroundStyle(.secondary) } - Text("Best Streak") + Text(L10n.activity.bestStreak) .font(.subheadline) .foregroundStyle(.secondary) } @@ -139,29 +139,29 @@ struct HealthRingsCard: View { HStack { Image(systemName: "heart.fill") .foregroundStyle(.red) - Text("Apple Health") + Text(L10n.health.appleHealth) .font(.headline.weight(.semibold)) Spacer() - Text("Today") + Text(L10n.health.today) .font(.caption) .foregroundStyle(.secondary) } HStack(spacing: 12) { HealthRingStat( - label: "Move", + label: L10n.health.move, value: "\(Int(snapshot.activeCaloricBurn))", unit: "kcal", color: .red ) HealthRingStat( - label: "Exercise", + label: L10n.health.exercise, value: "\(Int(snapshot.exerciseMinutes))", unit: "min", color: .green ) HealthRingStat( - label: "Stand", + label: L10n.health.stand, value: "\(snapshot.standHours)", unit: "hrs", color: .cyan @@ -175,7 +175,7 @@ struct HealthRingsCard: View { .font(.subheadline) .foregroundStyle(.secondary) Spacer() - Text("Resting HR") + Text(L10n.health.restingHR) .font(.caption) .foregroundStyle(.tertiary) } @@ -187,7 +187,7 @@ struct HealthRingsCard: View { } struct HealthRingStat: View { - let label: String + let label: LocalizedStringResource let value: String let unit: String let color: Color @@ -252,9 +252,9 @@ struct EmptyActivityView: View { Image(systemName: "figure.run.circle") .font(.system(size: 56)) .foregroundStyle(Theme.brand.opacity(0.6)) - Text("No workouts yet") + Text(L10n.activity.noWorkouts) .font(.title3.weight(.semibold)) - Text("Complete your first Tabata to see your activity here.") + Text(L10n.activity.noWorkoutsMessage) .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) diff --git a/tabatago-swift/TabataGo/Views/Tabs/HomeTab.swift b/tabatago-swift/TabataGo/Views/Tabs/HomeTab.swift index db2935c..93ebb3c 100644 --- a/tabatago-swift/TabataGo/Views/Tabs/HomeTab.swift +++ b/tabatago-swift/TabataGo/Views/Tabs/HomeTab.swift @@ -47,15 +47,15 @@ struct HomeTab: View { VStack(alignment: .leading, spacing: 24) { // ── Quick Stats Row ── HStack(spacing: 12) { - StatBadge(label: "Streak", value: "\(currentStreak)d", color: Theme.brand, icon: "flame.fill") - StatBadge(label: "This Week", value: "\(weeklyCount)", color: Theme.success, icon: "checkmark.circle.fill") - StatBadge(label: "All Time", value: "\(totalCount)", color: Theme.rest, icon: "trophy.fill") + StatBadge(label: L10n.home.streak, value: "\(currentStreak)d", color: Theme.brand, icon: "flame.fill") + StatBadge(label: L10n.home.thisWeek, value: "\(weeklyCount)", color: Theme.success, icon: "checkmark.circle.fill") + StatBadge(label: L10n.home.allTime, value: "\(totalCount)", color: Theme.rest, icon: "trophy.fill") } .padding(.horizontal) // ── Body Zone Grid ── VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Browse by Zone", subtitle: "Target specific muscle groups") + SectionHeader(title: L10n.home.browseTitle, subtitle: L10n.home.browseSubtitle) .padding(.horizontal) VStack(spacing: 12) { @@ -72,7 +72,7 @@ struct HomeTab: View { // ── Featured Workouts ── if !vm.featuredPrograms.isEmpty { VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Featured", subtitle: "Handpicked for you") + SectionHeader(title: L10n.home.featuredTitle, subtitle: L10n.home.featuredSubtitle) .padding(.horizontal) ScrollView(.horizontal, showsIndicators: false) { @@ -96,13 +96,13 @@ struct HomeTab: View { Image(systemName: "exclamationmark.triangle") .font(.title2) .foregroundStyle(.secondary) - Text("Failed to load programs") + Text(L10n.home.failedToLoad) .font(.subheadline.weight(.semibold)) Text(error) .font(.caption) .foregroundStyle(.secondary) .multilineTextAlignment(.center) - Button("Retry") { Task { await vm.refresh() } } + Button(String(localized: L10n.action.retry)) { Task { await vm.refresh() } } .buttonStyle(.bordered) .padding(.top, 4) } @@ -128,8 +128,8 @@ struct HomeTab: View { // ─── Sub-components ─────────────────────────────────────────────── struct SectionHeader: View { - let title: String - var subtitle: String? = nil + let title: LocalizedStringResource + var subtitle: LocalizedStringResource? = nil var body: some View { VStack(alignment: .leading, spacing: 2) { @@ -185,7 +185,7 @@ struct FeaturedProgramCard: View { Label("\(program.estimatedDuration)m", systemImage: "clock.fill") Label("\(program.estimatedCalories) kcal", systemImage: "flame.fill") if program.isFree { - Label("Free", systemImage: "checkmark.seal.fill") + Label(String(localized: L10n.home.free), systemImage: "checkmark.seal.fill") .foregroundStyle(Theme.success) } } @@ -220,7 +220,6 @@ struct ZoneCard: View { Spacer(minLength: 0) HStack(spacing: 4) { Text("Explore") - .font(.caption.weight(.semibold)) Image(systemName: "arrow.right") .font(.caption.weight(.semibold)) } @@ -238,22 +237,17 @@ struct ZoneCard: View { .frame(height: 140) } - private var zoneLabel: String { + private var zoneLabel: LocalizedStringResource { switch zone { - case "upper-body": return "Upper Body" - case "lower-body": return "Lower Body" - case "full-body": return "Full Body" - default: return zone.replacingOccurrences(of: "-", with: " ").capitalized + case "upper-body": return L10n.zone.upper + case "lower-body": return L10n.zone.lower + case "full-body": return L10n.zone.full + default: return LocalizedStringResource(stringLiteral: zone.replacingOccurrences(of: "-", with: " ").capitalized) } } - private var zoneDescription: String { - switch zone { - case "upper-body": return "Arms, chest, shoulders & back" - case "lower-body": return "Legs, glutes & core stability" - case "full-body": return "Total body burn, head to toe" - default: return "Targeted workouts" - } + private var zoneDescription: LocalizedStringResource { + L10n.zone.description(for: zone) } private var zoneIcon: String { diff --git a/tabatago-swift/TabataGo/Views/Tabs/MainTabView.swift b/tabatago-swift/TabataGo/Views/Tabs/MainTabView.swift index fd20ba1..c7c0dbf 100644 --- a/tabatago-swift/TabataGo/Views/Tabs/MainTabView.swift +++ b/tabatago-swift/TabataGo/Views/Tabs/MainTabView.swift @@ -16,29 +16,29 @@ struct MainTabView: View { } } - var label: String { + var label: LocalizedStringResource { switch self { - case .home: return "Home" - case .programs: return "Programs" - case .activity: return "Activity" - case .profile: return "Profile" + case .home: return L10n.tab.home + case .programs: return L10n.tab.programs + case .activity: return L10n.tab.activity + case .profile: return L10n.tab.profile } } } var body: some View { TabView(selection: $selectedTab) { - Tab(AppTab.home.label, systemImage: AppTab.home.icon, value: AppTab.home) { - HomeTab() + Tab(value: AppTab.home) { HomeTab() } label: { + Label(AppTab.home.label, systemImage: AppTab.home.icon) } - Tab(AppTab.programs.label, systemImage: AppTab.programs.icon, value: AppTab.programs) { - ProgramsTab() + Tab(value: AppTab.programs) { ProgramsTab() } label: { + Label(AppTab.programs.label, systemImage: AppTab.programs.icon) } - Tab(AppTab.activity.label, systemImage: AppTab.activity.icon, value: AppTab.activity) { - ActivityTab() + Tab(value: AppTab.activity) { ActivityTab() } label: { + Label(AppTab.activity.label, systemImage: AppTab.activity.icon) } - Tab(AppTab.profile.label, systemImage: AppTab.profile.icon, value: AppTab.profile) { - ProfileTab() + Tab(value: AppTab.profile) { ProfileTab() } label: { + Label(AppTab.profile.label, systemImage: AppTab.profile.icon) } } .tabViewStyle(.sidebarAdaptable) diff --git a/tabatago-swift/TabataGo/Views/Tabs/ProfileTab.swift b/tabatago-swift/TabataGo/Views/Tabs/ProfileTab.swift index e455e84..37ea27e 100644 --- a/tabatago-swift/TabataGo/Views/Tabs/ProfileTab.swift +++ b/tabatago-swift/TabataGo/Views/Tabs/ProfileTab.swift @@ -28,12 +28,14 @@ struct ProfileTab: View { } VStack(alignment: .leading, spacing: 4) { - Text(profile?.name ?? "Athlete") + Text(profile?.name ?? String(localized: L10n.profile.athlete)) .font(.title3.weight(.bold)) - Text(profile?.goal.label ?? "") - .font(.subheadline) - .foregroundStyle(.secondary) - Text("Joined \(profile?.joinDate.formatted(date: .abbreviated, time: .omitted) ?? "")") + if let goal = profile?.goal { + Text(goal.label) + .font(.subheadline) + .foregroundStyle(.secondary) + } + Text(String(format: String(localized: L10n.profile.joinedFmt), profile?.joinDate.formatted(date: .abbreviated, time: .omitted) ?? "")) .font(.caption) .foregroundStyle(.tertiary) } @@ -43,13 +45,15 @@ struct ProfileTab: View { } // ── Subscription ────────────────────────────────── - Section("Subscription") { + Section(String(localized: L10n.profile.subscription)) { if profile?.subscription.isPremium == true { HStack { - Label("Premium Active", systemImage: "crown.fill") + Label(String(localized: L10n.paywall.premiumActive), systemImage: "crown.fill") .foregroundStyle(Theme.brand) Spacer() - Text(profile?.subscription == .premiumYearly ? "Yearly" : "Monthly") + Text(profile?.subscription == .premiumYearly + ? String(localized: L10n.profile.yearly) + : String(localized: L10n.profile.monthly)) .font(.subheadline) .foregroundStyle(.secondary) } @@ -58,7 +62,7 @@ struct ProfileTab: View { showingPaywall = true } label: { HStack { - Label("Upgrade to Premium", systemImage: "crown") + Label(String(localized: L10n.paywall.upgradePrompt), systemImage: "crown") .foregroundStyle(Theme.brand) Spacer() Image(systemName: "chevron.right") @@ -71,36 +75,36 @@ struct ProfileTab: View { } // ── Fitness Profile ─────────────────────────────── - Section("Fitness Profile") { - ProfileRow(label: "Level", value: profile?.fitnessLevel.label ?? "—", icon: "chart.bar") - ProfileRow(label: "Goal", value: profile?.goal.label ?? "—", icon: "target") - ProfileRow(label: "Weekly Goal", value: "\(profile?.weeklyFrequency ?? 3)x / week", icon: "calendar") + Section(String(localized: L10n.profile.fitnessProfile)) { + ProfileRow(label: L10n.profile.level, value: String(localized: profile?.fitnessLevel.label ?? L10n.level.beginner), icon: "chart.bar") + ProfileRow(label: L10n.profile.goal, value: String(localized: profile?.goal.label ?? L10n.goal.cardio), icon: "target") + ProfileRow(label: L10n.profile.weeklyGoal, value: String(format: String(localized: L10n.profile.weeklyGoalFmt), profile?.weeklyFrequency ?? 3), icon: "calendar") } // ── Settings ────────────────────────────────────── Section { NavigationLink(destination: SettingsView()) { - Label("Settings", systemImage: "gearshape") + Label(String(localized: L10n.settings.title), systemImage: "gearshape") } NavigationLink(destination: PrivacyPolicyView()) { - Label("Privacy Policy", systemImage: "hand.raised") + Label(String(localized: L10n.policy.privacyTitle), systemImage: "hand.raised") } NavigationLink(destination: TermsOfServiceView()) { - Label("Terms of Service", systemImage: "doc.text") + Label(String(localized: L10n.policy.termsTitle), systemImage: "doc.text") } } // ── App Info ────────────────────────────────────── Section { HStack { - Text("Version") + Text(String(localized: L10n.settings.version)) Spacer() Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0") .foregroundStyle(.secondary) } } } - .navigationTitle("Profile") + .navigationTitle(String(localized: L10n.tab.profile)) .navigationBarTitleDisplayMode(.large) .sheet(isPresented: $showingPaywall, onDismiss: syncSubscription) { PaywallView() @@ -118,13 +122,13 @@ struct ProfileTab: View { } struct ProfileRow: View { - let label: String + let label: LocalizedStringResource let value: String let icon: String var body: some View { HStack { - Label(label, systemImage: icon) + Label(String(localized: label), systemImage: icon) Spacer() Text(value) .foregroundStyle(.secondary) diff --git a/tabatago-swift/TabataGo/Views/Tabs/ProgramsTab.swift b/tabatago-swift/TabataGo/Views/Tabs/ProgramsTab.swift index ff0e28d..2462d29 100644 --- a/tabatago-swift/TabataGo/Views/Tabs/ProgramsTab.swift +++ b/tabatago-swift/TabataGo/Views/Tabs/ProgramsTab.swift @@ -25,7 +25,7 @@ struct ProgramsTab: View { /// Label for the level toolbar button. private var levelMenuLabel: String { - selectedLevel ?? "All Levels" + selectedLevel ?? String(localized: L10n.programs.allLevels) } var body: some View { @@ -33,8 +33,8 @@ struct ProgramsTab: View { ScrollView { VStack(spacing: 16) { // ── Zone Segmented Control ──────────────────── - Picker("Body Zone", selection: $selectedZone) { - Text("All").tag(String?.none) + Picker(String(localized: L10n.programs.filterZone), selection: $selectedZone) { + Text(L10n.programs.all).tag(String?.none) ForEach(zones, id: \.self) { zone in Text(zone.replacingOccurrences(of: "-", with: " ").capitalized) .tag(Optional(zone)) @@ -48,7 +48,7 @@ struct ProgramsTab: View { ProgressView().frame(minHeight: 120) } else if filtered.isEmpty { ContentUnavailableView( - "No Programs Found", + String(localized: L10n.programs.noneFound), systemImage: "magnifyingglass", description: Text("Try changing your filters.") ) @@ -67,16 +67,16 @@ struct ProgramsTab: View { } .padding(.top, 8) } - .navigationTitle("Programs") + .navigationTitle(String(localized: L10n.tab.programs)) .navigationBarTitleDisplayMode(.large) - .searchable(text: $searchText, prompt: "Search workouts...") + .searchable(text: $searchText, prompt: String(localized: L10n.programs.searchPrompt)) .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { Button { selectedLevel = nil } label: { - Label("All Levels", systemImage: selectedLevel == nil ? "checkmark" : "") + Label(String(localized: L10n.programs.allLevels), systemImage: selectedLevel == nil ? "checkmark" : "") } Divider() ForEach(levels, id: \.self) { level in diff --git a/tabatago-swift/TabataGoWatch/Complications/TabataGoComplication.swift b/tabatago-swift/TabataGoWatch/Complications/TabataGoComplication.swift index 098713b..a07d964 100644 --- a/tabatago-swift/TabataGoWatch/Complications/TabataGoComplication.swift +++ b/tabatago-swift/TabataGoWatch/Complications/TabataGoComplication.swift @@ -38,14 +38,14 @@ struct TabataProvider: TimelineProvider { } private func relativeLabel(for date: Date?) -> String { - guard let date else { return "Not started" } + guard let date else { return String(localized: LocalizedStringResource("watch.complication.notStarted")) } let days = Calendar.current.dateComponents([.day], from: Calendar.current.startOfDay(for: date), to: Calendar.current.startOfDay(for: Date())).day ?? 0 switch days { - case 0: return "Today" - case 1: return "Yesterday" - default: return "\(days) days ago" + case 0: return String(localized: LocalizedStringResource("watch.complication.today")) + case 1: return String(localized: LocalizedStringResource("watch.complication.yesterday")) + default: return String(format: String(localized: LocalizedStringResource("watch.complication.daysAgoFmt")), days) } } } @@ -82,13 +82,13 @@ struct RectangularComplicationView: View { Image(systemName: "bolt.fill") .foregroundStyle(.orange) .font(.system(size: 10, weight: .bold)) - Text("\(entry.streak) day streak") + Text(String(format: String(localized: LocalizedStringResource("watch.complication.dayStreakFmt")), entry.streak)) .font(.system(size: 12, weight: .bold, design: .rounded)) } Text(entry.lastWorkoutLabel) .font(.system(size: 11)) .foregroundStyle(.secondary) - Text("Open TabataGo →") + Text(String(localized: LocalizedStringResource("watch.complication.openApp"))) .font(.system(size: 10, weight: .medium)) .foregroundStyle(.orange) } @@ -108,7 +108,7 @@ struct CornerComplicationView: View { .font(.system(size: 14, weight: .bold)) .foregroundStyle(.orange) } - .widgetLabel("\(entry.streak) day streak") + .widgetLabel(String(format: String(localized: LocalizedStringResource("watch.complication.dayStreakFmt")), entry.streak)) } } @@ -123,7 +123,7 @@ struct TabataGoComplication: Widget { .containerBackground(.black, for: .widget) } .configurationDisplayName("TabataGo") - .description("Your current workout streak.") + .description(String(localized: LocalizedStringResource("watch.complication.description"))) .supportedFamilies([ .accessoryCircular, .accessoryRectangular, diff --git a/tabatago-swift/TabataGoWatch/Resources/Localizable.xcstrings b/tabatago-swift/TabataGoWatch/Resources/Localizable.xcstrings new file mode 100644 index 0000000..ab98a17 --- /dev/null +++ b/tabatago-swift/TabataGoWatch/Resources/Localizable.xcstrings @@ -0,0 +1,207 @@ +{ + "sourceLanguage" : "en", + "strings" : { + + "watch.phase.getReady" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "BEREIT" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "GET READY" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "LISTO" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "PRÊT" } } + } + }, + + "watch.phase.warmUp" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "AUFWÄRMEN" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "WARM UP" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "CALENTAMIENTO" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "ÉCHAUFFEMENT" } } + } + }, + + "watch.phase.work" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "ARBEIT" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "WORK" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "TRABAJO" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "TRAVAIL" } } + } + }, + + "watch.phase.rest" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "PAUSE" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "REST" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "DESCANSO" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "REPOS" } } + } + }, + + "watch.phase.break" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "PAUSE" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "BREAK" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "PAUSA" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "PAUSE" } } + } + }, + + "watch.phase.coolDown" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "ABKÜHLEN" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "COOL DOWN" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "ENFRIAMIENTO" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "RÉCUPÉRATION" } } + } + }, + + "watch.phase.done" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "FERTIG" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "DONE" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "HECHO" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "TERMINÉ" } } + } + }, + + "watch.idle.startOnPhone" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Workout auf\ndeinem iPhone starten" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Start a workout\non your iPhone" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Inicia un entrenamiento\nen tu iPhone" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Démarrer un entraînement\nsur votre iPhone" } } + } + }, + + "watch.idle.connected" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Verbunden" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Connected" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Conectado" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Connecté" } } + } + }, + + "watch.idle.noPhone" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Kein Telefon" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "No phone" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin teléfono" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pas de téléphone" } } + } + }, + + "watch.activity.today" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Heute" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Today" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Hoy" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aujourd'hui" } } + } + }, + + "watch.activity.move" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Bewegung" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Move" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Mover" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bouger" } } + } + }, + + "watch.activity.exercise" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Sport" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Exercise" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejercicio" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exercice" } } + } + }, + + "watch.activity.stand" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Stehen" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Stand" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Estar de pie" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Debout" } } + } + }, + + "watch.activity.streak" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Serie" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "streak" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "racha" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "série" } } + } + }, + + "watch.complication.notStarted" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht begonnen" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Not started" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "No iniciado" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pas commencé" } } + } + }, + + "watch.complication.today" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Heute" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Today" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Hoy" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aujourd'hui" } } + } + }, + + "watch.complication.yesterday" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Gestern" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Yesterday" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Ayer" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Hier" } } + } + }, + + "watch.complication.daysAgoFmt" : { + "comment" : "printf format string — %d = number of days", + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "vor %d Tagen" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "%d days ago" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "hace %d días" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "il y a %d jours" } } + } + }, + + "watch.complication.dayStreakFmt" : { + "comment" : "printf format string — %d = streak count", + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "%d Tage in Folge" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "%d day streak" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "%d días seguidos" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "%d jours de suite" } } + } + }, + + "watch.complication.openApp" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "TabataGo öffnen →" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Open TabataGo →" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Abrir TabataGo →" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ouvrir TabataGo →" } } + } + }, + + "watch.complication.description" : { + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "Dein aktueller Workout-Streak." } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "Your current workout streak." } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "Tu racha de entrenamiento actual." } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "Votre série d'entraînements actuelle." } } + } + } + + }, + "version" : "1.0" +} diff --git a/tabatago-swift/TabataGoWatch/Utilities/WatchL10n.swift b/tabatago-swift/TabataGoWatch/Utilities/WatchL10n.swift new file mode 100644 index 0000000..5134a96 --- /dev/null +++ b/tabatago-swift/TabataGoWatch/Utilities/WatchL10n.swift @@ -0,0 +1,54 @@ +import Foundation + +/// Type-safe string keys for the Watch target's `Localizable.xcstrings`. +/// Usage: Text(WatchL10n.phase.work) or String(localized: WatchL10n.idle.connected) +enum WatchL10n { + + enum phase { + static let getReady = LocalizedStringResource("watch.phase.getReady") + static let warmUp = LocalizedStringResource("watch.phase.warmUp") + static let work = LocalizedStringResource("watch.phase.work") + static let rest = LocalizedStringResource("watch.phase.rest") + static let `break` = LocalizedStringResource("watch.phase.break") + static let coolDown = LocalizedStringResource("watch.phase.coolDown") + static let done = LocalizedStringResource("watch.phase.done") + + static func label(for phase: WatchPhase) -> LocalizedStringResource { + switch phase { + case .prep: return getReady + case .warmup: return warmUp + case .work: return work + case .rest: return rest + case .interBlockRest: return `break` + case .cooldown: return coolDown + case .complete: return done + } + } + } + + enum idle { + static let startOnPhone = LocalizedStringResource("watch.idle.startOnPhone") + static let connected = LocalizedStringResource("watch.idle.connected") + static let noPhone = LocalizedStringResource("watch.idle.noPhone") + } + + enum activity { + static let today = LocalizedStringResource("watch.activity.today") + static let move = LocalizedStringResource("watch.activity.move") + static let exercise = LocalizedStringResource("watch.activity.exercise") + static let stand = LocalizedStringResource("watch.activity.stand") + static let streak = LocalizedStringResource("watch.activity.streak") + } + + enum complication { + static let notStarted = LocalizedStringResource("watch.complication.notStarted") + static let today = LocalizedStringResource("watch.complication.today") + static let yesterday = LocalizedStringResource("watch.complication.yesterday") + /// printf format: takes one Int (number of days). e.g. "%d days ago" + static let daysAgoFmt = LocalizedStringResource("watch.complication.daysAgoFmt") + /// printf format: takes one Int (streak count). e.g. "%d day streak" + static let dayStreakFmt = LocalizedStringResource("watch.complication.dayStreakFmt") + static let openApp = LocalizedStringResource("watch.complication.openApp") + static let description = LocalizedStringResource("watch.complication.description") + } +} diff --git a/tabatago-swift/TabataGoWatch/Views/WatchActivityView.swift b/tabatago-swift/TabataGoWatch/Views/WatchActivityView.swift index 5f2f6b5..67b0cb7 100644 --- a/tabatago-swift/TabataGoWatch/Views/WatchActivityView.swift +++ b/tabatago-swift/TabataGoWatch/Views/WatchActivityView.swift @@ -20,7 +20,7 @@ struct WatchActivityView: View { VStack(spacing: 14) { // ── Rings ────────────────────────────────────────── - Text("Today") + Text(LocalizedStringResource("watch.activity.today")) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) @@ -29,15 +29,15 @@ struct WatchActivityView: View { RingView(progress: moveProgress, color: Color(red: 1.0, green: 0.23, blue: 0.19), icon: "flame.fill", - label: "Move") + label: LocalizedStringResource("watch.activity.move")) RingView(progress: exerciseProgress, color: .green, icon: "figure.run", - label: "Exercise") + label: LocalizedStringResource("watch.activity.exercise")) RingView(progress: standProgress, color: Color(red: 0.04, green: 0.80, blue: 0.97), icon: "figure.stand", - label: "Stand") + label: LocalizedStringResource("watch.activity.stand")) } Divider() @@ -50,7 +50,7 @@ struct WatchActivityView: View { Text("\(streak) day\(streak == 1 ? "" : "s")") .font(.system(size: 14, weight: .bold, design: .rounded)) Spacer() - Text("streak") + Text(LocalizedStringResource("watch.activity.streak")) .font(.system(size: 11)) .foregroundStyle(.secondary) } @@ -123,7 +123,7 @@ private struct RingView: View { let progress: Double let color: Color let icon: String - let label: String + let label: LocalizedStringResource var body: some View { VStack(spacing: 4) { diff --git a/tabatago-swift/TabataGoWatch/Views/WatchIdleView.swift b/tabatago-swift/TabataGoWatch/Views/WatchIdleView.swift index 57e92eb..6de08e6 100644 --- a/tabatago-swift/TabataGoWatch/Views/WatchIdleView.swift +++ b/tabatago-swift/TabataGoWatch/Views/WatchIdleView.swift @@ -13,7 +13,7 @@ struct WatchIdleView: View { Text("TabataGo") .font(.system(size: 18, weight: .bold, design: .rounded)) - Text("Start a workout\non your iPhone") + Text(LocalizedStringResource("watch.idle.startOnPhone")) .font(.system(size: 13)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) @@ -23,7 +23,7 @@ struct WatchIdleView: View { Circle() .fill(.green) .frame(width: 6, height: 6) - Text("Connected") + Text(LocalizedStringResource("watch.idle.connected")) .font(.system(size: 11)) .foregroundStyle(.secondary) } @@ -32,7 +32,7 @@ struct WatchIdleView: View { Circle() .fill(.gray) .frame(width: 6, height: 6) - Text("No phone") + Text(LocalizedStringResource("watch.idle.noPhone")) .font(.system(size: 11)) .foregroundStyle(.secondary) } diff --git a/tabatago-swift/TabataGoWatch/Views/WatchPlayerView.swift b/tabatago-swift/TabataGoWatch/Views/WatchPlayerView.swift index d1306b5..6f1aa8d 100644 --- a/tabatago-swift/TabataGoWatch/Views/WatchPlayerView.swift +++ b/tabatago-swift/TabataGoWatch/Views/WatchPlayerView.swift @@ -92,23 +92,23 @@ struct WatchPlayerView: View { private func watchPhaseColor(_ phase: WatchPhase) -> Color { switch phase { - case .prep, .warmup: return .orange - case .work: return Color(red: 1.0, green: 0.42, blue: 0.21) + case .prep, .warmup: return .orange + case .work: return Color(red: 1.0, green: 0.42, blue: 0.21) case .rest, .interBlockRest: return Color(red: 0.35, green: 0.78, blue: 0.98) - case .cooldown: return .cyan - case .complete: return .green + case .cooldown: return .cyan + case .complete: return .green } } - private func watchPhaseLabel(_ phase: WatchPhase) -> String { + private func watchPhaseLabel(_ phase: WatchPhase) -> LocalizedStringResource { switch phase { - case .prep: return "GET READY" - case .warmup: return "WARM UP" - case .work: return "WORK" - case .rest: return "REST" - case .interBlockRest: return "BREAK" - case .cooldown: return "COOL DOWN" - case .complete: return "DONE" + case .prep: return LocalizedStringResource("watch.phase.getReady") + case .warmup: return LocalizedStringResource("watch.phase.warmUp") + case .work: return LocalizedStringResource("watch.phase.work") + case .rest: return LocalizedStringResource("watch.phase.rest") + case .interBlockRest: return LocalizedStringResource("watch.phase.break") + case .cooldown: return LocalizedStringResource("watch.phase.coolDown") + case .complete: return LocalizedStringResource("watch.phase.done") } } }