All checks were successful
whatever the operator called their instance, `instance.name` was never rendered in the running app at all, and there was no favicon anywhere — `index.html` had no `<link rel="icon">` and `public/` held only fonts and theme-init.js. Where the line is drawn, and why: - **The api never decodes an image.** Cropping, scaling and the conversion to PNG happen on a canvas in the browser; the api checks the PNG signature, reads the IHDR dimensions at their fixed offsets and enforces the caps. An image library would put a decoder in front of attacker-supplied bytes AND would have to be carried through the `--network none` offline build. Reading two big-endian integers is not decoding. - **SVG is refused**, with its own error message rather than a generic "not a PNG": it can carry script, and serving it from our own origin would be a cross-site-scripting vector. An operator who tried one should learn that it is deliberate. - **The crop is driven by number inputs, not by dragging.** A drag-only cropper excludes keyboard and switch users outright; a number input is arrow-key operable and screen-reader readable without any custom aria. The resulting pixel size is stated in text, not only drawn as a frame. - **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has already resolved `data-theme` before first paint, so the correct logo is the one painted rather than the one that appears after a flash. Without a dark variant the LIGHT logo carries both themes — the operator's own asset shown unchanged beats one they did not choose (the rule #307 extends to ponds). The settings screen warns; it never blocks. - **The favicon link is static, its resource dynamic.** index.html stays a static file and the api answers with the uploaded icon or a shipped default — that route must never 404, or the browser keeps its generic icon for good. The default is generated by a script from Node's own zlib (`gen-default-favicon.mjs`), for the same offline-build reason. - Both favicon sizes are uploaded together: one source, one crop, so the tab icon and the home-screen icon can never disagree. - Branding is served WITHOUT a session, because the login screen carries it and the browser fetches the favicon before anyone signs in. The admin screen says so — an operator may not expect their logo to be public. - The metadata is not writable through the settings endpoint: it describes bytes on disk, and hand-writing it would claim an asset that is not there. `./data/branding` follows the three-step rule #303 paid for: env default + `data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the `mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and closes the hole that made #303's variant invisible: the nightly archive skips a missing directory WORDLESSLY, so the fence now demands that every `*_DIR` the backup env declares actually travels in the archive. Verified against the real defect — removing the line fails it by name. Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start so #307 is the same event with a different scope, not a second id. Verified: api suite 103 files green (a lone `public-api` ECONNRESET under local parallel load, green in isolation — the documented local flake); branding suite 12 tests against a real directory; crop arithmetic unit tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the new section (overflow 0); and the whole flow walked in the browser: upload → crop 780×180 → stored as 512×118 → logo in the sidebar linking home with the instance name as its accessible name → topbar wordmark following `instance.name` → light logo still shown under `data-theme="dark"`.
140 lines
10 KiB
JSON
140 lines
10 KiB
JSON
{
|
|
"bad_request": "Die Anfrage ist ungültig.",
|
|
"unauthorized": "Bitte melde dich an, um fortzufahren.",
|
|
"forbidden": "Dir fehlt die Berechtigung für diese Aktion.",
|
|
"not_found": "Die angeforderte Ressource existiert nicht.",
|
|
"conflict": "Die Anfrage steht im Widerspruch zum aktuellen Zustand.",
|
|
"gone": "Diese Ressource ist nicht mehr verfügbar.",
|
|
"payload_too_large": "Die übermittelten Daten sind zu groß.",
|
|
"rate_limited": "Zu viele Anfragen — bitte versuche es später erneut.",
|
|
"internal_error": "Interner Serverfehler.",
|
|
"registration_closed": "Die Registrierung ist auf dieser Instanz derzeit geschlossen.",
|
|
"token_invalid": "Dieser Link ist ungültig oder abgelaufen.",
|
|
"login_failed": "Benutzername/E-Mail oder Passwort ist falsch.",
|
|
"login_backoff": "Zu viele Fehlversuche — bitte warte ein paar Minuten.",
|
|
"email_unverified": "Bitte bestätige zuerst deine E-Mail-Adresse.",
|
|
"account_disabled": "Dieses Konto wurde deaktiviert.",
|
|
"password_incorrect": "Das aktuelle Passwort ist falsch.",
|
|
"csrf_origin_mismatch": "Die Anfrage kam von einer unerwarteten Herkunft.",
|
|
"cannot_revoke_current_session": "Beende deine aktuelle Sitzung über die Abmeldung.",
|
|
"personal_pond_undeletable": "Der persönliche Teich kann nicht gelöscht werden.",
|
|
"quota_exceeded": "Das Kontingent ist erreicht (Limit: {{limit}}).",
|
|
"slug_taken": "Dieser Adressname ist in diesem Teich bereits vergeben.",
|
|
"page_document_too_large": "Die Seite ist zu groß (Limit: {{limitBytes}} Bytes).",
|
|
"invalid_page_state": "Der übermittelte Seiteninhalt ist ungültig.",
|
|
"page_trashed": "Diese Seite wurde in den Papierkorb verschoben.",
|
|
"page_cycle": "Eine Seite kann nicht in ihren eigenen Teilbaum verschoben werden.",
|
|
"page_depth_exceeded": "Seiten lassen sich höchstens {{max}} Ebenen tief verschachteln.",
|
|
"label_name_taken": "Ein Label mit diesem Namen existiert auf dieser Ebene bereits.",
|
|
"label_cycle": "Ein Label kann nicht in seinen eigenen Teilbaum verschoben werden.",
|
|
"label_depth_exceeded": "Labels lassen sich höchstens {{max}} Ebenen tief verschachteln.",
|
|
"label_has_pages": "Diesem Label sind noch Seiten zugeordnet; bitte bestätigen, um sie zu lösen.",
|
|
"label_wrong_pond": "Dieses Label gehört zu einem anderen Teich.",
|
|
"unsupported_file_type": "Dieser Dateityp wird nicht unterstützt.",
|
|
"upload_type_not_allowed": "Dieser Dateityp ist auf dieser Instanz nicht erlaubt.",
|
|
"file_too_large": "Die Datei ist zu groß (Limit: {{limitBytes}} Bytes).",
|
|
"converter_unavailable": "Der Dokument-Konverter ist derzeit nicht verfügbar. Bitte versuche es später erneut.",
|
|
"converter_timeout": "Die Konvertierung hat zu lange gedauert und wurde abgebrochen.",
|
|
"conversion_failed": "Dieses Dokument konnte nicht konvertiert werden.",
|
|
"renderer_unavailable": "Der PDF-Renderer ist derzeit nicht verfügbar. Bitte versuche es später erneut.",
|
|
"render_failed": "Diese Seite konnte nicht als PDF gerendert werden.",
|
|
"import_unsupported_format": "Nur Word- (.docx) und OpenDocument-Dokumente (.odt) können importiert werden.",
|
|
"import_vault_invalid_zip": "Das Archiv ist kein gültiger Obsidian-Vault (ZIP mit Markdown-Notizen).",
|
|
"import_vault_too_large": "Der entpackte Vault ist zu groß für einen Import.",
|
|
"network": "Der Server war nicht erreichbar.",
|
|
"grant_exists": "Diese Berechtigung existiert bereits.",
|
|
"plugin_invalid_zip": "Die hochgeladene Datei ist kein gültiges ZIP-Archiv.",
|
|
"plugin_bad_structure": "Das Plugin-Paket hat eine ungültige Struktur.",
|
|
"plugin_invalid_manifest": "Das Plugin-Manifest ist ungültig.",
|
|
"plugin_api_incompatible": "Dieses Plugin benötigt eine nicht unterstützte Plugin-API-Version.",
|
|
"plugin_too_large": "Das Plugin-Paket ist zu groß (Limit: {{limitBytes}} Bytes).",
|
|
"plugin_missing_bundle": "Ein Code-Plugin muss ein plugin.js-Bundle enthalten.",
|
|
"plugin_missing_styles": "Ein Style-Plugin muss eine styles.css-Datei enthalten.",
|
|
"plugin_css_unsafe": "Das Plugin-Stylesheet verwendet nicht erlaubte Konstrukte.",
|
|
"plugin_version_not_higher": "Ein Update muss eine höhere Version als das installierte haben.",
|
|
"plugin_required_cannot_uninstall": "Ein erforderliches Plugin kann nicht deinstalliert werden.",
|
|
"plugin_not_found": "Dieses Plugin ist nicht installiert.",
|
|
"plugin_not_optional": "Nur optionale Plugins können pro Teich aktiviert werden.",
|
|
"grant_pond_admin_scope": "Eine Teich-Admin-Berechtigung muss für den ganzen Teich und eine bestimmte Person gelten.",
|
|
"grant_pond_admin_personal_pond": "Der einzige Administrator eines persönlichen Teichs ist dessen Eigentümer.",
|
|
"grant_subject_id_mismatch": "Das Subjekt der Berechtigung ist widersprüchlich.",
|
|
"grant_scope_id_mismatch": "Der Geltungsbereich der Berechtigung ist widersprüchlich.",
|
|
"grant_scope_not_found": "Das Label oder die Seite, auf die sich diese Berechtigung bezieht, existiert in diesem Teich nicht.",
|
|
"grant_subject_not_found": "Diese Person existiert nicht.",
|
|
"grant_last_admin": "Die letzte Teich-Admin-Berechtigung kann nicht entfernt werden.",
|
|
"member_not_found": "Es existiert keine Person mit diesem Benutzernamen oder dieser E-Mail.",
|
|
"member_exists": "Diese Person ist bereits Mitglied dieses Teichs.",
|
|
"member_not_a_member": "Diese Person ist kein Mitglied dieses Teichs.",
|
|
"member_is_owner": "Die Mitgliedschaft des Teich-Eigentümers kann hier nicht geändert werden.",
|
|
"cannot_modify_self": "Du kannst diese Aktion nicht auf dein eigenes Konto anwenden.",
|
|
"last_site_admin": "Der letzte Site-Admin kann nicht entfernt werden.",
|
|
"setup_required": "Diese Instanz ist noch nicht eingerichtet. Bitte führe zuerst die Ersteinrichtung aus.",
|
|
"setup_locked": "Die Ersteinrichtung ist bereits abgeschlossen.",
|
|
"setup_admin_exists": "Es existiert bereits ein Site-Admin-Konto.",
|
|
"setup_admin_missing": "Lege zuerst das Site-Admin-Konto an.",
|
|
"smtp_test_failed": "Der SMTP-Test ist fehlgeschlagen. Bitte prüfe die Verbindungsdaten.",
|
|
"validation": {
|
|
"required": "Dieses Feld ist erforderlich.",
|
|
"taken": "Dieser Wert ist bereits vergeben.",
|
|
"username": {
|
|
"tooShort": "Der Benutzername braucht mindestens 3 Zeichen.",
|
|
"tooLong": "Der Benutzername darf höchstens 32 Zeichen haben.",
|
|
"charset": "Erlaubt sind nur Buchstaben, Ziffern und Bindestriche."
|
|
},
|
|
"password": {
|
|
"tooShort": "Das Passwort braucht mindestens 10 Zeichen.",
|
|
"tooLong": "Das Passwort darf höchstens 128 Zeichen haben.",
|
|
"tooCommon": "Dieses Passwort ist zu verbreitet."
|
|
},
|
|
"email": {
|
|
"invalid": "Bitte gib eine gültige E-Mail-Adresse ein."
|
|
},
|
|
"displayName": {
|
|
"required": "Bitte gib einen Anzeigenamen ein."
|
|
},
|
|
"labelColor": "Bitte gib eine Farbe wie #a1b2c3 ein.",
|
|
"tooLong": "Die Eingabe ist zu lang."
|
|
},
|
|
"comments_editors_only": "Kommentare sind in diesem Teich auf Bearbeitende beschränkt.",
|
|
"comment_not_author": "Nur die Autorin/der Autor kann diesen Kommentar ändern.",
|
|
"comment_has_replies": "Dieser Kommentar hat Antworten — den ganzen Thread kann nur eine Teich-Administration löschen.",
|
|
"comment_parent_invalid": "Antworten müssen sich auf einen Kommentar der obersten Ebene derselben Seite beziehen.",
|
|
"comment_not_root": "Nur Kommentare der obersten Ebene können als erledigt markiert werden.",
|
|
"maintenance_mode": "Die Instanz ist im Wartungsmodus, während ein Backup wiederhergestellt wird.",
|
|
"backup_connection_failed": "Der Nextcloud-Verbindungstest ist fehlgeschlagen.",
|
|
"backup_remote_disabled_by_policy": "Entfernte Backup-Ziele sind durch die Deployment-Richtlinie deaktiviert (BACKUP_ALLOWED_TARGETS ist leer).",
|
|
"backup_target_not_allowed": "Dieser Ziel-Host steht nicht in der Deployment-Richtlinie (BACKUP_ALLOWED_TARGETS).",
|
|
"backup_restore_confirm_mismatch": "Der Bestätigungstext stimmt nicht mit der Backup-ID überein.",
|
|
"backup_restore_running": "Es läuft bereits eine Wiederherstellung.",
|
|
"backup_remote_not_configured": "Es ist kein Nextcloud-Backup-Ziel konfiguriert.",
|
|
"backup_set_not_found": "Das gewählte Backup-Set wurde nicht gefunden.",
|
|
"scope_required": "Dieses API-Token hat nicht den erforderlichen Scope.",
|
|
"pond_not_found": "Der Teich existiert nicht.",
|
|
"classification_lower_forbidden": "Zum Herabstufen der Einstufung fehlt die Berechtigung (Teich-Admin erforderlich).",
|
|
"classified_upload_blocked": "Uploads auf eingestufte Seiten sind auf dieser Instanz blockiert.",
|
|
"vs_nfd_profile_violation": "Diese Einstellung würde vom VS-NfD-Referenzprofil abweichen — das Deployment erzwingt das Profil (VS_NFD_MODE=enforced).",
|
|
"plugin_not_pinned": "Dieses Plugin steht nicht auf der Allowlist (Hash-Pinning aktiv) — erst pinnen, dann installieren.",
|
|
"plugin_hash_mismatch": "Der Bundle-Hash weicht vom gepinnten Hash ab — das Bundle ist nicht das geprüfte (oder eine neue Version braucht ein Re-Pin).",
|
|
"font_family_reserved": "Dieser Schriftname gehört zum mitgelieferten Katalog und kann nicht überschrieben werden.",
|
|
"font_family_exists": "Eine eigene Schrift mit diesem Namen existiert bereits.",
|
|
"font_family_unusable": "Aus diesem Schriftnamen lässt sich kein Adressname bilden — bitte lateinische Buchstaben oder Ziffern verwenden.",
|
|
"font_file_empty": "Die Schriftdatei ist leer.",
|
|
"font_file_too_large": "Die Schriftdatei ist zu groß.",
|
|
"font_file_not_a_font": "Diese Datei ist keine WOFF2-/WOFF-Schriftdatei.",
|
|
"font_woff2_missing": "Zu jedem Schnitt gehört eine WOFF2-Datei; eine WOFF allein genügt nicht.",
|
|
"font_no_weights": "Es wurde keine Schriftdatei ausgewählt.",
|
|
"font_too_many_weights": "Diese Schrift hat bereits die höchstmögliche Zahl an Schnitten.",
|
|
"font_weight_exists": "Dieser Schnitt existiert für diese Schrift bereits.",
|
|
"font_weight_invalid": "Dieser Schnitt ist nicht zulässig.",
|
|
"font_one_weight_expected": "Es lässt sich nur ein Schnitt auf einmal ergänzen.",
|
|
"font_unexpected_field": "Die Anfrage enthält ein unerwartetes Feld.",
|
|
"branding_file_empty": "Die Bilddatei ist leer.",
|
|
"branding_file_too_large": "Die Bilddatei ist zu groß.",
|
|
"branding_file_missing": "Es wurde keine Bilddatei übermittelt.",
|
|
"branding_not_a_png": "Es konnte kein PNG erzeugt werden — bitte eine andere Bilddatei wählen.",
|
|
"branding_not_an_image": "Diese Datei ist kein unterstütztes Bild (PNG, JPEG oder WebP).",
|
|
"branding_svg_rejected": "SVG wird nicht angenommen: eine SVG-Datei kann Skripte enthalten. Bitte PNG, JPEG oder WebP verwenden.",
|
|
"branding_image_too_large": "Das Bild ist zu groß — bitte den Ausschnitt verkleinern.",
|
|
"branding_favicon_not_square": "Das Favicon muss quadratisch sein."
|
|
}
|