Frontend Strategy

Design Philosophy: Effortless UX

The Membership platform is built for non-technical users: club volunteers, small studio owners, personal trainers, and sports enthusiasts. The frontend design philosophy is rooted in the "Effortless UX" principles, which prioritize simplicity, guidance, and emotional connection over feature density and technical vocabulary.

The "Effortless UX" philosophy acknowledges that studio operators and club administrators have minimal time and willingness to engage with management software. Every interaction must be self-explanatory, require zero training, and complete in the fewest possible steps. Nobody wants to spend time with the software -- they want to spend time with their members, their classes, and their business. The platform must be invisible: it works, it is obvious, and it never makes the user feel stupid or lost.

Core Principles

1. Goal and Task Orientation

Every screen answers the question "What do you want to do?" rather than exposing the underlying data model. Users navigate by intent (e.g., "Add a new member", "View this month's payments") rather than by system concept (e.g., "Consumer entity management", "Transaction ledger"). The dashboard presents action tiles, not data tables.

2. Guided Wizards

Multi-step processes use wizard flows with clear progress indicators ("Step 2 of 4"). Each step collects only the minimum required information. Optional fields are hidden behind an "Advanced" expander. The wizard validates each step before allowing progression and provides a summary review before final confirmation.

3. Simple Language

The UI uses everyday language, not technical jargon. "Not yet paid" instead of "Payment status: pending". "Your membership" instead of "Active contract instance". "Add bank details" instead of "Create SEPA mandate". All labels, tooltips, and error messages are written at a reading level accessible to a 14-year-old.

4. Contextual Help

Every complex screen includes embedded help: tooltips on form fields, info banners explaining the purpose of the screen, and a floating chat/help button for deeper questions. First-time users see brief callout overlays highlighting key actions ("Tap here to add your first member").

5. Use-Case Navigation

The primary navigation is tile-based, organized by use case: "Members", "Billing", "Check-in", "Events". Frequently used actions are pinned as favorites. Recent actions are accessible from a "Recent" section on the dashboard. Deep-linked search allows jumping directly to any entity by name or ID.

6. Role-Based Simplification

Members see only what is relevant to them. The same data (e.g., a contract) is presented differently based on the role: a member sees "Your Fitness Membership -- 29.90/month", while an admin sees "CsrContract #4521 -- CsrContractPreDefined 'Premium Monthly' -- SEPA recurring, next billing 2026-03-01". The system hides complexity from those who do not need it.

7. Learning Support

New users are guided through onboarding with interactive tutorials. A "training mode" allows admins to explore the system with sample data before going live. Tooltips adapt: they show detailed explanations for the first three uses of a feature, then shrink to a brief reminder, then disappear (unless hovered). Video tutorials are linked from each help section.

8. Error Friendliness

Errors are opportunities, not punishments. Destructive actions require confirmation with a clear description of what will happen. Undo is available for common operations (member deletion is a soft archive with a 30-day recovery window). Error messages explain what went wrong and suggest what to do next ("The IBAN is invalid. Please check that you entered all digits correctly. Example: DE89 3704 0044 0532 0130 00").

9. System Support and Automation

The system helps wherever possible: autocomplete for addresses, IBAN formatting and bank name lookup, suggested next actions after completing a task ("Member created -- would you like to assign a membership now?"), and batch operations for repetitive tasks. Date pickers default to sensible values (next month for billing, today for start date).

10. Vibe Design

The UI creates an emotional connection through aesthetics, micro-animations, and storytelling. Success states use celebratory feedback (checkmark animation, color pulse). Loading states use branded skeleton screens instead of spinners. Illustrations and iconography reflect the sports/fitness domain. The overall experience feels warm, professional, and encouraging -- not clinical or bureaucratic.

Quality Checklist

Every screen and feature must pass this checklist before release:

# Criterion Question Pass Condition
1 Goal orientation Can the user identify what they can do on this screen within 3 seconds? Clear heading, action buttons visible above the fold
2 Simple language Are all labels, messages, and tooltips free of technical jargon? No statusCd, idMcEntity, or internal terms visible
3 Guided workflows Do multi-step processes have a progress indicator and step validation? Wizard pattern with "Step X of Y"
4 Contextual help Does the screen offer help without requiring the user to leave? At least one tooltip or help link per complex section
5 Use-case navigation Can the user reach this screen from a task-oriented navigation path? Reachable from dashboard tile or quick-action menu
6 Role-based simplification Does the screen adapt to the user's role? No admin-only elements visible to members
7 Learning support Is there onboarding guidance for first-time users? Callout or tutorial for new features
8 Error friendliness Do error states explain the problem and suggest a fix? Error messages include "what happened" + "what to do"
9 System support Does the system assist with autocomplete, defaults, or suggestions? At least one automation per data entry screen
10 Vibe design Does the screen feel polished, warm, and domain-appropriate? Consistent branding, no raw data dumps, pleasant transitions

UX/UI Design Principles

The Membership One platform targets non-technical users -- club volunteers, studio owners, trainers, and sports enthusiasts -- who have minimal tolerance for confusing interfaces and zero interest in reading manuals. The Effortless UX philosophy (defined above) provides the what; this section provides the why by grounding every design decision in established perceptual psychology and interaction design laws. Applying these principles systematically ensures that the platform feels intuitive, reduces cognitive burden, and guides users toward successful task completion without explicit instruction.

Every designer, developer, and QA tester on the team should internalize these ten principles. They are not abstract theory -- each one maps directly to concrete UI patterns used throughout Membership One.

1. Law of Proximity (Gesetz der Naehe)

Description: Elements placed close to each other are perceived as belonging together. The brain groups nearby items into a single unit before consciously analyzing them. Whitespace between groups signals separation.

Application in Membership One:

  • Member detail card: Name, email, and phone are clustered together in the top section of the member detail view. Billing information (IBAN, mandate reference, billing cycle) is separated by whitespace into its own card below. The user instantly perceives "personal info" and "financial info" as two distinct groups without needing a heading.
  • Form field grouping in wizards: The "Add Member" wizard groups related fields spatially: personal details (first name, last name, date of birth) in one cluster, address fields (street, city, postal code, country) in another, and contact fields (email, phone) in a third. Each cluster is separated by 24dp vertical spacing, while fields within a cluster use only 12dp spacing.
  • Dashboard tile clusters: The dashboard groups action tiles by domain: "Members" and "Check-in" tiles sit adjacent to each other (people-related), while "Billing" and "Payments" tiles form their own cluster (money-related). The spatial grouping communicates the relationship without labels.

Flutter Implementation Notes: Use consistent spacing tokens from spacing.dart. Within a group, use SizedBox(height: 12) between fields. Between groups, use SizedBox(height: 24) or a Divider. Wrap related fields in a Column with tight padding, and separate groups with larger padding or a visual container (Card).

2. Law of Similarity (Gesetz der Aehnlichkeit)

Description: Elements that share visual characteristics -- color, shape, size, or style -- are perceived as part of the same group, even if they are spatially separated. Similarity creates visual categories.

