Skip to content

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.

ScreenCompose a11y items requiredLive regionsFont-scale 200% acceptanceFocus order
Onboarding — ClassPickMin 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 bodyNone 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 nameTop-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”)NoneRegion description wraps; guild hint wraps; edit icon 48dp invisible target; CTA grows verticallyBack → Walker name row → edit icon → region card → guild card → primary CTA
HomeMin 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 × 4Streak 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
TreeViewerMin 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
QuestLogMin 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
QuestDetailMin 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
FactionOverviewMin 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)
FactionDetailMin 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
CombatScreenMin 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 CTAWalker 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 contentDescription or semantics { 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.xml via stringResource(R.string.*). Audit method: grep walkrpg-mobile source 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 uses one and other forms 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. Use NumberFormat.getNumberInstance(locale) or equivalent — no hardcoded format strings.
  • Quest names on system surfaces use name.en regardless 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 use name.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 at fontScale = 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_label or 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 required contentDescriptions 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 via resources.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.