feat: add complete multilanguage support for german and english

This commit is contained in:
Patryk Hegenberg 2026-01-21 15:44:37 +01:00
parent d4be30cf74
commit e5c61447a3
25 changed files with 500 additions and 168 deletions

View file

@ -33,6 +33,7 @@
"registerHaveAccount": "Bereits registriert? ", "registerHaveAccount": "Bereits registriert? ",
"registerLoginButton": "LOGIN", "registerLoginButton": "LOGIN",
"hubCycleComplete": "Zyklus beendet! Beende ihn in den Stats.",
"hubNoActiveCycle": "Kein aktiver Zyklus", "hubNoActiveCycle": "Kein aktiver Zyklus",
"hubCreateCycle": "Neuen Zyklus starten", "hubCreateCycle": "Neuen Zyklus starten",
"hubCycleLabel": "Zyklus", "hubCycleLabel": "Zyklus",
@ -153,6 +154,26 @@
"emomConfirm": "BESTÄTIGEN & BEENDEN", "emomConfirm": "BESTÄTIGEN & BEENDEN",
"emomRepsPerRound": "Wiederholungen pro Runde", "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", "questTabDailies": "TÄGLICH",
"questTabJourney": "REISE", "questTabJourney": "REISE",
"questEmptyDailies": "Keine täglichen Quests.\nKomm morgen wieder!", "questEmptyDailies": "Keine täglichen Quests.\nKomm morgen wieder!",
@ -203,6 +224,32 @@
"passwordsDoNotMatch": "Passwörter stimmen nicht überein", "passwordsDoNotMatch": "Passwörter stimmen nicht überein",
"confirmButton": "BESTÄTIGEN", "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", "guidePullupTitle": "Weighted Pull-Up",
"guidePullupLore": "Den Körper gegen die Schwerkraft zu ziehen, ist der ultimative Beweis für Oberkörperkraft.", "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", "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...", "lobbyStatusActive": "Raid startet...",
"lobbyStatusEntering": "Betrete das Schlachtfeld...", "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.", "connectivityError": "Keine Internetverbindung verfügbar.",
"connectivityMultiplayerError": "Für Multiplayer wird eine Internetverbindung benötigt.", "connectivityMultiplayerError": "Für Multiplayer wird eine Internetverbindung benötigt.",
@ -393,5 +478,60 @@
"errorNotFound": "Daten konnten nicht gefunden werden.", "errorNotFound": "Daten konnten nicht gefunden werden.",
"errorEntryNotUnique": "Dieser Eintrag ist bereits vergeben.", "errorEntryNotUnique": "Dieser Eintrag ist bereits vergeben.",
"errorAuthenticationFailed": "E-Mail oder Passwort falsch.", "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."
} }

View file

@ -33,6 +33,7 @@
"registerHaveAccount": "Already have an account? ", "registerHaveAccount": "Already have an account? ",
"registerLoginButton": "LOGIN", "registerLoginButton": "LOGIN",
"hubCycleComplete": "Cycle complete! Finish it in stats.",
"hubNoActiveCycle": "No active cycle", "hubNoActiveCycle": "No active cycle",
"hubCreateCycle": "Create New Cycle", "hubCreateCycle": "Create New Cycle",
"hubCycleLabel": "Cycle", "hubCycleLabel": "Cycle",
@ -153,6 +154,26 @@
"emomConfirm": "CONFIRM & FINISH", "emomConfirm": "CONFIRM & FINISH",
"emomRepsPerRound": "Reps per Round", "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", "questTabDailies": "DAILIES",
"questTabJourney": "JOURNEY", "questTabJourney": "JOURNEY",
"questEmptyDailies": "No daily quests available.\nCome back tomorrow!", "questEmptyDailies": "No daily quests available.\nCome back tomorrow!",
@ -203,6 +224,32 @@
"passwordsDoNotMatch": "Passwords do not match", "passwordsDoNotMatch": "Passwords do not match",
"confirmButton": "CONFIRM", "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", "guidePullupTitle": "Weighted Pull-Up",
"guidePullupLore": "Pulling your body against gravity is the ultimate proof of upper body strength.", "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", "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...", "lobbyStatusActive": "Raid is starting...",
"lobbyStatusEntering": "Entering Battle...", "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.", "connectivityError": "No internet connection available.",
"connectivityMultiplayerError": "Active internet connection required for multiplayer.", "connectivityMultiplayerError": "Active internet connection required for multiplayer.",
@ -407,5 +492,60 @@
"errorNotFound": "Data not found.", "errorNotFound": "Data not found.",
"errorEntryNotUnique": "Entry already exists.", "errorEntryNotUnique": "Entry already exists.",
"errorAuthenticationFailed": "E-Mail or Passwort wrong.", "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."
} }

View file

@ -57,26 +57,27 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
final l10n = AppLocalizations.of(context)!;
setState(() { setState(() {
_isLoading = false; _isLoading = false;
_errorMessage = _parseErrorMessage(e.toString()); _errorMessage = _parseErrorMessage(e.toString(), l10n);
}); });
ErrorHandler.showErrorSnackBar(context, e); ErrorHandler.showErrorSnackBar(context, e);
} }
} }
} }
String _parseErrorMessage(String error) { String _parseErrorMessage(String error, AppLocalizations l10n) {
if (error.contains('400')) { if (error.contains('400')) {
return 'Invalid email or password'; return l10n.loginErrorInvalid;
} else if (error.contains('SocketException') || } else if (error.contains('SocketException') ||
error.contains('Connection refused') || error.contains('Connection refused') ||
error.contains('Network is unreachable')) { error.contains('Network is unreachable')) {
return 'Could not connect to server.\nPlease check your internet connection.'; return l10n.loginErrorConnection;
} else if (error.contains('timeout')) { } else if (error.contains('timeout')) {
return 'Connection timeout.\nPlease try again.'; return l10n.loginErrorTimeout;
} }
return 'Login failed. Please try again.'; return l10n.loginErrorGeneric;
} }
@override @override

View file

