From 887551d1fc60c908a72ec0b86761a8d3a0806d7a Mon Sep 17 00:00:00 2001 From: et-nik Date: Thu, 17 Sep 2026 02:48:06 +0200 Subject: [PATCH] de and es langs --- frontend/src/__tests__/translations.spec.ts | 26 ++++ frontend/src/translations/de.ts | 161 ++++++++++++++++++++ frontend/src/translations/es.ts | 161 ++++++++++++++++++++ frontend/src/translations/index.ts | 6 +- 4 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 frontend/src/__tests__/translations.spec.ts create mode 100644 frontend/src/translations/de.ts create mode 100644 frontend/src/translations/es.ts diff --git a/frontend/src/__tests__/translations.spec.ts b/frontend/src/__tests__/translations.spec.ts new file mode 100644 index 0000000..e33ad95 --- /dev/null +++ b/frontend/src/__tests__/translations.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { translations } from '@/translations'; + +// The panel falls back to English for a whole locale, not per key: a key +// missing from a locale that exists is shown to its users as the raw key. +type Key = keyof typeof translations.en; + +function placeholders(text: string): string[] { + return (text.match(/\{\w+\}|(? { + const englishKeys = Object.keys(translations.en).sort(); + + for (const [locale, strings] of Object.entries(translations)) { + it(`${locale} defines exactly the English keys`, () => { + expect(Object.keys(strings).sort()).toEqual(englishKeys); + }); + + it(`${locale} keeps the English placeholders`, () => { + for (const [key, text] of Object.entries(strings)) { + expect(placeholders(text), key).toEqual(placeholders(translations.en[key as Key])); + } + }); + } +}); diff --git a/frontend/src/translations/de.ts b/frontend/src/translations/de.ts new file mode 100644 index 0000000..e6591ed --- /dev/null +++ b/frontend/src/translations/de.ts @@ -0,0 +1,161 @@ +export const de = { + // General + ftp_users: 'FTP-Benutzer', + ftp_users_admin: 'FTP', + create_user: 'Benutzer erstellen', + edit_user: 'Benutzer bearbeiten', + delete_user: 'Benutzer löschen', + delete_user_confirm: 'Möchten Sie den Benutzer „{username}“ wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.', + + // Node status + node_status: 'Node-Status', + status_installed: 'gameap-files ist installiert und läuft', + status_installing: 'Installation läuft...', + status_failed: 'Installation fehlgeschlagen', + status_not_installed: 'gameap-files ist nicht installiert', + install: 'Installieren', + retry_setup: 'Installation wiederholen', + update_installation: 'Aktualisieren', + update_installation_hint: 'Neueste Version von gameap-files neu installieren; Einstellungen und Benutzer bleiben erhalten', + installing: 'Wird installiert', + node_not_ready: 'gameap-files ist auf diesem Node nicht installiert. Bitte wenden Sie sich an den Administrator.', + + // Setup configuration + configure_installation: 'Installation konfigurieren', + settings: 'Einstellungen', + ftp_settings: 'FTP-Einstellungen', + sftp_settings: 'SFTP-Einstellungen', + ftp_address: 'Bind-Adresse', + ftp_address_hint: 'Für alle Schnittstellen leer lassen', + ftp_port: 'FTP-Port', + passive_port_min: 'Passiver Port (min.)', + passive_port_max: 'Passiver Port (max.)', + public_host: 'Öffentlicher Host', + public_host_hint: 'Externe IP für den Passivmodus', + enable_tls: 'TLS aktivieren', + tls_implicit_port: 'Port für implizites TLS', + sftp_port: 'SFTP-Port', + + // User fields + username: 'Benutzername', + username_placeholder: 'z. B. server1_user', + password: 'Passwort', + password_placeholder: 'Leer lassen für automatische Generierung', + password_hint: 'Wenn leer, wird automatisch ein sicheres Passwort generiert', + new_password: 'Neues Passwort', + new_password_placeholder: 'Leer lassen, um das aktuelle beizubehalten', + new_password_hint: 'Leer lassen, um das aktuelle Passwort beizubehalten', + home_dir: 'Home-Verzeichnis', + home_dir_placeholder: 'z. B. /srv/gameap/server1', + home_dir_hint: 'Wenn leer, wird das Serververzeichnis verwendet', + quota: 'Kontingent', + quota_placeholder: '10GB, 500MB oder 0 für unbegrenzt', + quota_hint: 'Größe mit Einheit angeben (B, KB, MB, GB, TB). Leer oder 0 = unbegrenzt', + enabled: 'Aktiviert', + disabled: 'Deaktiviert', + description: 'Beschreibung', + description_placeholder: 'Optionale Beschreibung des Benutzers', + status: 'Status', + actions: 'Aktionen', + unlimited: 'Unbegrenzt', + + // User created + user_created: 'Benutzer erfolgreich erstellt', + password_warning: 'Dieses Passwort wird nur einmal angezeigt. Bitte speichern Sie es an einem sicheren Ort.', + + // Access rules + access_rules: 'Zugriffsregeln', + add_rule: 'Regel hinzufügen', + path: 'Pfad', + path_placeholder: '/**', + permissions: 'Rechte', + read: 'Lesen', + write: 'Schreiben', + delete: 'Löschen', + list: 'Auflisten', + no_access_rules: 'Keine Zugriffsregeln konfiguriert', + + // Virtual paths + virtual_paths: 'Virtuelle Pfade', + add_path: 'Pfad hinzufügen', + virtual: 'Virtueller Pfad', + virtual_placeholder: '/shared', + target: 'Zielpfad', + target_placeholder: '/srv/gameap/shared', + read_only: 'Nur Lesen', + no_virtual_paths: 'Keine virtuellen Pfade konfiguriert', + + // SSH keys + ssh_keys: 'SSH-Schlüssel', + add_key: 'Schlüssel hinzufügen', + no_ssh_keys: 'Keine SSH-Schlüssel konfiguriert', + ssh_public_key: 'Öffentlicher SSH-Schlüssel', + ssh_key_placeholder: 'ssh-ed25519 AAAA... user@host', + ssh_key_hint: 'Fügen Sie Ihren öffentlichen SSH-Schlüssel ein (beginnt mit ssh-rsa, ssh-ed25519, ecdsa-sha2 usw.)', + + // Actions + save: 'Speichern', + cancel: 'Abbrechen', + close: 'Schließen', + retry: 'Erneut versuchen', + refresh: 'Neu laden', + all: 'Alle', + installed_short: 'Installiert', + not_installed_short: 'Nicht installiert', + failed_short: 'Fehlgeschlagen', + search_nodes: 'Nach Name oder IP suchen...', + copy: 'Kopieren', + copied: 'Kopiert!', + copy_failed: 'Kopieren fehlgeschlagen', + create: 'Erstellen', + add: 'Hinzufügen', + edit: 'Bearbeiten', + remove: 'Entfernen', + + // Admin page + admin_page_info: 'Zentrale Verwaltung der FTP-Benutzer', + admin_page_hint: 'Öffnen Sie einen bestimmten Server, um dessen FTP-Benutzer zu verwalten.', + nodes: 'Nodes', + users: 'Benutzer', + node_users: 'Node-Benutzer', + select_server: 'Server auswählen', + select_server_placeholder: 'Server auswählen...', + picker_search_servers: 'Server nach Name suchen...', + picker_no_servers: 'Auf diesem Node wurden keine Server gefunden', + picker_more: ':shown von :total angezeigt — Suche verfeinern', + picker_load_failed: 'Server konnten nicht geladen werden', + back_to_nodes: 'Zurück zu den Nodes', + create_ftp_user: 'FTP-Benutzer erstellen', + view_users: 'Benutzer anzeigen', + servers: 'Server', + users_label: 'Benutzer', + all_nodes: 'Alle Nodes', + all_servers: 'Alle Server', + all_statuses: 'Alle Status', + search_users: 'Benutzer suchen...', + expand_all: 'Alle ausklappen', + collapse_all: 'Alle einklappen', + no_nodes: 'Keine Nodes gefunden', + no_users_found: 'Keine Benutzer entsprechen den Filtern', + servers_count: '{count} Server', + users_count: '{count} Benutzer', + // Followed by " ?" in the delete dialog + confirm_delete_user: 'Möchten Sie den folgenden Benutzer wirklich löschen:', + + // Empty states + no_users: 'Für diesen Server sind keine FTP-Benutzer konfiguriert', + + // Errors + error_username_required: 'Benutzername ist erforderlich', + error_username_format: 'Der Benutzername muss mit einem Buchstaben beginnen und darf nur lateinische Buchstaben, Ziffern und Unterstriche enthalten (3-32 Zeichen)', + error_quota_format: 'Ungültiges Format. Verwenden Sie: 10GB, 500MB, 1TB usw.', + error_loading: 'Daten konnten nicht geladen werden', + error_saving: 'Änderungen konnten nicht gespeichert werden', + error_creating: 'Benutzer konnte nicht erstellt werden', + error_deleting: 'Benutzer konnte nicht gelöscht werden', + + // Server ability titles (resolved by the panel's privilege UI + // as plugins.files.abilities.) + 'abilities.ftp-users-manage': 'FTP/SFTP-Benutzer verwalten', + 'abilities.ftp-users-view': 'FTP/SFTP-Benutzer anzeigen', +}; diff --git a/frontend/src/translations/es.ts b/frontend/src/translations/es.ts new file mode 100644 index 0000000..0305be3 --- /dev/null +++ b/frontend/src/translations/es.ts @@ -0,0 +1,161 @@ +export const es = { + // General + ftp_users: 'Usuarios FTP', + ftp_users_admin: 'FTP', + create_user: 'Crear usuario', + edit_user: 'Editar usuario', + delete_user: 'Eliminar usuario', + delete_user_confirm: '¿Estás seguro de que quieres eliminar el usuario "{username}"? Esta acción no se puede deshacer.', + + // Node status + node_status: 'Estado del nodo', + status_installed: 'gameap-files está instalado y en ejecución', + status_installing: 'Instalación en curso...', + status_failed: 'Error de instalación', + status_not_installed: 'gameap-files no está instalado', + install: 'Instalar', + retry_setup: 'Reintentar instalación', + update_installation: 'Actualizar', + update_installation_hint: 'Reinstalar la última versión de gameap-files; se conservan la configuración y los usuarios', + installing: 'Instalando', + node_not_ready: 'gameap-files no está instalado en este nodo. Contacta con el administrador.', + + // Setup configuration + configure_installation: 'Configurar instalación', + settings: 'Configuración', + ftp_settings: 'Configuración de FTP', + sftp_settings: 'Configuración de SFTP', + ftp_address: 'Dirección de escucha', + ftp_address_hint: 'Déjalo vacío para usar todas las interfaces', + ftp_port: 'Puerto FTP', + passive_port_min: 'Puerto pasivo (mín.)', + passive_port_max: 'Puerto pasivo (máx.)', + public_host: 'Host público', + public_host_hint: 'IP externa para el modo pasivo', + enable_tls: 'Activar TLS', + tls_implicit_port: 'Puerto de TLS implícito', + sftp_port: 'Puerto SFTP', + + // User fields + username: 'Nombre de usuario', + username_placeholder: 'p. ej. server1_user', + password: 'Contraseña', + password_placeholder: 'Déjalo vacío para generarla automáticamente', + password_hint: 'Si lo dejas vacío, se generará automáticamente una contraseña segura', + new_password: 'Nueva contraseña', + new_password_placeholder: 'Déjalo vacío para mantener la actual', + new_password_hint: 'Déjalo vacío para mantener la contraseña actual', + home_dir: 'Directorio personal', + home_dir_placeholder: 'p. ej. /srv/gameap/server1', + home_dir_hint: 'Si lo dejas vacío, se usará el directorio del servidor', + quota: 'Cuota', + quota_placeholder: '10GB, 500MB o 0 (sin límite)', + quota_hint: 'Introduce el tamaño con la unidad (B, KB, MB, GB, TB). Vacío o 0 = sin límite', + enabled: 'Activo', + disabled: 'Inactivo', + description: 'Descripción', + description_placeholder: 'Descripción opcional del usuario', + status: 'Estado', + actions: 'Acciones', + unlimited: 'Sin límite', + + // User created + user_created: 'Usuario creado correctamente', + password_warning: 'Esta contraseña solo se mostrará una vez. Guárdala en un lugar seguro.', + + // Access rules + access_rules: 'Reglas de acceso', + add_rule: 'Añadir regla', + path: 'Ruta', + path_placeholder: '/**', + permissions: 'Permisos', + read: 'Leer', + write: 'Escribir', + delete: 'Eliminar', + list: 'Listar', + no_access_rules: 'No hay reglas de acceso configuradas', + + // Virtual paths + virtual_paths: 'Rutas virtuales', + add_path: 'Añadir ruta', + virtual: 'Ruta virtual', + virtual_placeholder: '/shared', + target: 'Ruta de destino', + target_placeholder: '/srv/gameap/shared', + read_only: 'Solo lectura', + no_virtual_paths: 'No hay rutas virtuales configuradas', + + // SSH keys + ssh_keys: 'Claves SSH', + add_key: 'Añadir clave', + no_ssh_keys: 'No hay claves SSH configuradas', + ssh_public_key: 'Clave pública SSH', + ssh_key_placeholder: 'ssh-ed25519 AAAA... user@host', + ssh_key_hint: 'Pega tu clave pública SSH (empieza por ssh-rsa, ssh-ed25519, ecdsa-sha2, etc.)', + + // Actions + save: 'Guardar', + cancel: 'Cancelar', + close: 'Cerrar', + retry: 'Reintentar', + refresh: 'Recargar', + all: 'Todos', + installed_short: 'Instalado', + not_installed_short: 'No instalado', + failed_short: 'Error', + search_nodes: 'Buscar por nombre o IP...', + copy: 'Copiar', + copied: '¡Copiado!', + copy_failed: 'No se pudo copiar', + create: 'Crear', + add: 'Añadir', + edit: 'Editar', + remove: 'Quitar', + + // Admin page + admin_page_info: 'Gestión centralizada de usuarios FTP', + admin_page_hint: 'Abre un servidor concreto para gestionar sus usuarios FTP.', + nodes: 'Nodos', + users: 'Usuarios', + node_users: 'Usuarios del nodo', + select_server: 'Seleccionar servidor', + select_server_placeholder: 'Elige un servidor...', + picker_search_servers: 'Buscar servidores por nombre...', + picker_no_servers: 'No se encontraron servidores en este nodo', + picker_more: 'Mostrando :shown de :total — acota la búsqueda', + picker_load_failed: 'No se pudieron cargar los servidores', + back_to_nodes: 'Volver a los nodos', + create_ftp_user: 'Crear usuario FTP', + view_users: 'Ver usuarios', + servers: 'servidor(es)', + users_label: 'usuario(s)', + all_nodes: 'Todos los nodos', + all_servers: 'Servidor: todos', + all_statuses: 'Estado: todos', + search_users: 'Buscar usuarios...', + expand_all: 'Expandir todo', + collapse_all: 'Contraer todo', + no_nodes: 'No se encontraron nodos', + no_users_found: 'Ningún usuario coincide con los filtros', + servers_count: '{count} servidor(es)', + users_count: '{count} usuario(s)', + // Followed by " ?" in the delete dialog + confirm_delete_user: '¿Estás seguro de que quieres eliminar el usuario', + + // Empty states + no_users: 'No hay usuarios FTP configurados para este servidor', + + // Errors + error_username_required: 'El nombre de usuario es obligatorio', + error_username_format: 'El nombre de usuario debe empezar por una letra y solo puede contener letras sin tildes ni ñ, números y guiones bajos (3-32 caracteres)', + error_quota_format: 'Formato no válido. Usa: 10GB, 500MB, 1TB, etc.', + error_loading: 'No se pudieron cargar los datos', + error_saving: 'No se pudieron guardar los cambios', + error_creating: 'No se pudo crear el usuario', + error_deleting: 'No se pudo eliminar el usuario', + + // Server ability titles (resolved by the panel's privilege UI + // as plugins.files.abilities.) + 'abilities.ftp-users-manage': 'Gestionar usuarios FTP/SFTP', + 'abilities.ftp-users-view': 'Ver usuarios FTP/SFTP', +}; diff --git a/frontend/src/translations/index.ts b/frontend/src/translations/index.ts index 21db39e..fa3760d 100644 --- a/frontend/src/translations/index.ts +++ b/frontend/src/translations/index.ts @@ -1,5 +1,7 @@ +import { de } from './de'; import { en } from './en'; +import { es } from './es'; import { ru } from './ru'; -export const translations = { en, ru }; -export { en, ru }; +export const translations = { en, ru, de, es }; +export { de, en, es, ru };