Files
sternboard/web/src/i18n.tsx
T

309 lines
12 KiB
TypeScript
Raw Normal View History

import { createContext, useContext, useState, type ReactNode } from "react";
export type Lang = "en" | "es";
const dict = {
en: {
"app.tagline": "Free retro boards. No signup.",
"home.title": "Create a retro board",
"home.newBoard": "New board",
"home.lead": "A ship makes a sternboard when it moves backward, stern first — looking back to move forward. That's a retro. (stern = back · board)",
"home.feat1": "No signup",
"home.feat2": "Shared timer, votes & grouping",
"home.feat3": "Retro step indicator (brainstorm → vote → discuss)",
"home.feat4": "Optional self-destruct on creation",
"home.feat5": "Public & private notes — publish when you want",
"home.custom": "Custom — define your columns",
"home.columns": "Columns",
"home.columnName": "Column name",
"home.addColumn": "+ Add column",
"home.boardTitle": "Board title",
"home.template": "Template",
"home.freeColumns": "Free columns (comma separated)",
"home.votesPerUser": "Votes per person",
"home.selfDestruct": "Self-destruct",
"home.never": "Never",
"home.hours": "{n}h",
"home.days": "{n}d",
"home.create": "Create board",
"home.creating": "Creating…",
"home.importTitle": "Import a board",
"home.importQuestion": "Have an export?",
"home.importJson": "Import JSON",
"home.importHint": "Load a .retro.json export",
"home.importMode": "What to import",
"home.importFull": "Everything (notes, votes, groups)",
"home.importStructure": "Only structure (columns + settings)",
"home.importBtn": "Choose file & import",
"board.exportJson": "Export JSON",
"join.title": "Enter your name to join",
"join.invitedTo": "You're invited to",
"join.yourName": "Your name",
"join.noPasswords": "Your name is visible to the team. No passwords.",
"join.namePlaceholder": "What's your name?",
"join.enter": "Enter board",
"join.nameRequired": "Name is required",
"state.createNew": "Create a new board",
"board.loading": "Loading…",
"board.share": "Copy invite link",
"board.copied": "Link copied",
"board.reveal": "Reveal all notes",
"board.hide": "Hide all notes",
"board.hidden": "hidden",
"board.revealOwn": "Reveal/hide my note",
"board.privateSection": "Private Section",
"board.publishAll": "Publish all",
"board.publish": "Publish",
"board.makePrivate": "Make private",
"board.typeHere": "Type here…\nPress Enter to save.",
"board.noCards": "No cards yet.",
"board.revealVotes": "Reveal votes",
"board.hideVotes": "Hide votes",
"board.settings": "Settings",
"board.votesWord": "votes",
"settings.modOnly": "Only moderators can change board settings.",
"board.addColumn": "Add column",
"phase.brainstorm": "Brainstorm",
"phase.voting": "Voting",
"phase.discuss": "Discuss",
"col.rename": "Rename",
"col.moveLeft": "Move left",
"col.moveRight": "Move right",
"col.delete": "Delete column",
"board.multiVote": "Multiple votes per item",
"board.groupName": "Group name…",
"board.ungroup": "Ungroup",
"board.addNote": "Add a note…",
"board.add": "Add",
"board.votesLeft": "{n} votes left",
"board.export": "Export Markdown",
"board.timer": "Timer",
"board.start": "Start",
"board.stop": "Stop",
"board.timeUp": "Time's up",
"board.actionItems": "Action items",
"board.addAction": "Add action item…",
"board.owner": "owner",
"board.expiresIn": "Self-destructs in {t}",
"board.destroyed": "This board was destroyed.",
"board.notFound": "Board not found.",
"board.moderator": "Moderator",
"common.delete": "Delete",
"common.save": "Save",
"common.cancel": "Cancel",
"common.on": "On",
"common.off": "Off",
"common.minutes": "min",
"toolbar.participants": "Participants",
"toolbar.actionPoints": "Action points",
"toolbar.export": "Export",
"toolbar.options": "Options",
"panel.participants": "Participants",
"panel.actionPoints": "Action Points",
"panel.people": "People",
"panel.you": "you",
"panel.showCreators": "Show card creators",
"panel.addActionPoint": "Add action point…",
"panel.todo": "To do",
"panel.done": "Done",
"export.title": "Export retrospective",
"export.options": "Export options",
"export.created": "Add creation date",
"export.exported": "Add export date",
"export.expandGroups": "Expand groups",
"export.authors": "Show author names",
"export.votes": "Show votes",
"export.preview": "Preview",
"export.copy": "Copy to clipboard",
"export.save": "Save as file",
"options.moderation": "Moderation",
"options.makeEveryone": "Everyone can moderate",
"options.makeSingle": "Only me can moderate",
"options.claim": "Claim retrospective",
"options.voting": "Voting",
"options.clearVotes": "Remove all votes",
"options.danger": "Danger zone",
"options.lock": "Lock (read-only)",
"options.unlock": "Unlock",
"options.remove": "Remove retrospective",
"options.removeConfirm": "Delete this retrospective for everyone?",
"board.lockedBanner": "🔒 Read-only (locked)",
},
es: {
"app.tagline": "Tableros de retro libres. Sin registro.",
"home.title": "Crea un tablero de retro",
"home.newBoard": "Nuevo tablero",
"home.lead": "Un barco hace un sternboard cuando avanza hacia atrás, popa primero — mirar atrás para avanzar. Eso es una retro. (stern = atrás · board = tablero)",
"home.feat1": "Sin registro",
"home.feat2": "Timer compartido, votos y agrupación",
"home.feat3": "Indicador de fase (lluvia → votación → discusión)",
"home.feat4": "Autodestrucción opcional al crearlo",
"home.feat5": "Notas públicas y privadas — publicas cuando quieres",
"home.custom": "Personalizado — define tus columnas",
"home.columns": "Columnas",
"home.columnName": "Nombre de la columna",
"home.addColumn": "+ Añadir columna",
"home.boardTitle": "Título del tablero",
"home.template": "Plantilla",
"home.freeColumns": "Columnas libres (separadas por comas)",
"home.votesPerUser": "Votos por persona",
"home.selfDestruct": "Autodestrucción",
"home.never": "Nunca",
"home.hours": "{n}h",
"home.days": "{n}d",
"home.create": "Crear tablero",
"home.creating": "Creando…",
"home.importTitle": "Importar un tablero",
"home.importQuestion": "¿Tienes un export?",
"home.importJson": "Importar JSON",
"home.importHint": "Carga un export .retro.json",
"home.importMode": "Qué importar",
"home.importFull": "Todo (notas, votos, grupos)",
"home.importStructure": "Solo estructura (columnas + ajustes)",
"home.importBtn": "Elegir archivo e importar",
"board.exportJson": "Exportar JSON",
"join.title": "Escribe tu nombre para entrar",
"join.invitedTo": "Te invitaron a",
"join.yourName": "Tu nombre",
"join.noPasswords": "Tu nombre será visible para el equipo. Sin contraseñas.",
"join.namePlaceholder": "¿Cómo te llamas?",
"join.enter": "Entrar al tablero",
"join.nameRequired": "El nombre es obligatorio",
"state.createNew": "Crear un tablero nuevo",
"board.loading": "Cargando…",
"board.share": "Copiar enlace de invitación",
"board.copied": "Enlace copiado",
"board.reveal": "Revelar todas",
"board.hide": "Ocultar todas",
"board.hidden": "oculta",
"board.revealOwn": "Revelar/ocultar mi nota",
"board.privateSection": "Sección privada",
"board.publishAll": "Publicar todas",
"board.publish": "Publicar",
"board.makePrivate": "Hacer privada",
"board.typeHere": "Escribe aquí…\nPulsa Enter para guardar.",
"board.noCards": "Aún no hay tarjetas.",
"board.revealVotes": "Revelar votos",
"board.hideVotes": "Ocultar votos",
"board.settings": "Ajustes",
"board.votesWord": "votos",
"settings.modOnly": "Solo los moderadores cambian los ajustes del tablero.",
"board.addColumn": "Añadir columna",
"phase.brainstorm": "Lluvia",
"phase.voting": "Votación",
"phase.discuss": "Discusión",
"col.rename": "Renombrar",
"col.moveLeft": "Mover ←",
"col.moveRight": "Mover →",
"col.delete": "Eliminar columna",
"board.multiVote": "Varios votos por ítem",
"board.groupName": "Nombre del grupo…",
"board.ungroup": "Deshacer grupo",
"board.addNote": "Añade una nota…",
"board.add": "Añadir",
"board.votesLeft": "{n} votos restantes",
"board.export": "Exportar Markdown",
"board.timer": "Temporizador",
"board.start": "Iniciar",
"board.stop": "Parar",
"board.timeUp": "Se acabó el tiempo",
"board.actionItems": "Conclusiones",
"board.addAction": "Añade una conclusión…",
"board.owner": "responsable",
"board.expiresIn": "Se autodestruye en {t}",
"board.destroyed": "Este tablero fue destruido.",
"board.notFound": "Tablero no encontrado.",
"board.moderator": "Moderador",
"common.delete": "Borrar",
"common.save": "Guardar",
"common.cancel": "Cancelar",
"common.on": "Sí",
"common.off": "No",
"common.minutes": "min",
"toolbar.participants": "Participantes",
"toolbar.actionPoints": "Conclusiones",
"toolbar.export": "Exportar",
"toolbar.options": "Opciones",
"panel.participants": "Participantes",
"panel.actionPoints": "Conclusiones",
"panel.people": "Personas",
"panel.you": "tú",
"panel.showCreators": "Mostrar autores de las tarjetas",
"panel.addActionPoint": "Añadir conclusión…",
"panel.todo": "Pendiente",
"panel.done": "Hecho",
"export.title": "Exportar retrospectiva",
"export.options": "Opciones de exportación",
"export.created": "Añadir fecha de creación",
"export.exported": "Añadir fecha de exportación",
"export.expandGroups": "Expandir grupos",
"export.authors": "Mostrar autores",
"export.votes": "Mostrar votos",
"export.preview": "Previsualización",
"export.copy": "Copiar al portapapeles",
"export.save": "Descargar archivo",
"options.moderation": "Moderación",
"options.makeEveryone": "Todos pueden moderar",
"options.makeSingle": "Solo yo modero",
"options.claim": "Reclamar retro",
"options.voting": "Votación",
"options.clearVotes": "Quitar todos los votos",
"options.danger": "Zona peligrosa",
"options.lock": "Bloquear (solo lectura)",
"options.unlock": "Desbloquear",
"options.remove": "Borrar retrospectiva",
"options.removeConfirm": "¿Borrar esta retrospectiva para todos?",
"board.lockedBanner": "🔒 Solo lectura (bloqueado)",
},
} as const;
type Key = keyof (typeof dict)["en"];
type I18nCtx = {
lang: Lang;
setLang: (l: Lang) => void;
t: (key: Key, vars?: Record<string, string | number>) => string;
};
const Ctx = createContext<I18nCtx | null>(null);
function detect(): Lang {
const saved = localStorage.getItem("lang");
if (saved === "en" || saved === "es") return saved;
return navigator.language.startsWith("es") ? "es" : "en";
}
export function I18nProvider({ children }: { children: ReactNode }) {
const [lang, setLangState] = useState<Lang>(detect);
const setLang = (l: Lang) => {
localStorage.setItem("lang", l);
setLangState(l);
};
const t = (key: Key, vars?: Record<string, string | number>) => {
let s: string = dict[lang][key] ?? key;
if (vars) for (const [k, v] of Object.entries(vars)) s = s.replace(`{${k}}`, String(v));
return s;
};
return <Ctx.Provider value={{ lang, setLang, t }}>{children}</Ctx.Provider>;
}
export function useI18n() {
const c = useContext(Ctx);
if (!c) throw new Error("useI18n fuera de I18nProvider");
return c;
}
export function LangSwitcher() {
const { lang, setLang } = useI18n();
return (
<div className="seg" role="group" aria-label="language">
<button className={lang === "es" ? "on" : ""} onClick={() => setLang("es")}>
ES
</button>
<button className={lang === "en" ? "on" : ""} onClick={() => setLang("en")}>
EN
</button>
</div>
);
}