Application in Membership One:

  • Status chips: All status indicators across the platform use the same pill-shaped StatusChip widget with identical dimensions and border radius. The color varies (green for active, amber for pending, red for failed), but the consistent shape signals "this is a status indicator" regardless of context -- member list, transaction table, or contract detail.
  • Action buttons across list views: Every list screen (members, transactions, billing statements, events) uses the same button placement and styling for its primary action: a teal FloatingActionButton in the bottom-right corner for "Add new". The consistency means users learn the pattern once and apply it everywhere.
  • Membership plan cards: All membership plan options in the purchase wizard use identical card dimensions, typography hierarchy, and layout structure (title, price, feature list, select button). Only the content differs. The visual similarity allows the user to compare plans by content alone, without being distracted by layout differences.

Flutter Implementation Notes: Enforce similarity through shared widgets. The StatusChip widget, data_table.dart, and wizard_scaffold.dart in shared/widgets/ are the canonical implementations. Never create ad-hoc status indicators or list layouts in feature code. If a new pattern emerges, promote it to shared/ so it remains consistent.

3. Law of Closure (Gesetz der Geschlossenheit)

Description: The brain completes incomplete shapes when a visual boundary is implied. A partial border, background color, or container edge is sufficient for users to perceive a bounded region and treat its contents as a unit.

Application in Membership One:

  • Progress bars in wizards: The wizard progress bar uses a segmented track where completed steps are filled (primary color) and remaining steps are outlined. Even when only 2 of 5 segments are filled, the user perceives the entire bar as one element and understands the remaining work intuitively. The partial fill implies the "complete" shape and motivates completion.
  • Card containers: Member cards, billing statement cards, and event cards use a white surface with 12dp rounded corners and a subtle shadow. The visual boundary groups the card's content (name, status, action buttons) into a single perceived object, even though these elements are separate widgets. No explicit border line is needed -- the background contrast and shadow provide closure.
  • Circular billing status indicators: The billing dashboard shows a circular progress ring for collection progress (e.g., 78% of invoices paid). The incomplete circle implies the full 100% target. Users perceive the missing arc as "remaining work" without needing a legend.

Flutter Implementation Notes: Use Card with shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)) consistently. For progress indicators, use LinearProgressIndicator or CircularProgressIndicator with the primary color for the filled portion and Colors.grey.shade200 for the track. The contrast between filled and unfilled regions leverages closure.

4. Law of Continuity (Gesetz der Kontinuitaet)

Description: Elements arranged along a line, curve, or continuous path are perceived as related and following a direction. The eye follows the smoothest path and groups elements along it.

Application in Membership One:

  • Stepper/wizard flow: The wizard component connects its steps with a horizontal line. Completed steps show a filled circle on the line, the current step shows a highlighted circle, and future steps show hollow circles. The continuous line communicates "these steps are part of one process" and implies forward direction.
  • Member activity timeline: The member detail screen shows an activity history as a vertical timeline with a continuous line connecting events (contract signed, payment received, check-in recorded). The vertical line implies chronological continuity and helps users scan the history from newest to oldest.
  • Breadcrumb navigation: Admin screens use breadcrumbs (Dashboard > Members > Max Mustermann) connected by chevron separators. The horizontal arrangement and directional separators create a continuous path that communicates hierarchical depth and enables backtracking.
  • Billing cycle visualization: The billing flow shows the lifecycle of an invoice as a horizontal progression: Created > Exported > Sent to Bank > Settled (or Failed). The horizontal arrangement with arrows implies forward movement through the billing process.

Flutter Implementation Notes: Use the Stepper widget or a custom WizardScaffold with a Row of step indicators connected by Expanded(child: Divider()). For timelines, use a ListView with a CustomPaint drawing a vertical line alongside each item. Ensure the connecting line is visually prominent (2dp width, secondary color) so the continuity is unmistakable.

5. Priming

Description: Prior experiences and expectations shape how users perceive and interact with new interfaces. Users bring mental models from other apps they use daily. Aligning with these expectations reduces learning time; violating them creates confusion.

Application in Membership One:

  • Familiar navigation patterns: The bottom navigation bar with icon-and-label tabs follows the pattern established by Instagram, WhatsApp, and virtually every popular mobile app. Users do not need to learn where to find navigation -- it is where they expect it. The pull-to-refresh gesture on list screens follows the universal mobile convention.
  • Sports-themed visual language: Target users spend their days in gyms, studios, and sports facilities. The color palette (teal/navy) evokes health and professionalism. Illustrations show fitness equipment, team activities, and movement. This domain-specific priming makes the app feel purpose-built rather than generic.
  • SaaS onboarding patterns: The onboarding wizard (add your organization, invite your first member, set up billing) follows the same pattern used by Slack, Trello, and other SaaS tools that the admin may have encountered. The checklist-with-progress-bar pattern is a well-established SaaS convention that primes the user to expect a guided setup.

Flutter Implementation Notes: Use BottomNavigationBar (not a drawer) for primary navigation on mobile. Implement RefreshIndicator on all list screens. Place the search bar at the top of list screens (where users expect it from Google, app stores, etc.). Use Material Design 3 components wherever possible, as they carry Google's investment in user familiarity.

6. Cognitive Overload (Kognitive Ueberlastung)

Description: When users are presented with too much information or too many choices simultaneously, their ability to process, decide, and act degrades sharply. Working memory is limited to approximately 4-7 items. Exceeding this threshold leads to decision paralysis, errors, and abandonment.

Application in Membership One:

  • Wizard decomposition: The "Add Member" process collects up to 20 data points, but the wizard breaks this into 4 steps of 4-5 fields each. Each step fits within working memory limits. The user focuses on one cluster at a time (personal data, then contact, then membership, then review).
  • Dashboard constraint: The dashboard shows exactly 5-6 key metric tiles (total members, active contracts, pending payments, upcoming events, recent check-ins, overdue invoices). Additional metrics are accessible via a "View all" link but are never shown by default. The constraint prevents the dashboard from becoming an overwhelming data wall.
  • Progressive disclosure: Advanced settings (custom SEPA mandates, billing schedule overrides, DATEV mapping) are hidden behind an "Advanced" expander or a separate settings screen. Default users never encounter these options. Only users who actively seek them out will find them, and those users have the expertise to handle the complexity.
  • Role-based simplification: A member sees a clean view with 4 navigation tabs (Home, Membership, Payments, Profile). An admin sees 8-10 tabs. A franchise director sees a different set. Each role sees only what is relevant, preventing information overload.

Flutter Implementation Notes: Limit visible options using Visibility, ExpansionTile, or conditional rendering based on userRole. For dashboards, use a GridView with a maximum crossAxisCount of 2-3 columns. Never show more than 6 cards on the initial dashboard view. Use the Riverpod role provider to conditionally include navigation destinations.

7. Fitts' Law (Fitts' Gesetz)

Description: The time required to move to a target is a function of the distance to the target and the size of the target. Larger targets that are closer to the current cursor/finger position are faster and easier to reach. This has direct implications for button sizing and placement.