@ -57,7 +57,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
if (mounted) { if (mounted) {
setState(() => _hasChanges = false); setState(() => _hasChanges = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bodyweight updated')), SnackBar(content: Text(AppLocalizations.of(context)!.profileBodyweightUpdated)),
); );
} }
} finally { } finally {
@ -66,6 +66,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
} }
void _showChangePasswordDialog() { void _showChangePasswordDialog() {
final l10n = AppLocalizations.of(context)!;
final oldPassCtrl = TextEditingController(); final oldPassCtrl = TextEditingController();
final newPassCtrl = TextEditingController(); final newPassCtrl = TextEditingController();
final confirmPassCtrl = TextEditingController(); final confirmPassCtrl = TextEditingController();
@ -74,7 +75,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Change Password'), title: Text(l10n.profileChangePassword),
content: Form( content: Form(
key: formKey, key: formKey,
child: Column( child: Column(
@ -83,22 +84,22 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
TextFormField( TextFormField(
controller: oldPassCtrl, controller: oldPassCtrl,
obscureText: true, obscureText: true,
decoration: const InputDecoration(labelText: 'Old Password'), decoration: InputDecoration(labelText: l10n.profileOldPassword),
validator: (v) => v?.isEmpty == true ? 'Required' : null, validator: (v) => v?.isEmpty == true ? l10n.commonRequired : null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: newPassCtrl, controller: newPassCtrl,
obscureText: true, obscureText: true,
decoration: const InputDecoration(labelText: 'New Password'), decoration: InputDecoration(labelText: l10n.profileNewPassword),
validator: (v) => (v?.length ?? 0) < 8 ? 'Min 8 chars' : null, validator: (v) => (v?.length ?? 0) < 8 ? l10n.profilePassMinChars : null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: confirmPassCtrl, controller: confirmPassCtrl,
obscureText: true, obscureText: true,
decoration: const InputDecoration(labelText: 'Confirm New'), decoration: InputDecoration(labelText: l10n.profileConfirmNewPassword),
validator: (v) => v != newPassCtrl.text ? 'Mismatch' : null, validator: (v) => v != newPassCtrl.text ? l10n.profilePassMismatch : null,
), ),
], ],
), ),
@ -106,7 +107,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('CANCEL'), child: Text(l10n.commonCancel),
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
@ -120,8 +121,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
); );
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Password changed successfully')), content: Text(l10n.profilePassChangedSuccess)),
); );
} }
} catch (e) { } catch (e) {
@ -137,7 +138,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
} }
} }
}, },
child: const Text('UPDATE'), child: Text(l10n.commonUpdate),
), ),
], ],
), ),
@ -149,6 +150,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
final currentConfig = _user?.avatarConfig != null final currentConfig = _user?.avatarConfig != null
? AvatarConfig.fromJson(_user!.avatarConfig!) ? AvatarConfig.fromJson(_user!.avatarConfig!)
: const AvatarConfig(); : const AvatarConfig();
final l10n = AppLocalizations.of(context)!;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -158,7 +160,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Text('Select Scenery', child: Text(l10n.profileSelectScenery,
style: Theme.of(context).textTheme.titleLarge), style: Theme.of(context).textTheme.titleLarge),
), ),
SizedBox( SizedBox(
@ -225,7 +227,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
const Icon(Icons.lock, color: Colors.white54), const Icon(Icons.lock, color: Colors.white54),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Lvl ${item.unlockLevel}', '${l10n.commonLevel} ${item.unlockLevel}',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -279,6 +281,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
void _confirmDangerAction( void _confirmDangerAction(
String title, String content, VoidCallback onConfirm) { String title, String content, VoidCallback onConfirm) {
final l10n = AppLocalizations.of(context)!;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@ -287,7 +290,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('CANCEL'), child: Text(l10n.commonCancel),
), ),
ElevatedButton( ElevatedButton(
style: style:
@ -296,7 +299,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
Navigator.pop(context); Navigator.pop(context);
onConfirm(); onConfirm();
}, },
child: const Text('CONFIRM'), child: Text(l10n.commonConfirm),
), ),
], ],
), ),
@ -307,6 +310,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
final currentConfig = _user?.avatarConfig != null final currentConfig = _user?.avatarConfig != null
? AvatarConfig.fromJson(_user!.avatarConfig!) ? AvatarConfig.fromJson(_user!.avatarConfig!)
: const AvatarConfig(); : const AvatarConfig();
final l10n = AppLocalizations.of(context)!;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -315,7 +319,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
backgroundColor: AppTheme.backgroundColor, backgroundColor: AppTheme.backgroundColor,
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Edit Appearance'), title: Text(l10n.profileEditAppearance),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
@ -325,7 +329,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
onPressed: () { onPressed: () {
Navigator.pop(context, _tempAvatarConfig); Navigator.pop(context, _tempAvatarConfig);
}, },
child: const Text('SAVE', child: Text(l10n.commonSave,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppTheme.primaryColor)), color: AppTheme.primaryColor)),
@ -409,7 +413,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Edit Profile'), title: Text(l10n.profileEditTitle),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'), onPressed: () => context.go('/hub'),
@ -418,7 +422,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
if (_hasChanges) if (_hasChanges)
TextButton( TextButton(
onPressed: _isLoading ? null : _saveBodyweight, onPressed: _isLoading ? null : _saveBodyweight,
child: const Text('SAVE', child: Text(l10n.commonSave,
style: TextStyle( style: TextStyle(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
@ -458,11 +462,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _showBackgroundSelector, onPressed: _showBackgroundSelector,
icon: const Icon(Icons.landscape), icon: const Icon(Icons.landscape),
label: const Text('CHANGE SCENERY'), label: Text(l10n.profileSelectScenery.toUpperCase()),
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
Text('Physical Stats', Text(l10n.profilePhysicalStats,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleLarge .titleLarge
@ -474,7 +478,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Current Bodyweight', Text(l10n.profileCurrentBodyweight,
style: Theme.of(context).textTheme.bodyMedium), style: Theme.of(context).textTheme.bodyMedium),
Row( Row(
children: [ children: [
@ -509,7 +513,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
Text('Training Focus', Text(l10n.profileTrainingFocus,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleLarge .titleLarge
@ -521,7 +525,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Accessory Template', Text(l10n.profileAccessoryTemplate,
style: Theme.of(context).textTheme.bodyMedium), style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildTemplateSelector(), _buildTemplateSelector(),
@ -529,7 +533,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
), ),
), ),
Text('Account Security', Text(l10n.profileAccountSecurity,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleLarge .titleLarge
@ -537,7 +541,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
ListTile( ListTile(
leading: const Icon(Icons.lock_outline), leading: const Icon(Icons.lock_outline),
title: const Text('Change Password'), title: Text(l10n.profileChangePassword),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: _showChangePasswordDialog, onTap: _showChangePasswordDialog,
), ),
@ -549,7 +553,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
const Divider(), const Divider(),
const SizedBox(height: 24), const SizedBox(height: 24),
Text('Danger Zone', Text(l10n.profileDangerZone,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleLarge .titleLarge
@ -567,13 +571,13 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
ListTile( ListTile(
leading: const Icon(Icons.refresh, leading: const Icon(Icons.refresh,
color: AppTheme.errorColor), color: AppTheme.errorColor),
title: const Text('Reset Progress', title: Text(l10n.profileResetProgress,
style: TextStyle(color: AppTheme.errorColor)), style: TextStyle(color: AppTheme.errorColor)),
subtitle: const Text( subtitle: Text(
'Resets Level, XP and Training History'), l10n.profileResetProgressSubtitle),
onTap: () => _confirmDangerAction( onTap: () => _confirmDangerAction(
'Reset Progress?', l10n.profileResetConfirmTitle,
'This will delete all your workouts and reset your Level to 1. This cannot be undone.', l10n.profileResetConfirmBody,
() async { () async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
await userRepo.resetProgress(); await userRepo.resetProgress();
@ -588,13 +592,13 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
ListTile( ListTile(
leading: const Icon(Icons.delete_forever, leading: const Icon(Icons.delete_forever,
color: AppTheme.errorColor), color: AppTheme.errorColor),
title: const Text('Delete Account', title: Text(l10n.profileDeleteAccount,
style: TextStyle(color: AppTheme.errorColor)), style: TextStyle(color: AppTheme.errorColor)),
subtitle: const Text( subtitle: Text(
'Permanently delete your account and data'), l10n.profileDeleteAccountSubtitle),
onTap: () => _confirmDangerAction( onTap: () => _confirmDangerAction(
'Delete Account?', l10n.profileDeleteConfirmTitle,
'Are you sure you want to delete your account? All data will be lost forever.', l10n.profileDeleteConfirmBody,
() async { () async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
@ -621,7 +625,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
if (mounted) context.go('/login'); if (mounted) context.go('/login');
}, },
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),
label: const Text('LOGOUT'), label: Text(l10n.commonLogout),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
), ),
@ -637,6 +641,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
Widget _buildTemplateSelector() { Widget _buildTemplateSelector() {
final current = _getTemplateFromSettings(_user?.inventorySettings ?? {}); final current = _getTemplateFromSettings(_user?.inventorySettings ?? {});
final l10n = AppLocalizations.of(context)!;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -644,31 +649,31 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
_RadioTile<AccessoryTemplate>( _RadioTile<AccessoryTemplate>(
value: AccessoryTemplate.none, value: AccessoryTemplate.none,
groupValue: current, groupValue: current,
title: 'Strength Only', title: l10n.templateStrengthOnly,
subtitle: 'Main Lifts + FSL. Pure & Fast.', subtitle: l10n.templateStrengthOnlyDesc,
onChanged: (val) => _updateTemplate(val!), onChanged: (val) => _updateTemplate(val!),
), ),
const Divider(height: 1), const Divider(height: 1),
_RadioTile<AccessoryTemplate>( _RadioTile<AccessoryTemplate>(
value: AccessoryTemplate.hypertrophy, value: AccessoryTemplate.hypertrophy,
groupValue: current, groupValue: current,
title: 'Hypertrophy Support', title: l10n.templateHypertrophy,
subtitle: 'Bodybuilding accessories to build muscle armor.', subtitle: l10n.templateHypertrophyDesc,
onChanged: (val) => _updateTemplate(val!), onChanged: (val) => _updateTemplate(val!),
), ),
const Divider(height: 1), const Divider(height: 1),
_RadioTile<AccessoryTemplate>( _RadioTile<AccessoryTemplate>(
value: AccessoryTemplate.conditioning, value: AccessoryTemplate.conditioning,
groupValue: current, groupValue: current,
title: 'The Engine (Conditioning)', title: l10n.templateConditioning,
subtitle: '15 min Kettlebell intervals to boost stamina.', subtitle: l10n.templateConditioningDesc,
onChanged: (val) => _updateTemplate(val!), onChanged: (val) => _updateTemplate(val!),
), ),
const Padding( Padding(
padding: EdgeInsets.fromLTRB(16, 24, 16, 8), padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text( child: Text(
'ACTIVE JOURNEYS', l10n.templateActiveJourneys,
style: TextStyle( style: const TextStyle(
color: Colors.grey, color: Colors.grey,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -678,8 +683,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
_RadioTile<AccessoryTemplate>( _RadioTile<AccessoryTemplate>(
value: AccessoryTemplate.journeyPullup, value: AccessoryTemplate.journeyPullup,
groupValue: current, groupValue: current,
title: 'Quest: The First Pull-Up', title: l10n.templatePullupJourney,
subtitle: 'Specific progression to master your bodyweight.', subtitle: l10n.templatePullupJourneyDesc,
onChanged: (val) => _updateTemplate(val!), onChanged: (val) => _updateTemplate(val!),
), ),
], ],

View file

@ -108,8 +108,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
if (!found) { if (!found) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Cycle complete! Finish it in stats.')), content: Text(AppLocalizations.of(context)!.hubCycleComplete)),
); );
} }
return; return;
@ -224,8 +224,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (sets >= 20) if (sets >= 20)
const Text('⚠️ HARDCORE MODE', Text(l10n.missionBriefingHardcore,
style: TextStyle( style: const TextStyle(
color: AppTheme.errorColor, color: AppTheme.errorColor,
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
class StartRaidButton extends StatefulWidget { class StartRaidButton extends StatefulWidget {
@ -82,7 +83,7 @@ class _StartRaidButtonState extends State<StartRaidButton>
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
'START RAID', AppLocalizations.of(context)!.lobbyStartRaid,
style: Theme.of(context).textTheme.labelLarge?.copyWith( style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontSize: 20, fontSize: 20,
color: Colors.black, color: Colors.black,

View file

@ -19,7 +19,7 @@ class QuestLogScreen extends ConsumerWidget {
length: 2, length: 2,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Quest Log'), title: Text(l10n.historyTitle),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'), onPressed: () => context.go('/hub'),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../domain/entities/avatar_config.dart'; import '../../domain/entities/avatar_config.dart';
import 'avatar_renderer.dart'; import 'avatar_renderer.dart';
@ -38,6 +39,7 @@ class _AvatarEditorState extends State<AvatarEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column( return Column(
children: [ children: [
Container( Container(
@ -52,9 +54,9 @@ class _AvatarEditorState extends State<AvatarEditor> {
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: SegmentedButton<String>( child: SegmentedButton<String>(
segments: const [ segments: [
ButtonSegment(value: 'male', label: Text('Male')), ButtonSegment(value: 'male', label: Text(l10n.genderMale)),
ButtonSegment(value: 'female', label: Text('Female')), ButtonSegment(value: 'female', label: Text(l10n.genderFemale)),
], ],
selected: {_gender}, selected: {_gender},
onSelectionChanged: (Set<String> newSelection) { onSelectionChanged: (Set<String> newSelection) {

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../gamification/data/repositories/quest_repository.dart'; import '../../../gamification/data/repositories/quest_repository.dart';
import '../../../../shared/data/local/app_database.dart'; import '../../../../shared/data/local/app_database.dart';
@ -11,6 +12,7 @@ class QuestBoardWidget extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final questRepo = ref.watch(questRepositoryProvider); final questRepo = ref.watch(questRepositoryProvider);
final l10n = AppLocalizations.of(context)!;
return StreamBuilder<List<QuestCollection>>( return StreamBuilder<List<QuestCollection>>(
stream: questRepo.watchQuests(), stream: questRepo.watchQuests(),
@ -39,7 +41,7 @@ class QuestBoardWidget extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'DAILY BOUNTIES', l10n.questDailyBounties,
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: AppTheme.secondaryColor, color: AppTheme.secondaryColor,
letterSpacing: 1.5, letterSpacing: 1.5,
@ -47,8 +49,9 @@ class QuestBoardWidget extends ConsumerWidget {
), ),
GestureDetector( GestureDetector(
onTap: () => context.go('/quests'), onTap: () => context.go('/quests'),
child: const Text('VIEW ALL >', child: Text(l10n.questViewAll,
style: TextStyle(fontSize: 10, color: Colors.grey)), style:
const TextStyle(fontSize: 10, color: Colors.grey)),
), ),
], ],
), ),

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/local/app_database.dart'; import '../../../../shared/data/local/app_database.dart';
import '../../data/repositories/quest_repository.dart'; import '../../data/repositories/quest_repository.dart';
@ -26,13 +27,14 @@ class _QuestItemState extends ConsumerState<QuestItem> {
await questRepo.claimQuest(widget.quest.id); await questRepo.claimQuest(widget.quest.id);
if (mounted) { if (mounted) {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Row( content: Row(
children: [ children: [
const Icon(Icons.check_circle, color: AppTheme.successColor), const Icon(Icons.check_circle, color: AppTheme.successColor),
const SizedBox(width: 8), const SizedBox(width: 8),
Text('Reward collected: ${widget.quest.rewardXP} XP!'), Text(l10n.questRewardCollected(widget.quest.rewardXP)),
], ],
), ),
backgroundColor: Colors.black87, backgroundColor: Colors.black87,
@ -164,7 +166,8 @@ class _QuestItemState extends ConsumerState<QuestItem> {
height: 12, height: 12,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white)) 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) else if (widget.quest.rewardItem != null && !isClaimed)
const Icon(Icons.inventory_2, const Icon(Icons.inventory_2,

View file

@ -214,8 +214,27 @@ class _ExerciseDetailRow extends StatelessWidget {
const _ExerciseDetailRow({required this.exercise}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column( child: Column(
@ -230,7 +249,7 @@ class _ExerciseDetailRow extends StatelessWidget {
margin: const EdgeInsets.only(right: 8), margin: const EdgeInsets.only(right: 8),
), ),
Text( Text(
exercise.exerciseName, _getLocalizedName(context, exercise.exerciseName),
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, color: AppTheme.textSecondary), fontWeight: FontWeight.bold, color: AppTheme.textSecondary),
), ),
@ -256,7 +275,7 @@ class _ExerciseDetailRow extends StatelessWidget {
), ),
), ),
Text( Text(
'${set.targetWeightTotal} kg', '${set.targetWeightTotal} ${l10n.unitKg}',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
Row( Row(

View file

@ -167,7 +167,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Error saving: $e'), content: Text(l10n.inventorySaveError(e.toString())),
backgroundColor: AppTheme.errorColor), backgroundColor: AppTheme.errorColor),
); );
} }
@ -238,7 +238,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
min: 10, min: 10,
max: 25, max: 25,
divisions: 6, divisions: 6,
label: '$_barWeight kg', label: '$_barWeight ${l10n.unitKg}',
activeColor: AppTheme.primaryColor, activeColor: AppTheme.primaryColor,
onChanged: (val) => setState(() { onChanged: (val) => setState(() {
_barWeight = val; _barWeight = val;
@ -246,7 +246,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
}), }),
), ),
), ),
Text('${_barWeight.toStringAsFixed(1)} kg', Text('${_barWeight.toStringAsFixed(1)} ${l10n.unitKg}',
style: style:
const TextStyle(fontWeight: FontWeight.bold)), const TextStyle(fontWeight: FontWeight.bold)),
], ],
@ -398,6 +398,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
} }
Widget _buildBandChip(String color, int resistance, bool isSelected) { Widget _buildBandChip(String color, int resistance, bool isSelected) {
final l10n = AppLocalizations.of(context)!;
return InkWell( return InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
@ -430,7 +431,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
if (isSelected) const SizedBox(width: 4), if (isSelected) const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
'$color (~${resistance}kg)', '$color (~$resistance${l10n.unitKg})',
style: TextStyle( style: TextStyle(
color: isSelected color: isSelected
? _getBandColor(color) ? _getBandColor(color)

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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 'package:slrpg_app/src/features/multiplayer/domain/entities/leaderboard_entry.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/data/repositories/user_repository.dart';
@ -20,10 +21,11 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final leaderboardAsync = ref.watch(leaderboardProvider); final leaderboardAsync = ref.watch(leaderboardProvider);
final currentUserAsync = ref.watch(userRepositoryProvider).getLocalUser(); final currentUserAsync = ref.watch(userRepositoryProvider).getLocalUser();
final l10n = AppLocalizations.of(context)!;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('HALL OF FAME'), title: Text(l10n.leaderboardTitle),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'), onPressed: () => context.go('/hub'),
@ -54,7 +56,7 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
leading: _buildRankBadge(entry.rank), leading: _buildRankBadge(entry.rank),
title: Text( title: Text(
entry.name.isEmpty entry.name.isEmpty
? 'Hero #${entry.id.substring(0, 5)}' ? l10n.leaderboardHero(entry.id.substring(0, 5))
: entry.name, : entry.name,
style: TextStyle( style: TextStyle(
fontWeight: fontWeight:
@ -62,7 +64,8 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
color: isMe ? AppTheme.primaryColor : Colors.white, 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), trailing: _buildAvatarPreview(entry),
), ),
); );
@ -71,7 +74,8 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
}); });
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')), error: (err, stack) =>
Center(child: Text(l10n.setupFailed(err.toString()))),
), ),
); );
} }

View file

@ -71,7 +71,7 @@ class _LobbyScreenState extends ConsumerState<LobbyScreen> {
extra: {'week': next['week'], 'day': next['day']}); 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)); return Center(child: Text(l10n.lobbyStatusActive));
}, },
@ -243,6 +243,7 @@ class _MemberCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card( return Card(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
child: ListTile( child: ListTile(
@ -253,7 +254,7 @@ class _MemberCard extends StatelessWidget {
? AvatarRenderer(config: member.avatar!, size: 40) ? AvatarRenderer(config: member.avatar!, size: 40)
: const CircleAvatar(child: Icon(Icons.person)), : const CircleAvatar(child: Icon(Icons.person)),
), ),
title: Text(member.username ?? 'Unknown'), title: Text(member.username ?? l10n.unknownMember),
trailing: member.isReady trailing: member.isReady
? const Icon(Icons.check_circle, color: Colors.green) ? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.circle_outlined, color: Colors.grey), : const Icon(Icons.circle_outlined, color: Colors.grey),

View file

@ -112,9 +112,10 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
if (mounted) { if (mounted) {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Setup failed: $e'), content: Text(l10n.setupFailed(e.toString())),
backgroundColor: AppTheme.errorColor, backgroundColor: AppTheme.errorColor,
duration: const Duration(seconds: 5), duration: const Duration(seconds: 5),
), ),
@ -196,7 +197,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
prefixIcon: Icon(Icons.lock), prefixIcon: Icon(Icons.lock),
), ),
validator: (v) => validator: (v) =>
(v?.length ?? 0) < 8 ? 'Min 8 characters' : null, (v?.length ?? 0) < 8 ? l10n.profilePassMinChars : null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(

View file

@ -133,7 +133,7 @@ class _BodyweightInputScreenState extends ConsumerState<BodyweightInputScreen> {
), ),
), ),
Text( Text(
_useKg ? 'kg' : 'lbs', _useKg ? l10n.unitKg : l10n.unitLbs,
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
), ),
], ],

View file

@ -179,10 +179,11 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
log('Stack trace: $stackTrace'); log('Stack trace: $stackTrace');
if (mounted) { 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') || if (e.toString().toLowerCase().contains('unique') ||
e.toString().toLowerCase().contains('email')) { e.toString().toLowerCase().contains('email')) {
message = 'Email already exists. Please login or use another email.'; message = l10n.setupEmailExists;
} }
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -261,14 +262,14 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
min: 10, min: 10,
max: 25, max: 25,
divisions: 3, divisions: 3,
label: '${_barWeight.toStringAsFixed(0)} kg', label: '${_barWeight.toStringAsFixed(0)} ${l10n.unitKg}',
activeColor: AppTheme.primaryColor, activeColor: AppTheme.primaryColor,
onChanged: (value) { onChanged: (value) {
setState(() => _barWeight = value); setState(() => _barWeight = value);
}, },
), ),
Text( Text(
'${_barWeight.toStringAsFixed(0)} kg', '${_barWeight.toStringAsFixed(0)} ${l10n.unitKg}',
style: Theme.of(context).textTheme.headlineMedium?.copyWith( style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
@ -459,7 +460,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
if (isSelected) const SizedBox(width: 4), if (isSelected) const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
'$color (~${resistance}kg)', '$color (~$resistance${AppLocalizations.of(context)!.unitKg})',
style: TextStyle( style: TextStyle(
color: isSelected ? Colors.white : AppTheme.textSecondary, color: isSelected ? Colors.white : AppTheme.textSecondary,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,

View file

@ -219,7 +219,7 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
const SizedBox(height: 32), const SizedBox(height: 32),
_ExerciseCard( _ExerciseCard(
title: l10n.strengthLegs, title: l10n.strengthLegs,
exerciseName: 'Back Squat', exerciseName: l10n.exerciseSquat,
icon: Icons.accessibility_new, icon: Icons.accessibility_new,
weightController: _squatWeightController, weightController: _squatWeightController,
repsController: _squatRepsController, repsController: _squatRepsController,
@ -231,8 +231,8 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
_AdaptiveExerciseCard( _AdaptiveExerciseCard(
slotTitle: l10n.strengthPull, slotTitle: l10n.strengthPull,
primaryName: 'Weighted Pull-up', primaryName: l10n.exercisePullup,
secondaryName: 'Pendlay Row', secondaryName: l10n.exerciseRow,
icon: Icons.north, icon: Icons.north,
isCapable: _canDoPullup, isCapable: _canDoPullup,
onToggleCapability: (val) { onToggleCapability: (val) {
@ -254,12 +254,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
repsController: _pullRepsController, repsController: _pullRepsController,
weightLabel: _canDoPullup weightLabel: _canDoPullup
? (_isAssistedPull ? (_isAssistedPull
? 'Band Assistance (kg)' ? l10n.bandAssistanceLabel
: 'Added Weight (kg)') : l10n.addWeightLabel)
: 'Row Weight (kg)', : l10n.rowWeightLabel,
// weightLabel: repsLabel: _canDoPullup ? l10n.repsLabel : l10n.reps5rmLabel,
// _canDoPullup ? 'Add. Weight (kg)' : 'Row Weight (kg)',
repsLabel: _canDoPullup ? 'Reps' : '5RM Reps (usually 5)',
showResults: true, showResults: true,
result1RM: _calculated1RMs['pullup'] ?? 0, result1RM: _calculated1RMs['pullup'] ?? 0,
resultTM: _calculatedTMs['pullup'] ?? 0, resultTM: _calculatedTMs['pullup'] ?? 0,
@ -268,8 +266,8 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
_AdaptiveExerciseCard( _AdaptiveExerciseCard(
slotTitle: l10n.strengthPush, slotTitle: l10n.strengthPush,
primaryName: 'Weighted Dip', primaryName: l10n.exerciseDip,
secondaryName: 'Bench Press', secondaryName: l10n.exerciseBench,
icon: Icons.south, icon: Icons.south,
isCapable: _canDoDip, isCapable: _canDoDip,
onToggleCapability: (val) { onToggleCapability: (val) {
@ -292,11 +290,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
repsController: _pushRepsController, repsController: _pushRepsController,
weightLabel: _canDoDip weightLabel: _canDoDip
? (_isAssistedDip ? (_isAssistedDip
? 'Band Assistance (kg)' ? l10n.bandAssistanceLabel
: 'Added Weight (kg)') : l10n.addWeightLabel)
: 'Weight (kg)', : l10n.weightLabel,
// weightLabel: _canDoDip ? 'Add. Weight (kg)' : 'Weight (kg)', repsLabel: l10n.repsLabel,
repsLabel: 'Reps',
showWeightInput: true, showWeightInput: true,
showResults: true, showResults: true,
result1RM: _calculated1RMs['dip'] ?? 0, result1RM: _calculated1RMs['dip'] ?? 0,
@ -416,7 +413,7 @@ class _ExerciseCard extends StatelessWidget {
: l10n.weightLabel, : l10n.weightLabel,
isDense: true), isDense: true),
onChanged: (_) => onChanged(), onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null, validator: (v) => v!.isEmpty ? l10n.commonRequired : null,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -428,7 +425,7 @@ class _ExerciseCard extends StatelessWidget {
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.repsLabel, isDense: true), labelText: l10n.repsLabel, isDense: true),
onChanged: (_) => onChanged(), 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) ...[ if (!isCapable) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Adjusted: ${"Wendler 5/3/1"}', l10n.adjustedWendler,
style: const TextStyle( style: const TextStyle(
color: AppTheme.secondaryColor, color: AppTheme.secondaryColor,
fontSize: 12, fontSize: 12,
@ -578,7 +575,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
decoration: InputDecoration( decoration: InputDecoration(
labelText: weightLabel, isDense: true), labelText: weightLabel, isDense: true),
onChanged: (_) => onChanged(), onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null, validator: (v) => v!.isEmpty ? l10n.commonRequired : null,
), ),
) )
else else
@ -592,7 +589,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
decoration: decoration:
InputDecoration(labelText: repsLabel, isDense: true), InputDecoration(labelText: repsLabel, isDense: true),
onChanged: (_) => onChanged(), 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, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(l10n.est1rm), Text(l10n.est1rm),
Text('${rm.toStringAsFixed(1)} kg', Text('${rm.toStringAsFixed(1)} ${l10n.unitKg}',
style: Theme.of(context).textTheme.bodyLarge), style: Theme.of(context).textTheme.bodyLarge),
], ],
), ),
@ -635,7 +632,7 @@ class _ResultBox extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(l10n.trainingMaxLabel), Text(l10n.trainingMaxLabel),
Text('${tm.toStringAsFixed(1)} kg', Text('${tm.toStringAsFixed(1)} ${l10n.unitKg}',
style: const TextStyle( style: const TextStyle(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),

View file

@ -176,8 +176,8 @@ class _PrivacyPolicyScreenState extends ConsumerState<PrivacyPolicyScreen> {
await Share.shareXFiles( await Share.shareXFiles(
[XFile(file.path)], [XFile(file.path)],
subject: 'SLRPG Data Export', subject: l10n.exportEmailSubject,
text: 'Your SLRPG training data (GDPR export)', text: l10n.exportEmailBody,
); );
if (mounted) { if (mounted) {
@ -207,7 +207,7 @@ class _PrivacyPolicyScreenState extends ConsumerState<PrivacyPolicyScreen> {
Future<void> _showDeleteAccountDialog() async { Future<void> _showDeleteAccountDialog() async {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final confirmationController = TextEditingController(); final confirmationController = TextEditingController();
final expectedText = l10n.localeName == 'de' ? 'LÖSCHEN' : 'DELETE'; final expectedText = l10n.deleteConfirmationWord;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,

View file

@ -208,15 +208,15 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
String getLabel(String id) { String getLabel(String id) {
switch (id) { switch (id) {
case 'squat': case 'squat':
return 'Squat'; return l10n.exerciseSquat;
case 'pullup': case 'pullup':
return 'Pull-up'; return l10n.exercisePullup;
case 'row': case 'row':
return 'Row'; return l10n.exerciseRow;
case 'dip': case 'dip':
return 'Dip'; return l10n.exerciseDip;
case 'bench': case 'bench':
return 'Bench'; return l10n.exerciseBench;
default: default:
return id; return id;
} }
@ -358,13 +358,13 @@ class _CurrentCycleCard extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
_StatRow( _StatRow(
label: l10n.exerciseSquat, label: l10n.exerciseSquat,
value: '${tms['squat'].toStringAsFixed(2)} kg'), value: '${tms['squat'].toStringAsFixed(2)} ${l10n.unitKg}'),
_StatRow( _StatRow(
label: getLabel(pullVariant), label: getLabel(pullVariant),
value: '${tms['pullup'].toStringAsFixed(2)} kg'), value: '${tms['pullup'].toStringAsFixed(2)} ${l10n.unitKg}'),
_StatRow( _StatRow(
label: getLabel(pushVariant), label: getLabel(pushVariant),
value: '${tms['dip'].toStringAsFixed(2)} kg'), value: '${tms['dip'].toStringAsFixed(2)} ${l10n.unitKg}'),
const SizedBox(height: 32), const SizedBox(height: 32),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
@ -471,6 +471,7 @@ class _DiffRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final diff = newVal - oldVal; final diff = newVal - oldVal;
final isPositive = diff > 0; final isPositive = diff > 0;
@ -491,8 +492,9 @@ class _DiffRow extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: AppTheme.successColor, fontWeight: FontWeight.bold)) color: AppTheme.successColor, fontWeight: FontWeight.bold))
else else
const Text('STALLED', Text(l10n.statsStalled,
style: TextStyle(color: AppTheme.secondaryColor, fontSize: 12)), style: const TextStyle(
color: AppTheme.secondaryColor, fontSize: 12)),
], ],
), ),
); );

View file

@ -20,8 +20,8 @@ class ExerciseGuideSheet extends StatelessWidget {
if (guide == null) { if (guide == null) {
return Container( return Container(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: const Text('No ancient scroll found for this technique.', child: Text(l10n.guideNotFound,
textAlign: TextAlign.center, style: TextStyle(color: Colors.grey)), textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey)),
); );
} }
@ -61,7 +61,7 @@ class ExerciseGuideSheet extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildDifficultyBadge(guide.difficulty), _buildDifficultyBadge(context, guide.difficulty),
const SizedBox(height: 24), const SizedBox(height: 24),
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -80,18 +80,18 @@ class ExerciseGuideSheet extends StatelessWidget {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
_buildSectionTitle(context, 'EXECUTION'), _buildSectionTitle(context, l10n.guideExecution),
const SizedBox(height: 16), const SizedBox(height: 16),
...guide.steps ...guide.steps
.asMap() .asMap()
.entries .entries
.map((entry) => _buildStep(entry.key + 1, entry.value)), .map((entry) => _buildStep(entry.key + 1, entry.value)),
const SizedBox(height: 32), const SizedBox(height: 32),
_buildSectionTitle(context, 'COMMON MISTAKES'), _buildSectionTitle(context, l10n.guideMistakes),
const SizedBox(height: 16), const SizedBox(height: 16),
...guide.commonMistakes.map((m) => _buildMistake(m)), ...guide.commonMistakes.map((m) => _buildMistake(m)),
const SizedBox(height: 32), const SizedBox(height: 32),
_buildSectionTitle(context, 'ATTRIBUTES AFFECTED'), _buildSectionTitle(context, l10n.guideAttributes),
const SizedBox(height: 16), const SizedBox(height: 16),
Wrap( Wrap(
spacing: 8, 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; Color color;
String text = diff;
switch (diff) { switch (diff) {
case 'Novice': case 'Novice':
color = Colors.green; color = Colors.green;
text = l10n.guideDiffNovice;
break; break;
case 'Adept': case 'Adept':
color = Colors.orange; color = Colors.orange;
text = l10n.guideDiffAdept;
break; break;
case 'Master': case 'Master':
color = AppTheme.errorColor; color = AppTheme.errorColor;
text = l10n.guideDiffMaster;
break; break;
default: default:
color = Colors.grey; color = Colors.grey;
@ -143,7 +149,7 @@ class ExerciseGuideSheet extends StatelessWidget {
border: Border.all(color: color.withValues(alpha: 0.5)), border: Border.all(color: color.withValues(alpha: 0.5)),
), ),
child: Text( child: Text(
diff.toUpperCase(), text.toUpperCase(),
style: TextStyle( style: TextStyle(
color: color, fontSize: 12, fontWeight: FontWeight.bold), color: color, fontSize: 12, fontWeight: FontWeight.bold),
), ),

View file

@ -502,15 +502,15 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
if (state.error != null) { if (state.error != null) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Battle')), appBar: AppBar(title: Text(l10n.battleTitle)),
body: Center(child: Text('Error: ${state.error}')), body: Center(child: Text(l10n.setupFailed(state.error.toString()))),
); );
} }
if (state.exercises.isEmpty) { if (state.exercises.isEmpty) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Battle')), appBar: AppBar(title: Text(l10n.battleTitle)),
body: const Center(child: Text('No exercises configured')), body: Center(child: Text(l10n.battleNoExercises)),
); );
} }
@ -737,7 +737,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
color: AppTheme.primaryColor), color: AppTheme.primaryColor),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
'${currentSet.targetWeightTotal} kg', '${currentSet.targetWeightTotal} ${l10n.unitKg}',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 24, fontSize: 24,
@ -842,16 +842,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
size: 48, color: AppTheme.successColor), size: 48, color: AppTheme.successColor),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Time Complete!', l10n.timerComplete,
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( Text(
'How many reps did you complete?', l10n.amrapResultBody,
style: TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
Row( Row(
@ -894,9 +894,9 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
backgroundColor: AppTheme.successColor, backgroundColor: AppTheme.successColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
child: const Text( child: Text(
'CONFIRM', l10n.commonConfirm,
style: TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -1165,7 +1165,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
AppBar _buildAppBar(AppLocalizations l10n) { AppBar _buildAppBar(AppLocalizations l10n) {
return AppBar( return AppBar(
title: Text('Week ${widget.week} - Day ${widget.day}'), title: Text(l10n.battleWeekDay(widget.week, widget.day)),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => _showAbandonDialog(l10n), onPressed: () => _showAbandonDialog(l10n),
@ -1322,7 +1322,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( 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( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 24, fontSize: 24,
@ -1394,6 +1394,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
int completedHP, int completedHP,
int totalHP, int totalHP,
) { ) {
final l10n = AppLocalizations.of(context)!;
final state = ref.watch(battleControllerProvider); final state = ref.watch(battleControllerProvider);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -1435,7 +1436,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
), ),
), ),
Text( Text(
'${currentSet.repsTarget} Reps per Round', l10n.battleRepsPerRound(currentSet.repsTarget),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
], ],
@ -1505,7 +1506,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
'WEIGHT: ${currentSet.targetWeightTotal} kg', l10n.battleWeightKg(currentSet.targetWeightTotal),
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
@ -127,6 +128,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final progress = 1.0 - (_secondsRemaining / widget.intervalSeconds); final progress = 1.0 - (_secondsRemaining / widget.intervalSeconds);
return Column( return Column(
@ -139,7 +141,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
border: Border.all(color: AppTheme.primaryColor), border: Border.all(color: AppTheme.primaryColor),
), ),
child: Text( child: Text(
'ROUND ${widget.currentSet} / ${widget.totalSets}', l10n.timerRound(widget.currentSet, widget.totalSets),
style: const TextStyle( style: const TextStyle(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -192,7 +194,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
widget.currentSet == 1 && widget.currentSet == 1 &&
_secondsRemaining == widget.intervalSeconds) _secondsRemaining == widget.intervalSeconds)
Text( Text(
'READY?', l10n.timerReady,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.labelLarge .labelLarge
@ -200,7 +202,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
) )
else if (!_isRunning) else if (!_isRunning)
Text( Text(
'PAUSED', l10n.timerPaused,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.labelLarge .labelLarge
@ -222,8 +224,8 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
icon: const Icon(Icons.play_arrow), icon: const Icon(Icons.play_arrow),
label: Text(widget.currentSet == 1 && label: Text(widget.currentSet == 1 &&
_secondsRemaining == widget.intervalSeconds _secondsRemaining == widget.intervalSeconds
? 'IGNITE ENGINE' ? l10n.timerIgnite
: 'RESUME'), : l10n.timerResume),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.successColor, backgroundColor: AppTheme.successColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
@ -237,7 +239,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _pauseTimer, onPressed: _pauseTimer,
icon: const Icon(Icons.pause), icon: const Icon(Icons.pause),
label: const Text('PAUSE'), label: Text(l10n.timerPause),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.errorColor, foregroundColor: AppTheme.errorColor,
side: const BorderSide(color: AppTheme.errorColor), side: const BorderSide(color: AppTheme.errorColor),

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:async'; import 'dart:async';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
class TimerWidget extends StatefulWidget { class TimerWidget extends StatefulWidget {
@ -96,6 +97,7 @@ class _TimerWidgetState extends State<TimerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final progress = widget.durationSeconds > 0 final progress = widget.durationSeconds > 0
? _secondsRemaining / widget.durationSeconds ? _secondsRemaining / widget.durationSeconds
: 0.0; : 0.0;
@ -129,11 +131,11 @@ class _TimerWidgetState extends State<TimerWidget> {
), ),
), ),
if (_isCompleted) if (_isCompleted)
const Padding( Padding(
padding: EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text( child: Text(
'COMPLETE!', l10n.timerComplete,
style: TextStyle( style: const TextStyle(
color: AppTheme.successColor, color: AppTheme.successColor,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -153,7 +155,7 @@ class _TimerWidgetState extends State<TimerWidget> {
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _isRunning ? _pause : _start, onPressed: _isRunning ? _pause : _start,
icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow), icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
label: Text(_isRunning ? 'PAUSE' : 'START'), label: Text(_isRunning ? l10n.timerPause : 'START'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor:
_isRunning ? Colors.orange : AppTheme.primaryColor, _isRunning ? Colors.orange : AppTheme.primaryColor,
@ -168,7 +170,7 @@ class _TimerWidgetState extends State<TimerWidget> {
OutlinedButton.icon( OutlinedButton.icon(
onPressed: _reset, onPressed: _reset,
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: const Text('RESET'), label: Text(l10n.timerReset),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.primaryColor, foregroundColor: AppTheme.primaryColor,
side: const BorderSide(color: AppTheme.primaryColor), side: const BorderSide(color: AppTheme.primaryColor),
@ -182,7 +184,7 @@ class _TimerWidgetState extends State<TimerWidget> {
OutlinedButton.icon( OutlinedButton.icon(
onPressed: _skip, onPressed: _skip,
icon: const Icon(Icons.skip_next), icon: const Icon(Icons.skip_next),
label: const Text('SKIP'), label: Text(l10n.timerSkip),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey, foregroundColor: Colors.grey,
side: const BorderSide(color: Colors.grey), side: const BorderSide(color: Colors.grey),
@ -196,7 +198,7 @@ class _TimerWidgetState extends State<TimerWidget> {
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _reset, onPressed: _reset,
icon: const Icon(Icons.replay), icon: const Icon(Icons.replay),
label: const Text('RESTART'), label: Text(l10n.timerRestart),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor, backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,

View file

@ -82,7 +82,7 @@ class WorkoutContent extends StatelessWidget {
children: [ children: [
_InfoBox( _InfoBox(
label: l10n.battleWeight, label: l10n.battleWeight,
value: '${currentSet.targetWeightTotal} kg', value: '${currentSet.targetWeightTotal} ${l10n.unitKg}',
), ),
_InfoBox( _InfoBox(
label: l10n.battleReps, label: l10n.battleReps,