# Accessibility, Localization & Quality

```mermaid
flowchart TB
  Experience[Room class concert Studio or control surface] --> A11y[Input visual audio cognitive motion and comfort variants]
  Experience --> Locale[Strings speech captions fonts layout and cultural adaptation]
  Experience --> Telemetry[Versioned functional quality and accessibility signals]
  A11y --> Tier[Per-client-tier realization and fallback]
  Locale --> Tier
  Tier --> Eval[Per-surface automated and human evaluation gates]
  Telemetry --> Eval
  Eval --> Release[Named tier locale platform and cohort release]
  Release --> Observe[Regression support and rollback evidence]
```

Accessibility, localization, and quality are release inputs for every supported
surface/tier. Equivalent member jobs require explicit modality fallbacks and
current evidence rather than a single global conformance claim.

Three commitments decide whether the Lilith metaverse is something everyone can
actually use, in their own language, with proof that it works: an avatar in a 3D
room has to be legible to a screen-reader user and a photosensitive user and a
one-thumbed mobile user; a class spoken in English has to read and sound right
to an attendee in Seoul or Riyadh standing in the same room; and every one of
those claims has to be measured against a hard threshold before it ships. V3
treats all three as engineering surfaces with code behind them, not aspirations
in a deck. The accessibility modes and the localization pipeline live as
decision engines inside the largest library in V3 —
`@oshun/tenant-lilith-commons` (`libs/v3/lilith-commons`, a 13,458-line
`src/index.ts` with a 4,536-line, 143-case test suite). The telemetry schema,
the eight per-surface eval gates, and the WCAG conformance gate live one layer
out in `@oshun/analytics`, deliberately adjacent to V1's evaluation pipeline so
release criteria stay versioned with the rest of Oshun. The shared string
catalog and locale formatting are V1's `@oshun/i18n`, reused rather than forked.
This page covers what is real in each, what is honestly a seam, and the gate
machinery that refuses to lie about either. The section hub is
[../V3_features.md](../V3_features.md).

## What ships, honestly

The line between real and referenced is consistent across all three areas:

- **Real and tested — the gates and validators.** Every accessibility mode and
  every localization surface is a `create*Plan()` / `build*Report()` pair: a
  deterministic engine that takes a plan, checks it against named domain
  thresholds (flash rate ≤ 3 Hz, contrast ≥ 4.5, caption p95 ≤ 1 000 ms), and
  returns a schema-stamped report carrying a `passed` boolean, a
  `qaReviewStatus` of `passed` / `blocked`, and an enumerated `failures[]` list.
  These are fail-loud decision engines, not config tables — `buildV3…Report`
  returns `blocked` and names the breach the instant a budget regresses. The
  143-case suite asserts both the pass path and the block path for each mode.
- **Real evidence, fail-closed gates.** The eval-set manifest, the telemetry
  schema, the eight Grafana boards, and the WCAG QA manifest are real typed
  artifacts in `@oshun/analytics` with committed observed runs and validation
  evidence; the gate functions reject publication on any missing surface, absent
  threshold, or breached metric.
- **Referenced by string — the in-world rendering.** The caption widget floating
  above an avatar's head, the deuteranopia palette swap on an asana cue, the
  RTL-mirrored UMG HUD — the actual Unreal rendering is content addressed by
  path. `lilith-commons` owns the data, the policy, and the gate; the UE
  `V3Telemetry` module is an honest seam that reports its owned surface and
  instruments nothing (see
  [../architecture/observability-performance-security-and-launch.md](../architecture/observability-performance-security-and-launch.md)).

## Accessibility in 3D

### The seven machine-gated modes

`lilith-commons` declares ten accessibility/localization capabilities in its
package descriptor, each carrying an `operationalMetric` string that is the gate
in shorthand. Seven are accessibility modes, each with a builder and a report
schema:

- **Live avatar captions** (`buildV3LiveAvatarCaptionReport`, schema
  `v3.accessibility.live-avatar-captions.v1`). Speech-to-text captions are
  required on all three surfaces — `web`, `mobile`, `vr` — anchored above the
  speaker's avatar. The report computes per-surface p95 latency from
  `renderedAtMsBySurface − spokenAtMs`, enforces the
  `V3_LIVE_AVATAR_CAPTION_LATENCY_BUDGET_MS = 1_000` ms budget, and verifies the
  anchor genuinely floats above the head (`worldPositionMeters.y > 1.2`,
  `followsAvatarTransform`, screen position in the top 40%). The test asserts a
  block with the exact string
  `caption surface mobile p95 latency 1240ms exceeds 1000ms` when a surface
  regresses.
