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? ",
"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."
}

View file

@ -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."
}

View file

@ -57,26 +57,27 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
}
} 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

View file

@ -57,7 +57,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
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<ProfileScreen> {
}
void _showChangePasswordDialog() {
final l10n = AppLocalizations.of(context)!;
final oldPassCtrl = TextEditingController();
final newPassCtrl = TextEditingController();
final confirmPassCtrl = TextEditingController();
@ -74,7 +75,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
);
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<ProfileScreen> {
}
}
},
child: const Text('UPDATE'),
child: Text(l10n.commonUpdate),
),
],
),
@ -149,6 +150,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('CANCEL'),
child: Text(l10n.commonCancel),
),
ElevatedButton(
style:
@ -296,7 +299,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
Navigator.pop(context);
onConfirm();
},
child: const Text('CONFIRM'),
child: Text(l10n.commonConfirm),
),
],
),
@ -307,6 +310,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
),
),
const SizedBox(height: 32),
Text('Training Focus',
Text(l10n.profileTrainingFocus,
style: Theme.of(context)
.textTheme
.titleLarge
@ -521,7 +525,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
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<ProfileScreen> {
),
),
),
Text('Account Security',
Text(l10n.profileAccountSecurity,
style: Theme.of(context)
.textTheme
.titleLarge
@ -537,7 +541,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
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<ProfileScreen> {
),
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
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<ProfileScreen> {
_RadioTile<AccessoryTemplate>(
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<AccessoryTemplate>(
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<AccessoryTemplate>(
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<ProfileScreen> {
_RadioTile<AccessoryTemplate>(
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!),
),
],

View file

@ -108,8 +108,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
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<HubScreen> {
),
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)),

View file

@ -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<StartRaidButton>
),
const SizedBox(width: 12),
Text(
'START RAID',
AppLocalizations.of(context)!.lobbyStartRaid,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontSize: 20,
color: Colors.black,

View file

@ -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'),

View file

@ -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<AvatarEditor> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Container(
@ -52,9 +54,9 @@ class _AvatarEditorState extends State<AvatarEditor> {
Padding(
padding: const EdgeInsets.all(16),
child: SegmentedButton<String>(
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<String> newSelection) {

View file

@ -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<List<QuestCollection>>(
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)),
),
],
),

View file

@ -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<QuestItem> {
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<QuestItem> {
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,

View file

@ -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(

View file

@ -167,7 +167,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
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<InventoryScreen> {
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<InventoryScreen> {
}),
),
),
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<InventoryScreen> {
}
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<InventoryScreen> {
if (isSelected) const SizedBox(width: 4),
Expanded(
child: Text(
'$color (~${resistance}kg)',
'$color (~$resistance${l10n.unitKg})',
style: TextStyle(
color: isSelected
? _getBandColor(color)

View file

@ -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<LeaderboardScreen> {
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<LeaderboardScreen> {
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<LeaderboardScreen> {
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<LeaderboardScreen> {
});
},
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']});
}
});
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),

View file

@ -112,9 +112,10 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
}
} 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<AvatarSetupScreen> {
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(

View file

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

View file

@ -179,10 +179,11 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
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<InventorySetupScreen> {
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<InventorySetupScreen> {
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,

View file

@ -219,7 +219,7 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
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<StrengthTestScreen> {
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<StrengthTestScreen> {
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<StrengthTestScreen> {
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<StrengthTestScreen> {
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)),

View file

@ -176,8 +176,8 @@ class _PrivacyPolicyScreenState extends ConsumerState<PrivacyPolicyScreen> {
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<PrivacyPolicyScreen> {
Future<void> _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<bool>(
context: context,

View file

@ -208,15 +208,15 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
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)),
],
),
);

View file

@ -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),
),

View file

@ -502,15 +502,15 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
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<BattleScreen> {
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<BattleScreen> {
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<BattleScreen> {
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<BattleScreen> {
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<BattleScreen> {
),
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<BattleScreen> {
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<BattleScreen> {
),
),
Text(
'${currentSet.repsTarget} Reps per Round',
l10n.battleRepsPerRound(currentSet.repsTarget),
style: const TextStyle(color: Colors.grey),
),
],
@ -1505,7 +1506,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
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,

View file

@ -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<EmomTimerWidget>
@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<EmomTimerWidget>
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<EmomTimerWidget>
widget.currentSet == 1 &&
_secondsRemaining == widget.intervalSeconds)
Text(
'READY?',
l10n.timerReady,
style: Theme.of(context)
.textTheme
.labelLarge
@ -200,7 +202,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
)
else if (!_isRunning)
Text(
'PAUSED',
l10n.timerPaused,
style: Theme.of(context)
.textTheme
.labelLarge
@ -222,8 +224,8 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
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<EmomTimerWidget>
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),

View file

@ -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<TimerWidget> {
@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<TimerWidget> {
),
),
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<TimerWidget> {
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<TimerWidget> {
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<TimerWidget> {
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<TimerWidget> {
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,

View file

@ -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,