Application in Membership One:

  • Floating Action Button (FAB): The primary action on each screen (add member, create event, start check-in) uses a large FAB (56dp diameter) positioned in the bottom-right corner -- directly in the thumb zone for right-handed users (and reachable for left-handed users). Its size makes it the easiest target on the screen to hit.
  • Destructive action placement: Delete and cancel buttons are intentionally smaller than primary buttons (32dp height vs. 48dp), positioned away from the primary action, and require a confirmation dialog. This makes accidental destructive actions physically harder to trigger.
  • Bottom navigation bar: The most-used features are in the bottom navigation bar, which is always within thumb reach. The bar uses 48dp height with generous touch targets. Each tab has a minimum 44x44dp hit area, exceeding the 44dp minimum recommended by Apple and Google.
  • 44dp minimum touch targets: All interactive elements (buttons, checkboxes, list items, chips) have a minimum 44x44dp touch target area, even if the visual element is smaller. This is enforced via MaterialTapTargetSize.padded and ConstrainedBox wrappers.

Flutter Implementation Notes: Set materialTapTargetSize: MaterialTapTargetSize.padded in the app theme. Use SizedBox(height: 48) for primary buttons and SizedBox(height: 32) for secondary/destructive buttons. Place the FAB using Scaffold(floatingActionButton:) with floatingActionButtonLocation: FloatingActionButtonLocation.endFloat. Test all touch targets with Flutter's debugCheckIntrinsicSizes.

8. Hick's Law (Hick's Gesetz)

Description: The time to make a decision increases logarithmically with the number of available options. Fewer choices lead to faster decisions. When users must choose from too many options, they hesitate, make errors, or abandon the task entirely.

Application in Membership One:

  • Navigation tab limit: The bottom navigation bar contains a maximum of 5 tabs. Even though the platform has 10+ feature modules, they are grouped and nested so that the top-level navigation never exceeds 5 items (e.g., Home, Members, Billing, Events, More).
  • Membership plan selection: The plan selection screen shows a maximum of 3-4 plan cards. If the organization offers more plans, a "Show all plans" link loads the complete catalog. The initial constrained view helps 90% of users decide quickly.
  • Smart defaults: When creating a new member, the country defaults to the organization's country, the currency to EUR, and the start date to today. These defaults eliminate 3 decisions that the user would otherwise need to make. Only the truly variable fields (name, email, plan) require active input.
  • Quick actions: The dashboard offers 3-4 quick-action buttons ("Add Member", "View Payments", "Check-in", "New Event") rather than exposing all possible actions. The limited set reduces decision time for the most common tasks.

Flutter Implementation Notes: Use BottomNavigationBar with a maximum of 5 BottomNavigationBarItem entries. For plan selection, implement a ListView with a take(4) limit on the initial display. Set form field defaults in the TextEditingController initialization or the Riverpod state's initial value. For quick actions, hardcode the top 3-4 in the dashboard and allow user customization via a "Customize" option.

9. Zeigarnik Effect (Zeigarnik-Effekt)

Description: People remember uncompleted tasks better than completed ones. An interrupted or incomplete task creates cognitive tension that motivates the person to return and finish it. This can be leveraged in UI design to drive completion of multi-step processes.

Application in Membership One:

  • Onboarding checklist with progress: After initial setup, the admin dashboard shows an onboarding checklist: "3 of 5 steps completed" with a progress bar. Items like "Upload your logo", "Add your first member", and "Configure billing" remain visible with empty checkboxes until completed. The incomplete state creates a gentle urgency to finish setup.
  • Incomplete profile prompts: If a member has not added their bank details or verified their email, the profile screen shows a subtle banner: "Complete your profile -- 2 items remaining." The uncompleted items are listed with clear calls to action.
  • Abandoned wizard recovery: If a user starts the "Add Member" wizard and navigates away before completing it, the dashboard shows a persistent card: "Continue where you left off? You were adding a new member (Step 2 of 4)." The draft is saved locally, and the user can resume or discard.
  • Go-Live Checklist: The admin dashboard displays a Go-Live Checklist for new organizations (from Chapter 17): verify bank account, configure billing cycle, invite team members, test check-in. The checklist persists until all items are checked, motivating the admin to reach operational readiness.

Flutter Implementation Notes: Store wizard progress in SharedPreferences or flutter_secure_storage. Use a Riverpod provider to track onboarding completion state and expose it to the dashboard. The checklist widget should use CheckboxListTile with a LinearProgressIndicator at the top. Mark items as complete with an animated strikethrough (use AnimatedDefaultTextStyle with TextDecoration.lineThrough).

10. Emotional Design (Emotionale Gestaltung)

Description: Emotions profoundly influence how users perceive usability, remember experiences, and develop loyalty to a product. Positive emotional responses (delight, pride, accomplishment) increase tolerance for minor issues, improve perceived usability, and encourage continued engagement. Negative emotions (frustration, confusion, anxiety) drive abandonment.

Application in Membership One:

  • Celebratory milestones: When an organization reaches a milestone (10th member, 100th member, 1,000th check-in), the dashboard displays a brief celebration animation (confetti or a badge) with a congratulatory message. This acknowledges the user's achievement and creates a positive emotional association with the platform.
  • Encouraging empty states: Empty screens never show a cold "No data" message. Instead, the member list shows an illustration of a welcoming group with the text "Your member list is waiting for its first entry! Tap + to add a member." The event list shows a calendar illustration: "No events yet -- plan your first class or workshop." These messages are warm, actionable, and domain-appropriate.
  • Success feedback animations: After a successful action (member created, payment confirmed, check-in granted), the app displays a brief animation: a checkmark that draws itself, a green pulse on the status chip, or a slide-in success banner. These micro-interactions confirm the action emotionally, not just informationally.
  • Warm sports-themed illustrations: Onboarding screens, empty states, and error pages use custom illustrations featuring diverse people in sports and fitness contexts (yoga, running, team sports, gym). The illustrations reinforce the domain connection and create an emotional bond between the user and the product.

Flutter Implementation Notes: Use Lottie (lottie package) for milestone animations and success checkmarks. Store animation files in assets/animations/. For empty states, use empty_state.dart with an Image.asset and styled text. Implement success feedback with ScaffoldMessenger.showSnackBar() using a custom SnackBar with a green background and checkmark icon. Ensure all animations respect MediaQuery.disableAnimations for accessibility.


Principle-to-Effortless-UX Mapping

The following table maps each design principle to the Effortless UX Core Principles it most directly supports:

# Design Principle Primary Effortless UX Principle Secondary Principles
1 Law of Proximity 1. Goal and Task Orientation 5. Use-Case Navigation
2 Law of Similarity 5. Use-Case Navigation 9. System Support
3 Law of Closure 2. Guided Wizards 10. Vibe Design
4 Law of Continuity 2. Guided Wizards 5. Use-Case Navigation
5 Priming 7. Learning Support 10. Vibe Design
6 Cognitive Overload 6. Role-Based Simplification 2. Guided Wizards, 3. Simple Language
7 Fitts' Law 1. Goal and Task Orientation 9. System Support
8 Hick's Law 6. Role-Based Simplification 9. System Support
9 Zeigarnik Effect 7. Learning Support 2. Guided Wizards
10 Emotional Design 10. Vibe Design 8. Error Friendliness

Design Review Checklist (Gestalt Extension)

This checklist extends the Quality Checklist above with perception-specific checks. Every screen must pass both checklists before release.

