Build WCAG 2.2 AA semantic label infrastructure
epic-expense-type-selection-foundation-task-007 — Implement ExpenseTypeAccessibilityService providing: getSemanticLabel(ExpenseType, isSelected, isDisabled) returning a fully resolved Norwegian string per WCAG 2.2 Success Criterion 1.3.1, getLiveRegionAnnouncement(ExpenseTypePair blocked) for conflict announcements, and getFocusOrderHint(List<ExpenseType> activeTypes) for correct TalkBack/VoiceOver traversal order. All strings must be internationalisation-ready (ARB key references, no hardcoded text).
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
The key architectural decision is making the service depend on AppLocalizations (or an abstraction) rather than calling Localizations.of(context) internally — this keeps it a pure service testable without a widget tree. Define a LocalizationDelegate interface if needed. For getSemanticLabel, compose the string from three ARB fragments: the type name, the selected state, and the disabled state — this allows translators to adjust word order per language without code changes. For getFocusOrderHint, derive sort order from ExpenseTypeConfig.visualOrder (an integer field) — if this field doesn't exist yet, add it as part of this task.
WCAG 2.2 SC 1.3.1 compliance requires that the semantic label alone — without any visual context — conveys the full state to a screen reader user. Test with TalkBack on Android and VoiceOver on iOS in a manual review pass.
Testing Requirements
Unit tests using flutter_test with a stub/mock AppLocalizations that returns predictable English strings (to avoid test brittleness against Norwegian text changes). Test each method: getSemanticLabel for all combinations of isSelected × isDisabled (4 combinations) × at least 2 ExpenseType variants. getLiveRegionAnnouncement for a known conflicting pair. getFocusOrderHint for an empty list, single item, and multi-item list asserting sort order matches ExpenseTypeConfig visual order.
Assert no hardcoded Norwegian strings appear in the service file via a static analysis check (grep in CI).
The compatibility matrix might be under-specified in source documentation. If a new organisation adds expense types or redefines rules, hardcoded pairwise logic becomes a maintenance liability and can silently allow previously excluded combinations.
Mitigation & Contingency
Mitigation: Model the matrix as a const Map<ExpenseType, Set<ExpenseType>> rather than if-else chains; add a unit test that exhaustively asserts every pair combination so any future matrix change forces explicit test updates.
Contingency: If per-organisation matrix variants are requested before the epic closes, extract matrix loading into expense-type-config with an org-override slot and defer per-org configuration to the repository epic.
VoiceOver (iOS) and TalkBack (Android) handle Semantics widget announcements differently in Flutter. Live-region behaviour for disabled state changes is inconsistent across Flutter versions and may require platform-specific workarounds that are not yet documented.
Mitigation & Contingency
Mitigation: Write accessibility integration tests using Flutter's SemanticsController targeting both iOS and Android simulators from the outset; pin to a Flutter version known to handle Semantics.liveRegion correctly.
Contingency: If platform parity is unachievable before release, ship with a known gap documented in the WCAG audit log and schedule a dedicated accessibility sprint; do not block other epics.