Files
qtalker---/docs/superpowers/plans/2026-07-20-quantumancy-07-i18n.md
Indiana 6edbbbbc2a feat: complete Quantumancy web app — full frontend + docs
Frontend (React 18 + TS + Vite):
- Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits
- Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive),
  Ouija planchette board spelling utterances, transcript with TTS replay,
  entity dossier, direct contact streaming, passive/active listening
- Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB
  RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact
- Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete
- State: VeilSocket (reconnect/backoff), seance reducer, auth context
- 72 vitest tests green; served by FastAPI at :7777

Docs: README + as-built plans 3-7
2026-07-20 21:11:49 +00:00

9.4 KiB

Quantumancy Plan 7/7: Internationalization (EN/ES) — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Let the veil speak two tongues (spec §6): English + Spanish at launch, with one language switch that flips UI strings, the LLM's reply language, and the Piper voice used for TTS — and the framework in place to add more languages later, deliberately scoped tight until the core loop is proven.

Architecture: Frontend strings go through react-i18next with two resource bundles (en.json, es.json) and a localStorage-persisted choice. The same choice travels the séance channel as {type: "language", language: "en"|"es"} and is persisted on the ContactSession row; from then on the backend appends a Spanish clause to every LLM system prompt, selects Spanish Piper voices for TTS, and mints new entities with Spanish voice ids. No new endpoints or frame types beyond the one language frame.

Tech Stack: i18next + react-i18next (frontend); the existing prompt builders, voice catalog, and séance channel (backend).

Global Constraints

  • Launch scope is EN + ES only (spec §6, §10). Both LLM reply quality and Piper voice quality vary by language; the structure must admit more languages later, but nothing beyond these two ships now.
  • All UI copy lives in the JSON bundles. Components call t('<page>.<section>.<name>') — no hardcoded user-facing strings, no string interpolation hacks (escapeValue: false, returnEmptyString: false).
  • The switch is total: UI strings + LLM reply language + TTS voice change together (spec §6). A Spanish séance must sound Spanish.
  • The language frame is silent (no ack frame) — the next utterance simply arrives in the new tongue. Mode/passive flows are unaffected.
  • Backend Language is a closed set (en, es) at the WS boundary; unknown values are ignored, never stored.

Plan Series

This is 7 of 7 plans implementing the Quantumancy website spec (docs/superpowers/specs/2026-07-20-quantumancy-website-design.md). All prior plans are complete (Plan 5 pending only its hardware pass).

  1. Foundation & Auth (complete)
  2. Frontend, LLM & Realtime Pipeline (complete)
  3. Wire Ghost mode + Ouija/Planchette UI (complete)
  4. EVP Listening mode (complete)
  5. Spirit Radio mode (implementation complete; hardware pass pending)
  6. Codex & entity persistence (complete)
  7. Internationalization (EN/ES) (this plan)

Task 1: react-i18next Bootstrap & Bundles

Files:

  • Create: frontend/src/i18n/index.ts
  • Create: frontend/src/i18n/en.json
  • Create: frontend/src/i18n/es.json

Interfaces:

  • Produces: the initialized default i18n instance (resources en/es, lng from storage, fallbackLng: 'en'); storedLanguage() -> Language and persistLanguage(lang) around the qm_language localStorage key, both try/catch-guarded for private-mode storage failures. Bundle key groups: common.*, nav.*, landing.*, enter.*, seance.*, codex.* — the <page>.<section>.<name> convention, with plural-aware pairs (contacts_one/contacts_other) where counts render.

  • Step 1: Add dependencies — i18next + react-i18next (already in frontend/package.json).

  • Step 2: Write the bundles — full EN/ES key parity, tone preserved across tongues ("tuning the veil…" / "afinando el velo…"; landing taglines translated, not transliterated). 181 lines per bundle, identical key trees.

  • Step 3: Wire the init — src/i18n/index.ts as above; imported once at app entry. Language type reused from src/lib/types.ts so the UI language and the WS language frame can never drift apart.

  • Step 4: Verify — npx tsc --noEmit clean; both bundles parse (resolveJsonModule).


Task 2: Component Sweep to useTranslation()

