feat: add localization based translations for german and english to all screens

This commit is contained in:
Patryk Hegenberg 2026-01-12 12:46:19 +01:00
parent 6206cef61c
commit e1ac91cbde
18 changed files with 610 additions and 604 deletions

View file

@ -199,5 +199,112 @@
"secureAccountBody": "Wähle ein starkes Passwort, um deinen Fortschritt zu schützen",
"confirmPasswordLabel": "Passwort bestätigen",
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
"confirmButton": "BESTÄTIGEN"
"confirmButton": "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",
"guidePullupMuscles": "Latissimus|Bizeps|Unterarme",
"guidePullupMistakes": "Schwingen (Kipping)|Halbe Wiederholungen|Schultern hochgezogen lassen",
"guideDipTitle": "Weighted Dip",
"guideDipLore": "Ein fundamentaler Druck-Move, um Mauern zu überwinden.",
"guideDipSteps": "Stütze dich auf die Barren, Arme gestreckt|Lehne dich leicht nach vorne für mehr Brust-Fokus|Senke den Körper ab, bis die Schultern unter den Ellbogen sind|Drücke dich explosiv zurück in die Ausgangsposition",
"guideDipMuscles": "Brust|Trizeps|Vordere Schulter",
"guideDipMistakes": "Zu wenig Tiefe|Ellbogen wandern zu weit nach außen",
"guideSquatTitle": "Low Bar Back Squat",
"guideSquatLore": "Die Mutter aller Schlachten. Trainiert den gesamten Körperpanzer.",
"guideSquatSteps": "Lege die Hantel auf dem hinteren Deltamuskel ab (nicht im Nacken)|Füße schulterbreit, Zehen leicht nach außen|Atme tief ein (Bracing) und schiebe die Hüfte nach hinten|Gehe in die Hocke (Hüfte unter Kniehöhe)|Drücke dich aus der Ferse/Mittelfuß wieder hoch",
"guideSquatMuscles": "Quadrizeps|Gesäß|Core|Rückenstrecker",
"guideSquatMistakes": "Knie fallen nach innen|Rücken rundet ein|Zu wenig Tiefe",
"guideBenchTitle": "Bench Press",
"guideBenchLore": "Der Standard-Test für reine Druckkraft.",
"guideBenchSteps": "Lege dich auf die Bank, Augen unter der Stange|Füße fest am Boden, leichter Bogen im Rücken (Brücke)|Senke die Hantel kontrolliert zur unteren Brust|Drücke die Hantel explosiv nach oben",
"guideBenchMuscles": "Brust|Trizeps|Vordere Schulter",
"guideBenchMistakes": "Ellbogen 90° abgespreizt (Verletzungsgefahr)|Hintern hebt ab",
"guideOhpTitle": "Overhead Press",
"guideOhpLore": "Ein Objekt über den Kopf zu stemmen erfordert pure Stabilität.",
"guideOhpSteps": "Stange liegt auf dem vorderen Schultermuskel|Fester Stand, Gesäß und Bauch maximal anspannen|Kopf leicht zurücknehmen, Stange vertikal nach oben drücken|Oben den Kopf \"durch das Fenster\" der Arme schieben",
"guideOhpMuscles": "Schultern|Trizeps|Core",
"guideOhpMistakes": "Hohlkreuz (Rücklage)|Beine helfen mit (Push Press)",
"guideRdlTitle": "Romanian Deadlift",
"guideRdlLore": "Baut die hintere Kette auf, essenziell für Stabilität.",
"guideRdlSteps": "Startposition stehend mit Hantel|Schiebe die Hüfte weit nach hinten, Beine bleiben fast gestreckt|Senke die Hantel am Bein entlang bis knapp unter das Knie|Spüre den Zug im Beinbeuger und richte dich wieder auf",
"guideRdlMuscles": "Beinbeuger (Hamstrings)|Gesäß|Unterer Rücken",
"guideRdlMistakes": "Rücken rundet ein|Hantel zu weit vom Körper weg",
"guideRowTitle": "Pendlay Row",
"guideRowLore": "Explosive Zugkraft vom Boden. Für einen starken Rücken.",
"guideRowSteps": "Oberkörper parallel zum Boden, Rücken gerade|Hantel liegt bei jeder Wiederholung tot auf dem Boden|Ziehe die Hantel explosiv zum unteren Brustbein|Kontrolliert ablegen, Spannung kurz lösen, neu ansetzen",
"guideRowMuscles": "Latissimus|Trapez|Hintere Schulter",
"guideRowMistakes": "Oberkörper richtet sich auf|Reißen mit Schwung",
"guideCurlTitle": "Barbell Curl",
"guideCurlLore": "Isolierte Kraft für den finalen Schlag.",
"guideCurlSteps": "Stehender Stand, Hantel im Untergriff|Ellbogen bleiben fixiert am Körper|Hantel zur Brust curlen, oben kurz halten|Langsam ablassen",
"guideCurlMuscles": "Bizeps",
"guideCurlMistakes": "Schwingen aus der Hüfte|Ellbogen wandern nach vorne",
"guideKbSwingTitle": "Kettlebell Swing",
"guideKbSwingLore": "Ballistische Kraft und Ausdauer. Die Hüfte ist der Motor.",
"guideKbSwingSteps": "Hüftbreiter Stand, KB vor dir am Boden|Hike-Pass: Ziehe die KB durch die Beine nach hinten|Hüfte explosiv strecken (Snap!), KB fliegt durch Hüftkraft auf Brusthöhe|KB kontrolliert zurückschwingen lassen",
"guideKbSwingMuscles": "Gesäß|Beinbeuger|Core|Ausdauer",
"guideKbSwingMistakes": "Kniebeuge statt Hüftbeuge|Arme heben das Gewicht",
"guideKbSnatchTitle": "Kettlebell Snatch",
"guideKbSnatchLore": "Der Zar der Kettlebell-Übungen. Totale Körperkontrolle.",
"guideKbSnatchSteps": "Starte wie beim Swing (Einarmig)|Hüftkraft beschleunigt die Kugel nach oben|Bei Kopfhöhe: Durchstoßen der Hand (\"Punch through\")|Sanftes Auffangen im Lockout über Kopf",
"guideKbSnatchMuscles": "Gesamter Körper|Schultern|Griffkraft",
"guideKbSnatchMistakes": "Kugel knallt auf den Unterarm|Zu wenig Hüftkraft",
"guideKbThrusterTitle": "Kettlebell Thruster",
"guideKbThrusterLore": "Eine brutale Kombination aus Squat und Press.",
"guideKbThrusterSteps": "KB in der Rack-Position (vor der Brust)|Tiefe Kniebeuge|Beim Aufstehen den Schwung nutzen, um KB über Kopf zu drücken|Zurück in die Rack-Position beim Absenken in den nächsten Squat",
"guideKbThrusterMuscles": "Beine|Schultern|Lunge (Cardio)",
"guideKbThrusterMistakes": "Pause zwischen Squat und Press|Rücken rundet im Squat",
"guideKbCleanPressTitle": "KB Clean & Press",
"guideKbCleanPressLore": "Zwei Bewegungen in Harmonie.",
"guideKbCleanPressSteps": "Clean: Ziehe die KB vom Boden in die Rack-Position vor der Brust|Press: Drücke sie strikt über den Kopf|Senke sie in Rack, dann zum Boden (oder Swing)",
"guideKbCleanPressMuscles": "Schultern|Rücken|Beine",
"guideKbCleanPressMistakes": "Clean knallt auf Arm|Hohlkreuz beim Press",
"guideFacePullTitle": "Band Face Pull",
"guideFacePullLore": "Schützt die Schultern vor dem Verschleiß des Kampfes.",
"guideFacePullSteps": "Band auf Kopfhöhe befestigen|Ziehe das Band zum Gesicht (Richtung Stirn/Augen)|Ellbogen hoch und weit nach außen ziehen|Schulterblätter hinten zusammenkneifen",
"guideFacePullMuscles": "Hintere Schulter|Rotatorenmanschette",
"guideFacePullMistakes": "Ellbogen zu tief|Kopf nach vorne schieben",
"guideAbWheelTitle": "Ab Wheel Rollout",
"guideAbWheelLore": "Ein Stahlkern, der jeden Treffer absorbiert.",
"guideAbWheelSteps": "Knie am Boden, Rad vor den Knien|Rolle nach vorne, halte den Rücken rund/stabil (Hollow Body)|Gehe nur so weit, wie du den Rücken stabil halten kannst|Ziehe dich aus dem Bauchmuskel zurück",
"guideAbWheelMuscles": "Core (Anti-Extension)",
"guideAbWheelMistakes": "Hohlkreuz (Gefährlich!)|Ziehen aus den Armen",
"guidePlankTitle": "Plank",
"guidePlankLore": "Unbeweglich wie ein Fels in der Brandung.",
"guidePlankSteps": "Unterarmstütz, Körper bildet eine Linie|Gesäß und Bauch fest anspannen|Schulterblätter auseinanderdrücken|Atmen nicht vergessen!",
"guidePlankMuscles": "Core",
"guidePlankMistakes": "Hüfte hängt durch|Gesäß zu hoch",
"codexTitle": "Kreaturen-Kodex",
"enemyIronGolemName": "Eisengolem",
"enemyIronGolemTitle": "Das Gewicht der Erde",
"enemyIronGolemDesc": "Geschmiedet aus den tektonischen Platten der tiefen Erde, existiert der Eisengolem nur, um die Schwachen zu zermalmen. Er verkörpert die unerbittliche Schwerkraft, die auf eine schwere Last wirkt.\n\nEr respektiert nur eines: Die rohe Kraft der BEINE, die seinem erdrückenden Gewicht standhalten können.",
"enemyIronGolemNemesis": "Kniebeugen-Nemesis",
"enemyGravityDemonName": "Schwerkraft-Dämon",
"enemyGravityDemonTitle": "Der Sog des Abgrunds",
"enemyGravityDemonDesc": "Ein Geist reiner Abwärtskraft, der sich an den Rücken von Abenteurern klammert. Er flüstert Lügen der Schwäche in dein Ohr, während er dich in den Abgrund zieht.\n\nNur wer einen Rücken aus Stahl und den Willen hat, sich hochzuziehen, kann seinem Griff entkommen.",
"enemyGravityDemonNemesis": "Klimmzug-Nemesis",
"enemyPressurePhantomName": "Druck-Phantom",
"enemyPressurePhantomTitle": "Der unsichtbare Zermalmer",
"enemyPressurePhantomDesc": "Eine ätherische Entität, die die Luft um dich herum komprimiert. Es versucht, Brust und Schultern derer kollabieren zu lassen, die es wagen, dagegen zu drücken.\n\nBesiege es, indem du mit explosiver Dip-Kraft durch den Schmerz drückst.",
"enemyPressurePhantomNemesis": "Dip-Nemesis"
}

