# Accessibility, Internationalization & Low-Bandwidth Strategy **Status:** Stable draft for MVP implementation **Related:** [03-content-format.md](03-content-format.md), [07-widget-sandboxing.md](07-widget-sandboxing.md), ADR-015, ADR-016 --- ## 1. Accessibility (target: WCAG 2.2 AA) Accessibility is a **review-gated content property** plus a **CI-gated platform property** — not a post-launch cleanup task. ### 1.1 Platform-level commitments - **Semantics first:** every interactive flow (problem attempt, editor, review) is usable with keyboard alone; focus order follows reading order; visible focus indicators meet 2.2's Focus Appearance; no keyboard traps (the MDX editor provides documented `Esc`-based escape from embedded editors). - **shadcn/ui + Radix primitives** give us accessible dialogs, menus, tabs out of the box; we add automated axe-core checks in Playwright CI for every top-level route and the five problem-type attempt flows, failing the build on regressions. - **Color & contrast:** design tokens enforce ≥ 4.5:1 text contrast in both themes; correctness feedback always pairs color with icon + text ("✓ Correct"); difficulty shown as text label, not color alone. - **Motion:** `prefers-reduced-motion` is propagated into widgets via the bridge `init` payload (doc 07 §3.4); streak/confetti animations are disabled accordingly. - **Forms & errors:** all answer inputs have programmatic labels; validation errors use `aria-describedby` + `role="alert"`; timed elements (none in MVP grading) are avoided by design — no problem has a time limit. - **Touch targets** ≥ 24 px (2.2 AA), with mobile attempt UI designed at 44 px. ### 1.2 Math accessibility - KaTeX renders with hidden MathML output enabled (KaTeX `output: 'htmlAndMathml'`), giving screen readers structured math on supporting stacks. - Content format supports an optional `speech` annotation per math block for cases where auto-derived reading is ambiguous; the review checklist (doc 06 §3.3 item 6) prompts for it on complex expressions. - Long expressions get horizontal scroll containers with `role="group"` + label rather than overflow clipping. ### 1.3 Content-level requirements (enforced by schema + review) - `Figure` blocks **require** `alt` (schema-level); decorative images use explicit `alt: ""` + `decorative: true`. - Diagrams authored as T1 declarative scenes must include a `description` field (rendered as the accessible name and available as expandable text for all users — benefits everyone on slow connections too). - Code blocks carry language identifiers for screen-reader pronunciation hints and are presented in `
` with proper `tabindex` for scrollable regions.
- T2 widgets must declare an **accessibility statement** in their registry manifest (keyboard support: full/partial/none; SR support) — rendered as a badge; widgets with `none` must be accompanied in-problem by an equivalent non-widget pathway (e.g., a numeric-entry fallback), enforced at content review.

### 1.4 Testing regime

- CI: axe-core (automated), eslint-plugin-jsx-a11y, contrast tokens test.
- Pre-release: manual screen-reader pass (NVDA + VoiceOver) on the attempt flow each minor release; documented script in `docs/qa/a11y-checklist.md` (future milestone).
- `/accessibility` statement page documents known gaps and a contact path (also satisfies EU accessibility-statement norms).

## 2. Internationalization

### 2.1 Two distinct problems

| Layer | Mechanism |
|---|---|
| **UI strings** | Standard message catalogs (ICU MessageFormat) via `next-intl`; keys in code, catalogs in `locales/{lang}.json`, community-translated via Weblate |
| **Content** | Translations are **versioned entities** flowing through review, not string catalogs |

### 2.2 Content translation model

- A translation of a problem is a linked entity: `Problem.translation_of = `, `translation_locale = "de"`, with its own versions and reviews (translation reviews require a reviewer grant in that locale, `reviewer:de:*`).
- The source version translated from is pinned (`translated_from_version_id`); when the source publishes a newer version, translations are flagged "outdated" with a structural diff of what changed — translators update and re-review.
- Search and recommendations prefer the user's locale, falling back to source-language content with a clear language badge.
- Locale-sensitive answer handling: numeric input accepts both `.` and `,` decimal separators based on locale with explicit display of the parsed value before submit (avoids silent misgrades).

### 2.3 Plumbing

- All user-visible dates/numbers via `Intl.*`; no hand-formatted strings.
- Full **RTL support**: logical CSS properties only (`margin-inline-start`, enforced by stylelint), `dir` attribute from locale, mirrored iconography where directional. Math remains LTR inside RTL text per convention (KaTeX handles this; we test Arabic UI + LTR math explicitly).
- Locale negotiation: explicit user setting > URL prefix (`/de/…` for indexable content pages) > `Accept-Language`. Content pages emit `hreflang` alternates for translated entities.
- Unicode hygiene: NFC normalization on all stored text; slug generation supports non-Latin scripts (transliteration optional, never required — `/problems/素数の性質` is valid).
- Source language of MVP is English, but **no English strings are hard-coded** — day-one catalogs prevent the retrofit tax.

## 3. Low-bandwidth & low-end device strategy

A core audience is learners on mid-range Android devices over constrained mobile data. Budgets are CI-enforced, not aspirational.

### 3.1 Performance budgets (CI-gated via Lighthouse CI on PRs)

| Metric | Budget |
|---|---|
| JS shipped on a problem attempt page (gzip) | ≤ 170 KB critical path |
| First Contentful Paint (Moto G4-class, 3G fast) | ≤ 2.5 s |
| Full problem page weight (no widgets) | ≤ 350 KB |
| Math rendering | server-rendered KaTeX HTML — **zero client math JS** for static math |

### 3.2 Techniques

- **Server-render everything readable.** Problem statements, math (KaTeX SSR), code highlighting (Shiki at render time) arrive as HTML; React hydrates only interactive islands (answer form, widgets). Next.js App Router server components carry the static majority.
- **Lazy interactive tiers:** T1 engines (e.g., JSXGraph) and T2 iframes load on intersection or tap-to-activate, with SSR-generated poster images (doc 07 §3.5). "Data saver" user setting forces tap-to-activate everywhere and disables prefetch.
- **Images:** responsive `srcset`, AVIF/WebP with fallbacks, dimensions always set (no CLS), thumbnails ≤ 25 KB; diagrams preferred as inline SVG (compresses superbly, scales, themable).
- **Fonts:** system font stack for UI (zero font bytes); KaTeX fonts subsetted + `font-display: swap`.
- **Caching for repeat visits:** immutable content-addressed assets, service worker (Workbox) precaching the app shell + last N visited lessons, enabling **offline re-reading and offline attempt drafting** (attempts queue and sync; grading remains server-side, clearly indicated as "pending sync").
- **API frugality:** list endpoints return sparse field sets by default (`?fields=` expansion); brotli compression; ETags on published content keyed by `content_hash` (perfect cache validators since versions are immutable).
- **No third-party trackers.** Self-hosted analytics (privacy + bytes).

### 3.3 Graceful degradation matrix

| Condition | Experience |
|---|---|
| No JS | Read everything (statements, hints behind `
`, solutions, discussions); MCQ/numeric attempts submit via plain `
` POST fallback; widgets show poster + description | | Save-Data header / data-saver setting | Tap-to-activate widgets, no prefetch, low-res images | | Offline (return visit) | Cached lessons readable; attempts queued | | Old browser (no ES2020) | Static reading experience with banner; no broken half-app | The no-JS form fallback is not a gimmick: it doubles as the accessibility safety net and keeps the platform usable in restrictive corporate/school environments.