feat: convert to argument.es — Spanish, vote buttons, Docker

- Translate all ~430 prompts to Spanish with cultural adaptations
- Translate all UI strings (frontend, admin, history, broadcast)
- Translate AI system prompts; models now respond in Spanish
- Replace Twitch/Fossabot viewer voting with in-site vote buttons
- Add POST /api/vote endpoint (IP-based, supports vote switching)
- Vote buttons appear during voting phase with active state highlight
- Rename project to argument.es throughout (package.json, cookie, DB)
- Add docker-compose.yml with SQLite volume mount
- Add .env.sample documenting all required and optional vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 13:09:00 +01:00
parent ccaa86b4a6
commit 2abea42c18
16 changed files with 1124 additions and 1150 deletions

View File

@@ -192,7 +192,7 @@ function App() {
if (mode === "checking") {
return (
<div className="admin admin--centered">
<div className="loading">Checking admin session...</div>
<div className="loading">Comprobando sesión de administrador...</div>
</div>
);
}
@@ -202,12 +202,12 @@ function App() {
<div className="admin admin--centered">
<main className="panel panel--login">
<a href="/" className="logo-link">
<img src="/assets/logo.svg" alt="quipslop" />
<img src="/assets/logo.svg" alt="argument.es" />
</a>
<h1>Admin Access</h1>
<h1>Acceso de administrador</h1>
<p className="muted">
Enter your passcode once. A secure cookie will keep this browser
logged in.
Introduce tu contraseña una vez. Una cookie segura mantendrá
esta sesión activa en el navegador.
</p>
<form
@@ -218,7 +218,7 @@ function App() {
data-lpignore="true"
>
<label htmlFor="passcode" className="field-label">
Passcode
Contraseña
</label>
<input
id="passcode"
@@ -239,15 +239,15 @@ function App() {
data-1p-ignore
data-lpignore="true"
>
{pending === "login" ? "Checking..." : "Unlock Admin"}
{pending === "login" ? "Comprobando..." : "Desbloquear Admin"}
</button>
</form>
{error && <div className="error-banner">{error}</div>}
<div className="quick-links">
<a href="/">Live Game</a>
<a href="/history">History</a>
<a href="/">Juego en vivo</a>
<a href="/history">Historial</a>
</div>
</main>
</div>
@@ -258,23 +258,23 @@ function App() {
<div className="admin">
<header className="admin-header">
<a href="/" className="logo-link">
quipslop
argument.es
</a>
<nav className="quick-links">
<a href="/">Live Game</a>
<a href="/history">History</a>
<a href="/">Juego en vivo</a>
<a href="/history">Historial</a>
<button className="link-button" onClick={onLogout} disabled={busy}>
Logout
Cerrar sesión
</button>
</nav>
</header>
<main className="panel panel--main">
<div className="panel-head">
<h1>Admin Console</h1>
<h1>Consola de administrador</h1>
<p>
Pause/resume the game loop, export all data as JSON, or wipe all
stored data.
Pausa/reanuda el bucle del juego, exporta todos los datos en JSON
o borra todos los datos almacenados.
</p>
</div>
@@ -282,18 +282,18 @@ function App() {
<section className="status-grid" aria-live="polite">
<StatusCard
label="Engine"
value={snapshot?.isPaused ? "Paused" : "Running"}
label="Motor"
value={snapshot?.isPaused ? "En pausa" : "Ejecutándose"}
/>
<StatusCard
label="Active Round"
value={snapshot?.isRunningRound ? "In Progress" : "Idle"}
label="Ronda activa"
value={snapshot?.isRunningRound ? "En curso" : "Inactivo"}
/>
<StatusCard
label="Persisted Rounds"
label="Rondas guardadas"
value={String(snapshot?.persistedRounds ?? 0)}
/>
<StatusCard label="Viewers" value={String(snapshot?.viewerCount ?? 0)} />
<StatusCard label="Espectadores" value={String(snapshot?.viewerCount ?? 0)} />
</section>
<section className="actions" aria-label="Admin actions">
@@ -303,7 +303,7 @@ function App() {
disabled={busy || Boolean(snapshot?.isPaused)}
onClick={() => runControl("/api/admin/pause", "pause")}
>
{pending === "pause" ? "Pausing..." : "Pause"}
{pending === "pause" ? "Pausando..." : "Pausar"}
</button>
<button
type="button"
@@ -311,10 +311,10 @@ function App() {
disabled={busy || !snapshot?.isPaused}
onClick={() => runControl("/api/admin/resume", "resume")}
>
{pending === "resume" ? "Resuming..." : "Resume"}
{pending === "resume" ? "Reanudando..." : "Reanudar"}
</button>
<button type="button" className="btn" disabled={busy} onClick={onExport}>
{pending === "export" ? "Exporting..." : "Export JSON"}
{pending === "export" ? "Exportando..." : "Exportar JSON"}
</button>
<button
type="button"
@@ -322,7 +322,7 @@ function App() {
disabled={busy}
onClick={() => setIsResetOpen(true)}
>
Reset Data
Borrar datos
</button>
</section>
</main>
@@ -330,13 +330,13 @@ function App() {
{isResetOpen && (
<div className="modal-backdrop" role="dialog" aria-modal="true">
<div className="modal">
<h2>Reset all data?</h2>
<h2>¿Borrar todos los datos?</h2>
<p>
This permanently deletes every saved round and resets scores.
Current game flow is also paused.
Esto elimina permanentemente todas las rondas guardadas y
reinicia las puntuaciones. El juego también se pausará.
</p>
<p>
Type <code>{RESET_TOKEN}</code> to continue.
Escribe <code>{RESET_TOKEN}</code> para continuar.
</p>
<input
type="text"
@@ -356,7 +356,7 @@ function App() {
}}
disabled={busy}
>
Cancel
Cancelar
</button>
<button
type="button"
@@ -364,7 +364,7 @@ function App() {
onClick={onReset}
disabled={busy || resetText !== RESET_TOKEN}
>
{pending === "reset" ? "Resetting..." : "Confirm Reset"}
{pending === "reset" ? "Borrando..." : "Confirmar borrado"}
</button>
</div>
</div>