Athena — roomy-mobile/archive/changes/2025-12-28-fix-runtime-locale-switching/design.md

Context

The app uses Flutter's localization system with ARB files and the intl_utils package for code generation. The generated S class provides two access patterns:

  1. Reactive: S.maybeOf(context) / S.of(context) - Creates InheritedWidget dependency, triggers rebuilds on locale change
  2. Static: S.current - Returns the last loaded instance, NO rebuild dependency

A global gStrings getter was introduced as a convenience accessor that uses S.current, inadvertently bypassing Flutter's reactive localization system in 364 places across 57 files.

Goals / Non-Goals

Goals

  • All user-facing text updates immediately when language is changed
  • No hot reload or restart required for language switching
  • Clear, documented patterns for accessing localized strings
  • No regression in existing functionality

Non-Goals

  • Changing the localization architecture (ARB files, intl_utils)
  • Adding new localization features
  • Supporting additional languages

Decisions

Decision 1: Migration Patterns by Context

| Context | Pattern | Rationale | |---------|---------|-----------| | Widgets/Views with BuildContext | context.strings | Reactive, triggers rebuilds | | Enums needing labels | String label(S strings) | Caller provides reactive strings | | Forms with validators | S get _strings => context?.strings ?? S.current | Forms have context available | | Services returning TurboResponse | S.current | Error messages reflect locale at capture time | | DTOs with placeholder factories | factory X.loading({required S strings}) | Caller provides reactive strings | | RandomService | method({required BuildContext context}) | Service methods receive context | | ViewModels | context.strings (remove fallback) | ViewModels have context access |

Alternatives Considered:

  1. Pass S everywhere: Too invasive, requires changing all call signatures
  2. Use S.current everywhere: Defeats reactive localization entirely
  3. Create reactive global stream: Over-engineering for this use case

Decision 2: Service Error Messages Use S.current

Services that return TurboResponse.fail() capture error messages at the moment the error occurs. Using S.current is acceptable because:

  • The error message reflects the locale at error generation time
  • Services don't have BuildContext access
  • Error messages are transient, displayed once

Decision 3: Delete Stale Duplicate Files

The following files are stale duplicates that should be deleted:

  • lib/l10n/l10n.dart - Partial copy of lib/generated/l10n.dart, only supports English
  • lib/l10n/intl/messages_all.dart - Stale message loader
  • lib/l10n/intl/messages_en.dart - Stale English messages

The canonical generated files are in lib/generated/.

Risks / Trade-offs

| Risk | Mitigation | |------|------------| | Large number of changes (364 usages) | Batch by pattern type, run tests after each batch | | Breaking existing tests | Tests should use same patterns as production code | | Missing edge cases | Comprehensive grep for gStrings after migration |

Migration Plan

Phase 1: Preparation

  1. Delete stale duplicate files first (no dependencies on them)
  2. Create regression tests that verify locale switching behavior

Phase 2: Migration by Pattern (in order of isolation)

  1. Enums (Pattern B) - Self-contained, easy to test
  2. Services (Pattern D) - Replace gStrings → S.current
  3. DTOs (Pattern E) - Add S parameter to factories
  4. RandomService (Pattern F) - Add BuildContext parameter
  5. Forms (Pattern C) - Add _strings getter
  6. ViewModels - Replace fallback patterns
  7. Widgets (Pattern A) - Replace gStrings → context.strings
  8. Extensions/Utilities - Context-dependent migration

Phase 3: Cleanup

  1. Delete lib/l10n/globals/g_strings.dart
  2. Update documentation
  3. Run flutter analyze and fix warnings
  4. Verify all tests pass

Rollback

If issues arise, the gStrings accessor can be temporarily restored. However, this masks the underlying bug and should only be used as emergency fallback.

Open Questions

None - all patterns have been decided during refinement.

Reacties

Nog geen reacties