- **Reduced motion** (`createV3ReducedMotionPlan` /
  `buildV3ReducedMotionReport`). Caps camera yaw at
  `V3_REDUCED_MOTION_CAMERA_YAW_CAP_DEGREES = 6`°, avatar motion at
  `…AVATAR_MOTION_CAP_CENTIMETERS = 1` cm, forces `teleport` locomotion,
  disables continuous parallax, and requires snap-turn-only. The "off" path
  deliberately emits an uncapped plan (yaw 12°, smooth-slide, parallax on) so
  the gate can prove it blocks the unsafe configuration.
- **Color-vision palettes** (`buildV3ColorVisionPaletteReport`). Three palettes
  — `deuteranopia`, `protanopia`, `tritanopia` — gated to
  `V3_COLOR_VISION_REQUIRED_PALETTE_COUNT = 3`, with asana-cue, marker, and
  label contrast each held to
  `V3_COLOR_VISION_ASANA_CUE_MIN_CONTRAST_RATIO = 4.5` (the WCAG 2.2 AA contrast
  floor), plus a ≥ 44 px hit area and ≥ 4 px cue line width per surface. A cue
  never depends on a single hue.
- **Photosensitive safe mode** (`buildV3PhotosensitiveSafeModeReport`). Holds
  observed flash rate to `V3_PHOTOSENSITIVE_SAFE_MAX_FLASH_RATE_HZ = 3` (the
  WCAG 2.2 SC 2.3.1 three-flash threshold), caps luminance delta, requires
  strobe attenuation ≥ 90%, bounds bloom intensity and emissive-pulse amplitude,
  locks auto-exposure, and disables rapid-cut transitions — the same constraint
  the concert Sequencer enforces on stage lighting.
- **One-handed mobile HUD** (`buildV3OneHandedMobileHudReport`). Docks nine
  primary targets into the bottom thumb zone, requiring each to start at ≥ 56%
  viewport height with a ≥ 44 px hit area; the inventory includes the
  `spatial-audio-off`, `audio-descriptions`, `photosensitive-safe`, and
  `caption-surface` toggles, so the other accessibility modes remain reachable
  one-handed.
- **Cognitive-load reduction** (`buildV3CognitiveLoadReductionReport`). Caps
  visible primary controls at 5, holds dwell time ≥ 1 200 ms and transition
  dwell ≥ 1 000 ms, limits instruction copy to ≤ 7 words, and requires
  icon+label pairs, progressive disclosure, and disabled autoplay prompts.
- **Keyboard / single-switch navigation**
  (`buildV3KeyboardSingleSwitchNavigationReport`). Full desktop operability with
  no pointer, scan cycle ≤ 1 200 ms — the gate that makes the 2D surfaces usable
  by switch-access users.

Each builder produces a report whose `failures[]` is computed from real
per-surface predicates, not a hardcoded verdict; flipping any input below its
floor changes the verdict and the enumerated reason. This is why the suite can
assert both `report.passed === true` on the canonical plan and a specific
failure string on a deliberately broken one.

### Audio descriptions, spatial-audio-off, and VR comfort — where the line is

The features brief lists more affordances than `lilith-commons` gates directly,
and the docs are explicit about which is which. **Audio descriptions** and
**spatial-audio-off** appear in the HUD reach inventory and the keyboard-nav
target set (`audio-description-toggle`, `mobile-hud:spatial-audio-off`) — so
they are gated for _reachability_ — but the audio behavior itself lives in
`libs/v3/spatial-audio`, whose renderer is an `hrtf-convolution` path with an
`ambisonic-stereo-fallback` (the flat-stereo mix for users with HRTF-induced
disorientation). Spatial-audio-off additionally carries its own verifier
(`verify:v3 spatial-audio-off-mode`) in the QA manifest. **VR comfort and
calibration** — vignetting, snap-degree presets, IPD, floor recalibration — are
specified in the features and architecture docs and gated through the WCAG QA
manifest rather than a `lilith-commons` builder. Calling this out is the honest
posture: the seven modes above have deterministic report engines; the rest are
real but live in other libraries or as QA-manifest checklist items.

### The WCAG 2.2 AA bar

The target is **WCAG 2.2 AA** for 2D surfaces, with the W3C XR accessibility
user-needs draft as a north star for the evolving XR work. Crucially,
accessibility parity is a _tiered-stack_ requirement: the Tier-2 web fallback
must clear the same AA bar as the canonical UE client, and the fallback's WCAG
conformance is itself a launch-readiness gate — gated, not aspirational.

## Localization

### Eighteen launch locales, real HUD strings, real RTL

