Repository navigation
Internationalization
The interface is bilingual (English and Spanish). The code is written in Spanish: every user-visible string is passed through t("<Spanish source>"), and the English translation lives in a dictionary keyed by that same Spanish string. There is no runtime library; the whole mechanism is src/components/language-provider.tsx, the dictionaries in src/i18n/en/, and the CI-enforced check scripts/idiomas.mjs.
| Name | Where it lives | What it controls |
|---|---|---|
| Interface language |
LanguageProvider, localStorage key proxybuilder.lang ("es" or "en") |
All UI text and Rust error messages as shown to the user. |
| Card language |
useUi().lang (src/stores/ui-store.ts), key inside proxybuilder.preferencias
|
Which language of a card's printing to fetch from Scryfall (en es fr de it pt ja ko ru zhs zht). Chosen in the decklist panel "Options" ("Card language"). |
| Design language |
PrintSettings.designLang, DesignLang (Rust) |
Which language the card designer renders text in; see Designer Overview. |
This page is only about the first.
Decision: the factory interface language is English; Spanish is chosen in Settings and remembered. Implementation: leerIdioma() in language-provider.tsx returns "es" only when localStorage["proxybuilder.lang"] === "es"; anything else (missing, corrupted, storage blocked) returns "en". index.html also ships <html lang="en">, and the provider keeps document.documentElement.lang in sync. The main README and the GitHub release notes are in English as well (README.es.md is the Spanish copy); Inferred: English is the default because the project targets a wider audience than its Spanish-speaking source code, while Spanish remains first-class because it is the language strings are authored in.
src/components/language-provider.tsx exports:
| Export | Purpose |
|---|---|
LanguageProvider |
Context provider; state lang, persisted on setLang. |
useLang() |
Returns { lang, setLang, t, mensaje }. Throws if used outside the provider. |
t(es, vars?) |
lang === "en" ? (EN[es] ?? es) : es, then {name} interpolation. |
mensaje(e) |
Any error (usually an AppError) as a sentence in the current language. |
paraTraducir(es) |
Identity function that marks a string "translate later, elsewhere". For text stored in data tables (e.g. MARCAS in src/lib/cartas.ts, FASES in src/features/decklist/acciones.ts, TEXTOS in src/lib/errores.ts) that is rendered later through t(variable). The check script sees paraTraducir("...") literals. |
traduccionSuelta(es) |
t() without the provider (reads proxybuilder.lang directly). Used only by LimiteError, which must wrap the provider so a crash inside the provider still shows a translated recovery screen. |
type Lang = "es" | "en" |
Key behaviours:
- Missing entry falls back to the Spanish source, never to an empty string or a technical key. That safety net also hides missing translations while developing in Spanish, which is exactly why the check script exists.
- Interpolation regex is
\{(\w+)\}; a placeholder with no matching value (or anull/undefinedvalue) is left as{name}rather than printed asundefined. Values arestring | number. - There is no plural mechanism. Strings like
"{n} cartas"map to"{n} cards"irrespective ofn; if a count of one must read correctly, write separate source strings. (Inferred: no pluralisation code exists; I found noIntl.PluralRulesuse.) - Number/date formatting is separate from
t(): call sites choose a locale fromlang(lang === "en" ? "en-GB" : "es-ES", e.g.src/features/stats/textos.ts,src/features/library/piezas.tsx). - The
tfunction is recreated only whenlangchanges (useMemo([lang])), so it is safe in dependency lists; components re-render on language change because the context value changes.
src/i18n/en/index.ts merges all areas into EN: Record<string, string> with object spread (later areas win silently on duplicate keys, hence the collision check below).
| File | Area (per its header) |
|---|---|
comun.ts |
Shared strings; starts with the ErrorCode messages from src/lib/errores.ts. |
ajustes.ts |
Settings dialog (appearance, updates, tweakcn themes). |
shell.ts |
Top bar, panel titles, save indicator, update button and toasts. |
lista.ts |
Decklist panel, resolve phases, card marks. |
vista.ts |
Preview pages, toolbar, card menu, "move to" dialog. |
exportar.ts |
Export panel, presets, alignment. |
biblioteca.ts |
Deck library. |
cartas.ts |
Add card, variants, card editor dialogs. |
extras.ts |
Statistics entry points and set browser. |
vincular.ts |
Link custom cards to Scryfall; wheel zoom; UI scale. |
estadisticas.ts |
Statistics dialog. |
disenar.ts |
The designer view (largest file). |
Each file is export const NAME: Record<string, string> = { "Spanish source": "English", ... } with tab indentation (Biome). A key can be a bare identifier if it is a single word (Ajustes: "Settings"), otherwise a quoted string.
- In the component, write the visible text through the hook:
const { t } = useLang();thent("Texto en español"). For text outside React, in a module-level table, wrap it inparaTraducir("...")and callt(value)where it is rendered. - Add the pair to the dictionary of the matching area in
src/i18n/en/<area>.ts. Reuse an existing key when the Spanish text is identical and means the same thing. - Run
pnpm idiomas(also part ofpnpm check, the pre-commit hook for staged.ts/.tsx, and CI). - Use
{name}placeholders for dynamic parts (t("Descargando la {version}…", { version })); never concatenate translated fragments.
Disambiguation rule: if the same Spanish word needs two different English translations in different places, make the Spanish source of the less generic case longer (e.g. add context words). Never introduce a second dictionary or a context argument.
If you change an existing Spanish source string, change its dictionary key too; otherwise the English entry becomes orphaned and the new string is reported as untranslated. The script does not report orphaned dictionary entries (unused keys), only missing ones.
What it does:
- Reads every
src/i18n/en/*.tsexceptindex.tsand extracts keys with a regex (lines starting with a quoted string or a bare identifier followed by:). A key found in two files is a collision (error):index.tswould otherwise let the last one win silently. - Walks
src/(skippingnode_modules, dot-dirs,bindings, the dictionary dir, and*.test.ts(x)) and collects every double-quoted literal int("...")andparaTraducir("...")(first argument, escapes for\"and\nhandled). - Exits with status 1, listing "N string(s) not translated to English" with the file where each was first seen; prints
idiomas: N cadenas, todas traducidas.on success.
Limits to keep in mind (all follow from the regexes):
- Only double-quoted literals directly inside
t(/paraTraducir(are seen.t(variable), single-quoted strings, template literals, or concatenations are invisible to the check, so route such text throughparaTraducir("...")at its definition. - Spanish text written in JSX without
t()is not detected at all. -
.test.ts(x)files are excluded.
Rust never writes user-facing prose. Commands return Result<T, AppError> where AppError = { code: ErrorCode, params: { [key]: string } } (src/bindings/AppError.ts, ErrorCode.ts; see IPC and Error Handling). On the screen:
// src/lib/errores.ts
export function mensajeDeError(e: unknown, t): string {
if (esAppError(e)) return t(TEXTOS[e.code], e.params);
if (e instanceof Error) return e.message;
return String(e);
}TEXTOS has one Spanish sentence per ErrorCode (Network, RateLimited, NotFound, Io, Db, InvalidInput, Cancelled, Pdf, Image, Unsupported, Busy, Internal), each wrapped in paraTraducir. esAppError accepts any object whose code is a key of TEXTOS, so adding an ErrorCode in Rust forces a matching entry (the Record<ErrorCode, string> type fails tsc otherwise). Components call toast.error(mensaje(e)).
Special cases are handled by the component that knows the context, using params: for example TemasTweakcn (src/features/settings/temas-tweakcn.tsx) checks e.params.motivo === "tema_enlace" to show "That is not a tweakcn theme link or id." instead of the generic invalid-input message. SaveIndicator shows mensaje(error) in a tooltip when autosave fails.
Rust-provided data that is displayed (Scryfall names, oracle text, release notes) is shown as-is: release notes in the update panel are English Markdown from the GitHub release.
SettingsDialog (src/features/settings/settings-dialog.tsx) renders a ToggleGroup with "Español" and "English" (their own names, not translated) and calls setLang. The change is immediate for all mounted components; there is no reload.
-
pnpm idiomas/scripts/idiomas.mjsis the only guard for completeness (no unit tests of the script). -
src/lib/__tests__/errores.test.tscoversesAppErrorandmensajeDeError. - Not covered: pluralisation, placeholders mismatched between Spanish and English entries (the script does not compare
{placeholders}across languages; check by eye).
- Add a third UI language: widen
Lang, addsrc/i18n/<code>/dictionaries with the same keys, maketraducir()pick the dictionary, extendleerIdioma()and the toggle inSettingsDialog, and generalisescripts/idiomas.mjs(it currently hard-codessrc/i18n/en). Inferred: that is the full list; nothing else readslang, except the locale pickers noted above. - Translating a new feature: see "Adding or changing a string" above.
- Frontend Architecture, Settings and Shell
- IPC and Error Handling, Conventions
- State Stores (card language preference)
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