View file

@ -199,5 +199,112 @@
"secureAccountBody": "Choose a strong password to protect your progress",
"confirmPasswordLabel": "Confirm Password",
"passwordsDoNotMatch": "Passwords do not match",
"confirmButton": "CONFIRM"
"confirmButton": "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",
"guidePullupMuscles": "Latissimus|Biceps|Forearms",
"guidePullupMistakes": "Swinging (Kipping)|Half reps|Shoulders shrugged up",
"guideDipTitle": "Weighted Dip",
"guideDipLore": "A fundamental pushing move to overcome walls.",
"guideDipSteps": "Support yourself on the bars, arms straight|Lean forward slightly for more chest focus|Lower body until shoulders are below elbows|Push explosively back to start position",
"guideDipMuscles": "Chest|Triceps|Front Delt",
"guideDipMistakes": "Not enough depth|Elbows flaring out too much",
"guideSquatTitle": "Low Bar Back Squat",
"guideSquatLore": "The mother of all battles. Trains the entire body armor.",
"guideSquatSteps": "Place bar on rear delts (not neck)|Feet shoulder width, toes slightly out|Inhale deeply (Bracing) and push hips back|Squat down (hip crease below knee)|Push up through heels/midfoot",
"guideSquatMuscles": "Quadriceps|Glutes|Core|Spinal Erectors",
"guideSquatMistakes": "Knees caving in|Back rounding|Not enough depth",
"guideBenchTitle": "Bench Press",
"guideBenchLore": "The standard test for pure pushing power.",
"guideBenchSteps": "Lie on bench, eyes under bar|Feet planted firmly, slight arch in back|Lower bar controlled to lower chest|Press bar explosively up",
"guideBenchMuscles": "Chest|Triceps|Front Delt",
"guideBenchMistakes": "Elbows flared 90° (Injury risk)|Butt lifting off bench",
"guideOhpTitle": "Overhead Press",
"guideOhpLore": "Pressing an object overhead requires pure stability.",
"guideOhpSteps": "Bar rests on front delts|Firm stance, squeeze glutes and abs|Move head back slightly, press bar vertically|At top, push head 'through the window' of arms",
"guideOhpMuscles": "Shoulders|Triceps|Core",
"guideOhpMistakes": "Excessive arching (Leaning back)|Using legs (Push Press)",
"guideRdlTitle": "Romanian Deadlift",
"guideRdlLore": "Builds the posterior chain, essential for stability.",
"guideRdlSteps": "Start standing with bar|Push hips far back, legs stay nearly straight|Lower bar along legs until just below knees|Feel stretch in hamstrings and stand back up",
"guideRdlMuscles": "Hamstrings|Glutes|Lower Back",
"guideRdlMistakes": "Back rounding|Bar drifting away from body",
"guideRowTitle": "Pendlay Row",
"guideRowLore": "Explosive pulling power from the floor. For a strong back.",
"guideRowSteps": "Torso parallel to floor, back straight|Bar rests dead on floor each rep|Pull bar explosively to lower sternum|Lower controlled, reset tension",
"guideRowMuscles": "Latissimus|Traps|Rear Delt",
"guideRowMistakes": "Torso raising up|Jerking with momentum",
"guideCurlTitle": "Barbell Curl",
"guideCurlLore": "Isolated power for the finishing strike.",
"guideCurlSteps": "Standing stance, underhand grip|Elbows fixed at sides|Curl bar to chest, squeeze at top|Lower slowly",
"guideCurlMuscles": "Biceps",
"guideCurlMistakes": "Swinging from hips|Elbows drifting forward",
"guideKbSwingTitle": "Kettlebell Swing",
"guideKbSwingLore": "Ballistic power and endurance. The hips are the engine.",
"guideKbSwingSteps": "Hip-width stance, KB on floor in front|Hike-Pass: Pull KB back between legs|Extend hips explosively (Snap!), KB flies to chest height|Let KB swing back controllably",
"guideKbSwingMuscles": "Glutes|Hamstrings|Core|Cardio",
"guideKbSwingMistakes": "Squatting instead of hinging|Arms lifting the weight",
"guideKbSnatchTitle": "Kettlebell Snatch",
"guideKbSnatchLore": "The Tsar of Kettlebell exercises. Total body control.",
"guideKbSnatchSteps": "Start like Swing (One arm)|Hip power accelerates ball upwards|At head height: Punch through handle|Soft catch in lockout overhead",
"guideKbSnatchMuscles": "Full Body|Shoulders|Grip",
"guideKbSnatchMistakes": "Bell slamming on forearm|Not enough hip power",
"guideKbThrusterTitle": "Kettlebell Thruster",
"guideKbThrusterLore": "A brutal combination of squat and press.",
"guideKbThrusterSteps": "KB in rack position (chest)|Deep squat|Use momentum from standing up to press KB overhead|Return to rack for next squat",
"guideKbThrusterMuscles": "Legs|Shoulders|Cardio",
"guideKbThrusterMistakes": "Pausing between squat and press|Back rounding in squat",
"guideKbCleanPressTitle": "KB Clean & Press",
"guideKbCleanPressLore": "Two movements in harmony.",
"guideKbCleanPressSteps": "Clean: Pull KB from floor to rack position|Press: Press strictly overhead|Lower to rack, then floor (or swing)",
"guideKbCleanPressMuscles": "Shoulders|Back|Legs",
"guideKbCleanPressMistakes": "Clean slams arm|Arching back during press",
"guideFacePullTitle": "Band Face Pull",
"guideFacePullLore": "Protects shoulders from the wear of battle.",
"guideFacePullSteps": "Attach band at head height|Pull band towards face (forehead/eyes)|Pull elbows high and wide|Squeeze shoulder blades together",
"guideFacePullMuscles": "Rear Delt|Rotator Cuff",
"guideFacePullMistakes": "Elbows too low|Head pushing forward",
"guideAbWheelTitle": "Ab Wheel Rollout",
"guideAbWheelLore": "A core of steel to absorb any hit.",
"guideAbWheelSteps": "Knees on floor, wheel in front|Roll forward, keep back rounded/stable (Hollow Body)|Go only as far as you can maintain stability|Pull back using abs",
"guideAbWheelMuscles": "Core (Anti-Extension)",
"guideAbWheelMistakes": "Arching back (Dangerous!)|Pulling with arms",
"guidePlankTitle": "Plank",
"guidePlankLore": "Immovable as a rock in the surf.",
"guidePlankSteps": "Forearm support, body forms a line|Squeeze glutes and abs hard|Push shoulder blades apart|Don't forget to breathe!",
"guidePlankMuscles": "Core",
"guidePlankMistakes": "Hips sagging|Butt too high",
"codexTitle": "Creature Codex",
"enemyIronGolemName": "Iron Golem",
"enemyIronGolemTitle": "The Weight of the Earth",
"enemyIronGolemDesc": "Forged from the tectonic plates of the Deep Earth, the Iron Golem exists only to crush the weak. It embodies the unrelenting force of gravity acting on a heavy load.\n\nIt respects only one thing: The raw power of the LEGS that can stand up against its crushing weight.",
"enemyIronGolemNemesis": "Squat Nemesis",
"enemyGravityDemonName": "Gravity Demon",
"enemyGravityDemonTitle": "The Abyssal Pull",
"enemyGravityDemonDesc": "A spirit of pure downward force that clings to the back of adventurers. It whispers lies of weakness into your ear while dragging you towards the abyss.\n\nOnly those with a back of steel and the will to pull themselves up can escape its grasp.",
"enemyGravityDemonNemesis": "Pull-up Nemesis",
"enemyPressurePhantomName": "Pressure Phantom",
"enemyPressurePhantomTitle": "The Invisible Crusher",
"enemyPressurePhantomDesc": "An ethereal entity that compresses the very air around you. It seeks to collapse the chest and shoulders of any who dare to push against it.\n\nDefeat it by pushing through the pain with explosive dipping power.",
"enemyPressurePhantomNemesis": "Dip Nemesis"
}

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 '../../../../shared/data/repositories/user_repository.dart';
import '../../../../core/theme/app_theme.dart';
@ -78,6 +79,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
@ -125,13 +127,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
const SizedBox(height: 32),
Text(
'WELCOME BACK',
l10n.loginWelcomeBack,
style: Theme.of(context).textTheme.displayMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Time to level up your strength',
l10n.loginSubtitle,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
@ -176,8 +178,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
enabled: !_isLoading,
decoration: const InputDecoration(
labelText: 'Email',
decoration: InputDecoration(
labelText: l10n.emailLabel,
prefixIcon: Icon(Icons.email_outlined),
),
onFieldSubmitted: (_) {
@ -185,10 +187,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
return l10n.emailEmptyError;
}
if (!value.contains('@')) {
return 'Please enter a valid email';
return l10n.emailInvalidError;
}
return null;
},
@ -203,7 +205,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
textInputAction: TextInputAction.done,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Password',
labelText: l10n.passwordLabel,
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
@ -221,10 +223,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
onFieldSubmitted: (_) => _handleLogin(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
return l10n.passwordEmptyError;
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
return l10n.passwordLengthError;
}
return null;
},
@ -248,8 +250,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
color: Colors.black,
),
)
: const Text(
'LOGIN',
: Text(
l10n.loginButton,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -263,14 +265,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Don't have an account? ",
l10n.loginNoAccount,
style: Theme.of(context).textTheme.bodyMedium,
),
TextButton(
onPressed: _isLoading
? null
: () => context.go('/register'),
child: const Text('REGISTER'),
child: Text(l10n.loginRegisterButton),
),
],
),

