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
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
Languageis 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).
- Foundation & Auth (complete)
- Frontend, LLM & Realtime Pipeline (complete)
- Wire Ghost mode + Ouija/Planchette UI (complete)
- EVP Listening mode (complete)
- Spirit Radio mode (implementation complete; hardware pass pending)
- Codex & entity persistence (complete)
- 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
i18ninstance (resourcesen/es,lngfrom storage,fallbackLng: 'en');storedLanguage() -> LanguageandpersistLanguage(lang)around theqm_languagelocalStorage 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 infrontend/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.tsas above; imported once at app entry.Languagetype reused fromsrc/lib/types.tsso the UI language and the WS language frame can never drift apart. -
Step 4: Verify —
npx tsc --noEmitclean; both bundles parse (resolveJsonModule).
Task 2: Component Sweep to useTranslation()
Files:
- Modify: every component/page with user-facing copy (
src/components/*.tsx, pages,App.tsxshell)
Interfaces:
-
Consumes: Task 1's bundles.
-
Step 1: Sweep — all visible strings via
const { t } = useTranslation()andt('…')(verified in the shared components:EntityCard,Transcript,TelemetryReadoutconsumet(); 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, withcommon.yes/no/goodbyekeys 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), anduseSeance().setLanguage(lang)so the backend follows in the same gesture (Task 3). -
Step 3: Verify —
npx tsc --noEmitclean; switching languages re-renders all strings without reload.
Task 3: The Language Frame & Backend Language State
Files:
- Modify:
backend/app/ws.py(languagemessage branch,ContactSession.languagepersistence) - Modify:
backend/app/models/contact_session.py(languagecolumn — landed with Plan 3 Task 2) - Modify:
frontend/src/state/seance.tsx(setLanguageaction + frame)
Interfaces:
-
Frame:
{type: "language", language: "en"|"es"}— accepted values only; anything else ignored. State:SeanceState.language(default"en"), consumed by everyspirit_servicecall site (fragment,wire_whisper,chat_stream,mint_profile) and by TTS voice selection. -
Step 1: Backend branch — on
language, updatestate.languageand persistContactSession.languagein the same write pattern asset_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 3VeilSocket) covers the reconnect edge. -
Step 3: Verify — covered by
tests/test_ws_session.pysession 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_voicelanguage fallback) - Assets:
backend/voices/es_ES-davefx-medium.onnx,backend/voices/es_MX-ald-medium.onnx(+.onnx.jsonconfigs) - Test:
backend/tests/test_prompts.py(Spanish clause tests)
Interfaces:
-
Produces:
language_clause(language) -> str—" Reply in Spanish."fores,""otherwise — appended toFRAGMENT_SYSTEM,WIRE_SYSTEM, andCHAT_SYSTEMvia their{language_clause}placeholders;ES_VOICE_IDS = ["davefx", "ald"];pick_voice(voice_id, language)falling back todavefxfor Spanish sessions,lessacfor English;mint_profilepassingES_VOICE_IDSinto the mint prompt whenlanguage == "es"so new spirits are born with Spanish throats. -
Step 1: Write the failing tests —
test_spanish_language_clause_appliedasserts "Spanish" appears infragment_system,wire_system, andchat_systemunder"es";test_fragment_system_carries_fiction_framing_not_paranormal_claimpins 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) andald(es_MX, medium) inbackend/voices/, registered in theVOICEScatalog with Spanish descriptions.normalize_profile(Plan 6) already accepts both ids as validvoice_ids. -
Step 4: Verify —
tests/test_prompts.pyPASS (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.