# Principle Review Question Pass Condition
G1 Proximity Are related elements visually grouped with tighter spacing than unrelated elements? Intra-group spacing < inter-group spacing (e.g., 12dp vs. 24dp)
G2 Similarity Do elements of the same type use the same visual treatment across the entire app? Status chips, action buttons, and cards are identical in shape and size everywhere
G3 Closure Do containers and progress indicators clearly communicate boundaries and completion? Cards have consistent border radius and shadow; progress bars show filled vs. unfilled
G4 Continuity Are sequential processes connected by a visual line, path, or directional cue? Wizards use a connected stepper; timelines use a vertical line
G5 Priming Does the screen follow conventions that users already know from popular apps? Navigation, gestures, and layout match established mobile/SaaS patterns
G6 Cognitive Load Does the screen show 7 or fewer primary elements/choices at once? Dashboard <= 6 tiles; forms <= 5 fields per step; navigation <= 5 tabs
G7 Fitts' Law Are primary actions large (>= 44dp) and positioned in the thumb zone? FAB >= 56dp; all touch targets >= 44dp; destructive actions require extra reach
G8 Hick's Law Are choices constrained to the minimum necessary for the current task? Plan selection <= 4 options; quick actions <= 4 items; defaults pre-filled
G9 Zeigarnik Effect Do incomplete tasks show progress and offer a path to completion? Onboarding checklist visible; abandoned wizards recoverable; profile completion prompted
G10 Emotional Design Does the screen create a positive emotional response? Empty states are encouraging; success has animation; branding is warm and domain-appropriate

Flutter Architecture

Technology Stack

Component Technology Purpose
Framework Flutter 3.41+ Cross-platform UI (Android, iOS, Web, Desktop)
Language Dart 3.11+ Type-safe, null-safe, ahead-of-time compiled
State Management Riverpod 2.x Reactive, testable, provider-based state
Routing GoRouter Declarative, deep-linkable routing with guards
HTTP Client Dio Interceptors for JWT, retry, logging
Data Models Freezed + json_serializable Immutable data classes with JSON de/serialization
Local Storage flutter_secure_storage Encrypted token storage (keychain/keystore)
Formatting intl Date, number, and currency formatting
Logging logger Structured logging for debugging

Project Structure

The app follows a feature-based architecture where each feature is self-contained with its own screens, state, and data layer. Shared infrastructure lives in core/, and reusable widgets live in shared/.

lib/
├── main.dart                          # App entry point
├── app.dart                           # MaterialApp with GoRouter + ProviderScope
├── core/
│   ├── api/
│   │   ├── api_client.dart            # Dio instance, base URL, interceptors
│   │   ├── auth_interceptor.dart      # JWT token injection, refresh on 401
│   │   ├── error_interceptor.dart     # Standardized error handling
│   │   └── endpoints.dart             # API path constants
│   ├── router/
│   │   ├── app_router.dart            # GoRouter configuration, route tree
│   │   ├── route_guards.dart          # Auth guard, role guard, setup guard
│   │   └── routes.dart                # Route path constants
│   ├── theme/
│   │   ├── app_theme.dart             # ThemeData (light + dark)
│   │   ├── colors.dart                # MEMBERSHIP color palette
│   │   ├── typography.dart            # IBM Plex Sans type scale
│   │   └── spacing.dart               # Spacing and sizing tokens
│   ├── models/                        # Shared domain models (Freezed)
│   ├── services/
│   │   ├── auth_service.dart          # Token storage, login state
│   │   ├── connectivity_service.dart  # Online/offline detection
│   │   └── storage_service.dart       # Local cache for offline mode
│   └── utils/                         # Formatters, validators, helpers
├── features/
│   ├── auth/
│   │   ├── screens/                   # Login, Register, ForgotPassword, ResetPassword
│   │   ├── providers/                 # AuthNotifier, auth state
│   │   └── data/                      # Auth API calls, DTOs
│   ├── dashboard/
│   │   ├── screens/                   # DashboardScreen, QuickActions
│   │   └── providers/                 # DashboardNotifier, summary data
│   ├── members/
│   │   ├── screens/                   # MemberList, MemberDetail, MemberForm
│   │   ├── providers/                 # MemberListNotifier, MemberDetailNotifier
│   │   └── data/                      # Member API, DTOs, mappers
│   ├── contracts/
│   │   ├── screens/                   # ContractList, ContractDetail, PurchaseWizard
│   │   ├── providers/                 # ContractNotifier
│   │   └── data/                      # Contract API, DTOs
│   ├── payments/
│   │   ├── screens/                   # TransactionList, TransactionDetail
│   │   ├── providers/                 # TransactionNotifier
│   │   └── data/                      # Transaction API, DTOs
│   ├── access/
│   │   ├── screens/                   # QRCodeScreen, CheckInResult
│   │   ├── providers/                 # AccessNotifier
│   │   └── data/                      # Device/entrance control API
│   ├── resources/
│   │   ├── screens/                   # ResourceList, BookingCalendar, BookingForm
│   │   ├── providers/                 # ResourceNotifier, BookingNotifier
│   │   └── data/                      # Resource API, DTOs
│   ├── events/
│   │   ├── screens/                   # EventList, EventDetail, CourseSchedule
│   │   ├── providers/                 # EventNotifier
│   │   └── data/                      # Event API, DTOs
│   ├── communication/
│   │   ├── screens/                   # Inbox, ComposeMessage, TemplateEditor
│   │   ├── providers/                 # CommunicationNotifier
│   │   └── data/                      # Communication API
│   ├── profile/
│   │   ├── screens/                   # ProfileView, ProfileEdit, BankAccounts
│   │   ├── providers/                 # ProfileNotifier
│   │   └── data/                      # Consumer API, DTOs
│   ├── settings/
│   │   ├── screens/                   # OrgSettings, PaymentConfig, Templates
│   │   ├── providers/                 # SettingsNotifier
│   │   └── data/                      # Entity settings API
│   └── import_export/
│       ├── screens/                   # ImportWizard, ExportScreen
│       ├── providers/                 # ImportNotifier
│       └── data/                      # Import API
└── shared/
    ├── widgets/
    │   ├── app_shell.dart             # Navigation shell (bottom nav / side rail)
    │   ├── data_table.dart            # Sortable, paginated data table
    │   ├── search_bar.dart            # Search with debounce
    │   ├── status_chip.dart           # Color-coded status indicator
    │   ├── wizard_scaffold.dart       # Multi-step wizard layout
    │   ├── empty_state.dart           # Illustrated empty state
    │   ├── error_view.dart            # Error state with retry
    │   ├── loading_skeleton.dart      # Branded skeleton loader
    │   └── confirm_dialog.dart        # Confirmation dialog
    └── extensions/                    # Dart extension methods

State Management Pattern

Each feature uses a Notifier (or AsyncNotifier) with Riverpod. State flows unidirectionally: user action -> provider method -> API call -> state update -> UI rebuild.

User taps "Load Members"
  -> MemberListNotifier.loadMembers()
    -> memberApiProvider.getMembers(page, size, filter)
      -> Dio HTTP GET /api/v1/members?page=0&size=20
        -> JSON response
      -> parse to List<MemberDto>
    -> state = AsyncData(memberListState.copyWith(members: result))
  -> MemberListScreen rebuilds with new data

Offline-first features (check-in QR code, cached transaction list) use a local storage layer that synchronizes with the server when connectivity returns.

Design System: MEMBERSHIP Branding

Color Palette

