Añadir un idioma
Hoy Kosmotheka está en español e inglés. Añadir un tercer idioma es posible, pero hay más sitios de los que parece: la mayoría se traducen sin tocar código, y unos pocos tienen «español o inglés» escrito a mano. Esta página los enumera todos, para que sepas el tamaño del trabajo antes de empezar. En los ejemplos, el idioma nuevo es el francés (fr).
Antes de empezar, abre un issue (ver cómo contribuir): un idioma hay que mantenerlo en cada versión, y es mejor saber quién lo hará.
Los textos
Sección titulada «Los textos»Todos los textos de la interfaz están en app/src/i18n/locales/, un archivo por idioma:
en.jsonyes.jsonhoy. Copiaen.jsoncomofr.jsony traduce los valores, nunca las claves.app/src/i18n/index.tses la lista de idiomas de la interfaz:import fr from "./locales/fr.json",fr: { translation: fr }enresources,"fr"ensupportedLanguagesy su nombre, en su propio idioma, enlanguageNames(TypeScript avisa si falta). De ahí leen los selectores de idioma (Ajustes, la cuenta y el alta), las novedades, el enlace de la privacidad y el idioma de las búsquedas: no hay que tocar nada más en la interfaz, salvo el enlace de la política de privacidad si la tienes en tu idioma (PRIVACYenabout-dialog.tsx; sin ella, se usa la inglesa).
La app elige el idioma del sistema (i18next-browser-languagedetector) y, si no tiene el suyo, cae en inglés.
Cosas del texto que se tropiezan:
- Plurales: una clave con
countlleva_oney_other(results_one,results_other). Algunos idiomas necesitan más formas; consulta las reglas de i18next. - Variables:
{{title}}y similares se dejan igual. - Etiquetas:
<link>RAWG</link>en los textos de créditos se queda; solo se traduce lo de dentro. - Sin texto en el código: si un texto no está en
en.json, no se puede traducir. Añádelo a los idiomas que ya existen.
Las listas cerradas
Sección titulada «Las listas cerradas»Algunas listas viven en el dominio (Rust) y su texto, en los archivos de arriba:
| Lista | Dónde está | Claves de texto |
|---|---|---|
| Géneros de cada tipo | crates/domain/src/genre.rs |
genres.* |
| Estados de lectura, de juego… por tipo | dominio | marks.statuses.*, marks.statusesByKind.<tipo>.* |
| Tipos de obra, formatos de ejemplar | dominio | types.*, typesPlural.*, formats.* |
| Roles de las personas y estados de las peticiones | dominio | people.roles.*, requests.status.* |
| Códigos de error | Rust y servidor | accountErrors.*, cloud.errors.*, invitations.errors.*… |
Un género o un estado se guarda por su clave (science_fiction), no por su texto: traducirlos no cambia ningún dato ni la sincronización. Pero cada clave de la lista tiene que existir en tu archivo; si falta una, la interfaz enseña la clave en bruto.
Lo que avisa de claves que faltan
Sección titulada «Lo que avisa de claves que faltan»npm run check:i18n (en app/, y en la integración continua) falla si una clave está en un idioma y no en otro. Compara todos los archivos de src/i18n/locales/, así que el tuyo entra solo. La única excepción son los genresByKind: un idioma puede necesitar ajustar un género según el tipo (en español, «Policiaca» para una película) y otro no, porque la app usa entonces el general (genres.<clave>).
Además, la auditoría (.claude/skills/audit/bin/facts.py) avisa de las claves que el código usa y no existen, y de las listas cerradas (contracts.json) a las que les falta texto.
Además, npm run typecheck y npm run lint tienen que seguir pasando, y conviene abrir la app en el idioma nuevo y recorrer las pantallas.
Dónde está la lista de cada parte
Sección titulada «Dónde está la lista de cada parte»Cada parte tiene una sola lista de idiomas, y lo demás la lee. Al añadir un idioma, tocas la de cada parte y nada más:
| Parte | Dónde está la lista | Qué se añade |
|---|---|---|
| La interfaz | app/src/i18n/index.ts: supportedLanguages, languageNames |
El código y el nombre del idioma en sí mismo, más su locales/<código>.json |
| La política de privacidad que enlaza «Acerca de» | PRIVACY en app/src/components/about-dialog.tsx |
La dirección de la política en el idioma nuevo. Opcional: sin ella se enseña la inglesa |
| TMDB (películas y series) | TMDB_TAGS en app/src-tauri/src/metadata/mod.rs y tmdbTag() en server/src/Enum/Locale.php |
Cómo escribe TMDB ese idioma (fr → fr-FR). Opcional: sin ella se pregunta en en-US |
| Las novedades | Los archivos de app/src/i18n/locales/ |
Nada: check:whats-new exige texto en cada idioma que tenga archivo |
| El servidor | server/src/Enum/Locale.php |
Un caso nuevo; los tres formularios lo validan contra él |
Los demás buscadores preguntan en su idioma de siempre: para libros y títulos, el idioma de la edición no depende del de la app.
Las novedades (whats-new.json)
Sección titulada «Las novedades (whats-new.json)»Cada línea de app/src/whats-new.json lleva el texto de cada idioma, por su código ("es": …, "en": …, y el nuevo). El inglés es obligatorio y es el respaldo: si una línea no tiene texto en el idioma de la persona, se enseña el inglés. npm run check:whats-new exige texto en todos los idiomas que tienen archivo en src/i18n/locales/, así que para un idioma nuevo hay que traducir todas las líneas de todas las versiones que quieras que se vean. No hay que tocar ni el script ni whats_new.rs.
El servidor
Sección titulada «El servidor»Dos cosas:
- La lista de idiomas (
localede la cuenta):server/src/Enum/Locale.php, un enum con un caso por idioma.RegisterAccountRequest,UpdateAccountRequestyGoogleStartRequestlo validan con#[Assert\Choice(callback: [Locale::class, 'values'])], así que añadir el caso basta. La app manda su idioma al crear la cuenta y al entrar con Google. Si TMDB lo escribe distinto, añade también su etiqueta entmdbTag(). - Los correos:
server/translations/emails.es.yamlyemails.en.yaml, con el asunto y el cuerpo de cada uno (confirmar la cuenta, recuperar la contraseña, el aviso de que alguien intentó usar tu correo, la contraseña cambiada, el segundo factor, la invitación…). Hace falta unemails.fr.yamlcon todas las claves. Son traducciones, no una lista. El idioma por defecto del servidor esen(config/packages/translation.yaml) y es el que usa si no hay traducción.
Los textos de error que el servidor devuelve son códigos (invalid_credentials), no frases: la app los traduce con sus archivos. No hay que tocarlos.
La documentación y la web
Sección titulada «La documentación y la web»Son aparte y se traducen cuando haya quien los mantenga:
- La documentación (
site/): Starlight tiene los idiomas ensite/astro.config.mjs(hoyespor defecto yen). Un idioma nuevo es una carpeta nueva ensite/src/content/docs/<idioma>/con la misma estructura de carpetas (los nombres de carpeta son los mismos en todos los idiomas, porque el selector de idioma enlaza a la misma página). Lo que no esté traducido sale en español con un aviso. - La web (
web/): una carpeta por idioma (web/es/,web/en/) con sus páginas, entre ellas la política de privacidad y la de borrar la cuenta. - La política de privacidad conviene tenerla en el idioma de quien usa la app: el enlace del diálogo «Acerca de» tiene que apuntar a ella.
Lista de comprobación
Sección titulada «Lista de comprobación»- Un issue abierto, y quién mantendrá el idioma.
locales/<código>.jsoncompleto y registrado eni18n/index.ts(resources,supportedLanguages,languageNames).- Géneros, estados, tipos, formatos y roles con texto; la app recorrida en el idioma nuevo.
- Opcional: la política de privacidad en
PRIVACYy la etiqueta de TMDB (TMDB_TAGS,Locale::tmdbTag()). whats-new.json: texto en el idioma nuevo en cada línea.- Servidor: un caso en
Localeyemails.<código>.yaml. - Documentación y web, cuando se pueda.
npm run typecheck,npm run lint,npm run check:i18n,npm run check:whats-new,cargo testymake check(enserver/).
© 2026 Kosmotheka
Kosmotheka es software libre: la aplicación y el servidor, bajo la AGPL-3.0-or-later; la especificación de operaciones, bajo la MIT. El nombre y el logotipo no entran en la licencia.
kosmotheka.com · Política de marca · Política de privacidad · info@kosmotheka.com