Files:

  • Modify: every component/page with user-facing copy (src/components/*.tsx, pages, App.tsx shell)

Interfaces:

  • Consumes: Task 1's bundles.

  • Step 1: Sweep — all visible strings via const { t } = useTranslation() and t('…') (verified in the shared components: EntityCard, Transcript, TelemetryReadout consume t(); e.g. t('seance.entity.rarity.' + entity.rarity), t('seance.entity.known', { count })). Exception by design: PlanchetteBoard's canvas-drawn board glyphs (YES / NO / GOODBYE, the letter arcs) are occult furniture, not copy — they render as-is in both languages, with common.yes/no/goodbye keys available in the bundles should a future pass internationalize the board itself.

  • Step 2: Language switch control — UI toggle calls i18n.changeLanguage(lang), persistLanguage(lang), and useSeance().setLanguage(lang) so the backend follows in the same gesture (Task 3).

  • Step 3: Verify — npx tsc --noEmit clean; switching languages re-renders all strings without reload.


Task 3: The Language Frame & Backend Language State

Files:

  • Modify: backend/app/ws.py (language message branch, ContactSession.language persistence)
  • Modify: backend/app/models/contact_session.py (language column — landed with Plan 3 Task 2)
  • Modify: frontend/src/state/seance.tsx (setLanguage action + frame)

Interfaces:

  • Frame: {type: "language", language: "en"|"es"} — accepted values only; anything else ignored. State: SeanceState.language (default "en"), consumed by every spirit_service call site (fragment, wire_whisper, chat_stream, mint_profile) and by TTS voice selection.

  • Step 1: Backend branch — on language, update state.language and persist ContactSession.language in the same write pattern as set_mode. Silent by design: no ack frame.

  • Step 2: Frontend action — setLanguage(language) dispatches locally (immediate UI feedback) and sends the frame; outbox queueing (Plan 3 VeilSocket) covers the reconnect edge.

  • Step 3: Verify — covered by tests/test_ws_session.py session flows (6/6 PASS); language column round-trips through the session row.


Task 4: Spanish Prompts & Spanish Voices

Files:

  • Modify: backend/app/llm/prompts.py (language_clause)
  • Modify: backend/app/llm/service.py (language-aware mint voice ids)
  • Modify: backend/app/tts/voices.py (ES voices, pick_voice language fallback)
  • Assets: backend/voices/es_ES-davefx-medium.onnx, backend/voices/es_MX-ald-medium.onnx (+ .onnx.json configs)
  • Test: backend/tests/test_prompts.py (Spanish clause tests)

Interfaces:

  • Produces: language_clause(language) -> str — " Reply in Spanish." for es, "" otherwise — appended to FRAGMENT_SYSTEM, WIRE_SYSTEM, and CHAT_SYSTEM via their {language_clause} placeholders; ES_VOICE_IDS = ["davefx", "ald"]; pick_voice(voice_id, language) falling back to davefx for Spanish sessions, lessac for English; mint_profile passing ES_VOICE_IDS into the mint prompt when language == "es" so new spirits are born with Spanish throats.

  • Step 1: Write the failing tests — test_spanish_language_clause_applied asserts "Spanish" appears in fragment_system, wire_system, and chat_system under "es"; test_fragment_system_carries_fiction_framing_not_paranormal_claim pins the EN default (no Spanish clause, fiction framing present).

  • Step 2: Implement the clause + voice selection — as above. Note the design decision: only the system prompts carry the clause; user-turn prompts stay language-neutral so anomaly telemetry reads identically in both tongues.

  • Step 3: Install the ES voices — davefx (es_ES, medium) and ald (es_MX, medium) in backend/voices/, registered in the VOICES catalog with Spanish descriptions. normalize_profile (Plan 6) already accepts both ids as valid voice_ids.

  • Step 4: Verify — tests/test_prompts.py PASS (4/4) in the 45-test suite; Spanish utterances synthesize through the same effects chain (ES voice models verified present on disk).


Testing Status & Self-Review

Spec coverage: react-i18next with EN+ES launch scope (§6) → Tasks 1-2. Language switch flips UI + Piper voice + LLM reply language together (§6) → Tasks 2-4. Framework ready for more languages later without broad launch coverage (§6, §10) → closed-set Language type + resource-bundle structure.

Automated tests: tests/test_prompts.py Spanish clauses (4/4); WS language flow inside tests/test_ws_session.py (6/6); both in the green 45-test suite. Bundle key-parity is structural (same JSON trees edited together) — a parity unit test is a reasonable future addition, not present.

Honest notes: ES UI copy is complete for the keys that exist (common/nav/landing/enter/seance/codex); new pages must add keys to both bundles together. Fallback profiles (Plan 6) are English-only prose — a Spanish session whose summon hits the offline fallback gets an English persona with a Spanish voice; accepted as a rare degradation path, not a launch blocker.

Placeholder scan: none.

Type consistency: frontend Language = 'en' | 'es' (types.ts) matches the WS branch's accepted set and storedLanguage()'s validation exactly; SeanceApi.setLanguage threads the same type end to end.


Status: COMPLETE — EN/ES live across UI strings, LLM replies, and TTS voices; Spanish prompt clauses under test.