Token Hex Usage
Primary #2CC5CE Primary buttons, active navigation, links, accents
Primary Dark #1A9BA3 Pressed states, active tab indicators
Primary Light #E0F7F8 Selected row background, badge background
Navy #0B3954 App bar, side navigation, headings, dark surfaces
Navy Light #1A4D6E Hover states on dark surfaces
Background #F2F4F6 Screen background, card container background
Surface #FFFFFF Cards, dialogs, form fields
Text Primary #1A1A2E Headings, body text
Text Secondary #6B7280 Labels, captions, placeholder text
Success #10B981 Check-in confirmed, payment complete, active status
Warning #F59E0B Pending status, attention needed
Error #EF4444 Failed payment, validation error, access denied
Info #3B82F6 Informational banners, tooltips

Typography

Font family: IBM Plex Sans (loaded via Google Fonts or bundled).

Style Size Weight Line Height Usage
Display 32sp Bold (700) 40sp Dashboard greeting, onboarding titles
H1 24sp SemiBold (600) 32sp Screen titles
H2 20sp SemiBold (600) 28sp Section headings
H3 16sp SemiBold (600) 24sp Card titles, list group headers
Body 14sp Regular (400) 20sp Body text, descriptions
Body Bold 14sp SemiBold (600) 20sp Emphasis, labels
Caption 12sp Regular (400) 16sp Secondary info, timestamps
Button 14sp SemiBold (600) 20sp Button labels
Overline 10sp Medium (500) 14sp Category tags, badges

Component Library

Cards -- The primary content container. Rounded corners (12dp), subtle elevation (1dp shadow), 16dp internal padding. Variants: standard (white surface), highlighted (primary light background), interactive (with onTap, ripple effect).

Buttons -- Three tiers: Primary (filled, primary color, white text), Secondary (outlined, primary border, primary text), Tertiary (text-only, primary text). Sizes: Large (48dp height, full width), Medium (40dp height), Small (32dp height). All buttons have minimum 44dp touch target. Loading state replaces label with spinner.

Inputs -- Material-style text fields with floating labels. Outlined variant by default. States: default, focused (primary border), error (red border + message below), disabled (gray). Helper text below the field. Character counter for limited fields. Password fields have visibility toggle.

Tab Switcher -- Horizontal tabs for switching views within a screen (e.g., "Active" / "Expired" / "All" contracts). Uses primary color for the active indicator. Supports swipe gesture on mobile.

Status Indicators -- Color-coded chips showing entity status. Pill-shaped with background color and text. Standardized mappings: Active = green, Pending = amber, Suspended = orange, Cancelled = red, Expired = gray, Draft = blue.

Progress Bar -- Used in wizards and import flows. Segmented bar showing completed, current, and remaining steps. Step labels below. Animated transitions between steps.

Pagination -- Appears below data tables. Shows current range ("1-20 of 150"), page size selector (10, 20, 50), and navigation arrows. On mobile, uses infinite scroll instead of explicit pagination.

Layout Types

Tab Layout (Main Screens) The primary layout for member-facing screens. Bottom navigation bar with 4-5 tabs. Content area fills the remaining space. Supports pull-to-refresh. Tab bar icons use the MEMBERSHIP icon set with labels below.

┌────────────────────────┐
│  App Bar (Navy)        │
│  Title + Actions       │
├────────────────────────┤
│                        │
│  Content Area          │
│  (scrollable)          │
│                        │
│                        │
├────────────────────────┤
│ 🏠  📋  🔑  💳  👤   │
│ Home  Mbr  Access Pay Profile│
└────────────────────────┘

Sub-Screen Layout (Detail Views) Used for detail pages, forms, and secondary views. Back arrow in the app bar. No bottom navigation (pushed on top of the tab layout). Scrollable content with optional floating action button.

┌────────────────────────┐
│  ← Back   Title        │
├────────────────────────┤
│                        │
│  Detail Content        │
│  (scrollable)          │
│                        │
│                    [FAB]│
└────────────────────────┘

Full-Screen Layout (Auth / Error / Onboarding) Used for login, registration, error pages, and onboarding tutorials. No navigation chrome. Centered content with branded background. Used only for flows that require the user's full attention.

┌────────────────────────┐
│                        │
│      [Logo]            │
│                        │
│  Centered Content      │
│  (form / message)      │
│                        │
│  [Primary Button]      │
│                        │
└────────────────────────┘

Admin Layout (Desktop / Tablet) Used for admin interfaces on larger screens. Persistent side navigation rail (collapsed to icons on tablet, expanded with labels on desktop). Content area uses a responsive grid. Top app bar with search, notifications, and user menu.

┌───┬─────────────────────┐
│ N │  Search  🔔  👤     │
│ a ├─────────────────────┤
│ v │                     │
│   │  Admin Content      │
│ R │  (responsive grid)  │
│ a │                     │
│ i │                     │
│ l │                     │
└───┴─────────────────────┘

Responsive Design Strategy

The app targets four breakpoint ranges:

Breakpoint Width Layout Navigation
Phone (compact) < 600dp Single column, bottom nav Bottom navigation bar
Phone (medium) 600-839dp Single column, wider cards Bottom navigation bar
Tablet 840-1199dp Two-column, side rail Collapsed side navigation rail
Desktop >= 1200dp Multi-column, side nav Expanded side navigation with labels

Responsive behavior: - Data tables on phone show a condensed card list. On tablet+, they show a full table with sortable columns. - Wizard steps on phone stack vertically. On tablet+, they show a horizontal stepper. - Dashboard tiles reflow from 1 column (phone) to 2 columns (tablet) to 3-4 columns (desktop). - Detail views on desktop use a master-detail split (list on left, detail on right). On phone, detail pushes as a new screen.

Offline Mode

The app must function in environments with unreliable connectivity (basements, gyms with poor reception, outdoor sports facilities).

Offline-capable features: - QR code display for check-in (cached locally with signed token, does not require server round-trip for display) - Member profile viewing (cached after last successful fetch) - Transaction history (cached with "last updated" indicator) - Pending form submissions (queued in local storage, synced when online)

Not available offline: - New purchases (requires server-side contract creation and payment processing) - Search (requires server-side query execution) - Real-time data (attendance counts, billing dashboard)

Sync strategy: - On connectivity restore, queued mutations are replayed in order. - Conflict resolution: server wins for financial data, last-write-wins for profile data. - Stale data indicator: cached screens show a subtle banner "Last updated 2 hours ago" with a refresh button.

Accessibility Guidelines

The app targets WCAG 2.1 Level AA compliance:

  • Color contrast: All text meets 4.5:1 ratio against background (verified for all color combinations in the palette).
  • Touch targets: Minimum 44x44dp for all interactive elements.
  • Screen reader: All images have alt text. All buttons have semantic labels. Navigation landmarks are annotated.
  • Keyboard navigation: Full tab-order support on web/desktop. Focus indicators visible.
  • Text scaling: UI adapts to system font size settings up to 200% without layout breakage.
  • Motion: Reduced motion mode disables all non-essential animations (respects system accessibility settings).
  • Color independence: Status is never communicated by color alone (always paired with text label or icon).

Right-to-Left (RTL) Support

RTL layout support is required from v1.0 to serve non-EU resident populations living in the EU (primarily Arabic speakers, approximately 1.5 million across Germany, France, Sweden, and the Netherlands). Even without expansion into RTL-dominant countries, these populations are real members of European sports clubs and fitness studios.

