Repository navigation
Internacionalización
English · Español
La interfaz está en 15 idiomas. El código está escrito en español: toda cadena visible para el usuario pasa por t("<cadena fuente en español>"), y cada uno de los demás idiomas es un diccionario cuya clave es esa misma cadena en español. No hay ninguna librería de i18n en ejecución; todo el mecanismo es la tabla de idiomas src/lib/idiomas.ts, src/components/language-provider.tsx, las funciones puras de src/i18n/traducir.ts, los diccionarios de src/i18n/<código>/ (los carga src/i18n/cargar.ts) y la comprobación scripts/idiomas.mjs, que se impone en CI.
| Nombre | Dónde vive | Qué controla |
|---|---|---|
| Idioma de la interfaz |
LanguageProvider, clave de localStorage proxybuilder.lang (cualquier código de LANGS) |
Todo el texto de la UI, el formato de números y fechas y los mensajes de error de Rust tal como los ve el usuario. |
| Idioma de las cartas |
useUi().lang (src/stores/ui-store.ts), clave dentro de proxybuilder.preferencias
|
En qué idioma se pide a Scryfall la impresión de una carta. Las opciones salen de IDIOMAS_SCRYFALL (en es fr de it pt ja ko ru zhs zht, con su nombre propio). Se elige en «Opciones» del panel de la lista («Idioma de las cartas»). |
| Idioma del diseño |
PrintSettings.designLang, DesignLang (Rust) |
En qué idioma pinta el texto el diseñador de cartas; véase Visión general del diseñador. |
Esta página trata sobre todo del primero. Los tres comparten los mismos códigos a propósito (siguiente sección).
Una sola tabla sirve para la interfaz y para los idiomas de diseño:
| Export | Propósito |
|---|---|
LANGS |
Los 15 códigos, en el orden de la tabla: es (fuente), en, fr, de, it, pt (Brasil), ro, pl, nl, ru, uk, ja, ko, zhs, zht. De ahí sale type Lang (que language-provider.tsx vuelve a exportar). |
IDIOMAS |
Un Idioma por código: codigo, nativo (el nombre en su propio idioma, nunca traducido, para que se reconozca en un selector), pais (país ISO de su bandera), locale (BCP-47) e impresas (si Scryfall tiene cartas impresas en él; falso en ro, pl, nl, uk). |
esLang(x) |
Guarda de tipo: si x es uno de los códigos. |
idioma(codigo) |
El Idioma de un código; uno desconocido devuelve el inglés. |
localeDe(lang) |
El locale BCP-47 (es-ES, en-GB, pt-BR, zh-Hans-CN, zh-Hant-TW…). |
IDIOMAS_SCRYFALL |
Los 11 idiomas con impresiones en Scryfall, en el orden de sus listas (el inglés primero), con el portugués llamado solo «Português», como en las cartas. Sustituye a tres listas que antes estaban escritas a mano (panel de la lista, filtros de variantes, biblioteca). |
Por qué los códigos de Scryfall (pt, zhs, zht) y no BCP-47: así el idioma de la interfaz, el idioma de diseño de una carta (DesignLang en Rust usa los mismos códigos en el mismo orden) y el idioma de un deck son la misma clave en todas partes, sin tabla de equivalencias. El BCP-47 vive solo en locale, para Intl y <html lang>.
Decisión: el idioma de fábrica de la interfaz es el inglés; cualquier otro se elige en Ajustes y se recuerda. Implementación: leerIdioma() (exportada desde language-provider.tsx) devuelve el código guardado cuando esLang lo acepta, y "en" en cualquier otro caso (ausente, desconocido, almacenamiento bloqueado). index.html incluye <html lang="en">. El README principal y las notas de las releases de GitHub también están en inglés (README.es.md es la copia en español). Deducido: el inglés es el idioma por defecto porque el proyecto apunta a un público más amplio que el de su código fuente en español, mientras que el español sigue siendo de primera clase porque es el idioma en el que se escriben las cadenas.
src/components/language-provider.tsx exporta:
| Export | Propósito |
|---|---|
LanguageProvider |
Proveedor de contexto; estado lang. |
useLang() |
Devuelve { lang, setLang, t, mensaje }. Lanza un error si se usa fuera del proveedor. |
leerIdioma() |
El idioma guardado, o "en". También la usa src/main.tsx. |
paraTraducir(es) |
Función identidad que marca una cadena como «traducir más tarde, en otro sitio». Para texto guardado en tablas de datos (p. ej. MARCAS en src/lib/cartas.ts, FASES en src/features/decklist/acciones.ts, TEXTOS en src/lib/errores.ts) que luego se pinta mediante t(variable). El script de comprobación ve los literales de paraTraducir("..."). |
traduccionSuelta(es, vars?) |
t() sin el proveedor (lee proxybuilder.lang directamente; usa el diccionario de ese idioma si ya está cargado y, si no, el inglés). Lo usa solo LimiteError, que debe envolver al proveedor para que un fallo dentro del proveedor siga mostrando una pantalla de recuperación traducida. |
type Lang |
Reexportación del tipo de src/lib/idiomas.ts. |
t(es, vars) es traducir(es, lang, vars, dic, EN) de src/i18n/traducir.ts, un módulo puro (sin React) para poder probarlo solo:
- Cadena de respaldo: idioma → inglés → cadena fuente en español. Una clave que le falta, por ejemplo, al diccionario polaco muestra la entrada inglesa (quien usa la app en polaco entiende mejor el inglés que el español); si al inglés también le falta, se muestra la fuente en español. Nunca una cadena vacía ni una clave técnica. Esa red de seguridad también oculta las traducciones que faltan mientras se programa, que es justo por lo que existe el script de comprobación.
-
Plurales. Un valor del diccionario es una cadena o un objeto
Plural(src/i18n/tipos.ts:zero?,one?,two?,few?,many?,other;Traduccion = string | Plural,Diccionario = Record<string, Traduccion>). La forma se elige porvars.nconIntl.PluralRules(locale)(en caché por locale); si falta la forma, o sinno es un número finito, se usaother. Cuando el valor sale del respaldo inglés, la forma la eligen las reglas de plural del inglés. -
Interpolación. Los marcadores
\{(\w+)\}se sustituyen desdevars; un marcador sin valor (o connull/undefined) se deja como{name}en lugar de imprimirse comoundefined. Los valores numéricos se escriben con elIntl.NumberFormatdel idioma de la interfaz (siempre, aunque el texto haya caído al inglés o al español). De ahí la regla: los recuentos que se pasan at()son números, nunca formateados antes contoLocaleString/toFixed. - El formato de números, fechas, moneda y tiempo relativo fuera de
t()usalocaleDe(lang):formatoMonedaensrc/features/stats/textos.ts, las fechas desrc/features/stats/tipos.tsx, las fechas, tiempos relativos y precios desrc/features/library/piezas.tsxy la fecha de los símbolos ensrc/features/disenar/recursos-dialog.tsx. - La función
tsolo se recrea cuando cambialang(useMemo([lang])), por lo que es segura en listas de dependencias; los componentes se vuelven a pintar al cambiar el idioma porque cambia el valor del contexto.
setLang(l) guarda el código en localStorage (si el almacenamiento falla, se ignora: el idioma vive solo en esta sesión), carga primero el diccionario (cargarDiccionario) y solo después cambia el estado. Mientras carga se queda en pantalla el idioma anterior, sin parpadeo de textos a medio traducir. Una ref (pedido) recuerda el último idioma pedido: si se piden dos seguidos, gana el último aunque el diccionario del primero llegue después.
<html lang> recibe el locale completo (localeDe(lang), p. ej. ja-JP, zh-Hant-TW) y no solo el código: WebKit lo usa para elegir las formas japonesas o chinas de los ideogramas que comparten ambas escrituras.
Cada idioma tiene src/i18n/<código>/ con un JSON por área (las mismas 15 áreas que el inglés). El español es la fuente, así que es/ tiene la misma organización pero cada cadena vale lo mismo que su clave: pnpm idiomas comprueba que cada cadena pedida tenga entrada también ahí, y la app nunca lo carga (cargar.ts deja es/ fuera de su glob). Cada fichero es un objeto plano { "Cadena fuente en español": "traducción", ... }, con la indentación de tabuladores de Biome. Son datos, no código: no hay TypeScript que escribir mal, y el script de comprobación los lee sin transpilar. Ya no hay index.ts: cargar.ts funde todos los ficheros de área de un idioma, así que una área nueva solo necesita su fichero. Las claves van siempre entrecomilladas, como exige JSON. Lo que antes rechazaba el compilador de TypeScript lo rechaza ahora pnpm idiomas: una clave repetida dentro de un mismo fichero (JSON.parse se quedaría con la última en silencio) y una clave en dos áreas.
| Fichero | Área |
|---|---|
comun.json |
Cadenas compartidas; empieza con los mensajes de ErrorCode de src/lib/errores.ts. |
ajustes.json |
Diálogo de Ajustes (apariencia, actualizaciones, temas de tweakcn). |
shell.json |
Barra superior, títulos de paneles, indicador de guardado, botón y toasts de actualización. |
lista.json |
Panel de la lista, fases de resolución, marcas de carta. |
vista.json |
Páginas de la vista previa, barra de herramientas, menú de carta, diálogo «mover a». |
exportar.json |
Panel de exportación, presets, alineación. |
biblioteca.json |
Biblioteca de mazos. |
cartas.json |
Diálogos de añadir carta, variantes y editor de carta. |
extras.json |
Puntos de entrada de las estadísticas y explorador de sets. |
vincular.json |
Vincular cartas propias con Scryfall; zoom con la rueda; escala de la UI. |
estadisticas.json |
Diálogo de estadísticas. |
disenar.json |
La vista del diseñador (el fichero más grande). |
tutorial.json |
Los tutoriales de las pantallas: el botón de ayuda, el contador de pasos y el título y el texto de cada paso (Tutoriales). |
mesa.json |
La mesa de juego: la entrada, el tablero, los menús, los diálogos, los atajos, las frases del registro de la partida y Ajustes > «Mesa de juego» (Mesa de juego). |
salas.json |
Jugar a través del servidor de salas: el diálogo «Con amigos…», las salas, la sala de espera, los playgroups y las invitaciones (Juego en línea). |
Un src/i18n/<código>/identicas.json opcional lista las claves en español cuya traducción es, con razón, idéntica a la inglesa en ese idioma (p. ej. «Deck» en alemán); véase el script de comprobación.
src/i18n/cargar.ts:
-
El inglés va en el paquete principal (
EN): es el respaldo de todos los idiomas, así que tiene que estar siempre. - Los demás son perezosos:
import.meta.glob(["./*/*.json", "!./en/*.json", "!./es/*.json", "!./*/identicas.json"], { import: "default" })crea un módulo perezoso por fichero de área.cargarDiccionariocarga juntos los ficheros del idioma elegido y los funde, así que el arranque no lleva los otros 13 diccionarios. -
cargarDiccionario(lang)carga un idioma una sola vez y lo guarda. El español no tiene diccionario (es la fuente); un idioma sin diccionario, o cuyo trozo no se puede cargar, se resuelve aundefinedy sus textos caen al inglés. -
diccionarioCargado(lang)devuelve el diccionario guardado, si lo hay.
src/main.tsx espera a cargarDiccionario(leerIdioma()) antes de montar (.finally(() => montar(raiz)), así que monta aunque falle la carga), para que la app nunca salga un instante en otro idioma; la ventana se enseña en paralelo (ensenarVentana()).
src/i18n/GLOSARIO.md fija la terminología de cada idioma para que los términos que se repiten salgan como en las cartas oficiales y siempre igual:
- En los idiomas con cartas impresas (
fr,de,it,pt,ru,ja,ko,zhs,zht) los términos de juego salen de cartas reales en Scryfall, con la carta de referencia anotada. -
ro,pl,nlyukno tienen cartas impresas: los términos siguen el uso de su comunidad y van marcados como sin fuente oficial. - Lista lo que nunca se traduce (nombres de productos, formatos de fichero,
.pbdeck, atajos de teclado, marcadores{x}) y el tratamiento de cada idioma (p. ej.vousen francés,duen alemán, formas impersonales en polaco, です/ます en japonés). - Una última tabla, «Términos de la mesa de juego», fija las acciones de juego de
mesa.jsonen cada idioma: los términos de las reglas oficiales donde hay cartas impresas y lo habitual en la comunidad (a menudo el inglés) enro,pl,nlyuk; con*, los no confirmados en una carta oficial.
- En el componente, escribe el texto visible a través del hook:
const { t } = useLang();y luegot("Texto en español"). Para texto fuera de React, en una tabla a nivel de módulo, envuélvelo enparaTraducir("...")y llama at(value)donde se pinte. - Añade el par al diccionario del área correspondiente en todos los idiomas (
src/i18n/<código>/<área>.json), siguiendosrc/i18n/GLOSARIO.md; enes/el valor es la propia clave ("Texto en español": "Texto en español"). Reutiliza una clave existente cuando el texto en español sea idéntico y signifique lo mismo. - Para un recuento, usa
{n}y pasa un número; en los idiomas con varias categorías de plural, escribe un objetoPluralcon exactamente las categorías de ese idioma (rusoone/few/many/other, japonés soloother…). - Ejecuta
pnpm idiomas(también forma parte depnpm check, del hook de pre-commit para los.ts/.tsxpreparados y de CI). - Usa marcadores
{name}para las partes dinámicas (t("Descargando la {version}…", { version })); nunca concatenes fragmentos traducidos.
Regla de desambiguación: si la misma palabra en español necesita dos traducciones distintas en sitios diferentes, haz más larga la cadena fuente del caso menos genérico (p. ej. añadiendo palabras de contexto). Nunca introduzcas un segundo diccionario ni un argumento de contexto.
Si cambias una cadena fuente en español existente, cambia también su clave en todos los diccionarios; si no, las entradas viejas quedan sin uso (un aviso) y la cadena nueva se reporta como que falta (un error).
Qué hace:
-
Cadenas pedidas (
leerPedidas). Recorresrc/(saltándosenode_modules, los directorios que empiezan por punto,bindings,src/i18n/y*.test.ts(x)) y recoge cada literal entre comillas dobles ent("...")yparaTraducir("...")(primer argumento; con los escapes\"y\nresueltos), con el fichero donde se vio cada una por primera vez. -
Idiomas. Carga
src/lib/idiomas.ts(leerTabla), que es código y por eso se sigue transpilando contypescripte importando. Luego lee cada directorio de diccionario (leerDiccionarios; se saltan los que empiezan por_): cada<área>.jsonse parsea conJSON.parse, y su texto en bruto se revisa por si una clave se repite en el mismo fichero, cosa queJSON.parseesconde sin avisar. -
Errores (estado de salida 1):
- un idioma de la tabla sin directorio
src/i18n/<código>/, o un directorio que no es de ningún idioma de la tabla; - una cadena pedida que no está en el diccionario fundido;
- una clave que está en dos áreas o repetida dentro de un mismo fichero;
- un fichero de área del inglés que le falta al idioma;
- marcadores
{x}distintos de los de la clave en español (también en cada forma de un plural); - un objeto de plural en una clave sin
{n}, una categoría que el idioma no tiene o una categoría que falta (las del idioma sonIntl.PluralRules(locale).resolvedOptions().pluralCategories); - un valor que no es ni una cadena ni un objeto de plural con
other; - en el español, que se comprueba aparte con
comprobarFuente: un valor que no es su propia clave, además de las cadenas que faltan, los ficheros de área que faltan y las claves repetidas de arriba (sin comprobar marcadores, plurales ni idénticas al inglés, porque cada valor es su clave).
- un idioma de la tabla sin directorio
-
Avisos (no fallan): claves que ya no se piden, y valores idénticos al inglés (solo si tienen letras y no son una excepción permitida: la lista global
IDENTICAS_PERMITIDASdel script o elidenticas.jsondel idioma). - Imprime una línea por idioma (
✓ <código>: N cadenasmás el número de avisos, o✖ <código>: …seguido de cada error).--detallelista además cada aviso.
leerPedidas, leerTabla, leerDiccionarios, comprobar y comprobarFuente se exportan para los tests de scripts/__tests__/idiomas.test.mjs.
Límites que conviene tener presentes:
- Solo se ven los literales entre comillas dobles directamente dentro de
t(/paraTraducir(.t(variable), las cadenas con comillas simples, las plantillas literales o las concatenaciones son invisibles para la comprobación, así que lleva ese texto porparaTraducir("...")en su definición. - El texto en español escrito en JSX sin
t()no se detecta en absoluto. - Los ficheros
.test.ts(x)están excluidos. - Una traducción equivocada pero bien formada no se puede detectar; el aviso de idénticas al inglés solo pilla las copias sin traducir.
Rust nunca escribe prosa para el usuario. Los comandos devuelven Result<T, AppError>, donde AppError = { code: ErrorCode, params: { [key]: string } } (src/bindings/AppError.ts, ErrorCode.ts; véase IPC y manejo de errores). En la pantalla:
// src/lib/errores.ts
export function mensajeDeError(e: unknown, t): string {
if (esAppError(e)) {
const clave = e.params.motivo ?? e.params.code;
const propio = clave ? MOTIVOS[e.code]?.[clave] : undefined;
return t(propio ?? TEXTOS[e.code], e.params);
}
if (e instanceof Error) return e.message;
return String(e);
}TEXTOS tiene una frase en español por cada ErrorCode (Network, RateLimited, NotFound, Io, Db, InvalidInput, Cancelled, Pdf, Image, Unsupported, Busy, Internal, Rejected, Server), cada una envuelta en paraTraducir. MOTIVOS añade frases propias para los motivos conocidos de un código (params.motivo, o params.code en el servidor de salas): los rechazos de la mesa de juego (Rejected: los del motor, NoExiste, SinPermiso, ZonaVacia, FueraDeTurno, Limite, NoSePuede, y los de la red y las repeticiones, como codigo, version, llena, espectador, partida_abierta), la actualización rechazada con una partida abierta (Busy con motivo: partida), los datos no válidos (InvalidInput: falta_lista, lista, lista_sin_nombre, codigo, deck, identidad), la conexión perdida (Network: sin_servidor, sin_respuesta, desconectado), NotFound con sin_sala, Unsupported con version y los códigos del servidor de salas (Server); un motivo desconocido usa la frase general del código. esAppError acepta cualquier objeto cuyo code sea una clave de TEXTOS, así que añadir un ErrorCode en Rust obliga a una entrada equivalente (el tipo Record<ErrorCode, string> hace fallar tsc si no). Los componentes llaman a toast.error(mensaje(e)).
Los casos especiales los gestiona el componente que conoce el contexto, usando params: por ejemplo TemasTweakcn (src/features/settings/temas-tweakcn.tsx) comprueba e.params.motivo === "tema_enlace" para mostrar «Eso no es un enlace ni un id de un tema de tweakcn.» en lugar del mensaje genérico de entrada inválida. SaveIndicator muestra mensaje(error) en un tooltip cuando falla el autoguardado.
El registro de la mesa de juego sigue la misma regla: cada evento del motor llega a la pantalla como dato y src/features/mesa/textos-eventos.ts escribe su frase con t() (FRASES, una por Accion, y los cambios especiales), según quien mira, así las cartas ocultas nunca se nombran (Mesa de juego).
Los datos que vienen de Rust y se muestran (nombres de Scryfall, texto de reglas, notas de la release) se muestran tal cual: las notas de la release en el panel de actualización son Markdown en inglés de la release de GitHub. Los avisos del diseñador (Aviso) llevan un kind y parámetros (algunos, un DesignLang); la pantalla los redacta y nombra cada idioma por su nombre propio.
SelectorIdioma (src/components/selector-idioma.tsx) es un Select con los idiomas, su bandera y su nombre propio (cada opción lleva lang={locale}, para que su nombre salga con las letras adecuadas). Se usa para el idioma de la interfaz en SettingsDialog (src/features/settings/settings-dialog.tsx, llama a setLang) y para el idioma de diseño en el panel de exportación y en la vista previa de Diseñar (véase Interfaz del diseñador). El cambio llega a todos los componentes montados en cuanto carga el diccionario; no hay recarga.
Las banderas las pinta <Bandera pais> de src/components/emoji.tsx con imágenes de Noto Color Emoji incluidas en src/assets/emoji/, no la letra de emoji del sistema: Segoe UI Emoji de Windows no tiene banderas y WebKit en macOS no pinta letras COLRv1. Véase Arquitectura del frontend.
-
pnpm idiomas/scripts/idiomas.mjsprotege la completitud, los marcadores y las formas de plural. Su lógica se prueba enscripts/__tests__/idiomas.test.mjs: un diccionario completo pasa; una clave que falta, un marcador cambiado y una forma de plural que falta fallan; un plural sin{n}o con una categoría que el idioma no tiene falla; las claves sin uso y las idénticas al inglés solo avisan; un fichero de área que falta falla, y una clave repetida dentro de un mismo fichero también; en el español (comprobarFuente), fallan un valor distinto de su clave, una cadena que falta y un fichero de área que falta. -
src/i18n/__tests__/traducir.test.ts: respaldo idioma → inglés → español, el plural ruso elegido porn, unnque no es número usaother, números con el formato de cada idioma ycargarDiccionario(el inglés ya está, el español no tiene y un idioma sin diccionario no rompe). -
src/lib/__tests__/idiomas.test.ts: la tabla (15 códigos sin repetir en el orden deLANGS, locales BCP-47 canónicos,IDIOMAS_SCRYFALLson los 11 con cartas impresas,esLangy el respaldo inglés deidioma). -
src/lib/__tests__/errores.test.tscubreesAppErrorymensajeDeError. - No cubierto: la calidad de las traducciones (se revisa contra
GLOSARIO.md).
-
Añadir un idioma de interfaz: añádelo a
LANGSy aIDIOMASensrc/lib/idiomas.ts; creasrc/i18n/<código>/con un JSON por área (los 15 del inglés, traducidos); añade sus términos y su tratamiento asrc/i18n/GLOSARIO.md; ejecutanode scripts/emoji-noto.mjspara copiar su bandera; ejecutapnpm idiomas. Si además tiene que ser idioma de diseño,DesignLangen Rust necesita el mismo código en la misma posición (véase Motor del diseñador). Deducido: en la pantalla no hace falta cambiar nada más, porque todo lo que usa idiomas recorre la tabla. - Traducir una feature nueva: véase «Añadir o cambiar una cadena» más arriba.
- Arquitectura del frontend, Ajustes y shell
- IPC y manejo de errores, Convenciones
- Stores de estado (preferencia del idioma de las cartas)
- Visión general del diseñador (idiomas de diseño), Tests y CI, Tutoriales, Mesa de juego, Juego en línea
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia