diff --git a/.codespellrc b/.codespellrc index 4b9a7e93..ca794a39 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,5 +1,5 @@ [codespell] -skip = .git,./tests/e2e/vendor,.venv,.ruff_cache,.mypy_cache,.pytest_cache,uv.lock,htmlcov,*.woff2,*.png,*.ico,*.svg,.superpowers,.serena,.claude,CLAUDE.md,app/static/css/site.css,./app/locales/de.json,./app/locales/fr.json,./app/locales/pt-br.json,./docs/de,./docs/en/docs/hinschg-reference.md,./docs-tech/plans,./docs-tech/legal +skip = .git,./tests/e2e/vendor,.venv,.ruff_cache,.mypy_cache,.pytest_cache,uv.lock,htmlcov,*.woff2,*.png,*.ico,*.svg,.superpowers,.serena,.claude,CLAUDE.md,app/static/css/site.css,./app/locales/de.json,./app/locales/fr.json,./app/locales/es.json,./app/locales/pt-br.json,./docs/de,./docs/en/docs/hinschg-reference.md,./docs-tech/plans,./docs-tech/legal check-hidden = false # covert — correct English legal term (secret recording), app/locales/en.json:431 # Tage — German text in changelog example, CHANGELOG.md:809 diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f135ce..0b2af389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- **Spanish (Spain) interface** (`es`): the full locale, in formal *usted*, in the language picker and + chosen automatically for a browser that asks for Spanish; counts group digits with a period. Report categories + fall back to English, as they do for French and Portuguese. + ## [2.1.1] — 2026-10-03 ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8133bafa..c871d0a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ an incident history are fine. A change that renames a setting, a label or a behaviour updates, in the same commit: -- all four locales in `app/locales/` (`en`, `de`, `fr`, `pt-br`), checked against +- all five locales in `app/locales/` (`en`, `de`, `fr`, `es`, `pt-br`), checked against [`docs-tech/i18n-review.md`](docs-tech/i18n-review.md); - `docs/en/docs/index.html`, `README.md` and `docker-compose.prod.yml`; - the Helm chart (`charts/openwhistle/`) and `ansible/roles/openwhistle/templates/env.j2`. diff --git a/README.md b/README.md index 7a61c86a..caa68a53 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ zero vendor lock-in, and privacy-first by design. - **4-eyes deletion** — Hard deletion requires two different admins (request + confirm); the same admin, or an account one of them made, gets HTTP 409. GDPR Art. 17 compliant. - **Immutable audit log** — Every admin action recorded with timestamp and username, shown as - readable labels in all four languages; CSV export keeps the machine codes; required by HinSchG §11 Abs. 5. + readable labels in all five languages; CSV export keeps the machine codes; required by HinSchG §11 Abs. 5. - **Search by case number or content** — Find a case by any part of its number or by a word in its description or messages. Content is decrypted in memory for that request only; no searchable index is stored, and the confidential name never matches. @@ -111,7 +111,7 @@ zero vendor lock-in, and privacy-first by design. - **Optional virus scanning** — Attachments can be checked against a ClamAV `clamd` daemon before they are stored (`CLAMAV_HOST`). Fail-closed: if the scanner is unreachable, the upload is refused rather than stored unscanned. -- **Internationalisation** — English, German, French and Brazilian Portuguese UI; language +- **Internationalisation** — English, German, French, Spanish and Brazilian Portuguese UI; language picker in the nav bar; a test keeps every key and placeholder present in every locale. - **WCAG 2.1 AA** — Skip-to-content link, ARIA labels, live regions, visible focus indicators, and keyboard-accessible language picker. diff --git a/app/api/reports.py b/app/api/reports.py index f0af574e..7837367f 100644 --- a/app/api/reports.py +++ b/app/api/reports.py @@ -545,7 +545,7 @@ async def set_language( next_url: str = Form("/submit", alias="next"), _csrf: None = Depends(validate_csrf), ) -> RedirectResponse: - safe_lang = {"en": "en", "de": "de", "fr": "fr", "pt-br": "pt-br"}.get(lang, "en") + safe_lang = {"en": "en", "de": "de", "fr": "fr", "es": "es", "pt-br": "pt-br"}.get(lang, "en") parsed = urlsplit(next_url) safe_path = _NEXT_ALLOWLIST.get(parsed.path) if safe_path is None and (org_path := _ORG_SUBMIT_RE.fullmatch(parsed.path)): diff --git a/app/i18n.py b/app/i18n.py index b7d3ce49..1adbd318 100644 --- a/app/i18n.py +++ b/app/i18n.py @@ -9,10 +9,10 @@ from starlette.requests import Request _LOCALES_DIR = Path(__file__).parent / "locales" -_SUPPORTED = frozenset({"en", "de", "fr", "pt-br"}) +_SUPPORTED = frozenset({"en", "de", "fr", "es", "pt-br"}) _DEFAULT = "en" # Explicit dict lookup severs CodeQL taint flow from user input to file path. -_LANG_MAP: dict[str, str] = {"en": "en", "de": "de", "fr": "fr", "pt-br": "pt-br"} +_LANG_MAP: dict[str, str] = {"en": "en", "de": "de", "fr": "fr", "es": "es", "pt-br": "pt-br"} # "pt" is treated as an alias for "pt-br" in Accept-Language negotiation. _LANG_ALIAS: dict[str, str] = {"pt": "pt-br"} @@ -54,7 +54,7 @@ def get_lang(request: Request) -> str: # (a process-global mutation, unsafe for a stateless async app under # concurrent requests) and not Intl (server-side Python has none) — just the # one separator character each locale's number convention actually uses. -_THOUSANDS_SEPARATOR: dict[str, str] = {"en": ",", "de": ".", "fr": " ", "pt-br": "."} +_THOUSANDS_SEPARATOR: dict[str, str] = {"en": ",", "de": ".", "fr": " ", "es": ".", "pt-br": "."} def format_count(n: int, lang: str) -> str: diff --git a/app/locales/de.json b/app/locales/de.json index a483be2e..6e13e404 100644 --- a/app/locales/de.json +++ b/app/locales/de.json @@ -2,8 +2,6 @@ "nav.submit_report": "Meldung abgeben", "nav.check_status": "Status prüfen", "nav.toggle_theme": "Dunkelmodus", - "nav.lang_switch_label_de": "Auf Deutsch wechseln", - "nav.lang_switch_label_en": "Switch to English", "footer.tagline": "Sicheres Hinweisgebersystem", "footer.open_source": "Open Source (GPLv3)", "footer.admin": "Admin", @@ -344,8 +342,6 @@ "status.closed.body": "Diese Meldung wurde vollständig untersucht und abgeschlossen. Bitte lesen Sie den Kommunikationsverlauf unten für das Ergebnis und die ergriffenen Maßnahmen.", "nav.aria_label": "Hauptnavigation", "nav.lang_picker_label": "Sprache wählen", - "nav.lang_switch_label_fr": "Passer au français", - "nav.lang_switch_label_pt-br": "Auf Portugiesisch (Brasilien) wechseln", "footer.admin_title": "Anmeldung Meldestelle", "a11y.skip_to_content": "Zum Hauptinhalt springen", "submit.progress.label": "Formularfortschritt", diff --git a/app/locales/en.json b/app/locales/en.json index 8540743b..a981407a 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -2,8 +2,6 @@ "nav.submit_report": "Submit report", "nav.check_status": "Check status", "nav.toggle_theme": "Dark mode", - "nav.lang_switch_label_de": "Auf Deutsch wechseln", - "nav.lang_switch_label_en": "Switch to English", "footer.tagline": "Secure whistleblower reporting", "footer.open_source": "Open source (GPLv3)", "footer.admin": "Admin", @@ -344,8 +342,6 @@ "status.closed.body": "This report has been fully investigated and closed. Please review the communication thread below for the outcome and any actions taken.", "nav.aria_label": "Main navigation", "nav.lang_picker_label": "Select language", - "nav.lang_switch_label_fr": "Passer au français", - "nav.lang_switch_label_pt-br": "Switch to Portuguese (Brazil)", "footer.admin_title": "Reporting office login", "a11y.skip_to_content": "Skip to main content", "submit.progress.label": "Form progress", diff --git a/app/locales/es.json b/app/locales/es.json new file mode 100644 index 00000000..ac7800bd --- /dev/null +++ b/app/locales/es.json @@ -0,0 +1,778 @@ +{ + "nav.submit_report": "Enviar denuncia", + "nav.check_status": "Consultar estado", + "nav.toggle_theme": "Modo oscuro", + "footer.tagline": "Denuncias seguras de informantes", + "footer.open_source": "Código abierto (GPLv3)", + "footer.admin": "Administración", + "form.please_wait": "Espere…", + "error.eyebrow": "Error {code}", + "error.go_back": "Volver", + "error.detail.400": "No se ha podido procesar la solicitud.", + "error.detail.401": "Debe iniciar sesión para ver esta página.", + "error.detail.403": "No tiene permiso para ver esta página.", + "error.detail.404": "La página que busca no existe.", + "error.detail.409": "Esta acción entra en conflicto con el estado actual.", + "error.detail.422": "Los datos enviados no eran válidos.", + "error.detail.generic": "Algo ha salido mal.", + "error.detail.reporting_not_configured": "Las denuncias aún no están configuradas en este servicio. Póngase en contacto con su operador.", + "submit.sidebar.eyebrow": "Denuncia protegida", + "submit.sidebar.title": "Qué hace esta plataforma para protegerle", + "submit.sidebar.short": "Anónimo salvo que usted decida lo contrario. No se almacena ninguna dirección IP.", + "submit.sidebar.g01.title": "Sin registro de direcciones IP", + "submit.sidebar.g01.desc": "OpenWhistle no almacena su dirección IP, y la configuración del servidor web incluida tampoco la registra. En un dispositivo o una red del trabajo, su empleador aún podría ver que ha abierto este sitio.", + "submit.sidebar.g02.title": "Número de caso y PIN secreto", + "submit.sidebar.g02.desc": "Tras el envío recibirá un número de caso y un PIN secreto largo que solo usted tiene. Necesita ambos para volver a abrir su denuncia; no se pueden recuperar.", + "submit.sidebar.g03.title": "Sin rastreo", + "submit.sidebar.g03.desc": "Sin analítica, píxeles de rastreo ni solicitudes a terceros. Las únicas cookies son las que necesitan el formulario y su elección de idioma para funcionar.", + "submit.sidebar.g17.title": "Plazos de la HinSchG", + "submit.sidebar.g17.desc": "La oficina de denuncias debe confirmar la recepción en un plazo de 7 días y dar respuesta en un plazo de 3 meses. Aquí puede intercambiar mensajes con ella.", + "submit.sidebar.onion": "La red de su empleador puede ver que se ha visitado este sitio, pero no lo que usted envía. En una red del trabajo, utilice el Navegador Tor y esta dirección:", + "submit.eyebrow": "Denuncia segura", + "submit.title": "Enviar una denuncia", + "submit.alert.title": "Antes de enviar", + "submit.alert.body": "Guarde su número de caso y su PIN en un lugar seguro tras el envío. Son la única forma de acceder a su denuncia y no se pueden recuperar si se pierden.", + "submit.form.category": "Categoría", + "submit.form.category.placeholder": "Seleccione una categoría", + "submit.form.category.financial_fraud": "Fraude financiero", + "submit.form.category.workplace_safety": "Seguridad en el trabajo", + "submit.form.category.environmental": "Infracción medioambiental", + "submit.form.category.corruption": "Corrupción / soborno", + "submit.form.category.data_protection": "Infracción de la protección de datos", + "submit.form.category.discrimination": "Discriminación / acoso", + "submit.form.category.other": "Otra", + "submit.form.description": "Descripción", + "submit.form.description.placeholder": "Describa lo que ha observado con el mayor detalle posible. Incluya fechas, lugares y nombres si los conoce. Puede permanecer en el anonimato. No incluya información que pueda identificarle.", + "submit.form.description.hint": "Mínimo 10 caracteres. Solo texto sin formato.", + "submit.form.files": "Archivos de prueba (opcional)", + "submit.form.files.hint": "Hasta 5 archivos, de 10 MB cada uno. Tipos permitidos: PDF, JPEG, PNG, GIF, WebP, TXT, CSV, DOCX, XLSX.", + "submit.form.submit": "Enviar denuncia de forma segura", + "success.attachments.title": "Archivos adjuntos", + "success.title": "Denuncia enviada", + "success.subtitle": "Guarde de inmediato las credenciales que aparecen a continuación. Esta página no se volverá a mostrar.", + "success.warning.title": "Acción necesaria", + "success.warning.body": "Anote o guarde ahora su número de caso y su PIN. No se pueden recuperar si se pierden.", + "success.panel.header": "Sus datos de acceso: guárdelos bien", + "success.case_number": "Número de caso", + "success.pin": "PIN secreto", + "success.pin_shown_once": "El PIN se muestra una sola vez, en la respuesta al envío que creó esta denuncia, y no se puede volver a mostrar. Si no lo recibió, la denuncia llegó igualmente a la oficina de denuncias, pero usted no puede hacerle seguimiento; puede enviar una nueva denuncia que mencione este número de caso.", + "success.next.title": "Qué ocurre a continuación", + "success.next.body.html": "Acuse de recibo en un plazo de 7 días (§17 Abs. 1 HinSchG). Respuesta completa en un plazo de 3 meses desde el acuse de recibo. Use su número de caso y su PIN en cualquier momento para consultar el estado y comunicarse con la oficina de denuncias.", + "success.confirm.label.html": "He guardado mi número de caso y mi PIN secreto en un lugar seguro. Entiendo que no se pueden recuperar si se pierden.", + "success.confirm.button": "Continuar al estado de la denuncia", + "success.pending.title": "Su denuncia aún se está procesando", + "success.pending.body": "Su clic anterior en el botón de enviar aún se está procesando, por lo que todavía no podemos mostrar un resultado. Vuelva a comprobarlo en un momento: si la denuncia se envió, verá su número de caso y su PIN.", + "success.pending.kept": "Si no se envió, sus respuestas se conservan y puede volver a enviarla.", + "success.pending.check_again": "Comprobar de nuevo", + "status.form.title": "Consultar el estado de la denuncia", + "status.form.subtitle": "Introduzca su número de caso y su PIN secreto para ver su denuncia y las respuestas de la oficina de denuncias.", + "status.locked.title": "Muchos intentos erróneos", + "status.locked.body": "Ha habido muchos intentos erróneos con este número de caso. Esto no le bloquea: el número de caso y el PIN correctos siempre abren su denuncia. Un PIN no se puede adivinar, por lo que su denuncia sigue protegida. Un PIN perdido no se puede recuperar; si ya no lo tiene, envíe una nueva denuncia y mencione su antiguo número de caso.", + "status.locked.countdown": "segundos restantes", + "status.panel.header": "Consulta de denuncia", + "status.case_number": "Número de caso", + "status.case_number.placeholder": "OW-2026-00001", + "status.pin.label": "PIN secreto", + "status.pin.placeholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "status.pin.hint": "Introduzca el PIN exactamente como se mostró al enviar su denuncia.", + "status.pin.show": "Mostrar", + "status.pin.hide": "Ocultar", + "status.submit": "Ver estado de la denuncia", + "status.report.title": "Estado de la denuncia", + "status.meta.submitted": "Enviada", + "status.meta.acknowledged": "Acuse de recibo", + "status.meta.feedback_due": "Plazo de respuesta", + "status.thread.title": "Hilo de comunicación", + "status.attachments.title": "Archivos adjuntos", + "status.attachments.download": "Descargar", + "status.no_messages": "Aún no hay mensajes. La oficina de denuncias debe confirmar la recepción en un plazo de 7 días desde el envío.", + "status.sender.admin": "Oficina de denuncias", + "status.sender.user": "Usted (informante)", + "status.reply.title": "Enviar una respuesta", + "status.reply.label": "Su mensaje", + "status.reply.placeholder": "Su mensaje para la oficina de denuncias…", + "status.reply.button": "Enviar respuesta", + "status.other.title": "¿Necesita consultar otra denuncia?", + "status.other.button": "Consultar otra denuncia", + "login.title": "Oficina de denuncias", + "login.subtitle": "Solo para el personal autorizado de la oficina de denuncias.", + "login.panel.header": "Iniciar sesión", + "login.username": "Nombre de usuario", + "login.password": "Contraseña", + "login.submit": "Continuar", + "login.or": "o", + "login.sso": "Inicio de sesión único (SSO)", + "login.ldap_active": "Autenticación LDAP / Active Directory activa", + "login.local_review.button": "Entrar en la revisión local", + "mfa.title": "Autenticación en dos pasos", + "mfa.subtitle": "Introduzca el código de 6 dígitos de su aplicación de autenticación.", + "mfa.demo.title": "Modo de demostración", + "mfa.demo.body.html": "Introduzca 000000 para iniciar sesión.", + "mfa.panel.header": "Código del autenticador", + "mfa.code.label": "Código de autenticación", + "mfa.code.hint": "Abra su aplicación de autenticación e introduzca el código actual de 6 dígitos.", + "mfa.submit": "Verificar", + "mfa.back": "Volver al inicio de sesión", + "wizard.title": "Configuración inicial", + "wizard.subtitle": "Cree su cuenta de administrador para empezar. Este asistente se ejecuta una sola vez y no se puede volver a abrir una vez completado.", + "wizard.onetime.title": "Configuración única", + "wizard.onetime.body": "Después de crear la cuenta de administrador, este asistente dejará de estar accesible.", + "wizard.step1.header": "Paso 1: Configurar la aplicación de autenticación", + "wizard.step1.body": "Escanee el código QR con Google Authenticator, Aegis o cualquier aplicación TOTP compatible. La autenticación multifactor es obligatoria.", + "wizard.step1.manual": "O introduzca esta clave manualmente:", + "wizard.step2.header": "Paso 2: Crear la cuenta de administrador", + "wizard.setup_token": "Token de configuración", + "wizard.setup_token.hint": "Se imprime una sola vez en el registro de la aplicación al arrancar, o es el SETUP_TOKEN que usted configuró.", + "wizard.username": "Nombre de usuario", + "wizard.username.hint": "3–64 caracteres: letras, dígitos, espacios y . _ @ -", + "wizard.password": "Contraseña", + "wizard.password.hint": "Mínimo 12 caracteres. Use una contraseña segura y única.", + "wizard.password_confirm": "Confirmar contraseña", + "wizard.totp_code": "Código de verificación", + "wizard.totp_code.hint": "Introduzca el código actual de 6 dígitos de su aplicación de autenticación para confirmar la configuración.", + "wizard.telemetry.title": "Recuento de instalaciones (opcional)", + "wizard.telemetry.body.html": "Si da su consentimiento, OpenWhistle envía una solicitud al día a telemetry.wdkro.de con un identificador aleatorio generado en este servidor y el número de versión. Nada sobre denuncias, usuarios, organizaciones ni este equipo; el extremo receptor ve la dirección de la conexión, como cualquier servidor, y no la registra. Sirve para que el proyecto sepa cuántas instalaciones existen y en qué versión. Desactivado salvo que marque la casilla; puede cambiarlo en cualquier momento en Sistema. Qué se envía exactamente", + "wizard.telemetry.label": "Contar esta instalación", + "wizard.submit": "Crear cuenta de administrador", + "admin.dashboard.title": "Panel de denuncias", + "admin.dashboard.reporting_link": "Enlace de denuncia para sus empleados:", + "admin.logged_in_as": "Sesión iniciada como", + "admin.view_submit": "Ver formulario de denuncia", + "admin.ip_warning.title": "Dirección IP detectada en las cabeceras de la solicitud", + "admin.ip_warning.body": "OpenWhistle ha detectado que un proxy ascendente reenvía cabeceras con la dirección IP (X-Forwarded-For, X-Real-IP u otras similares) a la aplicación. Esto significa que la capa de red está filtrando datos de IP que podrían comprometer el anonimato del informante.", + "admin.ip_warning.hint": "Configure su proxy ascendente (Cloudflare, Traefik, nginx) para que elimine estas cabeceras antes de pasar las solicitudes a OpenWhistle. Consulte nginx/nginx.conf para ver ejemplos de configuración.", + "admin.ip_warning.dismiss": "Descartar", + "admin.nav.categories": "Categorías", + "admin.nav.users": "Usuarios", + "admin.nav.audit_log": "Registro de auditoría", + "admin.nav.stats": "Estadísticas", + "admin.nav.telephone_channel": "Canal telefónico", + "admin.nav.retention": "Conservación", + "admin.nav.organisations": "Organizaciones", + "admin.nav.menu": "Menú", + "admin.nav.aria_label": "Administración", + "admin.nav.group.cases": "Casos", + "admin.nav.group.setup": "Configuración", + "admin.nav.group.administration": "Administración", + "admin.filter.my_cases": "Mis casos", + "admin.table.header": "Todas las denuncias", + "admin.table.case_number": "Número de caso", + "admin.table.category": "Categoría", + "admin.table.status": "Estado", + "admin.table.submitted": "Enviada", + "admin.table.assignee": "Asignado a", + "admin.table.sla7": "SLA de 7 días", + "admin.table.sla3m": "SLA de 3 meses", + "admin.table.view": "Ver", + "admin.empty.title": "Aún no hay denuncias", + "admin.empty.body": "Las denuncias enviadas aparecerán aquí. Comparta el enlace de envío con su organización.", + "admin.sla.done": "✓ Hecho", + "admin.filter.all_statuses": "Todos los estados", + "admin.filter.showing": "Mostrando {from}–{to} de {total}", + "admin.filter.showing_all": "Mostrando las {total}", + "admin.filter.no_results": "Ninguna denuncia coincide con el filtro actual.", + "admin.filter.per_page": "por página", + "admin.pagination.prev": "Anterior", + "admin.pagination.next": "Siguiente", + "admin.pagination.page_of": "Página {page} de {total_pages}", + "admin.report.attachments.header": "Adjuntos", + "admin.report.attachments.download": "Descargar", + "admin.report.eyebrow": "Expediente del caso", + "admin.report.initial": "Denuncia inicial", + "admin.report.thread": "Hilo de comunicación", + "admin.report.no_messages": "Aún no hay mensajes.", + "admin.report.details": "Detalles de la denuncia", + "admin.report.history.header": "Historial", + "admin.report.actions.header": "Acciones", + "admin.report.reply.label": "Enviar respuesta al informante", + "admin.report.reply.placeholder": "Su respuesta al informante…", + "admin.report.reply.hint": "Esta respuesta será visible para el informante cuando consulte el estado de su denuncia.", + "admin.report.reply.button": "Enviar respuesta", + "admin.report.ack.header": "Acusar recibo", + "admin.report.ack.body": "Envíe el acuse de recibo al informante. El plazo de respuesta de 3 meses se cuenta entonces desde hoy, y nunca puede superar los tres meses y siete días desde la recepción (§17 HinSchG).", + "admin.report.ack.button": "Acusar recibo de la denuncia", + "admin.report.status.header": "Actualizar estado", + "admin.report.status.label": "Nuevo estado", + "admin.report.status.button": "Actualizar estado", + "admin.report.delete.header": "Eliminar denuncia", + "admin.report.delete.body": "Elimine de forma permanente esta denuncia y todos los mensajes (RGPD/DSGVO art. 17, derecho de supresión). Esta acción no se puede deshacer.", + "admin.report.delete.request": "Solicitar eliminación", + "admin.report.delete.confirm.title": "Confirmar eliminación permanente", + "admin.report.delete.confirm.body": "Se borrarán la denuncia, todos los mensajes y todos los datos asociados. No se puede deshacer.", + "admin.report.delete.cancel": "Cancelar", + "admin.report.delete.button": "Eliminar permanentemente", + "admin.back_dashboard": "Panel", + "admin.report.detail.submitted": "Enviada", + "admin.report.detail.sla7": "Acuse de recibo en 7 días (§17 I HinSchG)", + "admin.report.detail.sla3m": "Plazo de respuesta de 3 meses (§17 II HinSchG)", + "admin.report.sla.acknowledged": "Acuse de recibo enviado", + "admin.report.sla.not_ack": "aún sin acuse de recibo", + "admin.report.sla.day": "Día", + "admin.report.sla.days_remaining": "días restantes", + "admin.report.sla.overdue": "vencido", + "admin.whistleblower": "Informante", + "admin.sender.admin": "Oficina de denuncias", + "session.expiry.warning": "Su sesión caduca en", + "session.expiry.extend": "Ampliar sesión", + "session.expiry.dismiss": "Descartar", + "session.expiry.expired_title": "Sesión caducada", + "session.expiry.expired_body": "En breve se le redirigirá a la página de inicio de sesión.", + "system.receipt_message": "Su denuncia ha sido recibida. Recibirá un acuse de recibo en un plazo de 7 días, conforme a lo exigido por el §17 HinSchG.", + "status.label.received": "Recibida", + "status.label.acknowledged": "Acuse de recibo enviado", + "status.label.in_progress": "En curso", + "status.label.in_review": "En revisión", + "status.label.pending_feedback": "Pendiente de respuesta", + "status.label.closed": "Cerrada", + "category.label.financial_fraud": "Fraude financiero", + "category.label.workplace_safety": "Seguridad en el trabajo", + "category.label.environmental": "Infracción medioambiental", + "category.label.corruption": "Corrupción / soborno", + "category.label.data_protection": "Infracción de la protección de datos", + "category.label.discrimination": "Discriminación / acoso", + "category.label.other": "Otra", + "status.progress.title": "Progreso de la denuncia", + "status.progress.received.label": "Recibida", + "status.progress.received.desc": "Su denuncia ha sido recibida y está pendiente de la revisión inicial por parte de la oficina de denuncias.", + "status.progress.acknowledged.label": "En revisión", + "status.progress.acknowledged.desc": "La oficina de denuncias ha acusado recibo de su denuncia. Se ha iniciado una investigación. Recibirá una respuesta final antes del plazo de respuesta.", + "status.progress.in_progress.label": "Investigación activa", + "status.progress.in_progress.desc": "Su denuncia se está investigando activamente. La oficina de denuncias puede ponerse en contacto con usted con más preguntas a través de este hilo de comunicación.", + "status.progress.closed.label": "Cerrada", + "status.progress.closed.desc": "La investigación ha finalizado. Consulte el hilo de comunicación anterior para conocer el resultado y las medidas adoptadas.", + "admin.report.status.confirm_close": "¿Seguro que desea cerrar esta denuncia? El informante lo verá como el resultado final. Considere añadir antes un mensaje de cierre.", + "admin.report.status.confirm_downgrade": "¿Seguro que desea devolver esta denuncia a un estado anterior?", + "admin.flash.deleted": "Denuncia eliminada permanentemente:", + "admin.report.status.no_change": "El estado ya tiene este valor.", + "admin.report.status.current": "{status} (actual)", + "admin.report.status.none": "No hay más transiciones de estado disponibles.", + "admin.report.assign.header": "Asignación del caso", + "admin.report.assign.label": "Asignado a", + "admin.report.assign.unassigned": "Sin asignar", + "admin.report.assign.button": "Guardar asignación", + "admin.report.notes.header": "Notas internas", + "admin.report.notes.placeholder": "Nota interna (nunca visible para el informante)…", + "admin.report.notes.button": "Añadir nota", + "admin.report.notes.none": "Aún no hay notas internas.", + "admin.report.links.header": "Casos vinculados", + "admin.report.links.placeholder": "Número de caso, p. ej. OW-2026-00001", + "admin.report.links.format_hint": "Formato: OW-YYYY-NNNNN", + "admin.report.links.button": "Vincular caso", + "admin.report.links.none": "No hay casos vinculados.", + "admin.report.links.remove": "Desvincular", + "admin.report.links.remove_confirm": "¿Eliminar el vínculo con {case}?", + "admin.report.export.header": "Exportar", + "admin.report.export_pdf": "Exportar PDF", + "admin.report.audit.header": "Registro de actividad", + "admin.report.audit.none": "Aún no se ha registrado ninguna actividad.", + "admin.report.delete.pending.title": "Eliminación solicitada", + "admin.report.delete.pending.body": "Solicitada por {username}. Otro administrador debe confirmarla para ejecutar la eliminación.", + "admin.report.delete.confirm_4eyes": "Confirmar y eliminar definitivamente", + "admin.report.delete.confirm_dialog": "¿Eliminar definitivamente esta denuncia? Esta acción no se puede deshacer.", + "admin.report.delete.cancel_request": "Cancelar solicitud", + "admin.report.delete.cannot_self": "Usted solicitó esta eliminación. Otro administrador debe confirmarla.", + "role.label.admin": "Administrador", + "role.label.case_manager": "Gestor del caso", + "role.label.superadmin": "Superadministrador", + "admin.categories.title": "Gestión de categorías", + "admin.categories.add.header": "Añadir nueva categoría", + "admin.categories.add.slug": "Slug", + "admin.categories.add.slug.hint": "Solo minúsculas y guiones bajos. Se usa como identificador interno.", + "admin.categories.add.slug.placeholder": "p. ej. seguridad_laboral", + "admin.categories.add.label_en": "Etiqueta (inglés)", + "admin.categories.add.label_de": "Etiqueta (alemán)", + "admin.categories.add.sort_order": "Orden", + "admin.categories.add.button": "Añadir categoría", + "admin.categories.list.header": "Categorías existentes", + "admin.categories.col.slug": "Slug", + "admin.categories.col.label_en": "Inglés", + "admin.categories.col.label_de": "Alemán", + "admin.categories.col.sort": "Orden", + "admin.categories.col.status": "Estado", + "admin.categories.default": "predeterminada", + "admin.categories.active": "activa", + "admin.categories.inactive": "inactiva", + "admin.categories.deactivate": "Desactivar", + "admin.categories.reactivate": "Reactivar", + "admin.categories.empty": "No hay categorías configuradas.", + "admin.users.title": "Usuarios administradores", + "admin.users.add.header": "Añadir nuevo usuario administrador", + "admin.users.add.username": "Nombre de usuario", + "admin.users.add.username.title": "Letras, dígitos, espacios y . _ @ -", + "admin.users.add.password": "Contraseña temporal", + "admin.users.add.role": "Rol", + "admin.users.add.organisation": "Organización", + "admin.users.add.button": "Crear usuario", + "admin.users.add.totp_note": "En el primer inicio de sesión, el nuevo usuario configura su aplicación de autenticación y después sustituye la contraseña que usted ha elegido aquí.", + "admin.users.list.header": "Usuarios actuales", + "admin.users.table.username": "Nombre de usuario", + "admin.users.table.role": "Rol", + "admin.users.table.organisation": "Organización", + "admin.users.table.status": "Estado", + "admin.users.table.actions": "Acciones", + "admin.users.active": "activo", + "admin.users.inactive": "inactivo", + "admin.users.deactivate": "Desactivar", + "admin.users.deactivate_confirm": "¿Desactivar a {username}?", + "admin.users.reactivate": "Reactivar", + "admin.users.change_role": "Cambiar rol", + "admin.users.role_for": "Rol de {username}", + "admin.users.cannot_self": "No puede desactivar su propia cuenta.", + "admin.users.last_admin": "No se puede desactivar. Es el último administrador activo.", + "admin.users.you_tag": "(usted)", + "admin.audit.title": "Registro de auditoría", + "admin.audit.export_csv": "Exportar CSV", + "admin.audit.filter.action": "Filtrar por acción", + "admin.audit.filter.all": "Todas las acciones", + "admin.audit.filter.report_id": "Filtrar por ID de denuncia", + "admin.audit.filter.apply": "Aplicar", + "admin.audit.table.time": "Fecha y hora", + "admin.audit.table.admin": "Administrador", + "admin.audit.table.action": "Acción", + "admin.audit.table.report": "Caso", + "admin.audit.table.detail": "Detalle", + "admin.audit.empty": "No se han encontrado entradas de auditoría.", + "admin.stats.title": "Estadísticas", + "admin.stats.total_reports": "Total de denuncias", + "admin.stats.sla_rate": "Cumplimiento del plazo de 7 días", + "admin.stats.by_status": "Denuncias por estado", + "admin.stats.by_category": "Denuncias por categoría", + "admin.stats.no_data": "Aún no hay datos.", + "status.closed.title": "Caso cerrado", + "status.closed.body": "Esta denuncia ha sido investigada por completo y cerrada. Consulte el hilo de comunicación que figura a continuación para conocer el resultado y las medidas adoptadas.", + "nav.aria_label": "Navegación principal", + "nav.lang_picker_label": "Seleccionar idioma", + "footer.admin_title": "Acceso a la oficina de denuncias", + "a11y.skip_to_content": "Saltar al contenido principal", + "submit.progress.label": "Progreso del formulario", + "submit.progress.step_of": "Paso {step} de {total}", + "submit.step.mode.short": "Modalidad", + "submit.step.location.short": "Ubicación", + "submit.step.category.short": "Categoría", + "submit.step.description.short": "Detalles", + "submit.step.attachments.short": "Archivos", + "submit.step.review.short": "Revisión", + "submit.step.mode.title": "¿Cómo desea presentar la denuncia?", + "submit.step.mode.label": "Modalidad de presentación", + "submit.step.mode.anonymous.label": "Anónima", + "submit.step.mode.anonymous.desc": "No le pedimos nombre ni datos de contacto y no almacenamos ninguno. La oficina de denuncias solo puede contactar con usted mediante mensajes en esta plataforma.", + "submit.step.mode.confidential.label": "Confidencial", + "submit.step.mode.confidential.desc": "Puede indicar, si lo desea, su nombre y datos de contacto. Se almacenan cifrados y solo los ve el personal de la oficina de denuncias que gestiona su caso.", + "submit.step.mode.confidential.info.title": "Denuncia confidencial", + "submit.step.mode.confidential.info.body": "Los campos siguientes son opcionales. Todo lo que introduzca se almacena cifrado y solo se descifra para el personal de la oficina de denuncias que gestiona su caso.", + "submit.step.mode.confidential.name.label": "Su nombre (opcional)", + "submit.step.mode.confidential.name.placeholder": "Nombre completo", + "submit.step.mode.confidential.name.hint": "Opcional. Se almacena cifrado.", + "submit.step.mode.confidential.contact.label": "Información de contacto (opcional)", + "submit.step.mode.confidential.contact.placeholder": "Teléfono, dirección u otro dato de contacto", + "submit.step.mode.confidential.contact.hint": "Opcional. Se almacena cifrado.", + "submit.step.mode.secure_email.label": "Correo electrónico anónimo para notificaciones (opcional)", + "submit.step.mode.secure_email.placeholder": "p. ej. sunombre@protonmail.com", + "submit.step.mode.secure_email.hint": "Opcional. Si lo facilita, recibirá una breve notificación (sin el contenido de la denuncia) cuando la oficina de denuncias envíe una respuesta. Utilice un servicio de correo anónimo como ProtonMail para mayor privacidad.", + "submit.step.location.title": "¿A qué ubicación se refiere?", + "submit.step.location.label": "Ubicación", + "submit.step.location.placeholder": "Seleccione una ubicación (opcional)", + "submit.step.location.hint": "Opcional. Seleccione la sucursal o ubicación relacionada con su denuncia.", + "submit.step.location.none": "Ninguna ubicación especificada", + "submit.step.category.hint": "Seleccione la categoría que mejor describa la infracción que denuncia.", + "submit.step.attachments.info": "Adjunte cualquier prueba que apoye su denuncia. Este paso es opcional. Puede hacer clic en Siguiente para omitirlo.", + "submit.step.attachments.privacy": "Antes de almacenar sus archivos, eliminamos los datos ocultos que podrían identificarle: la ubicación y los datos de la cámara en las fotos, y el autor en los archivos PDF y Word/Excel (.docx/.xlsx). El contenido visible de los archivos no se modifica; si tiene dudas, guárdelo antes como PDF.", + "submit.step.attachments.already_attached": "Ya adjuntado:", + "submit.step.attachments.already_attached.hint": "Su navegador no puede mostrar aquí los archivos seleccionados anteriormente, pero están guardados. Haga clic en Siguiente para conservarlos tal cual, o elija archivos nuevos a continuación para sustituirlos.", + "submit.step.attachments.remove": "Quitar", + "submit.step.attachments.remove.label": "Quitar {name}", + "submit.step.review.title": "Revise su denuncia", + "submit.step.review.warning.title": "Última oportunidad para revisar", + "submit.step.review.warning.body": "Compruebe todos los datos antes de enviar. Una vez enviada, la denuncia no se puede modificar.", + "submit.nav.next": "Siguiente", + "submit.nav.back": "Atrás", + "submit.nav.restart": "Empezar de nuevo", + "submit.error.mode_required": "Seleccione una modalidad de presentación.", + "submit.error.category_required": "Seleccione una categoría.", + "submit.error.description_too_short": "La descripción debe tener al menos 10 caracteres.", + "submit.error.description_too_long": "La descripción no debe superar los 10 000 caracteres.", + "submit.error.email_invalid": "Compruebe la dirección de correo electrónico: debe incluir un nombre, una @ y un dominio, como proton.me.", + "submit.error.invalid_location": "La ubicación seleccionada no es válida.", + "submit.error.session_incomplete": "Su sesión está incompleta. Empiece de nuevo.", + "submit.error.submit_failed": "Su denuncia NO se ha enviado. Sus respuestas se conservan: compruébelas y vuelva a enviarla.", + "submit.error.attachments_too_large": "El tamaño total de los adjuntos es demasiado grande. Quite un archivo o reduzca su tamaño e inténtelo de nuevo.", + "submit.error.attachments_no_room": "El servidor no puede aceptar adjuntos en este momento. Continúe sin ellos o inténtelo más tarde; después de enviar la denuncia podrá añadir detalles en una respuesta.", + "status.deadlines.title": "Plazos legales (HinSchG)", + "status.deadlines.ack": "Acuse de recibo en 7 días (§17 I)", + "status.deadlines.ack_done": "Acuse de recibo emitido el {date}", + "status.deadlines.ack_remaining": "Quedan {days} días", + "status.deadlines.ack_overdue": "Vencido", + "status.deadlines.feedback": "Información de seguimiento en 3 meses (§17 II)", + "status.deadlines.feedback_done": "Entregada", + "status.deadlines.feedback_remaining": "Quedan {days} días (vence el {date})", + "status.deadlines.feedback_overdue": "Vencido", + "admin.nav.locations": "Ubicaciones", + "admin.filter.all_locations": "Todas las ubicaciones", + "admin.locations.title": "Gestión de ubicaciones", + "admin.locations.add.header": "Añadir nueva ubicación", + "admin.locations.add.name": "Nombre de la ubicación", + "admin.locations.add.name.placeholder": "p. ej. Sede de Berlín", + "admin.locations.add.code": "Código de la ubicación", + "admin.locations.add.code.placeholder": "p. ej. BERLIN-HQ", + "admin.locations.add.code.hint": "Identificador único. Letras, números, guiones y guiones bajos.", + "admin.locations.add.description": "Descripción (opcional)", + "admin.locations.add.description.placeholder": "Breve descripción de esta ubicación", + "admin.locations.add.sort_order": "Orden", + "admin.locations.add.button": "Añadir ubicación", + "admin.locations.list.header": "Ubicaciones existentes", + "admin.locations.col.name": "Nombre", + "admin.locations.col.code": "Código", + "admin.locations.col.description": "Descripción", + "admin.locations.col.status": "Estado", + "admin.locations.active": "activa", + "admin.locations.inactive": "inactiva", + "admin.locations.deactivate": "Desactivar", + "admin.locations.reactivate": "Reactivar", + "admin.locations.empty": "No hay ubicaciones configuradas. Las denuncias no solicitarán una ubicación.", + "admin.organisations.title": "Gestión de organizaciones", + "admin.organisations.subtitle": "Gestione los inquilinos en despliegues multiinquilino. Cada organización tiene sus propias denuncias, categorías, ubicaciones y usuarios administradores.", + "admin.organisations.add.header": "Crear organización", + "admin.organisations.add.name": "Nombre de la organización", + "admin.organisations.add.name.placeholder": "p. ej. Acme GmbH", + "admin.organisations.add.slug": "Slug", + "admin.organisations.add.slug.hint": "Forma parte del enlace de denuncia /submit/, que es público: cualquiera puede comprobar si un slug existe, así que elija uno que no le importe que se conozca. Solo letras minúsculas, números y guiones.", + "admin.organisations.add.slug.placeholder": "p. ej. acme", + "admin.organisations.add.button": "Crear organización", + "admin.organisations.list.header": "Todas las organizaciones", + "admin.organisations.col.name": "Nombre", + "admin.organisations.col.slug": "Slug", + "admin.organisations.col.link": "Enlace de denuncia", + "admin.organisations.col.status": "Estado", + "admin.organisations.col.created": "Creada", + "admin.organisations.active": "activa", + "admin.organisations.inactive": "inactiva", + "admin.organisations.default": "predeterminada", + "admin.organisations.deactivate": "Desactivar", + "admin.organisations.deactivate_confirm": "¿Desactivar {name}? Esta acción no se puede deshacer.", + "admin.organisations.copy_link": "Copiar enlace de denuncia", + "admin.organisations.empty": "No se han encontrado organizaciones.", + "admin.report.detail.mode": "Modalidad de presentación", + "admin.report.detail.location": "Ubicación", + "admin.report.confidential.header": "Datos confidenciales del informante", + "admin.report.confidential.name": "Nombre", + "admin.report.confidential.contact": "Contacto", + "admin.report.confidential.secure_email_set": "Correo electrónico seguro registrado (notificaciones activas)", + "admin.report.confidential.email_note": "Se notificará al informante cuando haya nuevas respuestas del administrador.", + "admin.report.identity.reveal": "Mostrar identidad", + "admin.report.identity.export": "Exportar PDF con identidad", + "admin.report.identity.export_hint": "La exportación también registra una revelación auditada.", + "admin.report.identity.reason": "¿Por qué la necesita?", + "admin.report.identity.reason_hint": "De 10 a 500 caracteres. Se registra en el registro de auditoría con su nombre. No escriba aquí el nombre del informante.", + "admin.report.identity.reason_error": "Indique un motivo de entre 10 y 500 caracteres.", + "admin.report.identity.shown_once": "Se muestra solo en esta página. Al recargarla se vuelve a ocultar.", + "admin.report.identity.handler_only": "Solo la persona que gestiona este caso puede ver la identidad.", + "admin.nav.logout": "Cerrar sesión", + "admin.report.internal_badge": "INTERNO", + "admin.report.audit.full_trail": "Registro de auditoría completo →", + "admin.telephone.title": "Canal de denuncias telefónico", + "admin.telephone.subtitle": "El § 16 Abs. 3 HinSchG exige que los canales de denuncia admitan denuncias orales y por escrito. OpenWhistle es el canal por escrito. Esta página le guía en la puesta en marcha del canal oral por teléfono.", + "admin.telephone.legal_req.label": "Requisito legal:", + "admin.telephone.legal_req.body.html": "Los empleadores con 50 o más empleados deben mantener una oficina de denuncias interna (§ 12 HinSchG). Sus canales deben permitir denuncias orales, por teléfono u otro medio de transmisión de voz, y por escrito (§ 16 Abs. 3). A petición del informante, debe ser posible una reunión presencial. OpenWhistle cubre el canal por escrito; usted debe proporcionar el oral por separado.", + "admin.telephone.legal_req.aria_label": "Aviso legal", + "admin.telephone.checklist.title": "Lista de verificación de cumplimiento", + "admin.telephone.checklist.intro": "Compruebe que su canal de denuncias telefónico cumple los siguientes requisitos:", + "admin.telephone.checklist.1": "Se publica internamente un número de teléfono específico, accesible para todos los empleados.", + "admin.telephone.checklist.2": "El canal es confidencial: las llamadas son atendidas por personal formado e imparcial o por un defensor externo (ombudsman).", + "admin.telephone.checklist.3": "La persona que atiende la llamada mantiene confidencial la identidad del informante (§ 8 HinSchG).", + "admin.telephone.checklist.4": "El personal registra la denuncia telefónica en OpenWhistle con un número de caso: literalmente solo con el consentimiento del informante y, en caso contrario, como resumen de su contenido (§ 11 Abs. 2 HinSchG).", + "admin.telephone.checklist.5": "Se informa al informante sobre el canal mediante comunicaciones internas, intranet o carteles.", + "admin.telephone.checklist.6": "Las llamadas se graban solo con consentimiento (§ 11 Abs. 2 HinSchG) y cualquier grabación se trata conforme al RGPD.", + "admin.telephone.checklist.7": "El canal está operativo durante el horario laboral habitual (o las 24 horas, todos los días, en los sectores críticos).", + "admin.telephone.checklist.8": "Si se recurre a un defensor externo, su carta de encargo incluye un contrato de encargo de tratamiento de datos (DPA) conforme al art. 28 del RGPD (DSGVO).", + "admin.telephone.options.title": "Opciones de implantación", + "admin.telephone.options.internal.title": "Línea directa interna", + "admin.telephone.options.internal.body": "Designe a un empleado formado (responsable de cumplimiento, RR. HH.) como operador del canal. Asigne un número de marcación directa específico. Documente el calendario de turnos.", + "admin.telephone.options.internal.note": "Ideal para: organizaciones que ya cuentan con una función de cumplimiento o jurídica.", + "admin.telephone.options.external.title": "Defensor externo (ombudsman)", + "admin.telephone.options.external.body": "Contrate a un defensor externo certificado (Ombudsmann) o a un proveedor de servicios de cumplimiento. Recibe las llamadas en su nombre y remite las denuncias anonimizadas a OpenWhistle.", + "admin.telephone.options.external.note": "Ideal para: organizaciones pequeñas o cuando la imparcialidad es fundamental.", + "admin.telephone.integration.title": "Integración con OpenWhistle", + "admin.telephone.integration.1.html": "El personal introduce la denuncia verbal mediante el formulario de presentación de OpenWhistle. Pida el consentimiento de la persona que llama antes de crear la denuncia en su nombre.", + "admin.telephone.integration.2.html": "Seleccione la modalidad «Confidencial» si la persona que llama consiente que se registre su nombre, de modo que el informante reciba un número de caso propio.", + "admin.telephone.integration.3.html": "Facilite el número de caso y el PIN a la persona que llama para que pueda dar seguimiento a su denuncia a través de la página de estado del informante.", + "admin.telephone.integration.4.html": "Documente la denuncia telefónica en las notas de administración (nunca en el hilo de mensajes público visible para el informante).", + "admin.telephone.recording.title": "Grabación solo con consentimiento (§ 11 Abs. 2 HinSchG)", + "admin.telephone.recording.warning.label": "Importante:", + "admin.telephone.recording.warning.body.html": "El § 11 Abs. 2 HinSchG permite la grabación de una denuncia telefónica solo con el consentimiento del informante. Sin él, la oficina de denuncias documenta la llamada mediante un resumen escrito. Anuncie cualquier grabación al inicio de la llamada y pida el consentimiento. Grabar sin consentimiento puede ser punible conforme al §201 StGB (vulneración de la confidencialidad de la palabra hablada).", + "admin.telephone.recording.script.intro": "Guion recomendado de apertura de la llamada (adáptelo a su organización):", + "admin.telephone.recording.script.text.html": "“Gracias por llamar al canal de denuncias confidencial de [Nombre de la organización]. Esta llamada es atendida por [Nombre/Cargo], que está sujeto a una estricta confidencialidad. Su denuncia se tratará conforme a la Hinweisgeberschutzgesetz (HinSchG). Esta llamada no se está grabando. Puede permanecer en el anonimato. Adelante con su denuncia.”", + "admin.telephone.refs.title": "Referencias legales", + "admin.telephone.refs.16": "HinSchG § 16 — Canales de denuncia, forma oral y escrita (Abs. 3):", + "admin.telephone.refs.eu": "Directiva (UE) 2019/1937 — art. 9 — Confidencialidad:", + "admin.retention.title": "Política de conservación de datos", + "admin.retention.subtitle": "Eliminación automática de las denuncias cerradas tras el periodo de conservación configurado. Cumple la limitación del plazo de conservación del art. 5, apdo. 1, letra e) del RGPD y el §11 Abs. 5 HinSchG.", + "admin.retention.stat.status": "Estado", + "admin.retention.stat.enabled": "Activada", + "admin.retention.stat.disabled": "Desactivada", + "admin.retention.stat.period": "Periodo de conservación", + "admin.retention.stat.period.value": "{days} días", + "admin.retention.stat.period.sub": "({years} años)", + "admin.retention.stat.next_run": "Próxima ejecución programada", + "admin.retention.enabled_notice": "La conservación está activada por defecto desde la v1.5.0. Una denuncia cerrada se elimina {days} días después de su cierre (HinSchG §11 Abs. 5: tres años después de la conclusión del procedimiento). OpenWhistle se publicó en 2026, por lo que ninguna instalación guarda una denuncia cerrada hace tanto tiempo: no se ha eliminado nada antes de tiempo y las primeras eliminaciones están a años de distancia.", + "admin.retention.disabled_notice.html": "La conservación está desactivada actualmente. Para activar la eliminación automática, establezca RETENTION_ENABLED=true en la configuración de su entorno. Opcionalmente, ajuste RETENTION_DAYS (por defecto: 1095 = 3 años).", + "admin.retention.how.title": "Cómo funciona", + "admin.retention.how.1": "Todos los días a las 03:00 UTC se ejecuta el programador de conservación.", + "admin.retention.how.2.html": "Las denuncias con estado «cerrada» cuya marca de tiempo closed_at sea anterior a {days} días se eliminan de forma permanente.", + "admin.retention.how.3": "Se eliminan todos los datos asociados: mensajes, adjuntos, notas y referencias del registro de auditoría (el número de caso se conserva en la entrada de auditoría de la eliminación automática).", + "admin.retention.how.4.html": "Por cada denuncia eliminada se escribe una entrada inmutable en el registro de auditoría (report.auto_deleted) en la que constan el número de caso, la fecha de cierre, el plazo de conservación y la base de cumplimiento normativo.", + "admin.retention.how.note.html": "Nota: Las denuncias en cualquier estado distinto de «cerrada» nunca se eliminan automáticamente, sea cual sea su antigüedad.", + "admin.retention.legal.title": "Base jurídica", + "admin.retention.legal.gdpr5.title": "RGPD art. 5.1.e) — limitación del plazo de conservación", + "admin.retention.legal.gdpr5.body": "Los datos personales no deben conservarse más tiempo del necesario para la finalidad con la que se recogieron. Conservar indefinidamente las denuncias cerradas vulneraría este principio.", + "admin.retention.legal.hinschg.title": "HinSchG §11 Abs. 5 — obligación de documentación", + "admin.retention.legal.hinschg.body.html": "La documentación de una denuncia debe eliminarse 3 años después de concluido el procedimiento; solo puede conservarse más tiempo mientras ello sea necesario y proporcionado. El plazo de conservación predeterminado de 1095 días (3 años) coincide con este plazo. Modifique RETENTION_DAYS únicamente con asesoramiento jurídico específico.", + "admin.retention.legal.gdpr17.title": "RGPD art. 17 — derecho de supresión", + "admin.retention.legal.gdpr17.body": "Las personas pueden solicitar la eliminación de sus datos personales. Tanto el mecanismo de eliminación automática como el flujo manual de eliminación con doble verificación (4 ojos) ya existente permiten atender las solicitudes de supresión.", + "admin.retention.config.title": "Referencia de configuración", + "admin.retention.config.col.variable": "Variable", + "admin.retention.config.col.default": "Predeterminado", + "admin.retention.config.col.current": "Actual", + "admin.retention.config.col.description": "Descripción", + "admin.retention.config.enabled.desc": "Activa la eliminación automática de las denuncias cerradas antiguas.", + "admin.retention.config.days.desc": "Días que deben transcurrir desde el cierre hasta que una denuncia se elimine automáticamente.", + "admin.retention.audit_link": "Ver el registro de eliminaciones automáticas", + "admin.nav.system": "Sistema", + "admin.system.title": "Sistema y actualizaciones", + "admin.system.subtitle": "Versión instalada y estado de las actualizaciones.", + "admin.system.stat.current": "Versión instalada", + "admin.system.stat.status": "Estado de la actualización", + "admin.system.stat.latest": "Última versión publicada", + "admin.system.status.disabled": "Comprobación desactivada", + "admin.system.status.never_checked": "Aún no comprobado", + "admin.system.status.update_available": "Actualización disponible", + "admin.system.status.ahead": "Más reciente que la última versión", + "admin.system.status.up_to_date": "Actualizado", + "admin.system.update_available_notice": "Hay disponible una versión más reciente ({latest}).", + "admin.system.view_release": "Ver versión", + "admin.system.disabled_notice.html": "La comprobación de actualizaciones está desactivada. Establezca UPDATE_CHECK_ENABLED=true para que OpenWhistle consulte GitHub a diario en busca de nuevas versiones.", + "admin.system.last_checked": "Última comprobación:", + "admin.system.privacy.title": "Privacidad", + "admin.system.privacy.body.html": "Cuando está activada, la comprobación de actualizaciones realiza una única solicitud a la API pública de versiones de GitHub. No envía datos de la instancia ni de las denuncias, solo una solicitud HTTP estándar. El resultado se guarda en caché y se actualiza una vez al día.", + "admin.system.telemetry.title": "Recuento de instalaciones", + "admin.system.telemetry.state": "Estado", + "admin.system.telemetry.on": "Activado", + "admin.system.telemetry.off": "Desactivado", + "admin.system.telemetry.body.html": "Mientras esté activado, OpenWhistle envía esta única solicitud una vez al día y nada más: ni nombre de host, ni dirección, ni nada sobre denuncias, usuarios u organizaciones. El destino registra la hora, el identificador y la versión, pero no la dirección desde la que se envió la solicitud. Recuento de instalaciones", + "admin.system.telemetry.id": "Identificador de esta instalación", + "admin.system.telemetry.id_placeholder": "se genera al activarlo", + "admin.system.telemetry.last_sent": "Último envío correcto", + "admin.system.telemetry.never": "Nunca", + "admin.system.telemetry.locked.demo": "Nunca se cuenta: se trata de una instancia de demostración o de revisión local.", + "admin.system.telemetry.locked.env_off": "Desactivado mediante TELEMETRY_ENABLED=false en el entorno. Elimínelo para decidirlo aquí.", + "admin.system.telemetry.locked.env_on": "Activado mediante TELEMETRY_ENABLED=true en el entorno. Elimínelo para decidirlo aquí.", + "admin.system.telemetry.turn_on": "Contar esta instalación", + "admin.system.telemetry.turn_off": "Dejar de contar", + "admin.system.telemetry.reset": "Restablecer identificador", + "admin.system.telemetry.reset_confirm": "¿Asignar a esta instalación un nuevo identificador aleatorio? El siguiente envío contará como una instalación nueva.", + "admin.system.telemetry.reset_note": "Genera un nuevo identificador aleatorio. El siguiente envío parecerá entonces una instalación nueva y nada la vincula con la anterior.", + "admin.system.config.title": "Configuración", + "admin.system.config.enabled.desc": "Activa la comprobación diaria de actualizaciones en GitHub (opcional; no se envían datos a GitHub).", + "admin.system.config.telemetry.desc": "Vacío: decide el interruptor de arriba. false: siempre desactivado. true: siempre activado. Una demostración nunca se cuenta.", + "admin.system.all_releases": "Todas las versiones", + "admin.system.integrity.title": "Integridad de los archivos", + "admin.system.integrity.status": "Integridad", + "admin.system.integrity.ok": "Todos los archivos verificados", + "admin.system.integrity.issues": "Se han encontrado problemas", + "admin.system.integrity.unavailable": "Manifiesto no disponible", + "admin.system.integrity.checked": "{count} archivos comprobados", + "admin.system.integrity.missing": "Archivos que faltan", + "admin.system.integrity.modified": "Archivos modificados", + "admin.system.integrity.extra": "Archivos inesperados", + "admin.system.integrity.generated": "Manifiesto generado", + "admin.system.integrity.manifest_hash": "SHA-256 del manifiesto", + "admin.system.integrity.recheck": "Volver a comprobar", + "admin.system.integrity.note.html": "Verifica los archivos de la aplicación frente a un manifiesto incorporado a esta imagen durante la compilación. Esto detecta modificaciones accidentales, despliegues incompletos y corrupción. No es a prueba de manipulaciones: un atacante con capacidad de escritura en el contenedor también podría alterar el manifiesto de referencia.", + "admin.system.integrity.unavailable_note": "No hay ningún manifiesto de integridad. Es lo esperable fuera de una imagen Docker publicada (por ejemplo, en desarrollo local).", + "status.error.invalid": "Ninguna denuncia coincide con este número de caso y PIN. Compruebe ambos e inténtelo de nuevo.", + "status.error.inline": "Compruebe el número de caso y el PIN: ambos deben coincidir exactamente con los de su denuncia.", + "status.reply.sent": "Su respuesta se ha enviado.", + "status.reply.closed": "El caso se cerró antes de que llegara su mensaje, por lo que no se envió. Si tiene algo nuevo que comunicar, presente una nueva denuncia.", + "demo.status.hint": "Pruebe estas denuncias de ejemplo. Seleccione una fila para rellenar su número de caso y su PIN.", + "demo.login.hint": "Pruébelo ahora con estas credenciales.", + "demo.login.click_to_fill": "Rellenar", + "demo.login.totp": "Código del autenticador (siguiente paso)", + "demo.login.fill": "Rellenar credenciales", + "demo.login.filled": "✓ Rellenado", + "demo.banner.label": "Aviso de demostración", + "demo.banner.text": "Esta es una demostración pública: no introduzca denuncias reales. Todos los datos se eliminan cada 6 horas.", + "demo.banner.short": "Demostración: se reinicia cada 6 horas", + "demo.badge": "DEMO", + "nav.home": "Inicio de {name}", + "success.copy": "Copiar al portapapeles", + "success.copy.case_number": "Copiar número de caso", + "success.copy.pin": "Copiar PIN", + "success.copied": "Copiado", + "login.error.required": "Introduzca su nombre de usuario y su contraseña.", + "login.error.username_required": "Introduzca su nombre de usuario.", + "login.error.password_required": "Introduzca su contraseña.", + "login.error.locked": "Demasiados intentos fallidos. El inicio de sesión de esta cuenta está en pausa durante un tiempo; inténtelo de nuevo más tarde.", + "login.error.invalid": "El nombre de usuario o la contraseña no son correctos.", + "login.error.deactivated": "Esta cuenta se ha desactivado. Pida a un administrador que la reactive.", + "login.error.sso_failed": "El inicio de sesión único no se completó. Inténtelo de nuevo; si sigue fallando, pida a su administrador que revise el proveedor de identidad.", + "login.error.sso_expired": "El inicio de sesión único no se completó: la sesión caducó o no se pudo verificar la respuesta del proveedor de identidad. Vuelva a iniciar sesión desde el principio.", + "login.error.sso_identity": "El proveedor de identidad no envió los datos de cuenta necesarios para iniciar su sesión. Pida a su administrador que revise su configuración.", + "login.error.sso_unlinked": "Ninguna cuenta de aquí está vinculada a esta identidad de inicio de sesión único. Inicie sesión con su contraseña y elija «Vincular inicio de sesión único» en Mi cuenta.", + "mfa.error.invalid": "Ese código no es válido. Introduzca el código actual de 6 dígitos de su aplicación de autenticación.", + "mfa.setup.error.invalid": "Ese código no es válido. Escanee de nuevo el código QR e introduzca el código actual de 6 dígitos.", + "wizard.error.summary": "Revise estos campos:", + "wizard.error.setup_token": "El token de configuración falta o es incorrecto.", + "wizard.error.setup_token_locked": "Demasiados tokens de configuración incorrectos. Inténtelo de nuevo más tarde.", + "wizard.error.username": "Introduzca un nombre de usuario de 3 a 64 caracteres: letras, dígitos, espacios y . _ @ -", + "wizard.error.password": "Introduzca una contraseña de al menos 12 caracteres y como máximo 72 bytes (una letra con acento cuenta como dos).", + "wizard.error.password_confirm": "Las dos contraseñas no coinciden.", + "wizard.error.totp_code": "Ese código no es válido. Escanee el código QR e introduzca el código actual de 6 dígitos.", + "admin.search.label": "Buscar denuncias", + "admin.search.placeholder": "Número de caso o palabras de la denuncia", + "admin.search.hint": "Busca coincidencias en el número de caso y en las palabras de la descripción o de los mensajes. El contenido se descifra solo para esta búsqueda y nunca se indexa; el nombre confidencial nunca se busca.", + "admin.search.button": "Buscar", + "admin.search.clear": "Borrar búsqueda", + "admin.search.no_results": "Ninguna denuncia coincide con «{query}».", + "admin.view_submit.title": "Abre el formulario público de denuncia en esta pestaña", + "admin.table.unassigned": "Sin asignar", + "admin.audit.filter.report_id.placeholder": "ID de denuncia", + "admin.audit.filter.show_views": "Mostrar consultas de casos", + "admin.audit.filter.clear": "Borrar filtros", + "audit.action.report.status_changed": "Estado modificado", + "audit.action.report.assigned": "Caso asignado", + "audit.action.report.unassigned": "Asignación retirada", + "audit.action.report.note_added": "Nota interna añadida", + "audit.action.report.message_sent": "Respuesta enviada al informante", + "audit.action.report.delete_requested": "Eliminación solicitada", + "audit.action.report.delete_confirmed": "Eliminación confirmada", + "audit.action.report.attachment_downloaded": "Adjunto descargado", + "audit.action.audit.exported": "Registro de auditoría exportado", + "audit.action.report.delete_cancelled": "Solicitud de eliminación cancelada", + "audit.action.report.acknowledged": "Acuse de recibo confirmado", + "audit.action.report.link_added": "Caso vinculado", + "audit.action.report.link_removed": "Vínculo de caso eliminado", + "audit.action.report.auto_deleted": "Eliminada automáticamente (plazo de conservación)", + "audit.action.report.viewed": "Denuncia abierta", + "audit.action.report.identity_revealed": "Identidad mostrada", + "audit.action.report.content_searched": "Búsqueda en el contenido de denuncias", + "audit.action.category.created": "Categoría creada", + "audit.action.category.updated": "Categoría actualizada", + "audit.action.category.deactivated": "Categoría desactivada", + "audit.action.location.created": "Ubicación creada", + "audit.action.location.deactivated": "Ubicación desactivada", + "audit.action.location.reactivated": "Ubicación reactivada", + "audit.action.admin.created": "Usuario creado", + "audit.action.admin.role_changed": "Rol de usuario modificado", + "audit.action.admin.deactivated": "Usuario desactivado", + "audit.action.admin.reactivated": "Usuario reactivado", + "audit.action.auth.login": "Sesión iniciada", + "audit.action.auth.local_review_login": "Sesión iniciada mediante revisión local", + "audit.action.auth.totp_setup": "Aplicación de autenticación configurada", + "audit.action.auth.password_spraying_suspected": "Posible ataque de pulverización de contraseñas", + "audit.action.org.created": "Organización creada", + "audit.action.org.deactivated": "Organización desactivada", + "audit.action.telemetry.enabled": "Recuento de instalaciones activado", + "audit.action.telemetry.disabled": "Recuento de instalaciones desactivado", + "audit.action.telemetry.id_reset": "Identificador de instalación restablecido", + "audit.detail.old_status": "De", + "audit.detail.new_status": "A", + "audit.detail.old": "De", + "audit.detail.new": "A", + "audit.detail.from": "De", + "audit.detail.to": "A", + "audit.detail.linked_with": "Vinculado con", + "audit.detail.slug": "Identificador", + "audit.detail.label_en": "Etiqueta en inglés", + "audit.detail.action": "Cambio", + "audit.detail.username": "Usuario", + "audit.detail.role": "Rol", + "audit.detail.location_code": "Código de ubicación", + "audit.detail.name": "Nombre", + "audit.detail.org_id": "ID de organización", + "audit.detail.source": "Origen", + "audit.detail.case_number": "Número de caso", + "audit.detail.closed_at": "Cerrado", + "audit.detail.retention_days": "Conservación (días)", + "audit.detail.reason": "Motivo", + "audit.detail.term": "Término de búsqueda", + "audit.detail.hits": "Denuncias coincidentes", + "audit.detail.via": "Mediante", + "audit.detail.via.pdf": "Exportación a PDF", + "audit.detail.reason_recorded": "Motivo registrado (véase el registro de auditoría)", + "audit.detail.reason_unreadable": "Motivo ilegible (error de descifrado)", + "mfa.setup.title": "Configurar la autenticación en dos pasos", + "mfa.setup.subtitle": "Proteja su cuenta vinculando una aplicación de autenticación. Es obligatorio antes de poder usar el área de administración.", + "mfa.setup.step1.header": "Paso 1: escanee el código QR", + "mfa.setup.step1.body": "Abra su aplicación de autenticación (por ejemplo, Aegis, Google Authenticator o Bitwarden) y escanee el código QR que aparece a continuación.", + "mfa.setup.qr.alt": "Código QR para configurar su aplicación de autenticación", + "mfa.setup.manual.summary": "¿No puede escanearlo? Introduzca la clave manualmente", + "mfa.setup.manual.label": "Clave secreta", + "mfa.setup.step2.header": "Paso 2: verifique", + "mfa.setup.step2.body": "Introduzca el código de 6 dígitos que muestra su aplicación de autenticación para confirmar la configuración.", + "mfa.setup.code.label": "Código de verificación", + "mfa.setup.code.hint": "Introduzca el código de 6 dígitos de su aplicación de autenticación.", + "mfa.setup.submit": "Activar la autenticación en dos pasos", + "upload.error.too_large": "'{name}' es demasiado grande ({size}). Cada archivo puede tener como máximo 10 MB.", + "upload.error.bad_extension": "'{name}' no se puede adjuntar. Permitidos: PDF, JPEG, PNG, GIF, WebP, TXT, CSV, DOCX, XLSX.", + "upload.error.legacy_office": "'{name}' es un archivo antiguo de Word/Excel (.doc/.xls) del que no se puede eliminar el autor. Guárdelo como .docx o .xlsx y vuelva a adjuntarlo.", + "upload.error.bad_type": "'{name}' tiene un tipo de archivo no admitido ({type}). Permitidos: PDF, imágenes, texto, Word, Excel.", + "upload.error.content_mismatch": "'{name}' no contiene lo que indica su extensión '{ext}' (el archivo puede estar dañado o disfrazado). Guárdelo de nuevo e inténtelo otra vez.", + "upload.error.too_many": "Demasiados archivos. Puede adjuntar como máximo {max} archivos.", + "upload.error.metadata": "No se pudieron comprobar los detalles ocultos (autor, ubicación, dispositivo) de '{name}', por lo que no se aceptó. Guárdelo de nuevo o expórtelo como PDF.", + "upload.error.malware": "'{name}' no se aceptó: el antivirus lo marcó. Si cree que el archivo es seguro, expórtelo como PDF e inténtelo de nuevo.", + "upload.error.scan_unavailable": "No se pudo comprobar '{name}' en busca de virus en este momento y no se guardó. Inténtelo de nuevo en unos minutos.", + "login.error.sso_link_session": "No se vinculó nada: su sesión había terminado. Inicie sesión y vuelva a elegir «Vincular inicio de sesión único» en Mi cuenta.", + "admin.sso.link": "Vincular inicio de sesión único", + "admin.sso.unlink": "Desvincular inicio de sesión único", + "admin.sso.unlink_confirm": "¿Desvincular el inicio de sesión único de su cuenta? Después iniciará sesión solo con su contraseña.", + "admin.sso.result.linked": "El inicio de sesión único está vinculado. Puede iniciar sesión con él; seguirá siendo necesario su código del autenticador.", + "admin.sso.result.unlinked": "El inicio de sesión único no está vinculado a su cuenta.", + "admin.sso.result.failed": "No se vinculó el inicio de sesión único. Inténtelo de nuevo.", + "admin.sso.result.taken": "No vinculado: esta identidad de inicio de sesión único ya pertenece a otra cuenta.", + "admin.sso.result.only_way_in": "No desvinculado: su cuenta no tiene contraseña, por lo que el inicio de sesión único es su única forma de acceso.", + "admin.users.reset_totp": "Restablecer autenticador", + "admin.users.reset_totp_confirm": "¿Restablecer el autenticador de {username}? Su aplicación y su contraseña dejarán de funcionar y se cerrará su sesión. Recibirá una contraseña temporal que deberá entregarle; en el siguiente inicio de sesión configurará una aplicación nueva.", + "admin.users.totp_pending": "autenticador sin configurar", + "audit.action.admin.totp_reset": "Autenticador restablecido", + "audit.action.auth.sso_linked": "Inicio de sesión único vinculado", + "audit.action.auth.sso_unlinked": "Inicio de sesión único desvinculado", + "admin.users.reset_done.title": "Se ha restablecido el autenticador de {username}.", + "admin.users.reset_done.password": "Contraseña temporal, mostrada solo ahora. Entréguela en persona, no por correo electrónico. En el siguiente inicio de sesión conduce a una nueva aplicación de autenticación y después a una contraseña propia del usuario:", + "admin.users.reset_done.no_password": "Esta cuenta inicia sesión mediante el directorio o el inicio de sesión único, que no se modifican. En su siguiente inicio de sesión configurará una nueva aplicación de autenticación.", + "admin.nav.account": "Mi cuenta", + "account.title": "Mi cuenta", + "account.forced.title": "Elija su propia contraseña", + "account.forced.body": "Otra persona estableció su contraseña actual, por lo que la conoce. Cámbiela aquí antes de poder acceder a nada más.", + "account.password.changed": "Su contraseña se ha cambiado. Se han cerrado todas las demás sesiones de su cuenta.", + "account.details.header": "Cuenta", + "account.details.username": "Nombre de usuario", + "account.details.role": "Rol", + "account.details.organisation": "Organización", + "account.method.password": "Contraseña", + "account.method.set": "Establecida", + "account.method.not_set": "Ninguna", + "account.method.ldap": "Directorio (LDAP)", + "account.method.sso": "Inicio de sesión único", + "account.method.linked": "Vinculado", + "account.method.not_linked": "No vinculado", + "account.method.totp": "Aplicación de autenticación", + "account.method.always": "Obligatorio en cada inicio de sesión", + "account.password.header": "Cambiar contraseña", + "account.password.none_ldap": "Su cuenta inicia sesión mediante el directorio (LDAP). Cambie su contraseña allí.", + "account.password.none_sso": "Su cuenta no tiene contraseña aquí: inicia sesión mediante el inicio de sesión único. Cambie su contraseña en su proveedor de identidad.", + "account.password.demo": "En la demostración no se puede cambiar la contraseña de las cuentas de demostración, para que cualquier visitante pueda iniciar sesión.", + "account.password.current": "Contraseña actual", + "account.password.new": "Contraseña nueva", + "account.password.hint": "Al menos 12 caracteres, como máximo 72 bytes.", + "account.password.confirm": "Repita la contraseña nueva", + "account.password.totp_hint": "El código actual de 6 dígitos de su aplicación de autenticación. La sesión por sí sola no basta para cambiar la contraseña.", + "account.password.submit": "Cambiar contraseña", + "account.password.error.current_required": "Introduzca su contraseña actual.", + "account.password.error.current": "La contraseña actual no es correcta.", + "account.password.error.mismatch": "Las dos contraseñas nuevas no coinciden.", + "account.password.error.policy": "La contraseña nueva debe tener al menos 12 caracteres y como máximo 72 bytes.", + "account.password.error.unchanged": "La contraseña nueva debe ser distinta de la actual.", + "audit.action.auth.password_changed": "Contraseña propia cambiada", + "audit.action.admin.password_reset": "Contraseña restablecida", + "admin.telephone.refs.8": "HinSchG § 8 — Confidencialidad:", + "admin.telephone.refs.11": "HinSchG § 11 — Documentación; grabación solo con consentimiento (Abs. 2):" +} diff --git a/app/locales/fr.json b/app/locales/fr.json index 0e0fd8f0..92ffd36a 100644 --- a/app/locales/fr.json +++ b/app/locales/fr.json @@ -2,10 +2,6 @@ "nav.submit_report": "Soumettre un signalement", "nav.check_status": "Vérifier le statut", "nav.toggle_theme": "Mode sombre", - "nav.lang_switch_label_de": "Passer à l'allemand", - "nav.lang_switch_label_en": "Passer à l'anglais", - "nav.lang_switch_label_fr": "Passer au français", - "nav.lang_switch_label_pt-br": "Passer au portugais (Brésil)", "nav.aria_label": "Navigation principale", "nav.lang_picker_label": "Choisir la langue", "footer.tagline": "Signalement sécurisé des lanceurs d'alerte", diff --git a/app/locales/pt-br.json b/app/locales/pt-br.json index 836a1e1c..274164f8 100644 --- a/app/locales/pt-br.json +++ b/app/locales/pt-br.json @@ -2,10 +2,6 @@ "nav.submit_report": "Enviar denúncia", "nav.check_status": "Verificar status", "nav.toggle_theme": "Modo escuro", - "nav.lang_switch_label_de": "Mudar para alemão", - "nav.lang_switch_label_en": "Mudar para inglês", - "nav.lang_switch_label_fr": "Mudar para francês", - "nav.lang_switch_label_pt-br": "Mudar para português (Brasil)", "nav.aria_label": "Navegação principal", "nav.lang_picker_label": "Selecionar idioma", "footer.tagline": "Denúncias seguras para denunciantes", diff --git a/app/models/category.py b/app/models/category.py index 79505e00..df169bf3 100644 --- a/app/models/category.py +++ b/app/models/category.py @@ -36,7 +36,7 @@ class ReportCategory(Base): def label_for(self, lang: str) -> str: """The label for an admin's UI language. Only English and German are - stored (label_en/label_de) — every other UI language (fr, pt-br) falls + stored (label_en/label_de) — every other UI language (fr, es, pt-br) falls back to English, same as an empty label_de would.""" if lang == "de" and self.label_de: return self.label_de diff --git a/app/templates/base.html b/app/templates/base.html index 6eae8247..4f63f0f2 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -84,7 +84,7 @@ {{ icons.icon('chevron-down', 'u-caret') }}