Flutter RTL Infrastructure

Flutter provides built-in RTL support via the Directionality widget, which is automatically set based on the active locale. The MaterialApp respects the text direction of the resolved locale, so most standard widgets (Text, TextField, ListTile, Drawer, NavigationRail) adapt automatically.

// RTL is automatic when the locale is set to an RTL language
// The Directionality widget wraps the entire widget tree
MaterialApp(
  locale: const Locale('ar'), // Arabic -> automatic RTL
  // All child widgets inherit RTL directionality
);

Mirrored Layouts

For RTL languages (Arabic, Hebrew), all layouts must be mirrored:

Element LTR (e.g., English) RTL (e.g., Arabic)
Navigation rail Left side Right side
Drawer Opens from left Opens from right
Back button Left side of app bar Right side of app bar
List item leading icon Left Right
List item trailing icon Right Left
Text alignment Left-aligned Right-aligned
Progress indicators Left to right Right to left
Padding (start/end) start = left, end = right start = right, end = left

Implementation rule: Always use EdgeInsetsDirectional (start/end) instead of EdgeInsets (left/right). Always use Alignment.centerStart instead of Alignment.centerLeft. This ensures correct behavior in both LTR and RTL modes without conditional logic.

BiDi Text Handling

Forms and data displays must handle bidirectional text correctly:

  • Member names: An Arabic member name displayed alongside a German address must render each segment in its correct direction
  • Mixed-language content: Use Unicode BiDi control characters when programmatically combining LTR and RTL strings
  • Text input: Flutter's TextField handles RTL input natively when the locale is set correctly
  • Number display: Numbers in Arabic locale use Western Arabic numerals (0-9) by default, not Eastern Arabic numerals, unless explicitly configured

RTL-Aware Icons

Directional icons must flip for RTL locales:

// Use Directionality-aware icons
Icon(
  Directionality.of(context) == TextDirection.rtl
      ? Icons.arrow_back  // Flipped automatically by Flutter
      : Icons.arrow_back,
);

// Or use matchTextDirection on Image/Icon
Image.asset(
  'assets/icons/chevron_right.png',
  matchTextDirection: true, // Automatically flips for RTL
);

Icons that should not flip (they represent real-world objects, not direction): - Clock icons (time always flows clockwise) - Media controls (play/pause/stop) - Search icon (magnifying glass) - Checkmark icon

RTL Testing Strategy

Every screen must be tested in both LTR and RTL modes:

  1. Automated widget tests: Run each widget test twice -- once with Directionality(textDirection: TextDirection.ltr) and once with TextDirection.rtl
  2. Visual regression tests: Golden file tests capture screenshots in both directions
  3. Manual QA checklist: Each screen has an RTL-specific checklist item in the quality gate
  4. CI enforcement: The CI pipeline runs flutter test with --dart-define=FORCE_RTL=true to validate RTL rendering
  5. Device testing: Arabic locale tested on physical Android and iOS devices during each release cycle

Performance Targets

The frontend must deliver near-instant responsiveness to support the "Effortless UX" philosophy. Users in a gym or studio environment expect the app to respond faster than opening a drawer or flipping a page.

Core Metrics

Metric Target Maximum Measurement
First Contentful Paint (FCP) < 0.2 seconds < 1.5 seconds Lighthouse, WebPageTest
Time to Interactive (TTI) < 0.5 seconds < 2.0 seconds Lighthouse
Largest Contentful Paint (LCP) < 1.0 seconds < 2.5 seconds Core Web Vitals
Cumulative Layout Shift (CLS) < 0.05 < 0.1 Core Web Vitals
First Input Delay (FID) < 50ms < 100ms Core Web Vitals
Lighthouse Performance Score > 95 > 90 Lighthouse CI

Any page exceeding the Maximum threshold is classified as a blocking bug and must be fixed before release.

Flutter Web WASM Compilation

Flutter Web is compiled to WebAssembly (WASM) for near-native execution performance:

  • WASM compilation eliminates JavaScript overhead and runs at near-native speed in modern browsers
  • Skia rendering via CanvasKit ensures pixel-perfect consistency across browsers
  • Initial load is optimized through deferred loading (deferred as) for feature modules
  • The WASM binary is aggressively cached via service worker with content-hash URLs
# Build for production with WASM
flutter build web --wasm --release --tree-shake-icons

Optimization Strategies

Lazy Loading and Code Splitting: - Each feature module (members, payments, events, etc.) is loaded on demand using Dart's deferred as imports - The initial bundle contains only the auth flow and shell navigation - Feature code is fetched in the background after the shell renders

Asset Optimization: - Images served in WebP format with AVIF fallback - SVG used for icons and illustrations (smaller than PNG, resolution-independent) - Font subsetting: only glyphs used in the app are included (IBM Plex Sans subset) - Gzip/Brotli compression on all static assets via CDN

Runtime Performance: - const constructors used wherever possible to minimize widget rebuilds - RepaintBoundary around complex list items and charts - AutomaticKeepAliveClientMixin for tab views to preserve state without re-rendering - Image caching with CachedNetworkImage for member photos and logos - Pagination and virtual scrolling for all lists (never load >50 items at once)

Monitoring: - Lighthouse CI integrated into GitLab pipeline -- every merge request is scored - Real User Monitoring (RUM) via Sentry Performance SDK tracks FCP, TTI, and LCP in production - Performance budgets enforced: if a merge request degrades any metric beyond the target, the pipeline fails

AI Integration

The platform integrates AI capabilities to reduce friction and support non-technical users:

Natural Language Search Members and admins can search using natural language: "show me members who haven't paid this month", "find all yoga classes on Tuesday". The query is parsed into structured API filters on the client side (for common patterns) or sent to a server-side NLP endpoint (for complex queries).

Chatbot / Conversational UI A floating chat button provides contextual assistance. The chatbot understands the current screen context and can answer questions ("How do I add a bank account?"), perform actions ("Create a new member named Max Mustermann"), and explain data ("Why is this payment marked as failed?"). Implemented as an MCP-compatible interface for flexibility in the underlying AI provider.

Smart Suggestions - After creating a member: "Would you like to assign a membership now?" - When a billing cycle fails: "3 transactions failed due to invalid IBANs. Would you like to see the affected members?" - During data import: auto-detect column mappings and suggest corrections. - In search: autocomplete with recently viewed and frequently accessed items.

Capacity Forecasting (Resource Module) The resource module uses historical booking and attendance data to predict future utilization. Admins see heatmaps of peak times, recommendations for class scheduling, and alerts when a resource is likely to be overbooked.


Internationalization (I18N) Implementation

Overview

The Membership Flutter app supports all 24 official EU languages from day one. Internationalization is implemented using Flutter's built-in gen-l10n tooling with ARB (Application Resource Bundle) files. Every user-visible string, date, number, and currency value is localized.

Flutter l10n Configuration

The l10n.yaml file in the project root configures the code generation:

# frontend/membership_app/l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
preferred-supported-locales: [en]
nullable-getter: false
untranslated-messages-file: untranslated.txt

Code generation is triggered by flutter gen-l10n (run automatically during flutter build and flutter run). The generated AppLocalizations class provides type-safe access to all translated strings.

ARB File Structure

Each of the 24 EU languages has its own ARB file in lib/l10n/:

