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
This commit is contained in:
229
frontend/src/i18n/en.json
Normal file
229
frontend/src/i18n/en.json
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "QUANTUMANCY",
|
||||
"loading": "tuning the veil…",
|
||||
"error": "something stirred in the static",
|
||||
"replay": "replay",
|
||||
"yes": "YES",
|
||||
"no": "NO",
|
||||
"goodbye": "GOODBYE",
|
||||
"dismiss": "dismiss"
|
||||
},
|
||||
"nav": {
|
||||
"home": "the threshold",
|
||||
"codex": "codex",
|
||||
"seance": "séance",
|
||||
"enter": "enter",
|
||||
"logout": "depart"
|
||||
},
|
||||
"landing": {
|
||||
"taglines": [
|
||||
"The wire hums with all of us.",
|
||||
"Every packet leaves a ghost.",
|
||||
"The static remembers your name.",
|
||||
"Somewhere between 88 and 108 MHz, someone is waiting.",
|
||||
"You were never alone on the line."
|
||||
],
|
||||
"heroKicker": "an instrument for speaking with the dead bandwidth",
|
||||
"cta": "BEGIN THE SÉANCE",
|
||||
"ctaCodex": "browse the codex",
|
||||
"stats": {
|
||||
"entities": "spirits catalogued",
|
||||
"sessions": "séances held",
|
||||
"utterances": "utterances recorded",
|
||||
"anomalies": "anomalies witnessed"
|
||||
},
|
||||
"tickerPrefix": "the veil, live",
|
||||
"modesTitle": "HOW THE VEIL ANSWERS",
|
||||
"modes": {
|
||||
"wire": {
|
||||
"name": "Wire Ghost",
|
||||
"desc": "Leave the line open. The network itself mutters — jitter, latency, stray DNS — and something downstream learns to speak with your accent."
|
||||
},
|
||||
"evp": {
|
||||
"name": "EVP Listening",
|
||||
"desc": "Lend the dead your microphone. We watch the voice band for syllables that were never spoken aloud: spikes 8dB above the room's silence."
|
||||
},
|
||||
"radio": {
|
||||
"name": "Spirit Radio",
|
||||
"desc": "An RTL-SDR sweeps the FM band like a lantern dragged across a dark field. Where the needle jumps, the radio dead push through."
|
||||
},
|
||||
"ouija": {
|
||||
"name": "Ouija / Direct Contact",
|
||||
"desc": "Ask plainly. The planchette drifts, then begins to spell — and the words arrive letter by letter, whether or not you wanted them."
|
||||
}
|
||||
},
|
||||
"featured": "most disturbed spirits",
|
||||
"featuredEmpty": "the registry is still blank — be the first to disturb it",
|
||||
"footer": "an interactive horror art installation — the spirits are fiction; the static is real",
|
||||
"footerColophon": "self-hosted · no cloud · the veil runs on your machine"
|
||||
},
|
||||
"enter": {
|
||||
"reconnect": "Reconnect",
|
||||
"firstContact": "First contact",
|
||||
"reconnectSub": "the veil remembers you. prove it.",
|
||||
"firstContactSub": "choose the name the spirits will know you by.",
|
||||
"username": "name",
|
||||
"password": "passphrase",
|
||||
"submitLogin": "step through",
|
||||
"submitRegister": "carve your name",
|
||||
"switchToRegister": "no name in the registry? first contact →",
|
||||
"switchToLogin": "already carved your name? reconnect →",
|
||||
"failed": "the veil rejected you: {{message}}",
|
||||
"smallPrint": "your medium handle is only ever stored on this server"
|
||||
},
|
||||
"seance": {
|
||||
"modes": {
|
||||
"wire": "Wire Ghost",
|
||||
"evp": "EVP",
|
||||
"radio": "Spirit Radio",
|
||||
"ouija": "Ouija"
|
||||
},
|
||||
"passive": "passive listening",
|
||||
"summon": "SUMMON",
|
||||
"summoning": "summoning…",
|
||||
"askPlaceholder": "ask the dead a question…",
|
||||
"ask": "ask",
|
||||
"status": {
|
||||
"attuning": "attuning — the veil is fingerprinting a presence…",
|
||||
"summoning": "the veil parts… something is crossing…",
|
||||
"gathering": "the spirits are gathering energy…"
|
||||
},
|
||||
"connection": {
|
||||
"connecting": "reaching across…",
|
||||
"open": "the line is open",
|
||||
"unstable": "the connection to the other side is unstable…",
|
||||
"closed": "the line is dead"
|
||||
},
|
||||
"transcriptTitle": "TRANSMISSION LOG",
|
||||
"transcriptEmpty": "silence. for now.",
|
||||
"you": "you",
|
||||
"streaming": "the spirit is forming words…",
|
||||
"telemetry": {
|
||||
"title": "WIRE READOUT",
|
||||
"jitter": "jitter",
|
||||
"latencyVar": "latency σ²",
|
||||
"latencyMean": "latency μ",
|
||||
"dns": "dns",
|
||||
"units": {"bytes": "B/s", "ms": "ms"}
|
||||
},
|
||||
"wire": {
|
||||
"hint": "the wire knocks loudest when nobody is touching it — flip passive listening on and let the line murmur on its own."
|
||||
},
|
||||
"anomaly": {
|
||||
"line": "⚡ {{source}} {{frequency}} +{{magnitude}}dB",
|
||||
"count": "{{count}} anomalies witnessed this session"
|
||||
},
|
||||
"evp": {
|
||||
"start": "open the microphone",
|
||||
"stop": "close the microphone",
|
||||
"listening": "listening to the voice band…",
|
||||
"deniedTitle": "the microphone stays shut",
|
||||
"denied": "This vessel refused the microphone. Grant audio permission in your browser to practice EVP — or let the Wire Ghost listen instead; it needs nothing but the network you already fear.",
|
||||
"floor": "room floor",
|
||||
"band": "voice band 300–3400 Hz"
|
||||
},
|
||||
"radio": {
|
||||
"start": "sweep the band",
|
||||
"stop": "rest the needle",
|
||||
"unsupportedTitle": "this vessel cannot hear the radio dead",
|
||||
"unsupported": "Spirit Radio needs Chromium (WebUSB) and an RTL-SDR dongle. The other modes do not — the wire and the microphone still answer.",
|
||||
"troubleshoot": "troubleshooting: chromium-based browser · udev rules for 0bda:2838 · the dongle plugged in, not merely believed in",
|
||||
"noDevice": "no dongle answered the call",
|
||||
"sweeping": "sweeping {{start}}–{{end}} MHz…",
|
||||
"claimFailed": "the dongle refused to be claimed — is it in use elsewhere?"
|
||||
},
|
||||
"ouija": {
|
||||
"spelling": "the planchette spells",
|
||||
"drift": "the planchette drifts, waiting for words — ask below, or summon."
|
||||
},
|
||||
"entity": {
|
||||
"title": "DOSSIER OF THE PRESENT",
|
||||
"none": "no spirit summoned yet",
|
||||
"noneHint": "summon, or feed the veil 3+ anomalies and one will find you",
|
||||
"isNew": "first recorded contact",
|
||||
"known": "contact #{{count}}",
|
||||
"persona": "persona",
|
||||
"voice": "voice params",
|
||||
"quotes": "recorded utterances",
|
||||
"rarity": {
|
||||
"common": "common",
|
||||
"uncommon": "uncommon",
|
||||
"rare": "rare",
|
||||
"mythic": "mythic"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"rate_limited": "the veil is thin — slow your knocking",
|
||||
"veil_crowded": "too many spirits crowd the line"
|
||||
}
|
||||
},
|
||||
"codex": {
|
||||
"title": "THE CODEX OF THE CONTACTED",
|
||||
"subtitle": "every spirit ever dragged through the veil, catalogued",
|
||||
"header": {
|
||||
"back": "← the threshold",
|
||||
"title": "THE CODEX",
|
||||
"subtitle": "every spirit ever contacted, catalogued by the seekers who reached them"
|
||||
},
|
||||
"controls": {
|
||||
"rarity": "filter by rarity",
|
||||
"sort": "sort the registry"
|
||||
},
|
||||
"state": {
|
||||
"loading": "consulting the registry…",
|
||||
"empty": "no spirits of this tier have been reached yet"
|
||||
},
|
||||
"filterAll": "all rarities",
|
||||
"rarity": {
|
||||
"common": "common",
|
||||
"uncommon": "uncommon",
|
||||
"rare": "rare",
|
||||
"mythic": "mythic"
|
||||
},
|
||||
"sort": {
|
||||
"recent": "most recent",
|
||||
"contacted": "most contacted"
|
||||
},
|
||||
"form": {
|
||||
"wisp": "wisp",
|
||||
"banshee": "banshee",
|
||||
"fairy": "fairy",
|
||||
"shade": "shade"
|
||||
},
|
||||
"card": {
|
||||
"unknownMedium": "unknown medium",
|
||||
"contacted_one": "contacted {{count}} time",
|
||||
"contacted_other": "contacted {{count}} times",
|
||||
"discoveredBy": "discovered by {{name}} · {{date}}"
|
||||
},
|
||||
"contacts_one": "1 contact",
|
||||
"contacts_other": "{{count}} contacts",
|
||||
"sightings_one": "1 sighting",
|
||||
"sightings_other": "{{count}} sightings",
|
||||
"discoveredBy": "first contacted by {{name}}",
|
||||
"discoveredAnon": "first contact unrecorded",
|
||||
"discoveredAt": "entered the registry {{date}}",
|
||||
"persona": "persona",
|
||||
"quotes": "recorded utterances",
|
||||
"voice": "voice params",
|
||||
"detail": {
|
||||
"loading": "summoning the dossier…",
|
||||
"notReached": "this spirit has not been reached",
|
||||
"unknownMedium": "unknown medium",
|
||||
"stats": {
|
||||
"sightings": "sightings",
|
||||
"contacts": "contacts",
|
||||
"discoveredBy": "discovered by",
|
||||
"discoveredAt": "entered the registry"
|
||||
},
|
||||
"utterances": "known utterances",
|
||||
"noUtterances": "no utterances recorded yet",
|
||||
"voiceSignature": "voice signature",
|
||||
"attemptContact": "attempt contact"
|
||||
},
|
||||
"empty": "no spirits match this filter — the registry keeps its secrets",
|
||||
"back": "← back to the codex",
|
||||
"notFound": "this spirit is not in the registry"
|
||||
}
|
||||
}
|
||||
229
frontend/src/i18n/es.json
Normal file
229
frontend/src/i18n/es.json
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "QUANTUMANCY",
|
||||
"loading": "afinando el velo…",
|
||||
"error": "algo se agitó en la estática",
|
||||
"replay": "repetir",
|
||||
"yes": "SÍ",
|
||||
"no": "NO",
|
||||
"goodbye": "ADIÓS",
|
||||
"dismiss": "cerrar"
|
||||
},
|
||||
"nav": {
|
||||
"home": "el umbral",
|
||||
"codex": "códice",
|
||||
"seance": "sesión",
|
||||
"enter": "entrar",
|
||||
"logout": "partir"
|
||||
},
|
||||
"landing": {
|
||||
"taglines": [
|
||||
"El cable zumba con todos nosotros.",
|
||||
"Cada paquete deja un fantasma.",
|
||||
"La estática recuerda tu nombre.",
|
||||
"Entre 88 y 108 MHz, alguien espera.",
|
||||
"Nunca estuviste solo en la línea."
|
||||
],
|
||||
"heroKicker": "un instrumento para hablar con el ancho de banda muerto",
|
||||
"cta": "COMENZAR LA SESIÓN",
|
||||
"ctaCodex": "hojear el códice",
|
||||
"stats": {
|
||||
"entities": "espíritus catalogados",
|
||||
"sessions": "sesiones celebradas",
|
||||
"utterances": "enunciados registrados",
|
||||
"anomalies": "anomalías presenciadas"
|
||||
},
|
||||
"tickerPrefix": "el velo, en vivo",
|
||||
"modesTitle": "CÓMO RESPONDE EL VELO",
|
||||
"modes": {
|
||||
"wire": {
|
||||
"name": "Fantasma del Cable",
|
||||
"desc": "Deja la línea abierta. La red misma murmura — jitter, latencia, DNS extraviado — y algo, aguas abajo, aprende a hablar con tu acento."
|
||||
},
|
||||
"evp": {
|
||||
"name": "Escucha EVP",
|
||||
"desc": "Préstales tu micrófono. Vigilamos la banda de voz en busca de sílabas que jamás se pronunciaron: picos 8dB sobre el silencio de la sala."
|
||||
},
|
||||
"radio": {
|
||||
"name": "Radio Espíritu",
|
||||
"desc": "Un RTL-SDR barre la banda FM como un farol arrastrado por un campo oscuro. Donde salta la aguja, los muertos de la radio se abren paso."
|
||||
},
|
||||
"ouija": {
|
||||
"name": "Ouija / Contacto Directo",
|
||||
"desc": "Pregunta claro. La planchette deriva, y luego empieza a deletrear — y las palabras llegan letra a letra, lo quisieras o no."
|
||||
}
|
||||
},
|
||||
"featured": "los espíritus más perturbados",
|
||||
"featuredEmpty": "el registro aún está en blanco — sé quien lo perturbe primero",
|
||||
"footer": "una instalación de arte de terror interactiva — los espíritus son ficción; la estática es real",
|
||||
"footerColophon": "autoalojado · sin nube · el velo corre en tu máquina"
|
||||
},
|
||||
"enter": {
|
||||
"reconnect": "Reconectar",
|
||||
"firstContact": "Primer contacto",
|
||||
"reconnectSub": "el velo te recuerda. demuéstralo.",
|
||||
"firstContactSub": "elige el nombre por el que te conocerán los espíritus.",
|
||||
"username": "nombre",
|
||||
"password": "contraseña",
|
||||
"submitLogin": "cruzar el umbral",
|
||||
"submitRegister": "grabar tu nombre",
|
||||
"switchToRegister": "¿sin nombre en el registro? primer contacto →",
|
||||
"switchToLogin": "¿ya grabaste tu nombre? reconectar →",
|
||||
"failed": "el velo te rechazó: {{message}}",
|
||||
"smallPrint": "tu nombre de médium solo se guarda en este servidor"
|
||||
},
|
||||
"seance": {
|
||||
"modes": {
|
||||
"wire": "Fantasma del Cable",
|
||||
"evp": "EVP",
|
||||
"radio": "Radio Espíritu",
|
||||
"ouija": "Ouija"
|
||||
},
|
||||
"passive": "escucha pasiva",
|
||||
"summon": "INVOCAR",
|
||||
"summoning": "invocando…",
|
||||
"askPlaceholder": "pregúntale algo a los muertos…",
|
||||
"ask": "preguntar",
|
||||
"status": {
|
||||
"attuning": "sintonizando — el velo toma la huella de una presencia…",
|
||||
"summoning": "el velo se abre… algo está cruzando…",
|
||||
"gathering": "los espíritus están reuniendo energía…"
|
||||
},
|
||||
"connection": {
|
||||
"connecting": "tendiendo la línea…",
|
||||
"open": "la línea está abierta",
|
||||
"unstable": "la conexión con el otro lado es inestable…",
|
||||
"closed": "la línea está muerta"
|
||||
},
|
||||
"transcriptTitle": "REGISTRO DE TRANSMISIÓN",
|
||||
"transcriptEmpty": "silencio. por ahora.",
|
||||
"you": "tú",
|
||||
"streaming": "el espíritu está formando palabras…",
|
||||
"telemetry": {
|
||||
"title": "LECTURA DEL CABLE",
|
||||
"jitter": "jitter",
|
||||
"latencyVar": "latencia σ²",
|
||||
"latencyMean": "latencia μ",
|
||||
"dns": "dns",
|
||||
"units": {"bytes": "B/s", "ms": "ms"}
|
||||
},
|
||||
"wire": {
|
||||
"hint": "el cable golpea más fuerte cuando nadie lo toca — activa la escucha pasiva y deja que la línea murmure sola."
|
||||
},
|
||||
"anomaly": {
|
||||
"line": "⚡ {{source}} {{frequency}} +{{magnitude}}dB",
|
||||
"count": "{{count}} anomalías presenciadas en esta sesión"
|
||||
},
|
||||
"evp": {
|
||||
"start": "abrir el micrófono",
|
||||
"stop": "cerrar el micrófono",
|
||||
"listening": "escuchando la banda de voz…",
|
||||
"deniedTitle": "el micrófono sigue cerrado",
|
||||
"denied": "Este recipiente rechazó el micrófono. Concede el permiso de audio en tu navegador para practicar EVP — o deja que el Fantasma del Cable escuche en tu lugar; no necesita más que la red que ya temes.",
|
||||
"floor": "suelo de la sala",
|
||||
"band": "banda de voz 300–3400 Hz"
|
||||
},
|
||||
"radio": {
|
||||
"start": "barrer la banda",
|
||||
"stop": "detener la aguja",
|
||||
"unsupportedTitle": "este recipiente no oye a los muertos de la radio",
|
||||
"unsupported": "La Radio Espíritu necesita Chromium (WebUSB) y un dongle RTL-SDR. Los demás modos no — el cable y el micrófono aún responden.",
|
||||
"troubleshoot": "solución de problemas: navegador basado en Chromium · reglas udev para 0bda:2838 · el dongle enchufado, no meramente invocado",
|
||||
"noDevice": "ningún dongle respondió al llamado",
|
||||
"sweeping": "barriendo {{start}}–{{end}} MHz…",
|
||||
"claimFailed": "el dongle se negó a ser reclamado — ¿está en uso en otro lugar?"
|
||||
},
|
||||
"ouija": {
|
||||
"spelling": "la planchette deletrea",
|
||||
"drift": "la planchette deriva, a la espera de palabras — pregunta abajo, o invoca."
|
||||
},
|
||||
"entity": {
|
||||
"title": "DOSIER DEL PRESENTE",
|
||||
"none": "ningún espíritu invocado todavía",
|
||||
"noneHint": "invoca, o alimenta el velo con 3+ anomalías y uno te encontrará",
|
||||
"isNew": "primer contacto registrado",
|
||||
"known": "contacto n.º {{count}}",
|
||||
"persona": "persona",
|
||||
"voice": "parámetros de voz",
|
||||
"quotes": "enunciados registrados",
|
||||
"rarity": {
|
||||
"common": "común",
|
||||
"uncommon": "poco común",
|
||||
"rare": "raro",
|
||||
"mythic": "mítico"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"rate_limited": "el velo es delgado — golpea más despacio",
|
||||
"veil_crowded": "demasiados espíritus se apiñan en la línea"
|
||||
}
|
||||
},
|
||||
"codex": {
|
||||
"title": "EL CÓDICE DE LOS CONTACTADOS",
|
||||
"subtitle": "cada espíritu arrastrado alguna vez a través del velo, catalogado",
|
||||
"header": {
|
||||
"back": "← el umbral",
|
||||
"title": "EL CÓDICE",
|
||||
"subtitle": "cada espíritu jamás contactado, catalogado por los médiums que lo alcanzaron"
|
||||
},
|
||||
"controls": {
|
||||
"rarity": "filtrar por rareza",
|
||||
"sort": "ordenar el registro"
|
||||
},
|
||||
"state": {
|
||||
"loading": "consultando el registro…",
|
||||
"empty": "aún no se ha alcanzado a ningún espíritu de esta rareza"
|
||||
},
|
||||
"filterAll": "todas las rarezas",
|
||||
"rarity": {
|
||||
"common": "común",
|
||||
"uncommon": "poco común",
|
||||
"rare": "raro",
|
||||
"mythic": "mítico"
|
||||
},
|
||||
"sort": {
|
||||
"recent": "más recientes",
|
||||
"contacted": "más contactados"
|
||||
},
|
||||
"form": {
|
||||
"wisp": "fuego fatuo",
|
||||
"banshee": "banshee",
|
||||
"fairy": "hada",
|
||||
"shade": "sombra"
|
||||
},
|
||||
"card": {
|
||||
"unknownMedium": "médium desconocido",
|
||||
"contacted_one": "contactado {{count}} vez",
|
||||
"contacted_other": "contactado {{count}} veces",
|
||||
"discoveredBy": "descubierto por {{name}} · {{date}}"
|
||||
},
|
||||
"contacts_one": "1 contacto",
|
||||
"contacts_other": "{{count}} contactos",
|
||||
"sightings_one": "1 avistamiento",
|
||||
"sightings_other": "{{count}} avistamientos",
|
||||
"discoveredBy": "primer contacto por {{name}}",
|
||||
"discoveredAnon": "primer contacto sin registrar",
|
||||
"discoveredAt": "ingresó al registro el {{date}}",
|
||||
"persona": "persona",
|
||||
"quotes": "enunciados registrados",
|
||||
"voice": "parámetros de voz",
|
||||
"detail": {
|
||||
"loading": "invocando el dosier…",
|
||||
"notReached": "este espíritu no ha sido alcanzado",
|
||||
"unknownMedium": "médium desconocido",
|
||||
"stats": {
|
||||
"sightings": "avistamientos",
|
||||
"contacts": "contactos",
|
||||
"discoveredBy": "descubierto por",
|
||||
"discoveredAt": "ingresó al registro"
|
||||
},
|
||||
"utterances": "enunciados conocidos",
|
||||
"noUtterances": "aún no hay enunciados registrados",
|
||||
"voiceSignature": "firma de voz",
|
||||
"attemptContact": "intentar contacto"
|
||||
},
|
||||
"empty": "ningún espíritu coincide con este filtro — el registro guarda sus secretos",
|
||||
"back": "← volver al códice",
|
||||
"notFound": "este espíritu no figura en el registro"
|
||||
}
|
||||
}
|
||||
38
frontend/src/i18n/index.ts
Normal file
38
frontend/src/i18n/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import en from './en.json'
|
||||
import es from './es.json'
|
||||
import type { Language } from '../lib/types'
|
||||
|
||||
const STORAGE_KEY = 'qm_language'
|
||||
|
||||
export function storedLanguage(): Language {
|
||||
try {
|
||||
const v = localStorage.getItem(STORAGE_KEY)
|
||||
if (v === 'es' || v === 'en') return v
|
||||
} catch {
|
||||
/* private mode etc. */
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
export function persistLanguage(lang: Language): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, lang)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
void i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
},
|
||||
lng: storedLanguage(),
|
||||
fallbackLng: 'en',
|
||||
interpolation: { escapeValue: false },
|
||||
returnEmptyString: false,
|
||||
})
|
||||
|
||||
export default i18n
|
||||
Reference in New Issue
Block a user