From e5c61447a39dfbd1d7a5de1b272e3824d1ad1b66 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 21 Jan 2026 15:44:37 +0100 Subject: [PATCH] feat: add complete multilanguage support for german and english --- lib/l10n/app_de.arb | 142 +++++++++++++++++- lib/l10n/app_en.arb | 142 +++++++++++++++++- .../presentation/screens/login_screen.dart | 13 +- .../presentation/screens/profile_screen.dart | 107 ++++++------- .../presentation/screens/hub_screen.dart | 8 +- .../widgets/start_raid_button.dart | 3 +- .../presentation/screens/quest_log.dart | 2 +- .../presentation/widgets/avatar_editor.dart | 8 +- .../presentation/widgets/quest_board.dart | 9 +- .../presentation/widgets/quest_item.dart | 7 +- .../presentation/screens/history_screen.dart | 23 ++- .../screens/inventory_screen.dart | 9 +- .../screens/leaderboard_screen.dart | 12 +- .../presentation/screens/lobby_screen.dart | 5 +- .../screens/avatar_setup_screen.dart | 5 +- .../screens/bodyweight_input_screen.dart | 2 +- .../screens/inventory_setup_screen.dart | 11 +- .../screens/strength_test_screen.dart | 43 +++--- .../screens/privacy_policy_screen.dart | 6 +- .../presentation/screens/stats_screen.dart | 22 +-- .../widgets/exercise_guide_sheet.dart | 22 ++- .../presentation/screens/battle_screen.dart | 33 ++-- .../widgets/emom_timer_widget.dart | 14 +- .../presentation/widgets/timer_widget.dart | 18 ++- .../widgets/workout_content_widget.dart | 2 +- 25 files changed, 500 insertions(+), 168 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f161f1d..00142ca 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -33,6 +33,7 @@ "registerHaveAccount": "Bereits registriert? ", "registerLoginButton": "LOGIN", + "hubCycleComplete": "Zyklus beendet! Beende ihn in den Stats.", "hubNoActiveCycle": "Kein aktiver Zyklus", "hubCreateCycle": "Neuen Zyklus starten", "hubCycleLabel": "Zyklus", @@ -153,6 +154,26 @@ "emomConfirm": "BESTÄTIGEN & BEENDEN", "emomRepsPerRound": "Wiederholungen pro Runde", + "questDailyBounties": "TÄGLICHE KOPFGELDER", + "questViewAll": "ALLE ANZEIGEN >", + "questClaim": "EINSAMMELN", + "questRewardCollected": "Belohnung: {xp} XP erhalten!", + "@questRewardCollected": { + "placeholders": { + "xp": { + "type": "int" + } + } + }, + + "guideNotFound": "Keine Schriftrolle für diese Technik gefunden.", + "guideExecution": "AUSFÜHRUNG", + "guideMistakes": "HÄUFIGE FEHLER", + "guideAttributes": "BETROFFENE ATTRIBUTE", + "guideDiffNovice": "ANFÄNGER", + "guideDiffAdept": "FORTGESCHRITTEN", + "guideDiffMaster": "MEISTER", + "questTabDailies": "TÄGLICH", "questTabJourney": "REISE", "questEmptyDailies": "Keine täglichen Quests.\nKomm morgen wieder!", @@ -203,6 +224,32 @@ "passwordsDoNotMatch": "Passwörter stimmen nicht überein", "confirmButton": "BESTÄTIGEN", + "battleTitle": "Kampf", + "battleNoExercises": "Keine Übungen konfiguriert", + "battleWeekDay": "Woche {week} - Tag {day}", + "@battleWeekDay": { + "placeholders": { + "week": {"type": "int"}, + "day": {"type": "int"} + } + }, + "bodyweight": "Körpergewicht", + "battleBossDefeated": "BOSS BESIEGT!", + "timerComplete": "Zeit abgelaufen!", + "battleRepsPerRound": "{reps} Reps pro Runde", + "@battleRepsPerRound": { + "placeholders": { + "reps": {"type": "int"} + } + }, + "battleWeightKg": "GEWICHT: {weight} kg", + "@battleWeightKg": { + "placeholders": { + "weight": {"type": "double", "format": "decimalPattern"} + } + }, + "commonConfirm": "BESTÄTIGEN", + "guidePullupTitle": "Weighted Pull-Up", "guidePullupLore": "Den Körper gegen die Schwerkraft zu ziehen, ist der ultimative Beweis für Oberkörperkraft.", "guidePullupSteps": "Greife die Stange etwas weiter als schulterbreit (Obergriff)|Aktiviere den Core und ziehe die Schulterblätter nach unten/hinten|Ziehe dich hoch, bis das Kinn über der Stange ist|Lasse dich kontrolliert wieder ab", @@ -384,6 +431,44 @@ "lobbyStatusActive": "Raid startet...", "lobbyStatusEntering": "Betrete das Schlachtfeld...", + "unknownMember": "Unbekannt", + "leaderboardTitle": "HALL OF FAME", + "leaderboardHero": "Held #{id}", + "@leaderboardHero": { + "placeholders": { + "id": {"type": "String"} + } + }, + "leaderboardSubtitle": "Stufe {level} • {xp} XP", + "@leaderboardSubtitle": { + "placeholders": { + "level": {"type": "int"}, + "xp": {"type": "int"} + } + }, + "exportEmailSubject": "SLRPG Daten Export", + "exportEmailBody": "Deine SLRPG Trainingsdaten (DSGVO Export)", + "deleteConfirmationWord": "LÖSCHEN", + + "timerPause": "PAUSE", + "timerReset": "RESET", + "timerSkip": "SKIP", + "timerRestart": "NEUSTART", + "genderMale": "Männlich", + "genderFemale": "Weiblich", + + "timerIgnite": "STARTEN", + "timerResume": "FORTSETZEN", + "timerReady": "BEREIT?", + "timerPaused": "PAUSIERT", + "timerRound": "RUNDE {current} / {total}", + "@timerRound": { + "placeholders": { + "current": {"type": "int"}, + "total": {"type": "int"} + } + }, + "connectivityError": "Keine Internetverbindung verfügbar.", "connectivityMultiplayerError": "Für Multiplayer wird eine Internetverbindung benötigt.", @@ -393,5 +478,60 @@ "errorNotFound": "Daten konnten nicht gefunden werden.", "errorEntryNotUnique": "Dieser Eintrag ist bereits vergeben.", "errorAuthenticationFailed": "E-Mail oder Passwort falsch.", - "errorIllegalRequest": "Ungültige Anfrage." + "errorIllegalRequest": "Ungültige Anfrage.", + + "commonLevel": "Stufe", + "commonRequired": "Erforderlich", + "commonUpdate": "AKTUALISIEREN", + "commonEdit": "BEARBEITEN", + "commonLogout": "ABMELDEN", + "commonSave": "SPEICHERN", + "commonCancel": "ABBRECHEN", + "commonConfirm": "BESTÄTIGEN", + + "profileEditTitle": "Profil bearbeiten", + "profileEditAppearance": "Aussehen bearbeiten", + "profileSelectScenery": "Hintergrund wählen", + "profileBodyweightUpdated": "Körpergewicht aktualisiert", + "profileChangePassword": "Passwort ändern", + "profileOldPassword": "Altes Passwort", + "profileNewPassword": "Neues Passwort", + "profileConfirmNewPassword": "Bestätigen", + "profilePassMismatch": "Stimmt nicht überein", + "profilePassMinChars": "Min 8 Zeichen", + "profilePassChangedSuccess": "Passwort erfolgreich geändert", + "profilePhysicalStats": "Physische Werte", + "profileCurrentBodyweight": "Aktuelles Gewicht", + "profileTrainingFocus": "Trainingsfokus", + "profileAccessoryTemplate": "Assistenz-Template", + "profileAccountSecurity": "Kontosicherheit", + "profileDangerZone": "Gefahrenzone", + "profileResetProgress": "Fortschritt zurücksetzen", + "profileResetProgressSubtitle": "Setzt Level, XP und Historie zurück", + "profileResetConfirmTitle": "Fortschritt löschen?", + "profileResetConfirmBody": "Dies löscht alle Workouts und setzt Level auf 1. Nicht widerruflich.", + "profileDeleteAccount": "Account löschen", + "profileDeleteAccountSubtitle": "Löscht Account und Daten dauerhaft", + "profileDeleteConfirmTitle": "Account löschen?", + "profileDeleteConfirmBody": "Bist du sicher? Alle Daten gehen verloren.", + + "templateStrengthOnly": "Nur Stärke", + "templateStrengthOnlyDesc": "Hauptübungen + FSL. Pur & Schnell.", + "templateHypertrophy": "Hypertrophie Support", + "templateHypertrophyDesc": "Bodybuilding-Assistenz für Muskelpanzer.", + "templateConditioning": "Der Motor (Ausdauer)", + "templateConditioningDesc": "15 min Kettlebell Intervalle.", + "templateActiveJourneys": "AKTIVE REISEN", + "templatePullupJourney": "Quest: Der erste Klimmzug", + "templatePullupJourneyDesc": "Spezifische Progression für das eigene Körpergewicht.", + + "setupFailed": "Setup fehlgeschlagen: {error}", + "@setupFailed": { + "placeholders": { + "error": { + "type": "Object" + } + } + }, + "setupEmailExists": "E-Mail existiert bereits. Bitte einloggen." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ec35798..aebf3ba 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -33,6 +33,7 @@ "registerHaveAccount": "Already have an account? ", "registerLoginButton": "LOGIN", + "hubCycleComplete": "Cycle complete! Finish it in stats.", "hubNoActiveCycle": "No active cycle", "hubCreateCycle": "Create New Cycle", "hubCycleLabel": "Cycle", @@ -153,6 +154,26 @@ "emomConfirm": "CONFIRM & FINISH", "emomRepsPerRound": "Reps per Round", + "questDailyBounties": "DAILY BOUNTIES", + "questViewAll": "VIEW ALL >", + "questClaim": "CLAIM", + "questRewardCollected": "Reward collected: {xp} XP!", + "@questRewardCollected": { + "placeholders": { + "xp": { + "type": "int" + } + } + }, + + "guideNotFound": "No ancient scroll found for this technique.", + "guideExecution": "EXECUTION", + "guideMistakes": "COMMON MISTAKES", + "guideAttributes": "ATTRIBUTES AFFECTED", + "guideDiffNovice": "NOVICE", + "guideDiffAdept": "ADEPT", + "guideDiffMaster": "MASTER", + "questTabDailies": "DAILIES", "questTabJourney": "JOURNEY", "questEmptyDailies": "No daily quests available.\nCome back tomorrow!", @@ -203,6 +224,32 @@ "passwordsDoNotMatch": "Passwords do not match", "confirmButton": "CONFIRM", + "battleTitle": "Battle", + "battleNoExercises": "No exercises configured", + "battleWeekDay": "Week {week} - Day {day}", + "@battleWeekDay": { + "placeholders": { + "week": {"type": "int"}, + "day": {"type": "int"} + } + }, + "bodyweight": "Bodyweight", + "battleBossDefeated": "BOSS DEFEATED!", + "timerComplete": "Time Complete!", + "battleRepsPerRound": "{reps} Reps per Round", + "@battleRepsPerRound": { + "placeholders": { + "reps": {"type": "int"} + } + }, + "battleWeightKg": "WEIGHT: {weight} kg", + "@battleWeightKg": { + "placeholders": { + "weight": {"type": "double", "format": "decimalPattern"} + } + }, + "commonConfirm": "CONFIRM", + "guidePullupTitle": "Weighted Pull-Up", "guidePullupLore": "Pulling your body against gravity is the ultimate proof of upper body strength.", "guidePullupSteps": "Grip the bar slightly wider than shoulder width (overhand)|Engage core and pull shoulder blades down/back|Pull yourself up until chin is over the bar|Lower yourself with control", @@ -398,6 +445,44 @@ "lobbyStatusActive": "Raid is starting...", "lobbyStatusEntering": "Entering Battle...", + "unknownMember": "Unknown", + "leaderboardTitle": "HALL OF FAME", + "leaderboardHero": "Hero #{id}", + "@leaderboardHero": { + "placeholders": { + "id": {"type": "String"} + } + }, + "leaderboardSubtitle": "Level {level} • {xp} XP", + "@leaderboardSubtitle": { + "placeholders": { + "level": {"type": "int"}, + "xp": {"type": "int"} + } + }, + "exportEmailSubject": "SLRPG Data Export", + "exportEmailBody": "Your SLRPG training data (GDPR export)", + "deleteConfirmationWord": "DELETE", + + "timerPause": "PAUSE", + "timerReset": "RESET", + "timerSkip": "SKIP", + "timerRestart": "RESTART", + "genderMale": "Male", + "genderFemale": "Female", + + "timerIgnite": "IGNITE ENGINE", + "timerResume": "RESUME", + "timerReady": "READY?", + "timerPaused": "PAUSED", + "timerRound": "ROUND {current} / {total}", + "@timerRound": { + "placeholders": { + "current": {"type": "int"}, + "total": {"type": "int"} + } + }, + "connectivityError": "No internet connection available.", "connectivityMultiplayerError": "Active internet connection required for multiplayer.", @@ -407,5 +492,60 @@ "errorNotFound": "Data not found.", "errorEntryNotUnique": "Entry already exists.", "errorAuthenticationFailed": "E-Mail or Passwort wrong.", - "errorIllegalRequest": "Illegal Request." + "errorIllegalRequest": "Illegal Request.", + + "commonLevel": "Lvl", + "commonRequired": "Required", + "commonUpdate": "UPDATE", + "commonEdit": "EDIT", + "commonLogout": "LOGOUT", + "commonSave": "SAVE", + "commonCancel": "CANCEL", + "commonConfirm": "CONFIRM", + + "profileEditTitle": "Edit Profile", + "profileEditAppearance": "Edit Appearance", + "profileSelectScenery": "Select Scenery", + "profileBodyweightUpdated": "Bodyweight updated", + "profileChangePassword": "Change Password", + "profileOldPassword": "Old Password", + "profileNewPassword": "New Password", + "profileConfirmNewPassword": "Confirm New", + "profilePassMismatch": "Mismatch", + "profilePassMinChars": "Min 8 chars", + "profilePassChangedSuccess": "Password changed successfully", + "profilePhysicalStats": "Physical Stats", + "profileCurrentBodyweight": "Current Bodyweight", + "profileTrainingFocus": "Training Focus", + "profileAccessoryTemplate": "Accessory Template", + "profileAccountSecurity": "Account Security", + "profileDangerZone": "Danger Zone", + "profileResetProgress": "Reset Progress", + "profileResetProgressSubtitle": "Resets Level, XP and Training History", + "profileResetConfirmTitle": "Reset Progress?", + "profileResetConfirmBody": "This will delete all your workouts and reset your Level to 1. This cannot be undone.", + "profileDeleteAccount": "Delete Account", + "profileDeleteAccountSubtitle": "Permanently delete your account and data", + "profileDeleteConfirmTitle": "Delete Account?", + "profileDeleteConfirmBody": "Are you sure you want to delete your account? All data will be lost forever.", + + "templateStrengthOnly": "Strength Only", + "templateStrengthOnlyDesc": "Main Lifts + FSL. Pure & Fast.", + "templateHypertrophy": "Hypertrophy Support", + "templateHypertrophyDesc": "Bodybuilding accessories to build muscle armor.", + "templateConditioning": "The Engine (Conditioning)", + "templateConditioningDesc": "15 min Kettlebell intervals to boost stamina.", + "templateActiveJourneys": "ACTIVE JOURNEYS", + "templatePullupJourney": "Quest: The First Pull-Up", + "templatePullupJourneyDesc": "Specific progression to master your bodyweight.", + + "setupFailed": "Setup failed: {error}", + "@setupFailed": { + "placeholders": { + "error": { + "type": "Object" + } + } + }, + "setupEmailExists": "Email already exists. Please login or use another email." } diff --git a/lib/src/features/authentication/presentation/screens/login_screen.dart b/lib/src/features/authentication/presentation/screens/login_screen.dart index bac2393..22c4f9f 100644 --- a/lib/src/features/authentication/presentation/screens/login_screen.dart +++ b/lib/src/features/authentication/presentation/screens/login_screen.dart @@ -57,26 +57,27 @@ class _LoginScreenState extends ConsumerState { } } catch (e) { if (mounted) { + final l10n = AppLocalizations.of(context)!; setState(() { _isLoading = false; - _errorMessage = _parseErrorMessage(e.toString()); + _errorMessage = _parseErrorMessage(e.toString(), l10n); }); ErrorHandler.showErrorSnackBar(context, e); } } } - String _parseErrorMessage(String error) { + String _parseErrorMessage(String error, AppLocalizations l10n) { if (error.contains('400')) { - return 'Invalid email or password'; + return l10n.loginErrorInvalid; } else if (error.contains('SocketException') || error.contains('Connection refused') || error.contains('Network is unreachable')) { - return 'Could not connect to server.\nPlease check your internet connection.'; + return l10n.loginErrorConnection; } else if (error.contains('timeout')) { - return 'Connection timeout.\nPlease try again.'; + return l10n.loginErrorTimeout; } - return 'Login failed. Please try again.'; + return l10n.loginErrorGeneric; } @override diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index 21df2d8..039353b 100644 --- a/lib/src/features/authentication/presentation/screens/profile_screen.dart +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -57,7 +57,7 @@ class _ProfileScreenState extends ConsumerState { if (mounted) { setState(() => _hasChanges = false); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Bodyweight updated')), + SnackBar(content: Text(AppLocalizations.of(context)!.profileBodyweightUpdated)), ); } } finally { @@ -66,6 +66,7 @@ class _ProfileScreenState extends ConsumerState { } void _showChangePasswordDialog() { + final l10n = AppLocalizations.of(context)!; final oldPassCtrl = TextEditingController(); final newPassCtrl = TextEditingController(); final confirmPassCtrl = TextEditingController(); @@ -74,7 +75,7 @@ class _ProfileScreenState extends ConsumerState { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Change Password'), + title: Text(l10n.profileChangePassword), content: Form( key: formKey, child: Column( @@ -83,22 +84,22 @@ class _ProfileScreenState extends ConsumerState { TextFormField( controller: oldPassCtrl, obscureText: true, - decoration: const InputDecoration(labelText: 'Old Password'), - validator: (v) => v?.isEmpty == true ? 'Required' : null, + decoration: InputDecoration(labelText: l10n.profileOldPassword), + validator: (v) => v?.isEmpty == true ? l10n.commonRequired : null, ), const SizedBox(height: 16), TextFormField( controller: newPassCtrl, obscureText: true, - decoration: const InputDecoration(labelText: 'New Password'), - validator: (v) => (v?.length ?? 0) < 8 ? 'Min 8 chars' : null, + decoration: InputDecoration(labelText: l10n.profileNewPassword), + validator: (v) => (v?.length ?? 0) < 8 ? l10n.profilePassMinChars : null, ), const SizedBox(height: 16), TextFormField( controller: confirmPassCtrl, obscureText: true, - decoration: const InputDecoration(labelText: 'Confirm New'), - validator: (v) => v != newPassCtrl.text ? 'Mismatch' : null, + decoration: InputDecoration(labelText: l10n.profileConfirmNewPassword), + validator: (v) => v != newPassCtrl.text ? l10n.profilePassMismatch : null, ), ], ), @@ -106,7 +107,7 @@ class _ProfileScreenState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('CANCEL'), + child: Text(l10n.commonCancel), ), ElevatedButton( onPressed: () async { @@ -120,8 +121,8 @@ class _ProfileScreenState extends ConsumerState { ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Password changed successfully')), + SnackBar( + content: Text(l10n.profilePassChangedSuccess)), ); } } catch (e) { @@ -137,7 +138,7 @@ class _ProfileScreenState extends ConsumerState { } } }, - child: const Text('UPDATE'), + child: Text(l10n.commonUpdate), ), ], ), @@ -149,6 +150,7 @@ class _ProfileScreenState extends ConsumerState { final currentConfig = _user?.avatarConfig != null ? AvatarConfig.fromJson(_user!.avatarConfig!) : const AvatarConfig(); + final l10n = AppLocalizations.of(context)!; showModalBottomSheet( context: context, @@ -158,7 +160,7 @@ class _ProfileScreenState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.all(16), - child: Text('Select Scenery', + child: Text(l10n.profileSelectScenery, style: Theme.of(context).textTheme.titleLarge), ), SizedBox( @@ -225,7 +227,7 @@ class _ProfileScreenState extends ConsumerState { const Icon(Icons.lock, color: Colors.white54), const SizedBox(height: 4), Text( - 'Lvl ${item.unlockLevel}', + '${l10n.commonLevel} ${item.unlockLevel}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -279,6 +281,7 @@ class _ProfileScreenState extends ConsumerState { void _confirmDangerAction( String title, String content, VoidCallback onConfirm) { + final l10n = AppLocalizations.of(context)!; showDialog( context: context, builder: (context) => AlertDialog( @@ -287,7 +290,7 @@ class _ProfileScreenState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('CANCEL'), + child: Text(l10n.commonCancel), ), ElevatedButton( style: @@ -296,7 +299,7 @@ class _ProfileScreenState extends ConsumerState { Navigator.pop(context); onConfirm(); }, - child: const Text('CONFIRM'), + child: Text(l10n.commonConfirm), ), ], ), @@ -307,6 +310,7 @@ class _ProfileScreenState extends ConsumerState { final currentConfig = _user?.avatarConfig != null ? AvatarConfig.fromJson(_user!.avatarConfig!) : const AvatarConfig(); + final l10n = AppLocalizations.of(context)!; showModalBottomSheet( context: context, @@ -315,7 +319,7 @@ class _ProfileScreenState extends ConsumerState { backgroundColor: AppTheme.backgroundColor, builder: (context) => Scaffold( appBar: AppBar( - title: const Text('Edit Appearance'), + title: Text(l10n.profileEditAppearance), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), @@ -325,7 +329,7 @@ class _ProfileScreenState extends ConsumerState { onPressed: () { Navigator.pop(context, _tempAvatarConfig); }, - child: const Text('SAVE', + child: Text(l10n.commonSave, style: TextStyle( fontWeight: FontWeight.bold, color: AppTheme.primaryColor)), @@ -409,7 +413,7 @@ class _ProfileScreenState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text('Edit Profile'), + title: Text(l10n.profileEditTitle), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/hub'), @@ -418,7 +422,7 @@ class _ProfileScreenState extends ConsumerState { if (_hasChanges) TextButton( onPressed: _isLoading ? null : _saveBodyweight, - child: const Text('SAVE', + child: Text(l10n.commonSave, style: TextStyle( color: AppTheme.primaryColor, fontWeight: FontWeight.bold)), @@ -458,11 +462,11 @@ class _ProfileScreenState extends ConsumerState { child: OutlinedButton.icon( onPressed: _showBackgroundSelector, icon: const Icon(Icons.landscape), - label: const Text('CHANGE SCENERY'), + label: Text(l10n.profileSelectScenery.toUpperCase()), ), ), const SizedBox(height: 32), - Text('Physical Stats', + Text(l10n.profilePhysicalStats, style: Theme.of(context) .textTheme .titleLarge @@ -474,7 +478,7 @@ class _ProfileScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Current Bodyweight', + Text(l10n.profileCurrentBodyweight, style: Theme.of(context).textTheme.bodyMedium), Row( children: [ @@ -509,7 +513,7 @@ class _ProfileScreenState extends ConsumerState { ), ), const SizedBox(height: 32), - Text('Training Focus', + Text(l10n.profileTrainingFocus, style: Theme.of(context) .textTheme .titleLarge @@ -521,7 +525,7 @@ class _ProfileScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Accessory Template', + Text(l10n.profileAccessoryTemplate, style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 12), _buildTemplateSelector(), @@ -529,7 +533,7 @@ class _ProfileScreenState extends ConsumerState { ), ), ), - Text('Account Security', + Text(l10n.profileAccountSecurity, style: Theme.of(context) .textTheme .titleLarge @@ -537,7 +541,7 @@ class _ProfileScreenState extends ConsumerState { const SizedBox(height: 8), ListTile( leading: const Icon(Icons.lock_outline), - title: const Text('Change Password'), + title: Text(l10n.profileChangePassword), trailing: const Icon(Icons.chevron_right), onTap: _showChangePasswordDialog, ), @@ -549,7 +553,7 @@ class _ProfileScreenState extends ConsumerState { ), const Divider(), const SizedBox(height: 24), - Text('Danger Zone', + Text(l10n.profileDangerZone, style: Theme.of(context) .textTheme .titleLarge @@ -567,13 +571,13 @@ class _ProfileScreenState extends ConsumerState { ListTile( leading: const Icon(Icons.refresh, color: AppTheme.errorColor), - title: const Text('Reset Progress', + title: Text(l10n.profileResetProgress, style: TextStyle(color: AppTheme.errorColor)), - subtitle: const Text( - 'Resets Level, XP and Training History'), + subtitle: Text( + l10n.profileResetProgressSubtitle), onTap: () => _confirmDangerAction( - 'Reset Progress?', - 'This will delete all your workouts and reset your Level to 1. This cannot be undone.', + l10n.profileResetConfirmTitle, + l10n.profileResetConfirmBody, () async { setState(() => _isLoading = true); await userRepo.resetProgress(); @@ -588,13 +592,13 @@ class _ProfileScreenState extends ConsumerState { ListTile( leading: const Icon(Icons.delete_forever, color: AppTheme.errorColor), - title: const Text('Delete Account', + title: Text(l10n.profileDeleteAccount, style: TextStyle(color: AppTheme.errorColor)), - subtitle: const Text( - 'Permanently delete your account and data'), + subtitle: Text( + l10n.profileDeleteAccountSubtitle), onTap: () => _confirmDangerAction( - 'Delete Account?', - 'Are you sure you want to delete your account? All data will be lost forever.', + l10n.profileDeleteConfirmTitle, + l10n.profileDeleteConfirmBody, () async { setState(() => _isLoading = true); try { @@ -621,7 +625,7 @@ class _ProfileScreenState extends ConsumerState { if (mounted) context.go('/login'); }, icon: const Icon(Icons.logout), - label: const Text('LOGOUT'), + label: Text(l10n.commonLogout), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), @@ -637,6 +641,7 @@ class _ProfileScreenState extends ConsumerState { Widget _buildTemplateSelector() { final current = _getTemplateFromSettings(_user?.inventorySettings ?? {}); + final l10n = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -644,31 +649,31 @@ class _ProfileScreenState extends ConsumerState { _RadioTile( value: AccessoryTemplate.none, groupValue: current, - title: 'Strength Only', - subtitle: 'Main Lifts + FSL. Pure & Fast.', + title: l10n.templateStrengthOnly, + subtitle: l10n.templateStrengthOnlyDesc, onChanged: (val) => _updateTemplate(val!), ), const Divider(height: 1), _RadioTile( value: AccessoryTemplate.hypertrophy, groupValue: current, - title: 'Hypertrophy Support', - subtitle: 'Bodybuilding accessories to build muscle armor.', + title: l10n.templateHypertrophy, + subtitle: l10n.templateHypertrophyDesc, onChanged: (val) => _updateTemplate(val!), ), const Divider(height: 1), _RadioTile( value: AccessoryTemplate.conditioning, groupValue: current, - title: 'The Engine (Conditioning)', - subtitle: '15 min Kettlebell intervals to boost stamina.', + title: l10n.templateConditioning, + subtitle: l10n.templateConditioningDesc, onChanged: (val) => _updateTemplate(val!), ), - const Padding( - padding: EdgeInsets.fromLTRB(16, 24, 16, 8), + Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), child: Text( - 'ACTIVE JOURNEYS', - style: TextStyle( + l10n.templateActiveJourneys, + style: const TextStyle( color: Colors.grey, fontSize: 12, fontWeight: FontWeight.bold, @@ -678,8 +683,8 @@ class _ProfileScreenState extends ConsumerState { _RadioTile( value: AccessoryTemplate.journeyPullup, groupValue: current, - title: 'Quest: The First Pull-Up', - subtitle: 'Specific progression to master your bodyweight.', + title: l10n.templatePullupJourney, + subtitle: l10n.templatePullupJourneyDesc, onChanged: (val) => _updateTemplate(val!), ), ], diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart index 5caeeef..fc52ca4 100644 --- a/lib/src/features/dashboard/presentation/screens/hub_screen.dart +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -108,8 +108,8 @@ class _HubScreenState extends ConsumerState { if (!found) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cycle complete! Finish it in stats.')), + SnackBar( + content: Text(AppLocalizations.of(context)!.hubCycleComplete)), ); } return; @@ -224,8 +224,8 @@ class _HubScreenState extends ConsumerState { ), const SizedBox(height: 8), if (sets >= 20) - const Text('⚠️ HARDCORE MODE', - style: TextStyle( + Text(l10n.missionBriefingHardcore, + style: const TextStyle( color: AppTheme.errorColor, fontSize: 10, fontWeight: FontWeight.bold)), diff --git a/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart b/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart index ecb179f..6520f64 100644 --- a/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart +++ b/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; class StartRaidButton extends StatefulWidget { @@ -82,7 +83,7 @@ class _StartRaidButtonState extends State ), const SizedBox(width: 12), Text( - 'START RAID', + AppLocalizations.of(context)!.lobbyStartRaid, style: Theme.of(context).textTheme.labelLarge?.copyWith( fontSize: 20, color: Colors.black, diff --git a/lib/src/features/gamification/presentation/screens/quest_log.dart b/lib/src/features/gamification/presentation/screens/quest_log.dart index 28c54c3..60d2fc7 100644 --- a/lib/src/features/gamification/presentation/screens/quest_log.dart +++ b/lib/src/features/gamification/presentation/screens/quest_log.dart @@ -19,7 +19,7 @@ class QuestLogScreen extends ConsumerWidget { length: 2, child: Scaffold( appBar: AppBar( - title: const Text('Quest Log'), + title: Text(l10n.historyTitle), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/hub'), diff --git a/lib/src/features/gamification/presentation/widgets/avatar_editor.dart b/lib/src/features/gamification/presentation/widgets/avatar_editor.dart index 2e271f1..cd0c39b 100644 --- a/lib/src/features/gamification/presentation/widgets/avatar_editor.dart +++ b/lib/src/features/gamification/presentation/widgets/avatar_editor.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; import '../../domain/entities/avatar_config.dart'; import 'avatar_renderer.dart'; @@ -38,6 +39,7 @@ class _AvatarEditorState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Column( children: [ Container( @@ -52,9 +54,9 @@ class _AvatarEditorState extends State { Padding( padding: const EdgeInsets.all(16), child: SegmentedButton( - segments: const [ - ButtonSegment(value: 'male', label: Text('Male')), - ButtonSegment(value: 'female', label: Text('Female')), + segments: [ + ButtonSegment(value: 'male', label: Text(l10n.genderMale)), + ButtonSegment(value: 'female', label: Text(l10n.genderFemale)), ], selected: {_gender}, onSelectionChanged: (Set newSelection) { diff --git a/lib/src/features/gamification/presentation/widgets/quest_board.dart b/lib/src/features/gamification/presentation/widgets/quest_board.dart index 0cc6fcd..acc0c1f 100644 --- a/lib/src/features/gamification/presentation/widgets/quest_board.dart +++ b/lib/src/features/gamification/presentation/widgets/quest_board.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../gamification/data/repositories/quest_repository.dart'; import '../../../../shared/data/local/app_database.dart'; @@ -11,6 +12,7 @@ class QuestBoardWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final questRepo = ref.watch(questRepositoryProvider); + final l10n = AppLocalizations.of(context)!; return StreamBuilder>( stream: questRepo.watchQuests(), @@ -39,7 +41,7 @@ class QuestBoardWidget extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'DAILY BOUNTIES', + l10n.questDailyBounties, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppTheme.secondaryColor, letterSpacing: 1.5, @@ -47,8 +49,9 @@ class QuestBoardWidget extends ConsumerWidget { ), GestureDetector( onTap: () => context.go('/quests'), - child: const Text('VIEW ALL >', - style: TextStyle(fontSize: 10, color: Colors.grey)), + child: Text(l10n.questViewAll, + style: + const TextStyle(fontSize: 10, color: Colors.grey)), ), ], ), diff --git a/lib/src/features/gamification/presentation/widgets/quest_item.dart b/lib/src/features/gamification/presentation/widgets/quest_item.dart index c749f56..cae03a9 100644 --- a/lib/src/features/gamification/presentation/widgets/quest_item.dart +++ b/lib/src/features/gamification/presentation/widgets/quest_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/local/app_database.dart'; import '../../data/repositories/quest_repository.dart'; @@ -26,13 +27,14 @@ class _QuestItemState extends ConsumerState { await questRepo.claimQuest(widget.quest.id); if (mounted) { + final l10n = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.check_circle, color: AppTheme.successColor), const SizedBox(width: 8), - Text('Reward collected: ${widget.quest.rewardXP} XP!'), + Text(l10n.questRewardCollected(widget.quest.rewardXP)), ], ), backgroundColor: Colors.black87, @@ -164,7 +166,8 @@ class _QuestItemState extends ConsumerState { height: 12, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) - : const Text('CLAIM', style: TextStyle(fontSize: 12)), + : Text(AppLocalizations.of(context)!.questClaim, + style: const TextStyle(fontSize: 12)), ) else if (widget.quest.rewardItem != null && !isClaimed) const Icon(Icons.inventory_2, diff --git a/lib/src/features/history/presentation/screens/history_screen.dart b/lib/src/features/history/presentation/screens/history_screen.dart index 63f4df7..32a5372 100644 --- a/lib/src/features/history/presentation/screens/history_screen.dart +++ b/lib/src/features/history/presentation/screens/history_screen.dart @@ -214,8 +214,27 @@ class _ExerciseDetailRow extends StatelessWidget { const _ExerciseDetailRow({required this.exercise}); + String _getLocalizedName(BuildContext context, String rawName) { + final l10n = AppLocalizations.of(context)!; + switch (rawName) { + case 'Back Squat': + return l10n.exerciseSquat; + case 'Weighted Pull-up': + return l10n.exercisePullup; + case 'Pendlay Row': + return l10n.exerciseRow; + case 'Weighted Dip': + return l10n.exerciseDip; + case 'Bench Press': + return l10n.exerciseBench; + default: + return rawName; + } + } + @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( @@ -230,7 +249,7 @@ class _ExerciseDetailRow extends StatelessWidget { margin: const EdgeInsets.only(right: 8), ), Text( - exercise.exerciseName, + _getLocalizedName(context, exercise.exerciseName), style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.textSecondary), ), @@ -256,7 +275,7 @@ class _ExerciseDetailRow extends StatelessWidget { ), ), Text( - '${set.targetWeightTotal} kg', + '${set.targetWeightTotal} ${l10n.unitKg}', style: const TextStyle(fontWeight: FontWeight.bold), ), Row( diff --git a/lib/src/features/inventory/presentation/screens/inventory_screen.dart b/lib/src/features/inventory/presentation/screens/inventory_screen.dart index 72555bd..43d00a2 100644 --- a/lib/src/features/inventory/presentation/screens/inventory_screen.dart +++ b/lib/src/features/inventory/presentation/screens/inventory_screen.dart @@ -167,7 +167,7 @@ class _InventoryScreenState extends ConsumerState { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Error saving: $e'), + content: Text(l10n.inventorySaveError(e.toString())), backgroundColor: AppTheme.errorColor), ); } @@ -238,7 +238,7 @@ class _InventoryScreenState extends ConsumerState { min: 10, max: 25, divisions: 6, - label: '$_barWeight kg', + label: '$_barWeight ${l10n.unitKg}', activeColor: AppTheme.primaryColor, onChanged: (val) => setState(() { _barWeight = val; @@ -246,7 +246,7 @@ class _InventoryScreenState extends ConsumerState { }), ), ), - Text('${_barWeight.toStringAsFixed(1)} kg', + Text('${_barWeight.toStringAsFixed(1)} ${l10n.unitKg}', style: const TextStyle(fontWeight: FontWeight.bold)), ], @@ -398,6 +398,7 @@ class _InventoryScreenState extends ConsumerState { } Widget _buildBandChip(String color, int resistance, bool isSelected) { + final l10n = AppLocalizations.of(context)!; return InkWell( onTap: () { setState(() { @@ -430,7 +431,7 @@ class _InventoryScreenState extends ConsumerState { if (isSelected) const SizedBox(width: 4), Expanded( child: Text( - '$color (~${resistance}kg)', + '$color (~$resistance${l10n.unitKg})', style: TextStyle( color: isSelected ? _getBandColor(color) diff --git a/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart b/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart index 95acdaf..0e0b5e9 100644 --- a/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart +++ b/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import 'package:slrpg_app/src/features/multiplayer/domain/entities/leaderboard_entry.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/repositories/user_repository.dart'; @@ -20,10 +21,11 @@ class _LeaderboardScreenState extends ConsumerState { Widget build(BuildContext context) { final leaderboardAsync = ref.watch(leaderboardProvider); final currentUserAsync = ref.watch(userRepositoryProvider).getLocalUser(); + final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( - title: const Text('HALL OF FAME'), + title: Text(l10n.leaderboardTitle), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/hub'), @@ -54,7 +56,7 @@ class _LeaderboardScreenState extends ConsumerState { leading: _buildRankBadge(entry.rank), title: Text( entry.name.isEmpty - ? 'Hero #${entry.id.substring(0, 5)}' + ? l10n.leaderboardHero(entry.id.substring(0, 5)) : entry.name, style: TextStyle( fontWeight: @@ -62,7 +64,8 @@ class _LeaderboardScreenState extends ConsumerState { color: isMe ? AppTheme.primaryColor : Colors.white, ), ), - subtitle: Text('Level ${entry.level} • ${entry.xp} XP'), + subtitle: Text( + l10n.leaderboardSubtitle(entry.level, entry.xp)), trailing: _buildAvatarPreview(entry), ), ); @@ -71,7 +74,8 @@ class _LeaderboardScreenState extends ConsumerState { }); }, loading: () => const Center(child: CircularProgressIndicator()), - error: (err, stack) => Center(child: Text('Error: $err')), + error: (err, stack) => + Center(child: Text(l10n.setupFailed(err.toString()))), ), ); } diff --git a/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart b/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart index b372514..7048ab5 100644 --- a/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart +++ b/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart @@ -71,7 +71,7 @@ class _LobbyScreenState extends ConsumerState { extra: {'week': next['week'], 'day': next['day']}); } }); - return const Center(child: Text('Entering Battle...')); + return Center(child: Text(l10n.lobbyStatusEntering)); } return Center(child: Text(l10n.lobbyStatusActive)); }, @@ -243,6 +243,7 @@ class _MemberCard extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( @@ -253,7 +254,7 @@ class _MemberCard extends StatelessWidget { ? AvatarRenderer(config: member.avatar!, size: 40) : const CircleAvatar(child: Icon(Icons.person)), ), - title: Text(member.username ?? 'Unknown'), + title: Text(member.username ?? l10n.unknownMember), trailing: member.isReady ? const Icon(Icons.check_circle, color: Colors.green) : const Icon(Icons.circle_outlined, color: Colors.grey), diff --git a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart index 4bfb933..66e6aab 100644 --- a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart @@ -112,9 +112,10 @@ class _AvatarSetupScreenState extends ConsumerState { } } catch (e, stackTrace) { if (mounted) { + final l10n = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Setup failed: $e'), + content: Text(l10n.setupFailed(e.toString())), backgroundColor: AppTheme.errorColor, duration: const Duration(seconds: 5), ), @@ -196,7 +197,7 @@ class _AvatarSetupScreenState extends ConsumerState { prefixIcon: Icon(Icons.lock), ), validator: (v) => - (v?.length ?? 0) < 8 ? 'Min 8 characters' : null, + (v?.length ?? 0) < 8 ? l10n.profilePassMinChars : null, ), const SizedBox(height: 16), TextFormField( diff --git a/lib/src/features/onboarding/presentation/screens/bodyweight_input_screen.dart b/lib/src/features/onboarding/presentation/screens/bodyweight_input_screen.dart index f60f71b..191c05a 100644 --- a/lib/src/features/onboarding/presentation/screens/bodyweight_input_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/bodyweight_input_screen.dart @@ -133,7 +133,7 @@ class _BodyweightInputScreenState extends ConsumerState { ), ), Text( - _useKg ? 'kg' : 'lbs', + _useKg ? l10n.unitKg : l10n.unitLbs, style: Theme.of(context).textTheme.headlineMedium, ), ], diff --git a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart index 249110f..7170a6c 100644 --- a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart @@ -179,10 +179,11 @@ class _InventorySetupScreenState extends ConsumerState { log('Stack trace: $stackTrace'); if (mounted) { - String message = 'Setup failed: ${e.toString()}'; + final l10n = AppLocalizations.of(context)!; + String message = l10n.setupFailed(e.toString()); if (e.toString().toLowerCase().contains('unique') || e.toString().toLowerCase().contains('email')) { - message = 'Email already exists. Please login or use another email.'; + message = l10n.setupEmailExists; } ScaffoldMessenger.of(context).showSnackBar( @@ -261,14 +262,14 @@ class _InventorySetupScreenState extends ConsumerState { min: 10, max: 25, divisions: 3, - label: '${_barWeight.toStringAsFixed(0)} kg', + label: '${_barWeight.toStringAsFixed(0)} ${l10n.unitKg}', activeColor: AppTheme.primaryColor, onChanged: (value) { setState(() => _barWeight = value); }, ), Text( - '${_barWeight.toStringAsFixed(0)} kg', + '${_barWeight.toStringAsFixed(0)} ${l10n.unitKg}', style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: AppTheme.primaryColor, ), @@ -459,7 +460,7 @@ class _InventorySetupScreenState extends ConsumerState { if (isSelected) const SizedBox(width: 4), Expanded( child: Text( - '$color (~${resistance}kg)', + '$color (~$resistance${AppLocalizations.of(context)!.unitKg})', style: TextStyle( color: isSelected ? Colors.white : AppTheme.textSecondary, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, diff --git a/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart index a13f486..8b4ae85 100644 --- a/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart @@ -219,7 +219,7 @@ class _StrengthTestScreenState extends ConsumerState { const SizedBox(height: 32), _ExerciseCard( title: l10n.strengthLegs, - exerciseName: 'Back Squat', + exerciseName: l10n.exerciseSquat, icon: Icons.accessibility_new, weightController: _squatWeightController, repsController: _squatRepsController, @@ -231,8 +231,8 @@ class _StrengthTestScreenState extends ConsumerState { const SizedBox(height: 16), _AdaptiveExerciseCard( slotTitle: l10n.strengthPull, - primaryName: 'Weighted Pull-up', - secondaryName: 'Pendlay Row', + primaryName: l10n.exercisePullup, + secondaryName: l10n.exerciseRow, icon: Icons.north, isCapable: _canDoPullup, onToggleCapability: (val) { @@ -254,12 +254,10 @@ class _StrengthTestScreenState extends ConsumerState { repsController: _pullRepsController, weightLabel: _canDoPullup ? (_isAssistedPull - ? 'Band Assistance (kg)' - : 'Added Weight (kg)') - : 'Row Weight (kg)', - // weightLabel: - // _canDoPullup ? 'Add. Weight (kg)' : 'Row Weight (kg)', - repsLabel: _canDoPullup ? 'Reps' : '5RM Reps (usually 5)', + ? l10n.bandAssistanceLabel + : l10n.addWeightLabel) + : l10n.rowWeightLabel, + repsLabel: _canDoPullup ? l10n.repsLabel : l10n.reps5rmLabel, showResults: true, result1RM: _calculated1RMs['pullup'] ?? 0, resultTM: _calculatedTMs['pullup'] ?? 0, @@ -268,8 +266,8 @@ class _StrengthTestScreenState extends ConsumerState { const SizedBox(height: 16), _AdaptiveExerciseCard( slotTitle: l10n.strengthPush, - primaryName: 'Weighted Dip', - secondaryName: 'Bench Press', + primaryName: l10n.exerciseDip, + secondaryName: l10n.exerciseBench, icon: Icons.south, isCapable: _canDoDip, onToggleCapability: (val) { @@ -292,11 +290,10 @@ class _StrengthTestScreenState extends ConsumerState { repsController: _pushRepsController, weightLabel: _canDoDip ? (_isAssistedDip - ? 'Band Assistance (kg)' - : 'Added Weight (kg)') - : 'Weight (kg)', - // weightLabel: _canDoDip ? 'Add. Weight (kg)' : 'Weight (kg)', - repsLabel: 'Reps', + ? l10n.bandAssistanceLabel + : l10n.addWeightLabel) + : l10n.weightLabel, + repsLabel: l10n.repsLabel, showWeightInput: true, showResults: true, result1RM: _calculated1RMs['dip'] ?? 0, @@ -416,7 +413,7 @@ class _ExerciseCard extends StatelessWidget { : l10n.weightLabel, isDense: true), onChanged: (_) => onChanged(), - validator: (v) => v!.isEmpty ? 'Required' : null, + validator: (v) => v!.isEmpty ? l10n.commonRequired : null, ), ), const SizedBox(width: 12), @@ -428,7 +425,7 @@ class _ExerciseCard extends StatelessWidget { decoration: InputDecoration( labelText: l10n.repsLabel, isDense: true), onChanged: (_) => onChanged(), - validator: (v) => v!.isEmpty ? 'Required' : null, + validator: (v) => v!.isEmpty ? l10n.commonRequired : null, ), ), ], @@ -555,7 +552,7 @@ class _AdaptiveExerciseCard extends StatelessWidget { if (!isCapable) ...[ const SizedBox(height: 8), Text( - 'Adjusted: ${"Wendler 5/3/1"}', + l10n.adjustedWendler, style: const TextStyle( color: AppTheme.secondaryColor, fontSize: 12, @@ -578,7 +575,7 @@ class _AdaptiveExerciseCard extends StatelessWidget { decoration: InputDecoration( labelText: weightLabel, isDense: true), onChanged: (_) => onChanged(), - validator: (v) => v!.isEmpty ? 'Required' : null, + validator: (v) => v!.isEmpty ? l10n.commonRequired : null, ), ) else @@ -592,7 +589,7 @@ class _AdaptiveExerciseCard extends StatelessWidget { decoration: InputDecoration(labelText: repsLabel, isDense: true), onChanged: (_) => onChanged(), - validator: (v) => v!.isEmpty ? 'Required' : null, + validator: (v) => v!.isEmpty ? l10n.commonRequired : null, ), ), ], @@ -626,7 +623,7 @@ class _ResultBox extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.est1rm), - Text('${rm.toStringAsFixed(1)} kg', + Text('${rm.toStringAsFixed(1)} ${l10n.unitKg}', style: Theme.of(context).textTheme.bodyLarge), ], ), @@ -635,7 +632,7 @@ class _ResultBox extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.trainingMaxLabel), - Text('${tm.toStringAsFixed(1)} kg', + Text('${tm.toStringAsFixed(1)} ${l10n.unitKg}', style: const TextStyle( color: AppTheme.primaryColor, fontWeight: FontWeight.bold)), diff --git a/lib/src/features/settings/presentation/screens/privacy_policy_screen.dart b/lib/src/features/settings/presentation/screens/privacy_policy_screen.dart index 6c10af7..45a576b 100644 --- a/lib/src/features/settings/presentation/screens/privacy_policy_screen.dart +++ b/lib/src/features/settings/presentation/screens/privacy_policy_screen.dart @@ -176,8 +176,8 @@ class _PrivacyPolicyScreenState extends ConsumerState { await Share.shareXFiles( [XFile(file.path)], - subject: 'SLRPG Data Export', - text: 'Your SLRPG training data (GDPR export)', + subject: l10n.exportEmailSubject, + text: l10n.exportEmailBody, ); if (mounted) { @@ -207,7 +207,7 @@ class _PrivacyPolicyScreenState extends ConsumerState { Future _showDeleteAccountDialog() async { final l10n = AppLocalizations.of(context)!; final confirmationController = TextEditingController(); - final expectedText = l10n.localeName == 'de' ? 'LÖSCHEN' : 'DELETE'; + final expectedText = l10n.deleteConfirmationWord; final confirmed = await showDialog( context: context, diff --git a/lib/src/features/stats/presentation/screens/stats_screen.dart b/lib/src/features/stats/presentation/screens/stats_screen.dart index de9d183..8df5f29 100644 --- a/lib/src/features/stats/presentation/screens/stats_screen.dart +++ b/lib/src/features/stats/presentation/screens/stats_screen.dart @@ -208,15 +208,15 @@ class _StatsScreenState extends ConsumerState { String getLabel(String id) { switch (id) { case 'squat': - return 'Squat'; + return l10n.exerciseSquat; case 'pullup': - return 'Pull-up'; + return l10n.exercisePullup; case 'row': - return 'Row'; + return l10n.exerciseRow; case 'dip': - return 'Dip'; + return l10n.exerciseDip; case 'bench': - return 'Bench'; + return l10n.exerciseBench; default: return id; } @@ -358,13 +358,13 @@ class _CurrentCycleCard extends StatelessWidget { const SizedBox(height: 16), _StatRow( label: l10n.exerciseSquat, - value: '${tms['squat'].toStringAsFixed(2)} kg'), + value: '${tms['squat'].toStringAsFixed(2)} ${l10n.unitKg}'), _StatRow( label: getLabel(pullVariant), - value: '${tms['pullup'].toStringAsFixed(2)} kg'), + value: '${tms['pullup'].toStringAsFixed(2)} ${l10n.unitKg}'), _StatRow( label: getLabel(pushVariant), - value: '${tms['dip'].toStringAsFixed(2)} kg'), + value: '${tms['dip'].toStringAsFixed(2)} ${l10n.unitKg}'), const SizedBox(height: 32), SizedBox( width: double.infinity, @@ -471,6 +471,7 @@ class _DiffRow extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final diff = newVal - oldVal; final isPositive = diff > 0; @@ -491,8 +492,9 @@ class _DiffRow extends StatelessWidget { style: const TextStyle( color: AppTheme.successColor, fontWeight: FontWeight.bold)) else - const Text('STALLED', - style: TextStyle(color: AppTheme.secondaryColor, fontSize: 12)), + Text(l10n.statsStalled, + style: const TextStyle( + color: AppTheme.secondaryColor, fontSize: 12)), ], ), ); diff --git a/lib/src/features/wiki/presentation/widgets/exercise_guide_sheet.dart b/lib/src/features/wiki/presentation/widgets/exercise_guide_sheet.dart index 52afa14..e7dee5e 100644 --- a/lib/src/features/wiki/presentation/widgets/exercise_guide_sheet.dart +++ b/lib/src/features/wiki/presentation/widgets/exercise_guide_sheet.dart @@ -20,8 +20,8 @@ class ExerciseGuideSheet extends StatelessWidget { if (guide == null) { return Container( padding: const EdgeInsets.all(32), - child: const Text('No ancient scroll found for this technique.', - textAlign: TextAlign.center, style: TextStyle(color: Colors.grey)), + child: Text(l10n.guideNotFound, + textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey)), ); } @@ -61,7 +61,7 @@ class ExerciseGuideSheet extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 8), - _buildDifficultyBadge(guide.difficulty), + _buildDifficultyBadge(context, guide.difficulty), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), @@ -80,18 +80,18 @@ class ExerciseGuideSheet extends StatelessWidget { ), ), const SizedBox(height: 32), - _buildSectionTitle(context, 'EXECUTION'), + _buildSectionTitle(context, l10n.guideExecution), const SizedBox(height: 16), ...guide.steps .asMap() .entries .map((entry) => _buildStep(entry.key + 1, entry.value)), const SizedBox(height: 32), - _buildSectionTitle(context, 'COMMON MISTAKES'), + _buildSectionTitle(context, l10n.guideMistakes), const SizedBox(height: 16), ...guide.commonMistakes.map((m) => _buildMistake(m)), const SizedBox(height: 32), - _buildSectionTitle(context, 'ATTRIBUTES AFFECTED'), + _buildSectionTitle(context, l10n.guideAttributes), const SizedBox(height: 16), Wrap( spacing: 8, @@ -118,17 +118,23 @@ class ExerciseGuideSheet extends StatelessWidget { ); } - Widget _buildDifficultyBadge(String diff) { + Widget _buildDifficultyBadge(BuildContext context, String diff) { + final l10n = AppLocalizations.of(context)!; Color color; + String text = diff; + switch (diff) { case 'Novice': color = Colors.green; + text = l10n.guideDiffNovice; break; case 'Adept': color = Colors.orange; + text = l10n.guideDiffAdept; break; case 'Master': color = AppTheme.errorColor; + text = l10n.guideDiffMaster; break; default: color = Colors.grey; @@ -143,7 +149,7 @@ class ExerciseGuideSheet extends StatelessWidget { border: Border.all(color: color.withValues(alpha: 0.5)), ), child: Text( - diff.toUpperCase(), + text.toUpperCase(), style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.bold), ), diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart index 471efc4..dd1bc70 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -502,15 +502,15 @@ class _BattleScreenState extends ConsumerState { if (state.error != null) { return Scaffold( - appBar: AppBar(title: const Text('Battle')), - body: Center(child: Text('Error: ${state.error}')), + appBar: AppBar(title: Text(l10n.battleTitle)), + body: Center(child: Text(l10n.setupFailed(state.error.toString()))), ); } if (state.exercises.isEmpty) { return Scaffold( - appBar: AppBar(title: const Text('Battle')), - body: const Center(child: Text('No exercises configured')), + appBar: AppBar(title: Text(l10n.battleTitle)), + body: Center(child: Text(l10n.battleNoExercises)), ); } @@ -737,7 +737,7 @@ class _BattleScreenState extends ConsumerState { color: AppTheme.primaryColor), const SizedBox(width: 12), Text( - '${currentSet.targetWeightTotal} kg', + '${currentSet.targetWeightTotal} ${l10n.unitKg}', style: const TextStyle( color: Colors.white, fontSize: 24, @@ -842,16 +842,16 @@ class _BattleScreenState extends ConsumerState { size: 48, color: AppTheme.successColor), const SizedBox(height: 16), Text( - 'Time Complete!', + l10n.timerComplete, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), - const Text( - 'How many reps did you complete?', - style: TextStyle(color: Colors.grey), + Text( + l10n.amrapResultBody, + style: const TextStyle(color: Colors.grey), ), const SizedBox(height: 32), Row( @@ -894,9 +894,9 @@ class _BattleScreenState extends ConsumerState { backgroundColor: AppTheme.successColor, foregroundColor: Colors.white, ), - child: const Text( - 'CONFIRM', - style: TextStyle( + child: Text( + l10n.commonConfirm, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), @@ -1165,7 +1165,7 @@ class _BattleScreenState extends ConsumerState { AppBar _buildAppBar(AppLocalizations l10n) { return AppBar( - title: Text('Week ${widget.week} - Day ${widget.day}'), + title: Text(l10n.battleWeekDay(widget.week, widget.day)), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => _showAbandonDialog(l10n), @@ -1322,7 +1322,7 @@ class _BattleScreenState extends ConsumerState { ), const SizedBox(height: 4), Text( - '${nextSet.repsTarget} x ${nextSet.targetWeightTotal > 0 ? "${nextSet.targetWeightTotal} kg" : "Bodyweight"}', + '${nextSet.repsTarget} x ${nextSet.targetWeightTotal > 0 ? "${nextSet.targetWeightTotal} ${l10n.unitKg}" : l10n.bodyweight}', style: const TextStyle( color: Colors.white, fontSize: 24, @@ -1394,6 +1394,7 @@ class _BattleScreenState extends ConsumerState { int completedHP, int totalHP, ) { + final l10n = AppLocalizations.of(context)!; final state = ref.watch(battleControllerProvider); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -1435,7 +1436,7 @@ class _BattleScreenState extends ConsumerState { ), ), Text( - '${currentSet.repsTarget} Reps per Round', + l10n.battleRepsPerRound(currentSet.repsTarget), style: const TextStyle(color: Colors.grey), ), ], @@ -1505,7 +1506,7 @@ class _BattleScreenState extends ConsumerState { borderRadius: BorderRadius.circular(8), ), child: Text( - 'WEIGHT: ${currentSet.targetWeightTotal} kg', + l10n.battleWeightKg(currentSet.targetWeightTotal), style: Theme.of(context).textTheme.titleLarge?.copyWith( color: AppTheme.primaryColor, fontWeight: FontWeight.bold, diff --git a/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart b/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart index 2189521..1fc4dba 100644 --- a/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart +++ b/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; import 'package:audioplayers/audioplayers.dart'; @@ -127,6 +128,7 @@ class _EmomTimerWidgetState extends State @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final progress = 1.0 - (_secondsRemaining / widget.intervalSeconds); return Column( @@ -139,7 +141,7 @@ class _EmomTimerWidgetState extends State border: Border.all(color: AppTheme.primaryColor), ), child: Text( - 'ROUND ${widget.currentSet} / ${widget.totalSets}', + l10n.timerRound(widget.currentSet, widget.totalSets), style: const TextStyle( color: AppTheme.primaryColor, fontWeight: FontWeight.bold, @@ -192,7 +194,7 @@ class _EmomTimerWidgetState extends State widget.currentSet == 1 && _secondsRemaining == widget.intervalSeconds) Text( - 'READY?', + l10n.timerReady, style: Theme.of(context) .textTheme .labelLarge @@ -200,7 +202,7 @@ class _EmomTimerWidgetState extends State ) else if (!_isRunning) Text( - 'PAUSED', + l10n.timerPaused, style: Theme.of(context) .textTheme .labelLarge @@ -222,8 +224,8 @@ class _EmomTimerWidgetState extends State icon: const Icon(Icons.play_arrow), label: Text(widget.currentSet == 1 && _secondsRemaining == widget.intervalSeconds - ? 'IGNITE ENGINE' - : 'RESUME'), + ? l10n.timerIgnite + : l10n.timerResume), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.successColor, foregroundColor: Colors.white, @@ -237,7 +239,7 @@ class _EmomTimerWidgetState extends State child: OutlinedButton.icon( onPressed: _pauseTimer, icon: const Icon(Icons.pause), - label: const Text('PAUSE'), + label: Text(l10n.timerPause), style: OutlinedButton.styleFrom( foregroundColor: AppTheme.errorColor, side: const BorderSide(color: AppTheme.errorColor), diff --git a/lib/src/features/workout_runner/presentation/widgets/timer_widget.dart b/lib/src/features/workout_runner/presentation/widgets/timer_widget.dart index 659c561..3242085 100644 --- a/lib/src/features/workout_runner/presentation/widgets/timer_widget.dart +++ b/lib/src/features/workout_runner/presentation/widgets/timer_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'dart:async'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; class TimerWidget extends StatefulWidget { @@ -96,6 +97,7 @@ class _TimerWidgetState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final progress = widget.durationSeconds > 0 ? _secondsRemaining / widget.durationSeconds : 0.0; @@ -129,11 +131,11 @@ class _TimerWidgetState extends State { ), ), if (_isCompleted) - const Padding( - padding: EdgeInsets.only(top: 8), + Padding( + padding: const EdgeInsets.only(top: 8), child: Text( - 'COMPLETE!', - style: TextStyle( + l10n.timerComplete, + style: const TextStyle( color: AppTheme.successColor, fontSize: 16, fontWeight: FontWeight.bold, @@ -153,7 +155,7 @@ class _TimerWidgetState extends State { ElevatedButton.icon( onPressed: _isRunning ? _pause : _start, icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow), - label: Text(_isRunning ? 'PAUSE' : 'START'), + label: Text(_isRunning ? l10n.timerPause : 'START'), style: ElevatedButton.styleFrom( backgroundColor: _isRunning ? Colors.orange : AppTheme.primaryColor, @@ -168,7 +170,7 @@ class _TimerWidgetState extends State { OutlinedButton.icon( onPressed: _reset, icon: const Icon(Icons.refresh), - label: const Text('RESET'), + label: Text(l10n.timerReset), style: OutlinedButton.styleFrom( foregroundColor: AppTheme.primaryColor, side: const BorderSide(color: AppTheme.primaryColor), @@ -182,7 +184,7 @@ class _TimerWidgetState extends State { OutlinedButton.icon( onPressed: _skip, icon: const Icon(Icons.skip_next), - label: const Text('SKIP'), + label: Text(l10n.timerSkip), style: OutlinedButton.styleFrom( foregroundColor: Colors.grey, side: const BorderSide(color: Colors.grey), @@ -196,7 +198,7 @@ class _TimerWidgetState extends State { ElevatedButton.icon( onPressed: _reset, icon: const Icon(Icons.replay), - label: const Text('RESTART'), + label: Text(l10n.timerRestart), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, diff --git a/lib/src/features/workout_runner/presentation/widgets/workout_content_widget.dart b/lib/src/features/workout_runner/presentation/widgets/workout_content_widget.dart index 5a8a84e..fce85d2 100644 --- a/lib/src/features/workout_runner/presentation/widgets/workout_content_widget.dart +++ b/lib/src/features/workout_runner/presentation/widgets/workout_content_widget.dart @@ -82,7 +82,7 @@ class WorkoutContent extends StatelessWidget { children: [ _InfoBox( label: l10n.battleWeight, - value: '${currentSet.targetWeightTotal} kg', + value: '${currentSet.targetWeightTotal} ${l10n.unitKg}', ), _InfoBox( label: l10n.battleReps,