lib/l10n/
├── app_en.arb      ← Base language (English) — contains descriptions + placeholders
├── app_bg.arb      ← Bulgarian
├── app_hr.arb      ← Croatian
├── app_cs.arb      ← Czech
├── app_da.arb      ← Danish
├── app_nl.arb      ← Dutch
├── app_et.arb      ← Estonian
├── app_fi.arb      ← Finnish
├── app_fr.arb      ← French
├── app_de.arb      ← German
├── app_el.arb      ← Greek
├── app_hu.arb      ← Hungarian
├── app_ga.arb      ← Irish
├── app_it.arb      ← Italian
├── app_lv.arb      ← Latvian
├── app_lt.arb      ← Lithuanian
├── app_mt.arb      ← Maltese
├── app_pl.arb      ← Polish
├── app_pt.arb      ← Portuguese
├── app_ro.arb      ← Romanian
├── app_sk.arb      ← Slovak
├── app_sl.arb      ← Slovenian
├── app_es.arb      ← Spanish
└── app_sv.arb      ← Swedish

Base Language File (app_en.arb)

The English ARB file serves as the source of truth. It contains metadata annotations (@-prefixed keys) that describe each translation key for translators:

{
  "@@locale": "en",

  "appTitle": "Membership",
  "@appTitle": {
    "description": "The application title shown in the app bar and browser tab"
  },

  "memberListTitle": "Members",
  "@memberListTitle": {
    "description": "Title of the member list screen"
  },

  "memberListSearchHint": "Search by name, email, or member number...",
  "@memberListSearchHint": {
    "description": "Placeholder text in the member search field"
  },

  "memberListResultCount": "{count, plural, =0{No members found} =1{1 member found} other{{count} members found}}",
  "@memberListResultCount": {
    "description": "Result count shown below the member list",
    "placeholders": {
      "count": {
        "type": "int",
        "example": "42"
      }
    }
  },

  "memberFormFirstName": "First Name",
  "@memberFormFirstName": {
    "description": "Label for the first name field in the member form"
  },

  "memberFormFirstNameRequired": "First name is required",
  "@memberFormFirstNameRequired": {
    "description": "Validation error when first name is empty"
  },

  "paymentStatusNew": "Not yet processed",
  "paymentStatusAccepted": "Accepted",
  "paymentStatusExported": "Sent to bank",
  "paymentStatusPaid": "Paid",
  "paymentStatusSettled": "Settled",
  "paymentStatusFailed": "Payment failed",
  "paymentStatusReturned": "Returned by bank",

  "currencyAmount": "{amount}",
  "@currencyAmount": {
    "description": "Formatted currency amount",
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {
          "symbol": "EUR",
          "decimalDigits": 2
        }
      }
    }
  },

  "dateFormatted": "{date}",
  "@dateFormatted": {
    "description": "Formatted date",
    "placeholders": {
      "date": {
        "type": "DateTime",
        "format": "yMd"
      }
    }
  },

  "commonSave": "Save",
  "commonCancel": "Cancel",
  "commonDelete": "Delete",
  "commonEdit": "Edit",
  "commonSearch": "Search",
  "commonLoading": "Loading...",
  "commonErrorRetry": "Something went wrong. Please try again.",
  "commonNoResults": "No results found",

  "validationRequired": "This field is required",
  "validationInvalidEmail": "Please enter a valid email address",
  "validationInvalidIban": "Please enter a valid IBAN",
  "validationMinLength": "Must be at least {min} characters",
  "@validationMinLength": {
    "placeholders": {
      "min": { "type": "int" }
    }
  }
}

Translated Language File Example (app_de.arb)

{
  "@@locale": "de",

  "appTitle": "Mitgliederverwaltung",
  "memberListTitle": "Mitglieder",
  "memberListSearchHint": "Suche nach Name, E-Mail oder Mitgliedsnummer...",
  "memberListResultCount": "{count, plural, =0{Keine Mitglieder gefunden} =1{1 Mitglied gefunden} other{{count} Mitglieder gefunden}}",
  "memberFormFirstName": "Vorname",
  "memberFormFirstNameRequired": "Vorname ist erforderlich",

  "paymentStatusNew": "Noch nicht verarbeitet",
  "paymentStatusAccepted": "Angenommen",
  "paymentStatusExported": "An Bank gesendet",
  "paymentStatusPaid": "Bezahlt",
  "paymentStatusSettled": "Abgerechnet",
  "paymentStatusFailed": "Zahlung fehlgeschlagen",
  "paymentStatusReturned": "Von Bank zurückgegeben",

  "commonSave": "Speichern",
  "commonCancel": "Abbrechen",
  "commonDelete": "Löschen",
  "commonEdit": "Bearbeiten",
  "commonSearch": "Suchen",
  "commonLoading": "Wird geladen...",
  "commonErrorRetry": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
  "commonNoResults": "Keine Ergebnisse gefunden",

  "validationRequired": "Dieses Feld ist erforderlich",
  "validationInvalidEmail": "Bitte gib eine gültige E-Mail-Adresse ein",
  "validationInvalidIban": "Bitte gib eine gültige IBAN ein",
  "validationMinLength": "Mindestens {min} Zeichen erforderlich"
}

Locale Detection and Resolution

The app determines the user's locale through a priority cascade:

/// Locale resolution strategy (in priority order):
/// 1. User preference (stored in secure storage / user profile)
/// 2. Device/browser locale (Platform.localeName / window.locale)
/// 3. Entity default locale (from organization settings)
/// 4. Fallback: English (en)

class LocaleService {
  static const supportedLocales = [
    Locale('bg'), // Bulgarian
    Locale('hr'), // Croatian
    Locale('cs'), // Czech
    Locale('da'), // Danish
    Locale('nl'), // Dutch
    Locale('en'), // English
    Locale('et'), // Estonian
    Locale('fi'), // Finnish
    Locale('fr'), // French
    Locale('de'), // German
    Locale('el'), // Greek
    Locale('hu'), // Hungarian
    Locale('ga'), // Irish
    Locale('it'), // Italian
    Locale('lv'), // Latvian
    Locale('lt'), // Lithuanian
    Locale('mt'), // Maltese
    Locale('pl'), // Polish
    Locale('pt'), // Portuguese
    Locale('ro'), // Romanian
    Locale('sk'), // Slovak
    Locale('sl'), // Slovenian
    Locale('es'), // Spanish
    Locale('sv'), // Swedish
  ];

  /// Resolve the best locale from the cascade
  static Locale resolveLocale({
    Locale? userPreference,
    Locale? deviceLocale,
    Locale? entityDefault,
  }) {
    // 1. User preference (if set and supported)
    if (userPreference != null && supportedLocales.contains(userPreference)) {
      return userPreference;
    }

    // 2. Device/browser locale (language match, ignore country)
    if (deviceLocale != null) {
      final match = supportedLocales.firstWhere(
        (l) => l.languageCode == deviceLocale.languageCode,
        orElse: () => const Locale('en'),
      );
      if (match.languageCode != 'en' || deviceLocale.languageCode == 'en') {
        return match;
      }
    }

    // 3. Entity default
    if (entityDefault != null && supportedLocales.contains(entityDefault)) {
      return entityDefault;
    }

    // 4. Fallback
    return const Locale('en');
  }
}

MaterialApp Configuration

