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
123 lines
9.4 KiB
Markdown
123 lines
9.4 KiB
Markdown
# 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.
|
|
|
|
- [x] **Step 1: Add dependencies** — `i18next` + `react-i18next` (already in `frontend/package.json`).
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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).
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **Step 2: Frontend action** — `setLanguage(language)` dispatches locally (immediate UI feedback) and sends the frame; outbox queueing (Plan 3 `VeilSocket`) covers the reconnect edge.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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).
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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_id`s.
|
|
|
|
- [x] **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.
|