V3 enumerates **18 GA launch locales** with a hard floor of 16
(`V3_LAUNCH_LOCALIZATION_MIN_LOCALE_COUNT = 16`,
`V3_LAUNCH_LOCALIZATION_REQUIRED_LOCALE_IDS`): the three English variants
(`en-US/GB/IN`), `es-ES/MX`, `pt-BR`, `fr-FR/CA`, `de-DE`, `it-IT`, `hi-IN`,
`ja-JP`, `ko-KR`, `zh-CN/TW`, `ar`, `he`, and `yo-NG`. Yoruba is in the GA set
ahead of the African-region wave-2 launch it supports. Each locale carries a
`createV3LaunchLocalizationLocalePack` with its direction; `ar` and `he` are
`rtl`.

The six required HUD strings (`hud.enter`, `hud.stream-status`,
`hud.caption-anchor`, `hud.accessibility-controls`, `hud.tara-instructor-voice`,
`hud.saraswati-persona-voice`) are translated for all 18 locales in
`launch-locale-hud-translations.ts`, typed
`satisfies Record<V3LaunchLocaleId, Record<V3LaunchLocalizationHudStringId, string>>`
so the build fails if any locale is missing a string. The translations are
genuinely non-English — Yoruba renders "Enter Lilith" as `Wọ Lilith` and
"Captions" as `Ọ̀rọ̀ ìsàlẹ̀`, Hebrew as `כתוביות` — and
`buildV3LaunchLocalizationLocaleReport` computes a `differsFromEnglish`
predicate that _blocks_ a locale whose strings were merely copied through from
`en-US` (English regional variants legitimately share base wording).
`buildV3LaunchLocalizationReport` then ANDs, across every enumerated locale,
that HUD strings are localized, the UE-UMG and Tier-2-HTML HUD bundles are
QA-validated, subtitle anchors validate on `web/mobile/vr`, voice dubs are
approved, and — for `ar`/`he` specifically — the RTL layout validates. A single
unvalidated bundle returns `qaReviewStatus: 'blocked'` with a named failure.

### Voice dubbing and cultural adaptation

Two further localization engines round out the pipeline. The **voice-dubbing
pipeline** (`buildV3VoiceDubbingPipelineReport`) requires GA jobs for both
source kinds — `tara-instructor-class` and `saraswati-artist-speech` — each
targeting a second locale, clearing a GA quality floor of
`V3_VOICE_DUBBING_PIPELINE_MIN_GA_QUALITY_SCORE = 0.95`, and passing
translation, pronunciation, timing, and subtitle review _plus_ a rights gate
(voice consent, signature, scope lock, signoff, royalty route) and an
adversarial policy gate with zero breaches. The **cultural-adaptation review**
(`buildV3CulturalAdaptationReviewReport`) gates tradition-sensitive Tara and
Commons content through a documented process (anchored to a real
`V3/CULTURAL_ADAPTATION_REVIEW.md` reference and an ISO-timestamped signoff),
covering all launch locales with five required review roles, an adaptation
brief, a protected-terms glossary, documented prohibited claims, and an approved
release signoff applied to GA inventory — so a localized dub cannot flatten
lineage-specific material. These engines are what the festival programming and
venue content lean on; see
[./commons-venues-and-live-service.md](./commons-venues-and-live-service.md).

### The shared `@oshun/i18n` spine

Underneath the V3 in-world layer sits V1's `@oshun/i18n` (`libs/oshun/i18n`, 721
lines) — the canonical catalog for shared customer-facing shell strings. It is a
distinct, narrower layer: **8 V1 launch locales** (`en-US`, `es-US`, `fr-FR`,
`de-DE`, `ar`, `he`, `ja-JP`, `pt-BR`) covering 26 cross-shell message keys
(`assistant.disclosure.ai`, `evidence.provenance.open`, `privacy.dsar_received`,
…), each fully translated, not the 18-locale in-world HUD set. What makes it
load-bearing for V3 is the machinery the in-world layer reuses: a documented
`translate()` fallback chain (requested → language-only → `en-US`) that records
a `fallbackApplied` flag and emits a `TranslationFallbackEvent` so a missing
string logs a gap rather than rendering empty; `findFallbackGaps()`, which
enumerates every `<key, locale>` pair still resolving via fallback so a
pre-launch audit can prove zero gaps; an `RTL_LOCALES` set and `localeDirection`
helper; per-locale text-expansion budgets (`de-DE` 1.4×, `ja-JP` 0.7× relative
to `en-US`) that layout QA verifies; and real `Intl.*`-based date, currency,
number, and relative-time formatting per locale. The V3 promise that "a missing
string falls back to US English with a logged gap" is this code, exactly.