MaterialApp.router(
  localizationsDelegates: const [
    AppLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: LocaleService.supportedLocales,
  locale: currentLocale, // from Riverpod locale provider
  localeResolutionCallback: (deviceLocale, supportedLocales) {
    return LocaleService.resolveLocale(
      userPreference: savedUserLocale,
      deviceLocale: deviceLocale,
      entityDefault: entityDefaultLocale,
    );
  },
  // ...
);

Language Switcher

A language switcher is available in the Settings screen. It presents all 24 languages with their native names for easy identification:

Code Native Name English Name
bg Български Bulgarian
hr Hrvatski Croatian
cs Ceština Czech
da Dansk Danish
nl Nederlands Dutch
en English English
et Eesti Estonian
fi Suomi Finnish
fr Francais French
de Deutsch German
el Ελληνικα Greek
hu Magyar Hungarian
ga Gaeilge Irish
it Italiano Italian
lv Latviesu Latvian
lt Lietuviu Lithuanian
mt Malti Maltese
pl Polski Polish
pt Portugues Portuguese
ro Romana Romanian
sk Slovencina Slovak
sl Slovenscina Slovenian
es Espanol Spanish
sv Svenska Swedish

When a user selects a language: 1. The preference is saved to secure storage (persists across sessions) 2. If the user is logged in, the preference is synced to the user profile on the backend 3. The app rebuilds immediately with the new locale (no restart required) 4. All cached content (API responses with translated labels) is invalidated and refetched

Date, Number, and Currency Formatting

All formatting uses the intl package, which handles locale-specific rules automatically:

import 'package:intl/intl.dart';

// Date formatting (adapts to locale)
final dateFormatter = DateFormat.yMd(locale);  // de: 22.02.2026, en: 2/22/2026, fr: 22/02/2026
final timeFormatter = DateFormat.Hm(locale);   // de: 14:30, en: 2:30 PM, fr: 14:30
final fullDateTime = DateFormat.yMd(locale).add_Hm(); // Combined

// Number formatting
final numberFormatter = NumberFormat.decimalPattern(locale);
// de: 1.234,56    en: 1,234.56    fr: 1 234,56

// Currency formatting
final currencyFormatter = NumberFormat.currency(
  locale: locale,
  symbol: 'EUR',       // Always EUR for EU operations
  decimalDigits: 2,
);
// de: 1.234,56 EUR    en: EUR1,234.56    fr: 1 234,56 EUR

// Percentage
final percentFormatter = NumberFormat.percentPattern(locale);
// de: 19 %    en: 19%    fr: 19 %

// Compact numbers (for dashboards)
final compactFormatter = NumberFormat.compact(locale: locale);
// de: 1.234    en: 1.2K    fr: 1 234

Formatting Utilities

A centralized FormatService ensures consistent formatting across the app:

class FormatService {
  final String locale;

  FormatService(this.locale);

  String formatDate(DateTime date) => DateFormat.yMd(locale).format(date);
  String formatTime(DateTime time) => DateFormat.Hm(locale).format(time);
  String formatDateTime(DateTime dt) => DateFormat.yMd(locale).add_Hm().format(dt);
  String formatCurrency(double amount) =>
      NumberFormat.currency(locale: locale, symbol: 'EUR', decimalDigits: 2).format(amount);
  String formatNumber(num value) => NumberFormat.decimalPattern(locale).format(value);
  String formatPercent(double value) => NumberFormat.percentPattern(locale).format(value / 100);

  /// Format a relative time ("2 hours ago", "just now")
  String formatRelativeTime(DateTime dateTime) {
    final diff = DateTime.now().difference(dateTime);
    if (diff.inMinutes < 1) return AppLocalizations.of(context).timeJustNow;
    if (diff.inHours < 1) return AppLocalizations.of(context).timeMinutesAgo(diff.inMinutes);
    if (diff.inDays < 1) return AppLocalizations.of(context).timeHoursAgo(diff.inHours);
    if (diff.inDays < 30) return AppLocalizations.of(context).timeDaysAgo(diff.inDays);
    return formatDate(dateTime);
  }
}

Pluralization Rules

Pluralization is handled through ICU message format in ARB files. Different languages have different pluralization rules (e.g., Russian has singular, few, many; Arabic has zero, one, two, few, many, other). The intl package handles all CLDR plural categories.

Plural Categories by Language Group

Category Languages Example (English)
=0 (explicit zero) All "No members found"
one (singular) Most EU languages "1 member found"
two (dual) Irish (ga), Slovenian (sl) "2 members found"
few Czech, Croatian, Lithuanian, Latvian, Maltese, Polish, Romanian, Slovak, Slovenian "3 members found" (for 2-4 in Czech)
many Lithuanian, Maltese, Polish "12 members found" (specific ranges)
other (general plural) All "{count} members found"

ARB Pluralization Examples

English (simple: one/other):

"memberCount": "{count, plural, =0{No members} =1{1 member} other{{count} members}}"

Polish (one/few/many/other):

"memberCount": "{count, plural, =0{Brak członków} =1{1 członek} few{{count} członków} many{{count} członków} other{{count} członków}}"

Czech (one/few/other):

"memberCount": "{count, plural, =0{Žádní členové} =1{1 člen} few{{count} členové} other{{count} členů}}"

Slovenian (one/two/few/other):

"memberCount": "{count, plural, =0{Ni članov} =1{1 član} two{{count} člana} few{{count} člani} other{{count} članov}}"

Translation Key Conventions

All translation keys follow a consistent naming convention to ensure maintainability:

<feature>.<screen>.<element>[.<variant>]
Segment Description Examples
feature Feature module name member, payment, contract, event, checkin, common
screen Screen or component name list, detail, form, dialog, dashboard
element UI element title, subtitle, searchHint, emptyState, errorMessage
variant (optional) Variant or state required, invalid, success, loading

Examples:

member.list.title                     → "Members"
member.list.searchHint                → "Search by name..."
member.list.emptyState                → "No members yet. Add your first member."
member.form.firstNameLabel            → "First Name"
member.form.firstNameRequired         → "First name is required"
member.detail.contractSection         → "Active Membership"
member.detail.paymentHistoryTitle     → "Payment History"
payment.list.title                    → "Transactions"
payment.status.new                    → "Not yet processed"
payment.status.paid                   → "Paid"
contract.form.startDateLabel          → "Start Date"
contract.form.startDateRequired       → "Start date is required"
checkin.result.accessGranted          → "Welcome! Access granted."
checkin.result.accessDenied           → "Access denied."
checkin.result.accessDenied.expired   → "Your membership has expired."
common.action.save                    → "Save"
common.action.cancel                  → "Cancel"
common.validation.required            → "This field is required"
common.validation.invalidEmail        → "Please enter a valid email address"
common.time.justNow                   → "Just now"
common.time.minutesAgo               → "{minutes} minutes ago"

Rules: 1. Keys are always in English, regardless of the active locale 2. No abbreviations in keys (firstName not fName, password not pwd) 3. Labels end with Label, validation errors end with the rule name (Required, Invalid, MinLength) 4. Action buttons use common.action.<verb> for reusable actions 5. Status values use <feature>.status.<statusCode> mapping 6. Each ARB file must contain all keys present in the base app_en.arb -- the CI pipeline validates completeness