View file

@ -1,178 +1,7 @@
// import 'package:flutter/material.dart';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:go_router/go_router.dart';
// import '../../../../core/theme/app_theme.dart';
// import '../../../../core/constants/app_constants.dart';
// import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart';
// class RegisterScreen extends ConsumerStatefulWidget {
// const RegisterScreen({super.key});
// @override
// ConsumerState<RegisterScreen> createState() => _RegisterScreenState();
// }
// class _RegisterScreenState extends ConsumerState<RegisterScreen> {
// final _formKey = GlobalKey<FormState>();
// final _emailController = TextEditingController();
// // final _passwordController = TextEditingController();
// // final _confirmPasswordController = TextEditingController();
// // bool _obscurePassword = true;
// // bool _obscureConfirmPassword = true;
// @override
// void dispose() {
// _emailController.dispose();
// // _passwordController.dispose();
// // _confirmPasswordController.dispose();
// super.dispose();
// }
// void _handleRegister() {
// if (!_formKey.currentState!.validate()) return;
// ref.read(onboardingDataProvider.notifier).updateData({
// 'email': _emailController.text.trim(),
// // 'password': _passwordController.text,
// });
// context.go('/onboarding/welcome');
// }
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// leading: IconButton(
// icon: const Icon(Icons.arrow_back),
// onPressed: () => context.go('/login'),
// ),
// ),
// body: SafeArea(
// child: Center(
// child: SingleChildScrollView(
// padding: const EdgeInsets.all(24),
// child: Form(
// key: _formKey,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// Text(
// 'CREATE ACCOUNT',
// style: Theme.of(context).textTheme.displayMedium,
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 8),
// Text(
// 'Begin your strength journey',
// style: Theme.of(context).textTheme.bodyMedium,
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 48),
// TextFormField(
// controller: _emailController,
// keyboardType: TextInputType.emailAddress,
// decoration: const InputDecoration(
// labelText: 'Email',
// prefixIcon: Icon(Icons.email_outlined),
// ),
// validator: (value) {
// if (value == null || value.isEmpty) {
// return 'Please enter your email';
// }
// if (!value.contains('@')) {
// return 'Please enter a valid email';
// }
// return null;
// },
// ),
// // const SizedBox(height: 16),
// // TextFormField(
// // controller: _passwordController,
// // obscureText: _obscurePassword,
// // decoration: InputDecoration(
// // labelText: 'Password',
// // prefixIcon: const Icon(Icons.lock_outline),
// // suffixIcon: IconButton(
// // icon: Icon(
// // _obscurePassword
// // ? Icons.visibility_outlined
// // : Icons.visibility_off_outlined,
// // ),
// // onPressed: () {
// // setState(() => _obscurePassword = !_obscurePassword);
// // },
// // ),
// // ),
// // validator: (value) {
// // if (value == null || value.isEmpty) {
// // return 'Please enter a password';
// // }
// // if (value.length < 8) {
// // return 'Password must be at least 8 characters';
// // }
// // return null;
// // },
// // ),
// // const SizedBox(height: 16),
// // TextFormField(
// // controller: _confirmPasswordController,
// // obscureText: _obscureConfirmPassword,
// // decoration: InputDecoration(
// // labelText: 'Confirm Password',
// // prefixIcon: const Icon(Icons.lock_outline),
// // suffixIcon: IconButton(
// // icon: Icon(
// // _obscureConfirmPassword
// // ? Icons.visibility_outlined
// // : Icons.visibility_off_outlined,
// // ),
// // onPressed: () {
// // setState(() => _obscureConfirmPassword =
// // !_obscureConfirmPassword);
// // },
// // ),
// // ),
// // validator: (value) {
// // if (value != _passwordController.text) {
// // return 'Passwords do not match';
// // }
// // return null;
// // },
// // ),
// const SizedBox(height: 32),
// ElevatedButton(
// onPressed: _handleRegister,
// child: const Text('CONTINUE'),
// ),
// const SizedBox(height: 16),
// Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(
// 'Already have an account? ',
// style: Theme.of(context).textTheme.bodyMedium,
// ),
// TextButton(
// onPressed: () => context.go('/login'),
// child: const Text('LOGIN'),
// ),
// ],
// ),
// ],
// ),
// ),
// ),
// ),
// ),
// );
// }
// }
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 '../../../onboarding/presentation/screens/bodyweight_input_screen.dart';
@ -212,6 +41,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
leading: IconButton(
@ -240,13 +70,13 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
children: [
const Spacer(),
Text(
'CREATE ACCOUNT',
l10n.registerTitle,
style: Theme.of(context).textTheme.displayMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Begin your strength journey',
l10n.registerSubtitle,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
@ -256,18 +86,18 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
focusNode: _emailFocusNode,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
labelText: 'Email',
decoration: InputDecoration(
labelText: l10n.emailLabel,
prefixIcon: Icon(Icons.email_outlined),
helperText: 'You will use this to login',
helperText: l10n.registerEmailHelper,
),
onFieldSubmitted: (_) {},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
return l10n.emailEmptyError;
}
if (!value.contains('@')) {
return 'Please enter a valid email';
return l10n.emailInvalidError;
}
return null;
},
@ -279,8 +109,8 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
padding:
const EdgeInsets.symmetric(vertical: 16),
),
child: const Text(
'CONTINUE',
child: Text(
l10n.continueButton,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -293,12 +123,12 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Already have an account? ',
l10n.registerHaveAccount,
style: Theme.of(context).textTheme.bodyMedium,
),
TextButton(
onPressed: () => context.go('/login'),
child: const Text('LOGIN'),
child: Text(l10n.registerLoginButton),
),
],
),

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/constants/app_constants.dart';
import '../../../../core/debug/debug_config_screen.dart';
@ -153,6 +154,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
Future<int?> _showConditioningDialog() async {
int sets = 20;
final l10n = AppLocalizations.of(context)!;
return await showDialog<int>(
context: context,
@ -163,8 +165,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
final interval = (20 * 60) / sets;
return AlertDialog(
title: const Text(
'MISSION BRIEFING',
title: Text(
l10n.missionBriefingTitle,
style: TextStyle(
color: AppTheme.textSecondary,
fontWeight: FontWeight.bold,
@ -173,19 +175,19 @@ class _HubScreenState extends ConsumerState<HubScreen> {
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'The enemy is fleeing! We have a 20-minute window to intercept.',
Text(
l10n.missionBriefingBody,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Text(
'Combat Density: $sets Sets',
l10n.missionBriefingDensity(sets),
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold),
),
Text(
'Interval: Every ${interval.toStringAsFixed(0)} seconds',
l10n.missionBriefingInterval(interval.toStringAsFixed(0)),
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
@ -211,11 +213,11 @@ class _HubScreenState extends ConsumerState<HubScreen> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context, null),
child: const Text('ABORT'),
child: Text(l10n.abortButton),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, sets),
child: const Text('ENGAGE'),
child: Text(l10n.engageButton),
),
],
);
@ -229,6 +231,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
Widget build(BuildContext context) {
final userRepo = ref.watch(userRepositoryProvider);
final cycleRepo = ref.watch(cycleRepositoryProvider);
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: SafeArea(
@ -362,7 +365,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
child: Column(
children: [
Text(
'No active cycle',
l10n.hubNoActiveCycle,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
@ -370,7 +373,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
onPressed: () {
context.push('/onboarding/strength-test');
},
child: const Text('Create New Cycle'),
child: Text(l10n.hubCreateCycle),
),
],
),
@ -383,8 +386,11 @@ class _HubScreenState extends ConsumerState<HubScreen> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_StatBox(
label: 'Cycle', value: '#${cycle.cycleNumber}'),
_StatBox(label: 'Active', value: 'Yes'),
label: l10n.hubCycleLabel,
value: '#${cycle.cycleNumber}'),
_StatBox(
label: l10n.hubActiveLabel,
value: l10n.hubActiveYes),
],
),
),
@ -409,24 +415,24 @@ class _HubScreenState extends ConsumerState<HubScreen> {
children: [
_NavButton(
icon: Icons.history,
label: 'History',
label: l10n.navHistory,
onTap: () => context.go('/history'),
),
_NavButton(
icon: Icons.inventory_2_outlined,
label: 'Inventory',
label: l10n.navInventory,
onTap: () => context.go('/inventory'),
),
_NavButton(
icon: Icons.bar_chart,
label: 'Stats',
label: l10n.navStats,
onTap: () {
context.go('/stats');
},
),
_NavButton(
icon: Icons.auto_stories,
label: 'Codex',
label: l10n.navCodex,
onTap: () => context.go('/codex'),
),
],

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/constants/asset_paths.dart';
@ -8,9 +10,11 @@ class CodexScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Creature Codex'),
title: Text(l10n.codexTitle),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'),
@ -18,40 +22,31 @@ class CodexScreen extends StatelessWidget {
),
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
children: [
_LoreCard(
name: 'Iron Golem',
title: 'The Weight of the Earth',
description:
'Forged from the tectonic plates of the Deep Earth, the Iron Golem exists only to crush the weak. '
'It embodies the unrelenting force of gravity acting on a heavy load.\n\n'
'It respects only one thing: The raw power of the LEGS that can stand up against its crushing weight.',
name: l10n.enemyIronGolemName,
title: l10n.enemyIronGolemTitle,
description: l10n.enemyIronGolemDesc,
assetPath: AssetPaths.enemyIronGolem,
exercise: 'Squat Nemesis',
exercise: l10n.enemyIronGolemNemesis,
color: Colors.redAccent,
),
SizedBox(height: 24),
const SizedBox(height: 24),
_LoreCard(
name: 'Gravity Demon',
title: 'The Abyssal Pull',
description:
'A spirit of pure downward force that clings to the back of adventurers. '
'It whispers lies of weakness into your ear while dragging you towards the abyss.\n\n'
'Only those with a back of steel and the will to pull themselves up can escape its grasp.',
name: l10n.enemyGravityDemonName,
title: l10n.enemyGravityDemonTitle,
description: l10n.enemyGravityDemonDesc,
assetPath: AssetPaths.enemyGravityDemon,
exercise: 'Pull-up Nemesis',
exercise: l10n.enemyGravityDemonNemesis,
color: Colors.purpleAccent,
),
SizedBox(height: 24),
const SizedBox(height: 24),
_LoreCard(
name: 'Pressure Phantom',
title: 'The Invisible Crusher',
description:
'An ethereal entity that compresses the very air around you. '
'It seeks to collapse the chest and shoulders of any who dare to push against it.\n\n'
'Defeat it by pushing through the pain with explosive dipping power.',
name: l10n.enemyPressurePhantomName,
title: l10n.enemyPressurePhantomTitle,
description: l10n.enemyPressurePhantomDesc,
assetPath: AssetPaths.enemyPressurePhantom,
exercise: 'Dip Nemesis',
exercise: l10n.enemyPressurePhantomNemesis,
color: Colors.cyanAccent,
),
],

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 '../../data/repositories/quest_repository.dart';
import '../../../../shared/data/local/app_database.dart'; // Für QuestCollection Typ
@ -12,6 +13,7 @@ class QuestLogScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final questRepo = ref.watch(questRepositoryProvider);
final l10n = AppLocalizations.of(context)!;
return DefaultTabController(
length: 2,
@ -22,13 +24,13 @@ class QuestLogScreen extends ConsumerWidget {
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'),
),
bottom: const TabBar(
bottom: TabBar(
indicatorColor: AppTheme.primaryColor,
labelColor: AppTheme.primaryColor,
unselectedLabelColor: Colors.grey,
tabs: [
Tab(text: 'DAILIES'),
Tab(text: 'JOURNEY'),
Tab(text: l10n.questTabDailies),
Tab(text: l10n.questTabJourney),
],
),
),
@ -44,14 +46,14 @@ class QuestLogScreen extends ConsumerWidget {
final allQuests = snapshot.data ?? [];
// Filtern
final dailies = allQuests.where((q) => q.type == 'daily').toList();
final story = allQuests.where((q) => q.type != 'daily').toList();
return TabBarView(
children: [
_QuestList(quests: dailies, emptyMessage: 'No daily quests available.\nCome back tomorrow!'),
_QuestList(quests: story, emptyMessage: 'Your journey has just begun.'),
_QuestList(
quests: dailies, emptyMessage: l10n.questEmptyDailies),
_QuestList(quests: story, emptyMessage: l10n.questEmptyJourney),
],
);
},

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/local/app_database.dart';
@ -30,9 +31,10 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Quest Log'),
title: Text(l10n.historyTitle),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'),
@ -57,12 +59,12 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
),
const SizedBox(height: 16),
Text(
'No completed quests yet',
l10n.historyEmptyTitle,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
'Complete a workout to fill your journal',
l10n.historyEmptyBody,
style: Theme.of(context).textTheme.bodyMedium,
),
],
@ -109,6 +111,7 @@ class _WorkoutHistoryCard extends StatelessWidget {
final dateStr = DateFormat.yMMMd().format(workout.completedAt!);
final timeStr = DateFormat.jm().format(workout.completedAt!);
final exercises = _parseExercises();
final l10n = AppLocalizations.of(context)!;
final summary = exercises
.map((e) =>
@ -122,7 +125,7 @@ class _WorkoutHistoryCard extends StatelessWidget {
tilePadding: const EdgeInsets.all(16),
leading: _buildDateBadge(context, workout),
title: Text(
summary.isEmpty ? 'Unknown Workout' : summary,
summary.isEmpty ? l10n.historyUnknownWorkout : summary,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,

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 '../../../../core/constants/app_constants.dart';
@ -121,6 +122,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
}
Future<void> _saveChanges() async {
final l10n = AppLocalizations.of(context)!;
setState(() => _isLoading = true);
try {
final userRepo = ref.read(userRepositoryProvider);
@ -153,7 +155,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Inventory updated successfully')),
SnackBar(content: Text(l10n.inventoryUpdatedSuccess)),
);
setState(() {
_hasChanges = false;
@ -189,13 +191,14 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (_isLoading && !_hasChanges) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return Scaffold(
appBar: AppBar(
title: const Text('Manage Equipment'),
title: Text(l10n.inventoryTitle),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'),
@ -204,7 +207,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
if (_hasChanges)
TextButton(
onPressed: _saveChanges,
child: const Text('SAVE',
child: Text(l10n.saveButton,
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold)),
@ -222,7 +225,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Barbell Weight',
Text(l10n.inventoryBarbellWeight,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 16),
Row(
@ -253,7 +256,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
),
),
const SizedBox(height: 24),
Text('Quick Presets',
Text(l10n.inventoryPresets,
style: Theme.of(context)
.textTheme
.titleMedium
@ -270,29 +273,35 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildPresetChip('Home Gym', 'home'),
_buildPresetChip(l10n.inventoryPresetHome, 'home'),
const SizedBox(height: 8),
_buildPresetChip('Commercial', 'commercial'),
_buildPresetChip(
l10n.inventoryPresetCommercial, 'commercial'),
const SizedBox(height: 8),
_buildPresetChip('Minimal', 'minimal'),
_buildPresetChip(l10n.inventoryPresetMinimal, 'minimal'),
],
);
} else {
return Row(
children: [
Expanded(child: _buildPresetChip('Home Gym', 'home')),
Expanded(
child: _buildPresetChip(
l10n.inventoryPresetHome, 'home')),
const SizedBox(width: 8),
Expanded(
child: _buildPresetChip('Commercial', 'commercial')),
child: _buildPresetChip(
l10n.inventoryPresetCommercial, 'commercial')),
const SizedBox(width: 8),
Expanded(child: _buildPresetChip('Minimal', 'minimal')),
Expanded(
child: _buildPresetChip(
l10n.inventoryPresetMinimal, 'minimal')),
],
);
}
},
),
const SizedBox(height: 24),
Text('Plates Available',
Text(l10n.inventoryPlates,
style: Theme.of(context)
.textTheme
.titleMedium
@ -311,7 +320,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
);
}),
const SizedBox(height: 24),
Text('Resistance Bands (Assistance)',
Text(l10n.inventoryBands,
style: Theme.of(context)
.textTheme
.titleMedium
@ -367,7 +376,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
strokeWidth: 2,
),
)
: const Text('SAVE CHANGES'),
: Text(l10n.saveChangesButton),
),
],
),

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:drift/drift.dart' hide Column;
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/repositories/user_repository.dart';
@ -124,9 +125,10 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Choose Your Hero'),
title: Text(l10n.setupAvatarTitle),
actions: [
TextButton(
onPressed: _isLoading ? null : _handleFinish,
@ -135,7 +137,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: const Text('FINISH',
: Text(l10n.finishButton,
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor)),
@ -148,8 +150,8 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
padding: const EdgeInsets.all(16),
color: AppTheme.surfaceColor,
width: double.infinity,
child: const Text(
'This is how the legends will remember you.',
child: Text(
l10n.setupAvatarSubtitle,
textAlign: TextAlign.center,
style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
),
@ -169,25 +171,26 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
final passwordController = TextEditingController();
final confirmController = TextEditingController();
final formKey = GlobalKey<FormState>();
final l10n = AppLocalizations.of(context)!;
return showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Secure Your Account'),
title: Text(l10n.secureAccountTitle),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Choose a strong password to protect your progress'),
Text(l10n.secureAccountBody),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
obscureText: true,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Password',
decoration: InputDecoration(
labelText: l10n.passwordLabel,
prefixIcon: Icon(Icons.lock),
),
validator: (v) =>
@ -197,12 +200,12 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
TextFormField(
controller: confirmController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm Password',
decoration: InputDecoration(
labelText: l10n.confirmPasswordLabel,
prefixIcon: Icon(Icons.lock_outline),
),
validator: (v) => v != passwordController.text
? 'Passwords do not match'
? l10n.passwordsDoNotMatch
: null,
),
],
@ -211,7 +214,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('CANCEL'),
child: Text(l10n.cancelButton),
),
ElevatedButton(
onPressed: () {
@ -219,7 +222,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
Navigator.pop(context, passwordController.text);
}
},
child: const Text('CONFIRM'),
child: Text(l10n.confirmButton),
),
],
),

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 '../../../../core/constants/app_constants.dart';
@ -57,9 +58,11 @@ class _BodyweightInputScreenState extends ConsumerState<BodyweightInputScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Setup Profile'),
title: Text(l10n.setupProfileTitle),
// title: const Text('Setup Profile'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/onboarding/welcome'),
@ -81,12 +84,14 @@ class _BodyweightInputScreenState extends ConsumerState<BodyweightInputScreen> {
// Title
Text(
'What\'s your current bodyweight?',
l10n.bodyweightTitle,
// 'What\'s your current bodyweight?',
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 16),
Text(
'We need this to calculate your weighted calisthenics exercises',
l10n.bodyweightSubtitle,
// 'We need this to calculate your weighted calisthenics exercises',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 48),
@ -96,9 +101,9 @@ class _BodyweightInputScreenState extends ConsumerState<BodyweightInputScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
SegmentedButton<bool>(
segments: const [
ButtonSegment(value: true, label: Text('KG')),
ButtonSegment(value: false, label: Text('LBS')),
segments: [
ButtonSegment(value: true, label: Text(l10n.unitKg)),
ButtonSegment(value: false, label: Text(l10n.unitLbs)),
],
selected: {_useKg},
onSelectionChanged: (Set<bool> newSelection) {
@ -157,7 +162,7 @@ class _BodyweightInputScreenState extends ConsumerState<BodyweightInputScreen> {
// Continue Button
ElevatedButton(
onPressed: _handleContinue,
child: const Text('CONTINUE'),
child: Text(l10n.continueButton),
),
],
),

View file

@ -20,7 +20,7 @@ final class OnboardingDataProvider
argument: null,
retry: null,
name: r'onboardingDataProvider',
isAutoDispose: true,
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@ -41,7 +41,7 @@ final class OnboardingDataProvider
}
}
String _$onboardingDataHash() => r'78ff3d131a0d60e620aad25f25e1ee175d06aa89';
String _$onboardingDataHash() => r'639bee078cad2141ddbcb7e802af999a609dee01';
abstract class _$OnboardingData extends $Notifier<Map<String, dynamic>> {
Map<String, dynamic> build();

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 '../../../../core/constants/app_constants.dart';
@ -214,9 +215,10 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Equipment Setup'),
title: Text(l10n.setupEquipmentTitle),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/onboarding/strength-test'),
@ -235,17 +237,17 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
),
const SizedBox(height: 32),
Text(
'Equipment Inventory',
l10n.setupInventoryTitle,
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 8),
Text(
'Tell us what equipment you have available',
l10n.setupInventorySubtitle,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
Text(
'Barbell Weight',
l10n.inventoryBarbellWeight,
style: Theme.of(context)
.textTheme
.titleLarge
@ -272,7 +274,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
),
const SizedBox(height: 32),
Text(
'Quick Presets',
l10n.inventoryPresets,
style: Theme.of(context)
.textTheme
.titleLarge
@ -290,24 +292,29 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildPresetButton('Home Gym', 'home'),
_buildPresetButton(l10n.inventoryPresetHome, 'home'),
const SizedBox(height: 8),
_buildPresetButton('Commercial', 'commercial'),
_buildPresetButton(
l10n.inventoryPresetCommercial, 'commercial'),
const SizedBox(height: 8),
_buildPresetButton('Minimal', 'minimal'),
_buildPresetButton(
l10n.inventoryPresetMinimal, 'minimal'),
],
);
} else {
return Row(
children: [
Expanded(child: _buildPresetButton('Home Gym', 'home')),
Expanded(
child: _buildPresetButton(
l10n.inventoryPresetHome, 'home')),
const SizedBox(width: 8),
Expanded(
child:
_buildPresetButton('Commercial', 'commercial')),
child: _buildPresetButton(
l10n.inventoryPresetCommercial, 'commercial')),
const SizedBox(width: 8),
Expanded(
child: _buildPresetButton('Minimal', 'minimal')),
child: _buildPresetButton(
l10n.inventoryPresetMinimal, 'minimal')),
],
);
}
@ -315,7 +322,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
),
const SizedBox(height: 32),
Text(
'Available Plates',
l10n.inventoryPlates,
style: Theme.of(context)
.textTheme
.titleLarge
@ -335,7 +342,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
}).toList(),
const SizedBox(height: 32),
Text(
'Resistance Bands (Assistance)',
l10n.inventoryBands,
style: Theme.of(context)
.textTheme
.titleLarge
@ -343,7 +350,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
),
const SizedBox(height: 8),
Text(
'Select bands you have for pullup/dip assistance',
l10n.setupBandsSubtitle,
style: Theme.of(context)
.textTheme
.bodySmall
@ -399,7 +406,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
strokeWidth: 2,
),
)
: const Text('NEXT STEP'),
: Text(l10n.nextStepButton),
),
],
),

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.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 '../../../../shared/domain/logic/wendler_calculator.dart';
@ -183,9 +184,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Strength Test'),
title: Text(l10n.strengthTestTitle),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/onboarding/bodyweight'),
@ -206,17 +208,17 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
),
const SizedBox(height: 32),
Text(
'Combat Calibration',
l10n.strengthTestSubtitle,
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 8),
Text(
'We need to assess your current power level to assign the correct monsters.',
l10n.strengthTestBody,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
_ExerciseCard(
title: 'Leg Strength',
title: l10n.strengthLegs,
exerciseName: 'Back Squat',
icon: Icons.accessibility_new,
weightController: _squatWeightController,
@ -228,7 +230,7 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
),
const SizedBox(height: 16),
_AdaptiveExerciseCard(
slotTitle: 'Pull Strength',
slotTitle: l10n.strengthPull,
primaryName: 'Weighted Pull-up',
secondaryName: 'Pendlay Row',
icon: Icons.north,
@ -265,7 +267,7 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
),
const SizedBox(height: 16),
_AdaptiveExerciseCard(
slotTitle: 'Push Strength',
slotTitle: l10n.strengthPush,
primaryName: 'Weighted Dip',
secondaryName: 'Bench Press',
icon: Icons.south,
@ -317,7 +319,7 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
const SizedBox(width: 12),
Expanded(
child: Text(
'Your "Training Max" (TM) is your base combat power (90% of 1RM). For bodyweight exercises, we adjust the strategy.',
l10n.tmExplanation,
style: Theme.of(context)
.textTheme
.bodySmall
@ -330,7 +332,7 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
const SizedBox(height: 32),
ElevatedButton(
onPressed: _handleContinue,
child: const Text('CONTINUE'),
child: Text(l10n.continueButton),
),
],
),
@ -366,6 +368,7 @@ class _ExerciseCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
@ -408,8 +411,9 @@ class _ExerciseCard extends StatelessWidget {
RegExp(r'^\d+\.?\d{0,2}'))
],
decoration: InputDecoration(
labelText:
isBodyweight ? 'Add. Weight (kg)' : 'Weight (kg)',
labelText: isBodyweight
? l10n.addWeightLabel
: l10n.weightLabel,
isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
@ -421,8 +425,8 @@ class _ExerciseCard extends StatelessWidget {
controller: repsController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration:
const InputDecoration(labelText: 'Reps', isDense: true),
decoration: InputDecoration(
labelText: l10n.repsLabel, isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
@ -479,6 +483,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
@ -496,7 +501,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
children: [
Row(
children: [
Text('Can do 1 rep?',
Text(l10n.canDoOneRep,
style: TextStyle(
fontSize: 12,
color: isCapable
@ -513,7 +518,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('Assisted (Bands)?',
Text(l10n.isAssisted,
style: TextStyle(
fontSize: 12,
color: isAssisted
@ -610,6 +615,7 @@ class _ResultBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@ -619,7 +625,7 @@ class _ResultBox extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Est. 1RM'),
Text(l10n.est1rm),
Text('${rm.toStringAsFixed(1)} kg',
style: Theme.of(context).textTheme.bodyLarge),
],
@ -628,7 +634,7 @@ class _ResultBox extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Training Max (90%)'),
Text(l10n.trainingMaxLabel),
Text('${tm.toStringAsFixed(1)} kg',
style: const TextStyle(
color: AppTheme.primaryColor,

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/shared/data/local/tables.dart';
import '../../../../core/theme/app_theme.dart';
@ -177,10 +178,11 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
Widget build(BuildContext context) {
final cycleRepo = ref.watch(cycleRepositoryProvider);
final userRepo = ref.watch(userRepositoryProvider);
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Statistics & Cycles'),
title: Text(l10n.statsTitle),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'),
@ -235,7 +237,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
const SizedBox(height: 24),
],
Text(
'Progress Analysis',
l10n.statsProgressAnalysis,
style: Theme.of(context)
.textTheme
.titleLarge
@ -247,7 +249,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
child: Row(
children: [
_FilterChip(
label: 'Squat',
label: l10n.exerciseSquat,
isSelected: _selectedExercise == 'squat',
onTap: () => _onFilterChanged('squat', _selectedRange),
),
@ -297,19 +299,20 @@ class _CurrentCycleCard extends StatelessWidget {
final variants = user.exerciseVariants ?? {};
final pullVariant = variants['pull'] ?? 'pullup';
final pushVariant = variants['push'] ?? 'dip';
final l10n = AppLocalizations.of(context)!;
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;
}
@ -325,7 +328,7 @@ class _CurrentCycleCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'CYCLE ${cycle.cycleNumber}',
l10n.statsCycleTitle(cycle.cycleNumber),
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppTheme.primaryColor,
fontSize: 24,
@ -338,8 +341,8 @@ class _CurrentCycleCard extends StatelessWidget {
color: AppTheme.successColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'ACTIVE',
child: Text(
l10n.hubActiveYes,
style: TextStyle(
color: AppTheme.successColor,
fontWeight: FontWeight.bold,
@ -349,11 +352,12 @@ class _CurrentCycleCard extends StatelessWidget {
],
),
const Divider(height: 32),
Text('Current Training Maxes (TM)',
Text(l10n.statsCurrentTM,
style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 16),
_StatRow(
label: 'Squat', value: '${tms['squat'].toStringAsFixed(2)} kg'),
label: l10n.exerciseSquat,
value: '${tms['squat'].toStringAsFixed(2)} kg'),
_StatRow(
label: getLabel(pullVariant),
value: '${tms['pullup'].toStringAsFixed(2)} kg'),
@ -366,7 +370,7 @@ class _CurrentCycleCard extends StatelessWidget {
child: ElevatedButton.icon(
onPressed: onFinish,
icon: const Icon(Icons.upgrade),
label: const Text('FINISH CYCLE & LEVEL UP'),
label: Text(l10n.statsFinishCycle),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
@ -416,30 +420,32 @@ class _CycleFinishDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return AlertDialog(
title: const Text('Dungeon Cleared!'),
title: Text(l10n.statsCycleFinishedTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'You have defeated the guardians of this cycle. But deeper in the dungeon, stronger foes await...'),
Text(
l10n.statsCycleFinishedBody,
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
const Text('Your Training Maxes have increased:',
Text(l10n.statsTMIncreased,
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
_DiffRow(
name: 'Squat',
name: l10n.exerciseSquat,
oldVal: (oldTMs['squat'] as num).toDouble(),
newVal: (newTMs['squat'] as num).toDouble()),
_DiffRow(
name: 'Pull-up',
name: l10n.exercisePullup,
oldVal: (oldTMs['pullup'] as num).toDouble(),
newVal: (newTMs['pullup'] as num).toDouble()),
_DiffRow(
name: 'Dip',
name: l10n.exerciseDip,
oldVal: (oldTMs['dip'] as num).toDouble(),
newVal: (newTMs['dip'] as num).toDouble()),
],
@ -447,7 +453,7 @@ class _CycleFinishDialog extends StatelessWidget {
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('ENTER NEXT LEVEL'),
child: Text(l10n.statsEnterNextLevel),
),
],
);

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 '../../../../shared/domain/models/exercise_guide.dart';
@ -11,9 +12,10 @@ class ExerciseGuideSheet extends StatelessWidget {
Widget build(BuildContext context) {
String lookupId = exerciseId;
if (exerciseId.contains('kb_snatch')) lookupId = 'kb_snatch';
// Weitere Mappings hier falls nötig...
final guide = exerciseLibrary[lookupId];
final l10n = AppLocalizations.of(context)!;
final library = ExerciseGuide.getLibrary(l10n);
final guide = library[exerciseId];
if (guide == null) {
return Container(

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 'dart:async';
import '../../../../core/constants/asset_paths.dart';
@ -379,10 +380,12 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
}
}
if (totalVolume > 0)
if (totalVolume > 0) {
await questService.reportEvent(QuestTrigger.volume, data: totalVolume);
if (totalReps > 0)
}
if (totalReps > 0) {
await questService.reportEvent(QuestTrigger.repCount, data: totalReps);
}
if (widget.workoutId != null) {
final workoutRepo = ref.read(workoutRepositoryProvider);
@ -403,13 +406,14 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
ref.read(syncServiceProvider).sync();
}
}
final l10n = AppLocalizations.of(context)!;
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('RAID COMPLETE!'),
title: Text(l10n.battleRaidComplete),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -433,7 +437,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
Navigator.of(context).pop();
context.go('/hub');
},
child: const Text('BACK TO HUB'),
child: Text(l10n.battleBackToHub),
),
],
),
@ -442,12 +446,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
}
void _showLevelUpDialog(int oldLevel, int newLevel) {
final l10n = AppLocalizations.of(context)!;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.primaryColor,
title: const Text(
'LEVEL UP!',
title: Text(
l10n.levelUpTitle,
style: TextStyle(color: Colors.black),
),
content: Column(
@ -456,7 +461,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
const Icon(Icons.military_tech, size: 80, color: Colors.black),
const SizedBox(height: 16),
Text(
'You have grown stronger!',
l10n.levelUpBody,
style: Theme.of(context)
.textTheme
.bodyMedium
@ -465,7 +470,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
),
const SizedBox(height: 8),
Text(
'The monsters tremble at your new power.',
l10n.levelUpSubtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.black54, fontStyle: FontStyle.italic),
textAlign: TextAlign.center,
@ -483,8 +488,8 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child:
const Text('CONTINUE', style: TextStyle(color: Colors.black)),
child: Text(l10n.continueButton,
style: TextStyle(color: Colors.black)),
),
],
),
@ -504,6 +509,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (_isLoading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
@ -588,12 +594,12 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Abandon Raid?'),
content: const Text('Your progress will not be saved.'),
title: Text(l10n.battleAbandonTitle),
content: Text(l10n.battleAbandonBody),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('CANCEL'),
child: Text(l10n.cancelButton),
),
TextButton(
onPressed: () {
@ -602,7 +608,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
},
style: TextButton.styleFrom(
foregroundColor: AppTheme.errorColor),
child: const Text('ABANDON'),
child: Text(l10n.abandonButton),
),
],
),
@ -638,20 +644,9 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
}
Widget _buildRestScreen(Map<String, dynamic> inventory) {
// WorkoutSet? nextSet;
// Exercise? nextExerciseInfo;
// if (_currentSetIndex + 1 < _exercises[_currentExerciseIndex].sets.length) {
// nextExerciseInfo = _exercises[_currentExerciseIndex];
// nextSet = nextExerciseInfo.sets[_currentSetIndex + 1];
// } else if (_currentExerciseIndex + 1 < _exercises.length) {
// nextExerciseInfo = _exercises[_currentExerciseIndex + 1];
// if (nextExerciseInfo.sets.isNotEmpty) {
// nextSet = nextExerciseInfo.sets.first;
// }
// }
final nextExerciseInfo = _exercises[_currentExerciseIndex];
final nextSet = nextExerciseInfo.sets[_currentSetIndex];
final l10n = AppLocalizations.of(context)!;
return Container(
decoration: const BoxDecoration(
@ -672,7 +667,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'REST',
l10n.battleRest,
style: Theme.of(context).textTheme.displayLarge,
),
const SizedBox(height: 20),
@ -705,14 +700,15 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
const SizedBox(height: 20),
ElevatedButton(
onPressed: _skipRest,
child: const Text('SKIP REST'),
child: Text(l10n.battleSkipRest),
),
if (nextSet != null && nextExerciseInfo != null) ...[
const SizedBox(height: 24),
const Divider(color: Colors.white10, endIndent: 32, indent: 32),
const SizedBox(height: 12),
Text(
'UP NEXT: ${nextExerciseInfo.exerciseName.toUpperCase()}',
l10n.battleUpNext(
nextExerciseInfo.exerciseName.toUpperCase()),
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
@ -797,6 +793,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
const Shadow(color: Colors.black, blurRadius: 8, offset: Offset(0, 2))
],
);
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
@ -826,7 +823,8 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
border: Border.all(color: Colors.white24),
),
child: Text(
'WAVE ${_currentExerciseIndex + 1} / ${_exercises.length}',
l10n.battleWave(
_currentExerciseIndex + 1, _exercises.length),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
@ -893,7 +891,8 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
_showExerciseGuide(currentExercise.exerciseId),
),
Text(
'Set ${_currentSetIndex + 1} of ${currentExercise.sets.length}',
l10n.battleSet(_currentSetIndex + 1,
currentExercise.sets.length),
style: Theme.of(context)
.textTheme
.titleMedium
@ -904,10 +903,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_InfoBox(
label: 'WEIGHT',
label: l10n.battleWeight,
value: '${currentSet.targetWeightTotal} kg'),
_InfoBox(
label: 'REPS',
label: l10n.battleReps,
value:
'${currentSet.repsTarget}${currentSet.isAmrap ? "+" : ""}'),
],
@ -932,7 +931,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('ASSISTANCE',
Text(l10n.battleAssistance,
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 12,
@ -981,7 +980,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('COMPLETE SET',
child: Text(l10n.battleCompleteSet,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -1002,6 +1001,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
void _showAmrapDialog(WorkoutSet set) {
int tempReps = set.repsTarget;
final l10n = AppLocalizations.of(context)!;
showModalBottomSheet(
context: context,
@ -1017,16 +1017,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'🔥 AMRAP RESULT 🔥',
Text(
l10n.amrapResultTitle,
style: TextStyle(
color: AppTheme.secondaryColor,
fontWeight: FontWeight.bold,
fontSize: 20),
),
const SizedBox(height: 8),
const Text(
'Go all out! How many did you get?',
Text(
l10n.amrapResultBody,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 32),
@ -1070,7 +1070,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
),
child: const Text('CONFIRM RESULT',
child: Text(l10n.amrapConfirm,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
),
@ -1249,6 +1249,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
void _showEmomFinishDialog() {
final currentEx = _exercises[_currentExerciseIndex];
int setsCount = currentEx.sets.length;
final l10n = AppLocalizations.of(context)!;
showModalBottomSheet(
context: context,
@ -1269,7 +1270,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
size: 48, color: AppTheme.primaryColor),
const SizedBox(height: 16),
Text(
'MISSION ACCOMPLISHED',
l10n.emomFinishedTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
@ -1277,8 +1278,8 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
),
),
const SizedBox(height: 8),
const Text(
'Time is up. Did you push further?',
Text(
l10n.emomFinishedBody,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 32),
@ -1304,7 +1305,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
color: Colors.white,
),
),
const Text('SETS COMPLETED',
Text(l10n.emomSetsCompleted,
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 10,
@ -1334,7 +1335,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
backgroundColor: AppTheme.successColor,
foregroundColor: Colors.white,
),
child: const Text('CONFIRM & FINISH',
child: Text(l10n.emomConfirm,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
),

View file

@ -1,9 +1,9 @@
import '../logic/wendler_calculator.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
class ExerciseGuide {
final String title;
final String difficulty; // z.B. "Novice", "Adept", "Master"
final String rpgLore; // Ein kleiner Flavor-Text
final String difficulty;
final String rpgLore;
final List<String> steps;
final List<String> muscles;
final List<String> commonMistakes;
@ -16,218 +16,133 @@ class ExerciseGuide {
required this.muscles,
required this.commonMistakes,
});
}
final Map<String, ExerciseGuide> exerciseLibrary = {
'pullup': const ExerciseGuide(
title: 'Weighted Pull-Up',
static List<String> _split(String input) {
return input.split('|').map((e) => e.trim()).toList();
}
static Map<String, ExerciseGuide> getLibrary(AppLocalizations l10n) {
return {
'pullup': ExerciseGuide(
title: l10n.guidePullupTitle,
difficulty: 'Adept',
rpgLore:
'Den Körper gegen die Schwerkraft zu ziehen, ist der ultimative Beweis für Oberkörperkraft.',
steps: [
'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.',
],
muscles: ['Latissimus', 'Bizeps', 'Unterarme'],
commonMistakes: [
'Schwingen (Kipping)',
'Halbe Wiederholungen',
'Schultern hochgezogen lassen'
],
rpgLore: l10n.guidePullupLore,
steps: _split(l10n.guidePullupSteps),
muscles: _split(l10n.guidePullupMuscles),
commonMistakes: _split(l10n.guidePullupMistakes),
),
'dip': const ExerciseGuide(
title: 'Weighted Dip',
'dip': ExerciseGuide(
title: l10n.guideDipTitle,
difficulty: 'Adept',
rpgLore: 'Ein fundamentaler Druck-Move, um Mauern zu überwinden.',
steps: [
'Stütze dich auf die Barren, Arme gestreckt.',
'Lehne dich leicht nach vorne für mehr Brust-Fokus.',
'Senke den Körper ab, bis die Schultern unter den Ellbogen sind.',
'Drücke dich explosiv zurück in die Ausgangsposition.',
],
muscles: ['Brust', 'Trizeps', 'Vordere Schulter'],
commonMistakes: ['Zu wenig Tiefe', 'Ellbogen wandern zu weit nach außen'],
rpgLore: l10n.guideDipLore,
steps: _split(l10n.guideDipSteps),
muscles: _split(l10n.guideDipMuscles),
commonMistakes: _split(l10n.guideDipMistakes),
),
'squat': const ExerciseGuide(
title: 'Low Bar Back Squat',
'squat': ExerciseGuide(
title: l10n.guideSquatTitle,
difficulty: 'Master',
rpgLore:
'Die Mutter aller Schlachten. Trainiert den gesamten Körperpanzer.',
steps: [
'Lege die Hantel auf dem hinteren Deltamuskel ab (nicht im Nacken).',
'Füße schulterbreit, Zehen leicht nach außen.',
'Atme tief ein (Bracing) und schiebe die Hüfte nach hinten.',
'Gehe in die Hocke (Hüfte unter Kniehöhe).',
'Drücke dich aus der Ferse/Mittelfuß wieder hoch.',
],
muscles: ['Quadrizeps', 'Gesäß', 'Core', 'Rückenstrecker'],
commonMistakes: [
'Knie fallen nach innen',
'Rücken rundet ein',
'Zu wenig Tiefe'
],
rpgLore: l10n.guideSquatLore,
steps: _split(l10n.guideSquatSteps),
muscles: _split(l10n.guideSquatMuscles),
commonMistakes: _split(l10n.guideSquatMistakes),
),
'bench': const ExerciseGuide(
title: 'Bench Press',
'bench': ExerciseGuide(
title: l10n.guideBenchTitle,
difficulty: 'Novice',
rpgLore: 'Der Standard-Test für reine Druckkraft.',
steps: [
'Lege dich auf die Bank, Augen unter der Stange.',
'Füße fest am Boden, leichter Bogen im Rücken (Brücke).',
'Senke die Hantel kontrolliert zur unteren Brust.',
'Drücke die Hantel explosiv nach oben.',
],
muscles: ['Brust', 'Trizeps', 'Vordere Schulter'],
commonMistakes: [
'Ellbogen 90° abgespreizt (Verletzungsgefahr)',
'Hintern hebt ab'
],
rpgLore: l10n.guideBenchLore,
steps: _split(l10n.guideBenchSteps),
muscles: _split(l10n.guideBenchMuscles),
commonMistakes: _split(l10n.guideBenchMistakes),
),
'ohp': const ExerciseGuide(
title: 'Overhead Press',
'ohp': ExerciseGuide(
title: l10n.guideOhpTitle,
difficulty: 'Adept',
rpgLore: 'Ein Objekt über den Kopf zu stemmen erfordert pure Stabilität.',
steps: [
'Stange liegt auf dem vorderen Schultermuskel.',
'Fester Stand, Gesäß und Bauch maximal anspannen.',
'Kopf leicht zurücknehmen, Stange vertikal nach oben drücken.',
'Oben den Kopf "durch das Fenster" der Arme schieben.',
],
muscles: ['Schultern', 'Trizeps', 'Core'],
commonMistakes: ['Hohlkreuz (Rücklage)', 'Beine helfen mit (Push Press)'],
rpgLore: l10n.guideOhpLore,
steps: _split(l10n.guideOhpSteps),
muscles: _split(l10n.guideOhpMuscles),
commonMistakes: _split(l10n.guideOhpMistakes),
),
'rdl': const ExerciseGuide(
title: 'Romanian Deadlift',
'rdl': ExerciseGuide(
title: l10n.guideRdlTitle,
difficulty: 'Adept',
rpgLore: 'Baut die hintere Kette auf, essenziell für Stabilität.',
steps: [
'Startposition stehend mit Hantel.',
'Schiebe die Hüfte weit nach hinten, Beine bleiben fast gestreckt.',
'Senke die Hantel am Bein entlang bis knapp unter das Knie.',
'Spüre den Zug im Beinbeuger und richte dich wieder auf.',
],
muscles: ['Beinbeuger (Hamstrings)', 'Gesäß', 'Unterer Rücken'],
commonMistakes: ['Rücken rundet ein', 'Hantel zu weit vom Körper weg'],
rpgLore: l10n.guideRdlLore,
steps: _split(l10n.guideRdlSteps),
muscles: _split(l10n.guideRdlMuscles),
commonMistakes: _split(l10n.guideRdlMistakes),
),
'row': const ExerciseGuide(
title: 'Pendlay Row',
'row': ExerciseGuide(
title: l10n.guideRowTitle,
difficulty: 'Adept',
rpgLore: 'Explosive Zugkraft vom Boden. Für einen starken Rücken.',
steps: [
'Oberkörper parallel zum Boden, Rücken gerade.',
'Hantel liegt bei jeder Wiederholung tot auf dem Boden.',
'Ziehe die Hantel explosiv zum unteren Brustbein.',
'Kontrolliert ablegen, Spannung kurz lösen, neu ansetzen.',
],
muscles: ['Latissimus', 'Trapez', 'Hintere Schulter'],
commonMistakes: ['Oberkörper richtet sich auf', 'Reißen mit Schwung'],
rpgLore: l10n.guideRowLore,
steps: _split(l10n.guideRowSteps),
muscles: _split(l10n.guideRowMuscles),
commonMistakes: _split(l10n.guideRowMistakes),
),
'curl': const ExerciseGuide(
title: 'Barbell Curl',
'curl': ExerciseGuide(
title: l10n.guideCurlTitle,
difficulty: 'Novice',
rpgLore: 'Isolierte Kraft für den finalen Schlag.',
steps: [
'Stehender Stand, Hantel im Untergriff.',
'Ellbogen bleiben fixiert am Körper.',
'Hantel zur Brust curlen, oben kurz halten.',
'Langsam ablassen.',
],
muscles: ['Bizeps'],
commonMistakes: ['Schwingen aus der Hüfte', 'Ellbogen wandern nach vorne'],
rpgLore: l10n.guideCurlLore,
steps: _split(l10n.guideCurlSteps),
muscles: _split(l10n.guideCurlMuscles),
commonMistakes: _split(l10n.guideCurlMistakes),
),
'kb_swing': const ExerciseGuide(
title: 'Kettlebell Swing',
'kb_swing': ExerciseGuide(
title: l10n.guideKbSwingTitle,
difficulty: 'Adept',
rpgLore: 'Ballistische Kraft und Ausdauer. Die Hüfte ist der Motor.',
steps: [
'Hüftbreiter Stand, KB vor dir am Boden.',
'Hike-Pass: Ziehe die KB durch die Beine nach hinten.',
'Hüfte explosiv strecken (Snap!), KB fliegt durch Hüftkraft auf Brusthöhe.',
'KB kontrolliert zurückschwingen lassen.',
],
muscles: ['Gesäß', 'Beinbeuger', 'Core', 'Ausdauer'],
commonMistakes: ['Kniebeuge statt Hüftbeuge', 'Arme heben das Gewicht'],
rpgLore: l10n.guideKbSwingLore,
steps: _split(l10n.guideKbSwingSteps),
muscles: _split(l10n.guideKbSwingMuscles),
commonMistakes: _split(l10n.guideKbSwingMistakes),
),
'kb_snatch': const ExerciseGuide(
title: 'Kettlebell Snatch',
'kb_snatch': ExerciseGuide(
title: l10n.guideKbSnatchTitle,
difficulty: 'Master',
rpgLore: 'Der Zar der Kettlebell-Übungen. Totale Körperkontrolle.',
steps: [
'Starte wie beim Swing (Einarmig).',
'Hüftkraft beschleunigt die Kugel nach oben.',
'Bei Kopfhöhe: Durchstoßen der Hand ("Punch through").',
'Sanftes Auffangen im Lockout über Kopf.',
],
muscles: ['Gesamter Körper', 'Schultern', 'Griffkraft'],
commonMistakes: ['Kugel knallt auf den Unterarm', 'Zu wenig Hüftkraft'],
rpgLore: l10n.guideKbSnatchLore,
steps: _split(l10n.guideKbSnatchSteps),
muscles: _split(l10n.guideKbSnatchMuscles),
commonMistakes: _split(l10n.guideKbSnatchMistakes),
),
'kb_thruster': const ExerciseGuide(
title: 'Kettlebell Thruster',
'kb_thruster': ExerciseGuide(
title: l10n.guideKbThrusterTitle,
difficulty: 'Master',
rpgLore: 'Eine brutale Kombination aus Squat und Press.',
steps: [
'KB in der Rack-Position (vor der Brust).',
'Tiefe Kniebeuge.',
'Beim Aufstehen den Schwung nutzen, um KB über Kopf zu drücken.',
'Zurück in die Rack-Position beim Absenken in den nächsten Squat.',
],
muscles: ['Beine', 'Schultern', 'Lunge (Cardio)'],
commonMistakes: [
'Pause zwischen Squat und Press',
'Rücken rundet im Squat'
],
rpgLore: l10n.guideKbThrusterLore,
steps: _split(l10n.guideKbThrusterSteps),
muscles: _split(l10n.guideKbThrusterMuscles),
commonMistakes: _split(l10n.guideKbThrusterMistakes),
),
'kb_clean_press': const ExerciseGuide(
title: 'KB Clean & Press',
'kb_clean_press': ExerciseGuide(
title: l10n.guideKbCleanPressTitle,
difficulty: 'Adept',
rpgLore: 'Zwei Bewegungen in Harmonie.',
steps: [
'Clean: Ziehe die KB vom Boden in die Rack-Position vor der Brust.',
'Press: Drücke sie strikt über den Kopf.',
'Senke sie in Rack, dann zum Boden (oder Swing).',
],
muscles: ['Schultern', 'Rücken', 'Beine'],
commonMistakes: ['Clean knallt auf Arm', 'Hohlkreuz beim Press'],
rpgLore: l10n.guideKbCleanPressLore,
steps: _split(l10n.guideKbCleanPressSteps),
muscles: _split(l10n.guideKbCleanPressMuscles),
commonMistakes: _split(l10n.guideKbCleanPressMistakes),
),
'face_pull': const ExerciseGuide(
title: 'Band Face Pull',
'face_pull': ExerciseGuide(
title: l10n.guideFacePullTitle,
difficulty: 'Novice',
rpgLore: 'Schützt die Schultern vor dem Verschleiß des Kampfes.',
steps: [
'Band auf Kopfhöhe befestigen.',
'Ziehe das Band zum Gesicht (Richtung Stirn/Augen).',
'Ellbogen hoch und weit nach außen ziehen.',
'Schulterblätter hinten zusammenkneifen.',
],
muscles: ['Hintere Schulter', 'Rotatorenmanschette'],
commonMistakes: ['Ellbogen zu tief', 'Kopf nach vorne schieben'],
rpgLore: l10n.guideFacePullLore,
steps: _split(l10n.guideFacePullSteps),
muscles: _split(l10n.guideFacePullMuscles),
commonMistakes: _split(l10n.guideFacePullMistakes),
),
'ab_wheel': const ExerciseGuide(
title: 'Ab Wheel Rollout',
'ab_wheel': ExerciseGuide(
title: l10n.guideAbWheelTitle,
difficulty: 'Adept',
rpgLore: 'Ein Stahlkern, der jeden Treffer absorbiert.',
steps: [
'Knie am Boden, Rad vor den Knien.',
'Rolle nach vorne, halte den Rücken rund/stabil (Hollow Body).',
'Gehe nur so weit, wie du den Rücken stabil halten kannst.',
'Ziehe dich aus dem Bauchmuskel zurück.',
],
muscles: ['Core (Anti-Extension)'],
commonMistakes: ['Hohlkreuz (Gefährlich!)', 'Ziehen aus den Armen'],
rpgLore: l10n.guideAbWheelLore,
steps: _split(l10n.guideAbWheelSteps),
muscles: _split(l10n.guideAbWheelMuscles),
commonMistakes: _split(l10n.guideAbWheelMistakes),
),
'plank': const ExerciseGuide(
title: 'Plank',
'plank': ExerciseGuide(
title: l10n.guidePlankTitle,
difficulty: 'Novice',
rpgLore: 'Unbeweglich wie ein Fels in der Brandung.',
steps: [
'Unterarmstütz, Körper bildet eine Linie.',
'Gesäß und Bauch fest anspannen.',
'Schulterblätter auseinanderdrücken.',
'Atmen nicht vergessen!',
],
muscles: ['Core'],
commonMistakes: ['Hüfte hängt durch', 'Gesäß zu hoch'],
rpgLore: l10n.guidePlankLore,
steps: _split(l10n.guidePlankSteps),
muscles: _split(l10n.guidePlankMuscles),
commonMistakes: _split(l10n.guidePlankMistakes),
),
};
};
}
}