Phase 13-11 — A11y + bilingual acceptance matrix
Phase 13-11 — A11y + bilingual acceptance matrix
Execution reference for mobile-developer. Every item is a concrete pass/fail check. For the cross-cutting baseline (contrast ratios, tap target minimum, design tokens) see wiki/src/content/docs/ui/design-system.mdx §5 — this document does not repeat that material; it adds the per-screen layer on top.
Screens in scope (implemented 13-1..13-10): Onboarding (class pick), Home, TreeViewer, QuestLog, QuestDetail, FactionOverview, FactionDetail, CombatScreen, WalkerHpHud (component on Home). Crafting is out of scope (Phase 14+).
1. EN/PL switcher placement decision
Decision: Option A — Settings screen.
A gear icon (ic_settings, Material Symbols outlined) in the Home top-bar right slot (already reserved per home-screen.mdx primary layout [⚙ settings]). Tapping opens a new SettingsScreen destination pushed onto the nav stack. The locale switcher is one row inside.
Rationale against B (inline chip) and C (profile drawer): the home-screen wireframe already reserves the [⚙] slot for settings-category actions. A dedicated Settings screen matches the three-zone layout pattern established in 11e-1 (combat-screen) and 11e-2 (faction drill-down). An inline chip in the top-bar introduces a second interactive element in the top-40% zone, conflicting with the one-handed-while-walking baseline (design-system §5.1). A profile drawer does not exist in the current IA and would require a new nav pattern, adding scope beyond 13-11.
1.1 Component
Material 3 SegmentedButton (2 segments: “EN” / “PL”). Not a Switch — a segmented control communicates the current-selection-from-discrete-set semantic better than a toggle, and both options are always visible at a glance.
1.2 Effect timing
Immediate recomposition. No app restart. The locale change triggers a LocalConfiguration override via a CompositionLocal wrapper at the app root. Every stringResource(...) call recomposes on the next frame. Zero-restart is the acceptance criterion — mobile-developer verifies by toggling and confirming that all visible strings on the current back-stack update within 1 recomposition cycle.
1.3 Persistence
SharedPreferences (EncryptedSharedPreferences, consistent with SyncStateStore from 13-4). Key: walkrpg.locale.override. Values: "en", "pl", or absent (no key = follow system locale).
1.4 Default behavior
When walkrpg.locale.override is absent: follow system locale (Locale.getDefault().language). When system language is neither en nor pl: fall back to en. The Settings row shows the current effective locale in the segmented control regardless of how it was chosen (system-derived or explicit).
2. Per-screen acceptance checklist
Reference: design-system.mdx §5 for baseline; wireframes for element inventory.
| Screen | Compose a11y items required | Live regions | Font-scale 200% acceptance | Focus order |
|---|---|---|---|---|
| Onboarding — ClassPick | Min 5: class card (contentDescription = class name + EN description), primary CTA (“Begin as Cartographer”), back button (“Back to auth”), class name heading (heading role), class description body | None required (static selection screen) | Class description wraps freely; CTA label wraps to 2 lines maximum (“Begin as / Cartographer” acceptable); card height expands via wrapContentHeight; no truncation on class name | Top-to-bottom: back → class card(s) → primary CTA |
| Onboarding — WalkerCreation (screen 3) | Min 6: Walker name row (contentDescription = “Walker name: {value}”), edit-name icon button (“Edit name”), region card group (“Starting region: Plenny — {description}”), guild card group (“Your guild: Unfinished Guild — {hint}”), back button, primary CTA (“Register Walker”) | None | Region description wraps; guild hint wraps; edit icon 48dp invisible target; CTA grows vertically | Back → Walker name row → edit icon → region card → guild card → primary CTA |
| Home | Min 9: settings icon button (“Settings”), progression card section (“Level {N}, {N} of 180 nodes allocated, {M} points banked, next point: {hint}”), streak ribbon (“Day {N} streak, {bonus or prompt}”), quest pill (“Active quest: {name}, beat {n} of {m}”), each faction strip cell × 5 (“Faction name — Tier {n}” per cell), each bottom tab × 4 | Streak ribbon: LiveRegionMode.Polite on day count change. Quest pill: LiveRegionMode.Polite on beat change. WalkerHpHud: LiveRegionMode.Polite on HP change. | Progression card: Poziom N at --type-display scales to ~64px effective; caption rows wrap; card uses wrapContentHeight. Tab bar: labels drop to icons-only per design-system §5.5 rule (already in design-system — verify implementation). Region description: 1-line ellipsis fallback acceptable (secondary content). Banked banner wraps within card. | Settings icon (header) → progression card (non-interactive, traversed as section) → streak ribbon → quest pill → faction strip left-to-right (5 cells) → bottom tabs left-to-right |
| TreeViewer | Min 22 (one per node in cluster.first-steps + header elements): available-points counter (“7 points available”), cluster label (heading role), each node × 20 (“Node name — EN name / PL name — modifier summary — state: allocated/unallocated/locked/reachable — Cost: N point(s)”), mastery node (“Mastery: Pace and Pacing — choose one effect”), keystone node (“Keystone: Unshaken Step — locked, complete Quest 001 to unlock” or “Keystone: Unshaken Step — unlocked, allocate for 0 points”) | Available-points counter: LiveRegionMode.Polite on value change (after allocation). Node state: LiveRegionMode.Polite on allocation confirmation. | Node micro-labels hide at 200% — long-press popover scales and must not clip; popover uses scrolling container if content exceeds screen height. Available-points counter wraps. Cluster label wraps. Action buttons in confirmation dialog grow vertically. | Per wireframe focus order: available-points counter → cluster label → even-stride (entry) → left branch bottom-to-top → centre branch bottom-to-top → right branch bottom-to-top → mastery node → keystone node |
| QuestLog | Min 8 per state: screen title (heading), “ACTIVE (N)” section header, each active quest row (“Quest name, region, step N of M, N percent complete, active beat”), “COMPLETED (N)” section header + disclosure chevron (“Completed quests, collapsed — tap to expand”), each completed quest row (“Quest name, region, completed, N points”), hidden quest slot (“Hidden quest, region, allocate a keystone to reveal”), hidden-count hint composable (“N hidden quests — allocate a keystone to reveal them”, liveRegion=Polite per 13-10) | Active quest beat: LiveRegionMode.Polite on step progress change. Quest completion: LiveRegionMode.Assertive. Hidden quest revealed: LiveRegionMode.Assertive. HiddenCountHint: LiveRegionMode.Polite (already shipped in 13-10 — verify). | Active quest row: quest name wraps to 3 lines maximum; progress bar stays 4dp (not font-scaled); step label “Step 3 of 5” fits single line at 200% (short string). Section headers wrap. Filter chips grow; chip row scrolls horizontally if overflow. | Screen title → filter chip row (All/Active/Completed chips) → active section header → active quest rows top-to-bottom → completed section header → hidden section header → hidden quest slots |
| QuestDetail | Min 10: screen title (heading + back button “Back to quest log”), region badge, status pill (“Active” or “Completed”), step-progress fraction (“Step 3 of 5”), each step row (completed: “Step N — name, completed”; current: “Step N — name, current step”; locked: “Step N — locked”), issued-by row (“Guild commission”), rewards row (“N points on completion, faction rep outcome-dependent”), primary action button (“Start quest” / “Advance to step N” / “Complete quest”) | Step progress bar: LiveRegionMode.Polite on advance. Quest completion event: LiveRegionMode.Assertive. | Step text wraps freely (scrollable content zone). Locked step labels “LOCKED” pill uses wrapContentWidth. Progress bar 4dp unchanged. Action button grows vertically. | Back → region badge + status pill (non-interactive group) → progress fraction → step list top-to-bottom → issued-by → rewards → primary action |
| FactionOverview | Min 7 per faction card + sort control: sort button (“Sort factions”), each faction card × 5 (“Faction name — tier name — Tier N of 5 — N of M reputation”); list container must have group semantics announced as “Factions list, 5 items” | Faction rep change after sync: LiveRegionMode.Polite per faction card. Tier gain: LiveRegionMode.Assertive. | Faction name wraps to 2 lines. Tier pill uses wrapContentWidth. Rep bar stays 6dp. Rep numeric wraps below bar if needed. Five cards scroll on screen at 200% — single-screen guarantee dropped, scrolling container is the fallback (per design-system §5.5). | Sort button → faction cards top-to-bottom (each card fully traversed: name → tier → rep bar → rep value) |
| FactionDetail | Min 12+: back button, faction name heading, tier pill (“Tier name, Tier N of 5”), rep bar (“N of M reputation, N rep to next tier”), about section (text, non-interactive), each tier row (completed: “Tier N name, earned — {reward}”; current: “current — Tier N, N of M rep”; future: “Tier N name, locked, requires N rep”), each leader chip (“Leader name — role”), rivals section (each rival chip: “Rival: faction name — {reason}“) | Rep bar: LiveRegionMode.Polite on rep delta. Tier gain: LiveRegionMode.Assertive. | Content zone scrollable; all text wraps freely. Tier pill wrapContentWidth. Current-tier left-border highlight tracks row height. | Back → tier pill + rep bar (non-interactive group) → about text → tier rows top-to-bottom → leaders → keystones → rivals |
| CombatScreen | Min 14: Walker status group (“Walker: HP N of M, Energy N of M”), enemy status group (“Enemy name: HP N of M”), turn indicator (“Your turn, turn N” or “Enemy’s turn, turn N”), last-action log (“Turn N — {action result}”), hit/crit chip when visible (“Hit chance N%, crit chance N% at N.N multiplier”), Attack button (“Attack, costs 1 energy”), Use-Item button (“Use item, disabled, available Phase 12”), Retreat button (“Retreat, no energy cost”), stats-info icon button (“Combat stats — tap to view”), outcome card back-button/dismiss, outcome card result text, outcome card stats group, outcome card quest-advance row, outcome card primary CTA | Walker HP delta: LiveRegionMode.Assertive (Walker receiving damage is urgent). Enemy HP delta: LiveRegionMode.Polite. Turn indicator change: LiveRegionMode.Assertive. Outcome card result (victory/defeat): LiveRegionMode.Assertive. Already shipped partially in 13-8/13-9 — verify all regions fire correctly. | HP/energy numerics at --type-h2 scale to ~36px; two-column status bar wraps to vertical stack within each column (acceptable per wireframe §8.4). Action buttons grow via wrapContentHeight with minHeight = 56dp. Turn pill uses wrapContentWidth. Floating HP delta numerals are out-of-layout (float + fade) — no overflow risk. Last-action log wraps to 2 lines minimum. | Walker status group → enemy status group → turn indicator → last-action log → hit/crit chip (when visible) → Attack → Use-Item (disabled) → Retreat → stats icon button |
| WalkerHpHud (component) | Min 2: HP bar group (“Walker HP: N of M”), color-coded state description included in label (e.g. “Walker HP: 47 of 50, healthy” / “…warning” / “…critical”) | HP change: LiveRegionMode.Polite (already shipped in 13-9 per HpDeltaBar — verify it uses Polite not Assertive for home-screen ambient display) | HP label wraps; bar width fixed (not font-scaled) | Not independently traversable — part of Home focus order after streak ribbon |
3. TalkBack acceptance criteria
Cross-cutting. Every screen must satisfy all of the following before 13-11 ships.
- Every interactive element has a meaningful
contentDescription. No element announces as “Button” or “Image” or an auto-generated resource ID. - Every icon-only button has a text label via
contentDescriptionorsemantics { contentDescription = "..." }. Mandatory targets: settings gear (Home), edit-name pencil (WalkerCreation), sort control (FactionOverview), stats-info[i]button (CombatScreen), close/dismiss on all bottom sheets and modals. - Decorative images and purely visual indicators (e.g. pisarska linijka, hp bar texture overlay, crit screen-edge flash, animated footprint icons on post-walk sync) are marked
Modifier.semantics { invisibleToUser() }or equivalent. - Every screen transition: the first semantically meaningful element of the new screen receives focus on entry. For push navigation: the screen title or primary status element. For modal/dialog open: the dialog title. For bottom sheet open: the sheet’s first interactive element.
- Live regions announce on state change and do not re-announce identical content. Test: set streak to Day 5, navigate away and back — the streak ribbon must NOT re-announce if the value has not changed since last TalkBack focus.
- Lists with more than 10 items use group semantics so TalkBack reads “list, N items” before traversing. Currently relevant: TreeViewer node list (20 nodes). QuestLog and FactionOverview are at or near the threshold — apply group semantics preemptively.
- The mastery popover (TreeViewer) and stats bottom sheet (CombatScreen) must trap focus while open and return focus to the triggering element on dismiss.
4. Bilingual switching acceptance criteria
- After toggling EN to PL (or PL to EN) in Settings, every visible string on the current screen and on all screens reachable from the back-stack updates within 1 recomposition. Zero app restarts required.
- No string in the UI is hardcoded in Kotlin source. Every visible label resolves from
strings.xmlviastringResource(R.string.*). Audit method: grepwalkrpg-mobilesource for string literals in Composable functions; any hit is a fail. - Polish strings render correctly. All Polish diacritics (
ą ć ę ł ń ó ś ź żand uppercase) display without replacement characters. Verify with Literata (lore/display) and Inter (UI chrome) — both confirmed to carry full Polish diacritic support per design-system §2. - Polish plurals: at least one
<plurals>resource ships for step counts. The template-string workaround from 13-10 (b833aaf) is upgraded to a proper Android<plurals>resource with all required forms:zero,one,few(2-4),many(5+),other. Verify with N = 0, 1, 2, 5, 21. Example target:0 zadań / 1 zadanie / 2 zadania / 5 zadań / 21 zadań. The EN equivalent usesoneandotherforms only. - Date and number formatting follows the active locale. Step counts: EN uses comma-grouped thousands (
1,234); PL uses space-grouped thousands (1 234). Verify on PostWalkSyncScreen step delta and total-today labels. UseNumberFormat.getNumberInstance(locale)or equivalent — no hardcoded format strings. - Quest names on system surfaces use
name.enregardless of locale (W-level decision from faction-and-quest-log.md §3.1 — “EN names on system surfaces, PL names inside quest prose”). This is not a locale-switching bug; it is intentional. TalkBack announcements for quest names also usename.en.
5. 200% font-scale acceptance criteria
Cross-cutting. Verify every screen using @Preview(fontScale = 2f) Compose previews and on a physical device with OS font size set to largest.
- No primary content clips or truncates with ellipsis. Primary content = screen titles, body copy, button labels, quest names, node names, faction names, tier names, NPC names.
- Tooltips, snackbars, and error messages remain fully visible and do not overflow off-screen. Snackbar with “Open Quest Log” action (from 13-10 keystone unlock) must render legibly at 200%.
- All tap targets remain at minimum 48dp despite label growth. Verify: faction strip cells (Home) at 200% — if label overflows the cell, the cell height grows but the tap target must not shrink below 48dp.
- If a layout cannot fit at 200%, the required fallback is a scrolling container, not truncation. Screens that fall back to scrolling at 200% (document explicitly, mobile-developer adds a comment in the Composable):
- FactionOverview: five-card list scrolls when cards expand past one-screen height.
- QuestDetail steps list: already scrollable (content zone); no change needed, but verify scroll works correctly at 200%.
- TreeViewer node popover: scrolling container if modifier list exceeds screen height.
- CombatScreen stat-dictionary bottom sheet: already scrollable; verify at 200%.
- Tab bar: at 200% font scale, tab labels drop to icon-only per design-system §5.5. This rule must be implemented. Current status from 13-4/13-5: verify the icon-only fallback is actually wired (design-system specifies it, implementation may not have shipped it).
- Font scale is applied at Compose preview level via
@Preview(fontScale = 2f)— mobile-developer adds this annotation to every screen-level Composable preview that does not already have it.
6. Test surface
Mobile-developer adds the following tests for 13-11. Target: +25 to +40 tests.
Snapshot / Compose-test per screen at two font scales:
- For each screen listed in §2: one Compose test (or screenshot test if the project uses Paparazzi) at
fontScale = 1.0(baseline) and one atfontScale = 2.0. Confirm no assertion failures on layout overflow. Minimum: 9 screens × 2 scales = 18 tests. If some screens already have baseline tests (13-4 shipped 8 a11y items), add only the 200% variant.
Locale override test:
- One integration test: programmatically set
walkrpg.locale.override = "pl", navigate to Home, assert that a known EN string key (R.string.home_streak_day_labelor equivalent) resolves to its PL value. Then set"en", assert the EN value returns. This single round-trip test proves the override mechanism works end-to-end.
TalkBack semantic presence test (one per screen):
- Use
composeTestRule.onNodeWithContentDescription(...)to assert that the minimum requiredcontentDescriptions from §2 are present on each screen. Minimum one assertion per screen = 9 tests. Prioritize: settings gear icon, quest pill, node “Even Stride” on TreeViewer, Attack button on CombatScreen, Walker HP group on Home.
Plurals resource test:
- One parameterized test asserting the correct Polish plural form for N ∈ {0, 1, 2, 5, 21}. Asserts against the
<plurals>resource directly viaresources.getQuantityString(R.plurals.quest_count, n, n). Five assertions = 1 test method (parameterized or@RepeatedTest).
Total expected: 18 (font-scale) + 1 (locale) + 9 (TalkBack semantics) + 1 (plurals) = 29 minimum. Aiming for 35 with additional edge cases (e.g. faction strip at 200%, CombatScreen live-region spam guard).
7. Out of scope — explicit defer list
The following items are NOT part of 13-11. Do not implement or audit them in this sub-phase.
- Haptic feedback — deferred from 13-8 and 13-10 (haptic infra not wired in Phase 13). Remains deferred.
- Crafting screen — Phase 14+. No crafting surface exists on Android in Phase 13.
- Dark/light mode contrast re-audit — design-system.mdx §7 defers the light-mode contrast audit to Phase 11 (noting region accents may fail on white). Still deferred. 13-11 audits dark mode only.
- Region-accent re-audit for light mode — same as above.
- Right-to-left languages — game does not ship RTL. No RTL layout mirroring required.
- Voice control (beyond TalkBack) — out of scope. TalkBack is the mandatory screen reader target for 13-11. Switch Access compatibility is a follow-up after TalkBack is clean.
- Onboarding mock-auth screen (Screen 2) and Splash (Screen 1) — these screens are production-deferred per the wireframe (mock auth will be replaced by Firebase Auth). A11y items on these screens are low-priority; they are NOT in the per-screen table above. Include them only if time permits after the primary 9 screens pass.
- Settings screen visual design — 13-11 ships the Settings screen as a functional stub (locale switcher row + possibly a haptic/audio toggle row as a placeholder). Full Settings screen visual design and additional settings rows are post-13-11.
- PostWalkSync screen — omitted from the per-screen table in §2 because it has no persistent interactive elements beyond the Continue CTA, and its a11y items were explicitly designed in the post-walk-sync wireframe (§Accessibility — Post-walk sync). Mobile-developer verifies the PostWalkSync screen against that wireframe’s screen-reader traversal order at 200% font scale, but it does not require a new per-screen table row. One 200% font-scale snapshot test is sufficient.
This document is W-level autonomous. EN/PL switcher placement (Option A — Settings screen) is a ui-designer decision logged to ops/decisions/2026-05-20-13-11-ui-locale-switcher-placement.md. All other content is an acceptance matrix, not a new design decision.