## Telemetry, eval, and quality

### The versioned telemetry schema

V3 telemetry rides V1's analytics package and Grafana stack — it adds schemas
and boards, not infrastructure. `v3-telemetry-schema.ts` (published as
`schema-v3`) defines versioned events with an `eventId`, `surface`, consent
mode, and _non-PII_ property definitions: `oshun.accessibility.mode_changed`
(e.g. `mode: keyboard_single_switch`, `state: enabled`),
`oshun.localization.locale_selected` (`rtl_layout`, `voice_dub_available`,
`subtitle_anchor_available`), `oshun.tier_router.decision_recorded`,
`oshun.safety.report_submitted`, and `saraswati.music_sync.drift_measured`.
Trace IDs are intended to span a full session lifecycle, emitted over
OpenTelemetry; the world server is the proof point, with its OTLP exporter
opt-in behind `V3_WORLD_SERVER_OTEL` while Prometheus scraping needs no flag.
The eight tier-stack dashboards (`v3-grafana-dashboards.ts`) each carry a named
PagerDuty rotation and an alert that fires when its gate is not green.

### Eight eval-set release gates, fail-closed

The heart of "quality" is `v3-eval-sets-release-gates.ts`: eight required eval
surfaces, each with a hard threshold and a `direction` of `min` or `max`.

| Surface           | Metric                                   | Gate     | Observed |
| ----------------- | ---------------------------------------- | -------- | -------- |
| World-server tick | `snapshot_p99_ms`                        | ≤ 25 ms  | 18.4 ms  |
| Gateway latency   | `browser_pop_rtt_p99_ms`                 | ≤ 60 ms  | 44 ms    |
| Voice latency     | `mouth_to_ear_rtt_p95_ms`                | ≤ 80 ms  | 58 ms    |
| Music-sync drift  | `cross_attendee_drift_p95_ms`            | ≤ 25 ms  | 19 ms    |
| Aja accuracy      | `canonical_asana_min_per_asana_accuracy` | ≥ 0.92   | 0.94     |
| Persona-policy    | `adversarial_policy_breach_count`        | ≤ 0      | 0        |
| Crisis routing    | `crisis_routing_recall`                  | ≥ 1.0    | 1.0      |
| Harassment SLA    | `harassment_report_action_p95_minutes`   | ≤ 45 min | 31 min   |

`buildV3PerSurfaceEvalSetReport` and `evaluateV3ReleaseGateEvalRun` implement
the threshold logic directly
(`direction === 'min' ? value >= threshold : value <= threshold`) and **fail
closed**: publication is rejected if a required surface is missing, a threshold
is absent, an evidence path is missing, the CI state is not green, or any
observed metric misses its bar — a `needs_data` state for any missing metric,
`red` for any breach. The committed runs all pass, but the manifest is wired so
a regressed Aja accuracy or a single persona-policy breach cannot be papered
over at release. The eval gate is one constituent of the launch gate, not a
standalone green.

### The WCAG gate and the honestly-red launch gate

Accessibility folds into one machine-checked artifact too:
`v3-testing-qa-accessibility.ts` is a fail-closed QA gate whose checklist
includes `70.vitest-ts-coverage`, `70.axe-web-2d`,
`70.motion-photosensitive-audio` (wiring `verify:v3 reduced-motion-mode`,
`verify:v3 photosensitive-safe-mode`, `verify:v3 spatial-audio-off-mode`),
keyboard navigation, and a screen-reader audit, emitting
`V3/qa/testing-qa-accessibility-release-gate.json` (status `green`): Vitest at
84.6% meaningful-line coverage, Playwright on chromium+webkit with zero
serious/critical axe violations, and the WCAG 2.2 AA modes at 100% coverage with
a signed-off artifact.

All of this feeds the §75 launch-readiness gate, which is the output of
`scripts/v3/verify-v3-launch-readiness.mjs` executing every constituent verifier
and ANDing the results. Today it reads `status: red`, `passingGateCount: 30` of
41, `gaReady: false` — the correct, load-bearing state for a pre-GA build, with
the eval and accessibility gates green and the still-open gates (Pixel Streaming
fleet 5×, cross-platform certification, GA-inventory rollups) honestly red. That
red is the point: the gate is wired so its green is unforgeable. The full
launch-and-rollout machinery is covered in
[./operations-rollout-and-launch.md](./operations-rollout-and-launch.md), and
the observability, eval, and gate architecture in
[../architecture/observability-performance-security-and-launch.md](../architecture/observability-performance-security-and-launch.md).
