aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:07 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:07 +0200
commit3b2d03a919e56d14f8097844b97b844ddf526cd6 (patch)
tree31b222fedf6d608809eb47c45c5f431d7001d87a /packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
parent75065cc1ed5d9cd733871d478dc4f915663d795c (diff)
downloadmeshbay-3b2d03a919e56d14f8097844b97b844ddf526cd6.tar.gz
feat(hub): translate the web client into nine more languages
French, Spanish, Brazilian Portuguese, Simplified Chinese, Japanese, German, Italian, Dutch and Polish, all in the formal register. `hub`, `node` and `GEK` stay in English: they name the CLI, node.toml and the docs, and translating them would cut the interface off from everything an operator reads and types. Catalogues move out of i18n.js into locales/, one file per language, fetched with a dynamic import. A visitor downloads their language plus English as a fallback — about 36 KB rather than the ~180 KB that ten inlined catalogues would have cost everyone. i18n.js keeps only the loader, so the first render now waits for initLocale(). Three things the old code got wrong, none of them visible until there was a second language: - Resolution trimmed a tag to its base before matching, so a browser reporting pt-BR looked for a `pt` catalogue that does not exist and fell back to English. Matching is now exact first, then by base language. - Counted strings were single strings, so Polish could not express 1 plik / 2 pliki / 5 plików at all. t() selects through Intl.PluralRules; en.js gains the same treatment, which incidentally fixes "1 files". - Interpolation used String.replace, which reads `$&` in the replacement. A file named rap$&sody.mp3 rendered corrupted in its own delete dialog. Five strings were still hardcoded in app.js — the group name placeholder and the four visibility/join-policy descriptions — and are now keyed. test_locales.py holds the nine translations to the shape of en.js: same keys, a counted string stays counted everywhere, every plural entry covers each category Intl actually produces for that language, and the {placeholders} survive translation. Verified failing first, against a catalogue with a key removed, a placeholder dropped and the Polish `few` form deleted. The language menu also grew from one entry to ten, which overran a short viewport inside a dropdown that clipped instead of scrolling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/locales/es.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js402
1 files changed, 402 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
new file mode 100644
index 0000000..c09175b
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -0,0 +1,402 @@
+/**
+ * Spanish — translated from locales/en.js.
+ *
+ * Register: formal (usted).
+ * `hub`, `node` and `GEK` are left in English: they name the CLI
+ * (`meshbay-node`), node.toml and the docs, which are English throughout.
+ */
+
+export default {
+ // Nav
+ 'nav.toggle_menu': 'Mostrar el menú',
+ 'nav.light_mode': 'Tema claro',
+ 'nav.dark_mode': 'Tema oscuro',
+ 'nav.logout': 'Cerrar sesión',
+ 'nav.login': 'Iniciar sesión',
+
+ // Sidebar
+ 'sidebar.my_groups': 'Mis grupos',
+ 'sidebar.no_groups': 'Aún no hay grupos',
+ 'sidebar.discover': 'Descubrir',
+ 'sidebar.public_groups': 'Grupos públicos',
+
+ // Login
+ 'login.title': 'Iniciar sesión',
+ 'login.username': 'Nombre de usuario',
+ 'login.password': 'Contraseña',
+ 'login.submit': 'Iniciar sesión',
+ 'login.loading': 'Iniciando sesión...',
+ 'login.no_account': '¿No tiene cuenta?',
+ 'login.register_link': 'Crear una cuenta',
+
+ // Register
+ 'register.title': 'Crear una cuenta',
+ 'register.username': 'Nombre de usuario',
+ 'register.email': 'Correo electrónico',
+ 'register.password': 'Frase de contraseña (mínimo 12 caracteres)',
+ 'register.confirm': 'Confirmar contraseña',
+ 'register.submit': 'Crear la cuenta',
+ 'register.loading': 'Creando la cuenta...',
+ 'register.has_account': '¿Ya tiene una cuenta?',
+ 'register.login_link': 'Iniciar sesión',
+ 'register.success_title': 'Cuenta creada',
+ 'register.success_msg': 'Ya puede iniciar sesión con sus credenciales.',
+ 'register.go_login': 'Ir al inicio de sesión',
+ 'register.err_mismatch': 'Las contraseñas no coinciden',
+ 'register.err_min_len': {
+ one: 'Use al menos {n} carácter',
+ other: 'Use al menos {n} caracteres',
+ },
+ 'register.err_too_weak': 'Demasiado fácil de adivinar. Su frase de contraseña es lo '
+ + 'que protege sus claves allí donde están almacenadas — unas cuantas palabras sin '
+ + 'relación entre sí funcionan muy bien.',
+ 'register.strength': 'Solidez: unos {bits} bits. Protege la copia de sus claves que '
+ + 'se guarda en los nodes a los que se une, así que conviene acertar.',
+
+ // Home
+ 'home.welcome': 'Bienvenido a MeshBay',
+ 'home.no_groups': 'Todavía no es miembro de ningún grupo.',
+ 'home.browse_prefix': 'Explore los ',
+ 'home.browse_link': 'grupos públicos',
+ 'home.browse_suffix': ' o pida a un administrador de grupo que le invite.',
+ 'home.my_groups': 'Mis grupos',
+
+ // Explore
+ 'explore.title': 'Grupos públicos',
+ 'explore.search': 'Buscar grupos...',
+ 'explore.loading': 'Cargando...',
+ 'explore.empty': 'No hay grupos públicos disponibles.',
+ 'explore.create_group': 'Crear un grupo',
+ 'explore.join': 'Unirse',
+ 'explore.member': 'Miembro',
+
+ // Group page
+ 'group.default_name': 'Grupo',
+ 'group.tab_files': 'Archivos',
+ 'group.tab_chat': 'Chat',
+ 'group.tab_members': 'Miembros',
+ 'group.filter': 'Filtrar archivos...',
+ 'group.col_name': 'Nombre',
+ 'group.col_size': 'Tamaño',
+ 'group.col_type': 'Tipo',
+ 'group.col_date': 'Fecha',
+ 'group.empty_filter': 'Ningún archivo coincide con el filtro',
+ 'group.empty_dir': 'Esta carpeta está vacía',
+ 'group.download': 'Descargar',
+ 'group.play': 'Reproducir',
+ 'group.offline_title': 'Ningún node está en línea para este grupo en este momento.',
+ 'group.offline_hint': 'Los archivos aparecerán cuando se conecte un node que aloje '
+ + 'este grupo.',
+ 'group.upload': 'Subir',
+ 'group.mkdir': 'Nueva carpeta',
+ 'group.mkdir_prompt': 'Nombre de la nueva carpeta:',
+ 'group.view': 'Ver',
+ 'group.delete': 'Eliminar',
+ 'group.delete_confirm': '¿Eliminar {name}?',
+ 'group.delete_group': 'Eliminar el grupo',
+ 'group.delete_group_confirm': '¿Eliminar «{name}» de forma permanente y expulsar a '
+ + 'todos los miembros? Los archivos que están en el node se conservan.',
+ 'group.err_transport': 'Módulo de transporte no cargado',
+
+ // Status
+ 'status.idle': 'Inactivo',
+ 'status.discovering': 'Buscando nodes...',
+ 'status.connecting': 'Conectando por WebRTC...',
+ 'status.fetching': 'Obteniendo el índice...',
+ 'status.files': {
+ one: '{n} archivo',
+ other: '{n} archivos',
+ },
+ 'status.offline': 'Ningún node en línea',
+ 'status.error': 'Error de conexión',
+
+ // Chat
+ 'chat.empty': 'Todavía no hay mensajes. ¡Empiece la conversación!',
+ 'chat.placeholder': 'Escriba un mensaje...',
+ 'chat.send': 'Enviar',
+ 'chat.attach': 'Adjuntar archivo',
+
+ // Video player
+ 'video.loading': 'Cargando {name}...',
+ 'video.buffering': 'Almacenando en búfer...',
+ 'video.close': 'Cerrar (Esc)',
+ 'preview.pdf_fallback': 'Este navegador no mostrará el PDF integrado. Descárguelo '
+ + 'en su lugar — en cualquier caso se descifró aquí.',
+ 'group.upload_indexing': 'indexando…',
+ 'video.err_transport': 'Transporte no conectado',
+ 'video.err_mse': 'Códec no compatible con la reproducción en continuo: {codec}',
+
+ // Settings
+ 'settings.title': 'Ajustes',
+ 'settings.coming_soon': 'Próximamente.',
+ 'settings.downloads': 'Descargas',
+ 'settings.dl_auto': 'Guardar automáticamente',
+ 'settings.dl_auto_hint': 'Los archivos van directamente a la carpeta que elija más '
+ + 'abajo, sin ningún diálogo. Descargar veinte archivos deja allí veinte archivos.',
+ 'settings.dl_ask': 'Preguntar cada vez',
+ 'settings.dl_ask_hint': 'Un diálogo «Guardar como» por archivo — uno por archivo, '
+ + 'también cuando descarga una selección.',
+ 'settings.dl_folder': 'Carpeta: {name}',
+ 'settings.dl_no_folder': 'Ninguna carpeta elegida — las descargas van adonde las '
+ + 'ponga su navegador',
+ 'settings.dl_choose': 'Elegir carpeta',
+ 'settings.dl_change': 'Cambiar',
+ 'settings.dl_forget': 'Olvidar',
+ 'settings.dl_path_note': 'A una página web no se le puede indicar una ruta, así que '
+ + 'aquí no hay nada que escribir: su navegador concede acceso a la carpeta que '
+ + 'usted señale, y MeshBay solo escribe dentro de ella. Es posible que se le pida '
+ + 'confirmación una vez por sesión.',
+ 'settings.dl_unsupported': 'Este navegador no puede escribir en una carpeta de su '
+ + 'elección (no tiene File System Access API), de modo que las descargas van a su '
+ + 'propia carpeta de descargas. Chrome y Edge sí permiten elegir una.',
+ 'settings.profile': 'Perfil',
+ 'settings.username': 'Nombre de usuario',
+ 'settings.node_pins': 'Identidades de los nodes',
+ 'settings.node_pins_hint': 'La clave de identidad de cada node se memoriza la '
+ + 'primera vez que se conecta. Si cambia, la conexión se rechaza — algo esperable '
+ + 'solo cuando un operador reinstala un node. Confírmelo con él antes de borrarla.',
+ 'settings.node_pins_count': {
+ one: '{n} fijada',
+ other: '{n} fijadas',
+ },
+ 'settings.node_pins_clear': 'Borrar las identidades fijadas',
+ 'settings.danger': 'Eliminar la cuenta',
+ 'settings.delete_hint': 'Elimina su cuenta, sus pertenencias a grupos y sus '
+ + 'notificaciones del hub, y libera su nombre de usuario. No alcanza lo que vive '
+ + 'en los nodes: los archivos que subió se quedan donde los guarda su operador, y '
+ + 'cada node conserva la identidad que fijó para usted hasta que su operador la '
+ + 'retire. Los registros de conexión se guardan un año, como exige la ley.',
+ 'settings.delete_account': 'Eliminar mi cuenta',
+ 'settings.delete_confirm': 'Escriba su frase de contraseña para confirmar. '
+ + 'Esta acción no se puede deshacer.',
+ 'settings.delete_confirm_btn': 'Eliminar definitivamente',
+ 'settings.cancel': 'Cancelar',
+ 'settings.appearance': 'Apariencia',
+ 'settings.theme': 'Tema',
+ 'settings.theme_light': 'Claro',
+ 'settings.theme_dark': 'Oscuro',
+ 'settings.theme_system': 'Sistema',
+ 'settings.language': 'Idioma',
+ 'settings.about': 'Acerca de',
+ 'settings.version': 'Versión',
+ 'settings.protocol': 'Protocolo',
+ 'settings.role': 'Rol',
+ 'settings.groups': 'Notificaciones de grupo',
+ 'settings.notifications': 'Notificaciones',
+ 'settings.node_key': 'Vincular un node',
+ 'settings.node_key_desc': 'Si administra un node MeshBay, pegue aquí su clave '
+ + 'pública Ed25519 para vincularlo a su cuenta. Encontrará esa clave en el panel '
+ + 'de administración de su node.',
+ 'settings.node_key_placeholder': 'Pegue la clave pública Ed25519 del node (base64)',
+ 'settings.node_key_submit': 'Vincular el node',
+ 'settings.node_key_success': 'Node vinculado correctamente',
+ 'settings.node_key_current': 'Clave del node vinculado',
+
+ // Sidebar
+ 'sidebar.create_group': 'Crear un grupo',
+ 'sidebar.settings': 'Ajustes',
+ 'sidebar.admin': 'Administración',
+
+ // User menu
+ 'usermenu.language': 'Idioma',
+ 'usermenu.settings': 'Ajustes',
+ 'usermenu.theme_light': 'Tema claro',
+ 'usermenu.theme_dark': 'Tema oscuro',
+ 'usermenu.logout': 'Cerrar sesión',
+
+ // Admin panel
+ 'admin.title': 'Administración',
+ 'admin.tab_stats': 'Estadísticas',
+ 'admin.tab_users': 'Usuarios',
+ 'admin.tab_groups': 'Grupos',
+ 'admin.tab_nodes': 'Nodes',
+ 'admin.tab_logs': 'Registros',
+ 'admin.col_observed_ip': 'Visto desde',
+ 'admin.col_hint': 'Dirección anunciada',
+ 'admin.col_last_seen': 'Último anuncio',
+ 'admin.node_online': 'en línea',
+ 'admin.node_offline': 'desconectado',
+ 'admin.no_nodes': 'Ningún node registrado',
+ 'admin.nodes_hint': '«Visto desde» es la dirección desde la que llegó el anuncio, '
+ + 'en una petición firmada con la clave del node — esa es la que sirve para '
+ + 'responder a un requerimiento. La dirección anunciada es la que el node cree que '
+ + 'es la suya, aprendida de un servidor STUN y enviada aquí; sirve para alcanzar el '
+ + 'node y no prueba nada.',
+ 'admin.tab_blocklist': 'Lista de bloqueo',
+
+ // Admin stats
+ 'admin.stat_users': 'Usuarios',
+ 'admin.stat_groups': 'Grupos',
+ 'admin.stat_nodes': 'Nodes',
+ 'admin.stat_online': 'Nodes en línea',
+
+ // Admin users
+ 'admin.users_search': 'Buscar usuarios...',
+ 'admin.col_username': 'Nombre de usuario',
+ 'admin.col_role': 'Rol',
+ 'admin.col_status': 'Estado',
+ 'admin.col_created': 'Creación',
+ 'admin.col_actions': 'Acciones',
+ 'admin.col_email': 'Correo electrónico',
+ 'admin.col_groups': 'Grupos',
+ 'admin.no_users': 'No se ha encontrado ningún usuario',
+ 'admin.btn_suspend': 'Suspender',
+ 'admin.btn_unsuspend': 'Reactivar',
+ 'admin.btn_details': 'Detalles',
+ 'admin.btn_delete': 'Eliminar',
+ 'admin.delete_confirm': '¿Eliminar la cuenta «{user}»? Esta acción no se puede '
+ + 'deshacer. Los archivos que subió permanecen en los nodes que los alojan, y cada '
+ + 'node conserva la identidad que fijó hasta que su operador la retire. La '
+ + 'suspensión es la opción reversible.',
+ 'admin.user_detail': 'Detalles del usuario',
+ 'admin.btn_close': 'Cerrar',
+ 'admin.self_note': '(usted)',
+
+ // Admin groups
+ 'admin.col_name': 'Nombre',
+ 'admin.col_visibility': 'Visibilidad',
+ 'admin.col_members': 'Miembros',
+ 'admin.no_groups': 'No se ha encontrado ningún grupo',
+
+ // Admin logs
+ 'admin.col_time': 'Hora',
+ 'admin.col_event': 'Evento',
+ 'admin.col_user': 'Usuario',
+ 'admin.col_ip': 'IP',
+ 'admin.col_detail': 'Detalle',
+ 'admin.filter_all': 'Todos los eventos',
+ 'admin.no_logs': 'No se ha encontrado ningún registro',
+ 'admin.btn_load_more': 'Cargar más',
+
+ // Admin blocklist
+ 'admin.col_hash': 'Huella',
+ 'admin.col_reason': 'Motivo',
+ 'admin.col_date': 'Fecha',
+ 'admin.col_added_by': 'Añadida por',
+ 'admin.no_blocked': 'Ninguna huella bloqueada',
+ 'admin.add_hash': 'Añadir una huella a la lista de bloqueo',
+ 'admin.hash_placeholder': 'huella blake3 (64 caracteres hexadecimales)',
+ 'admin.reason_placeholder': 'Motivo',
+ 'admin.btn_block': 'Bloquear',
+ 'admin.btn_unblock': 'Desbloquear',
+
+ // Notifications
+ // Create group
+ 'create_group.title': 'Crear un grupo',
+ 'create_group.name': 'Nombre del grupo',
+ 'create_group.name_placeholder': 'p. ej. Fotos de familia, Archivos del proyecto...',
+ 'create_group.visibility': 'Visibilidad',
+ 'create_group.private': 'Privado',
+ 'create_group.private_desc': 'Solo los miembros invitados ven los archivos y acceden a ellos',
+ 'create_group.public': 'Público',
+ 'create_group.public_desc': 'Cualquiera puede descubrir y explorar este grupo',
+ 'create_group.join_policy': 'Política de acceso',
+ 'create_group.invite': 'Solo por invitación',
+ 'create_group.invite_desc': 'Los miembros deben ser invitados por un administrador',
+ 'create_group.open': 'Abierto (cualquiera puede unirse)',
+ 'create_group.open_desc': 'Cualquiera puede unirse sin aprobación',
+ 'create_group.description': 'Descripción',
+ 'create_group.description_hint': '¿De qué trata este grupo? (opcional)',
+ 'create_group.submit': 'Crear',
+ 'create_group.hint': 'Un grupo necesita un node para alojar los archivos. Puede '
+ + 'crear el grupo ahora y conectar un node más adelante.',
+ 'create_group.creating': 'Creando...',
+
+ // Members
+ 'members.col_role': 'Rol',
+ 'members.group_role': 'Rol en el grupo',
+ 'members.admin': 'Administrador',
+ 'members.group_admin': 'Administrador del grupo',
+ 'members.owner': 'Propietario',
+ 'members.member': 'Miembro',
+ 'members.invite_title': 'Invitar a un miembro',
+ 'members.username_placeholder': 'Nombre de usuario',
+ 'members.invite_btn': 'Invitar',
+ 'members.invite_needs_pairing': 'Las invitaciones se firman con la clave que este '
+ + 'node fijó para su navegador, así que empareje el navegador abajo antes de '
+ + 'invitar a nadie.',
+ 'members.invite_ask_operator': 'Solo el operador del node que aloja este grupo puede '
+ + 'invitar, desde un navegador emparejado con él.',
+ 'members.remove': 'Expulsar',
+ 'members.remove_confirm': '¿Expulsar a {user} de este grupo? Su cuenta, sus otros '
+ + 'grupos y los archivos que subió no se ven afectados.',
+ 'members.remove_hint': 'Expulsar a alguien le quita la pertenencia al grupo e impide '
+ + 'que el node le entregue la clave del grupo. No elimina su cuenta, y no recupera '
+ + 'la clave que ya tiene en su poder — rótela en el node (meshbay-node gek-init) si '
+ + 'eso importa.',
+ 'members.pair_title': 'Emparejar este navegador con su node',
+ 'members.pair_hint': 'Su node solo acepta acciones de operador — invitaciones, '
+ + 'eliminación de archivos — desde un navegador con el que se haya emparejado. '
+ + 'Ejecute `meshbay-node operator pair` en el node y escriba aquí el código. El '
+ + 'código nunca pasa por el hub.',
+ 'members.pair_btn': 'Emparejar',
+ 'members.pair_success': 'Este navegador ya está emparejado con el node.',
+ 'members.invite_code_ready': 'Código de invitación para {user} — envíeselo por el '
+ + 'medio que usen habitualmente. Funciona una sola vez y nunca pasa por el hub.',
+ 'members.invite_code_hint': 'Lo introduce la primera vez que abre este grupo. No '
+ + 'necesita que usted esté en línea en ese momento.',
+ 'transfers.title': 'Transferencias',
+ 'transfers.clear': 'Borrar las terminadas',
+ 'transfers.cancel': 'Cancelar',
+ 'transfers.open': 'Abrir',
+ 'transfers.done': 'Terminada',
+ 'transfers.cancelled': 'Cancelada',
+ 'transfers.failed': 'Fallida',
+ 'group.select': 'Seleccionar',
+ 'group.select_done': 'Listo',
+ 'group.actions': 'Acciones ({n})',
+ 'group.download_n': 'Descargar ({n})',
+ 'group.download_zip_n': 'Descargar las carpetas en zip ({n})',
+ 'group.delete_n': 'Eliminar ({n})',
+ 'group.delete_n_confirm': {
+ one: '¿Eliminar {n} elemento? {names}',
+ other: '¿Eliminar {n} elementos? {names}',
+ },
+ 'group.download_zip': {
+ one: 'Descargar en zip ({n} archivo)',
+ other: 'Descargar en zip ({n} archivos)',
+ },
+ 'group.zip_empty': 'Esa carpeta no contiene nada que descargar.',
+ 'group.zip_no_stream': 'Este navegador no puede escribir una descarga directamente '
+ + 'en el disco, así que {name} ({size}) tiene que construirse antes en memoria. Con '
+ + 'un archivo comprimido grande eso puede fallar. ¿Continuar?',
+ 'group.rmdir': 'Eliminar la carpeta',
+ 'group.rmdir_confirm': '¿Eliminar la carpeta «{name}»? Debe estar vacía.',
+ 'group.rmdir_not_empty': 'Esa carpeta no está vacía. Elimine antes lo que contiene — '
+ + 'aquí nada borra archivos que usted no ve.',
+ 'group.desc_edit': 'Editar la descripción',
+ 'group.desc_add': 'Añadir una descripción',
+ 'group.desc_save': 'Guardar',
+ 'group.desc_cancel': 'Cancelar',
+ 'group.desc_placeholder': 'Para qué sirve este grupo — los miembros lo ven en su '
+ + 'página de inicio.',
+ 'group.join_code_title': 'Este node necesita reconocerle',
+ 'group.join_code_hint': 'Pida el código de un solo uso a quien le invitó e '
+ + 'introdúzcalo aquí. Después, este navegador queda reconocido y no se le volverá a '
+ + 'preguntar.',
+ 'group.join_code_btn': 'Unirse',
+
+ 'notif.purge': 'Borrar todo',
+ 'notif.title': 'Notificaciones',
+ 'notif.empty': 'Ninguna notificación',
+ 'notif.mark_all_read': 'Marcar todo como leído',
+
+ // Sidebar
+ 'sidebar.search': 'Buscar archivos',
+
+ // Status
+
+ // Search
+ 'search.title': 'Buscar archivos',
+ 'search.placeholder': 'Buscar en todos los grupos...',
+ 'search.no_results': 'Ningún archivo coincide con su búsqueda.',
+ 'search.synced': 'sincronizado {ago}',
+ 'search.cache_note': 'La búsqueda usa el estado que tenía cada grupo la última vez '
+ + 'que lo abrió. Abra un grupo para poner sus archivos al día aquí.',
+ 'search.col_group': 'Grupo',
+ 'search.result_count': {
+ one: '{n} archivo encontrado',
+ other: '{n} archivos encontrados',
+ },
+ 'search.hint': 'Busca nombres de archivo en todos los índices de grupo en caché.',
+};