i18n: fail safe on empty strings, normalise German to Sie

D2 -- returnEmptyString: false. i18next defaults it to true, so an empty
translation was returned as valid and rendered as blank UI instead of falling
back to English. Verified safe first: zero empty-string values across all 8
locales, no addResourceBundle or runtime resource injection, no public/locales
for the HTTP backend, and the three t(key, '') call sites resolve against key
families fully populated in en and de.

D3 -- German formality normalised to Sie throughout, 101 strings. There is no
deliberate du island: Sie outnumbered du roughly 6:1 (~390 vs 65 addressed
strings), every namespace with more than ten addressed strings was
Sie-dominant, and the guest gallery plus all public/billing surfaces were
already 100% Sie. Even customer.*, the reported offender, was internally mixed
rather than consistently du. Two detection passes: du-pronouns (now zero) and
du-imperatives without a pronoun (Klicke…, Aktiviere…, Wähle…). Placeholders
verified mechanically unchanged. Left alone: ten 1st-person progress labels
(Lade Benutzer…, Prüfe…, Teste Verbindung…) -- those are label style, not
address, and normalising two of ten would have made it worse.

C7 -- removeUnusedKeys stays false, but the comment now carries measured
evidence instead of an estimate. The honest attempt was made: 61 preserve
globs derived mechanically from all 82 dynamic key templates in src (far more
than the 5 families previously named) plus 17 constant-table prefixes cut
removals from 422 to 158. Two things still block it. 47 of the remainder are
the base form of a plural key that src does pass to t(); i18next tries the
_other suffix first so nothing visibly breaks, but covering them needs a
literal pattern per key and forgetting one silently deletes a live key --
exactly the failure the flag prevents. And pruning is not idempotent: run for
real, extract had to run three times before --ci --dry-run came back clean,
each pass uncovering another removal, so i18n:ci would fail on a correct tree
until someone ran extract enough times.

Also adds the three settings.analytics keys that 9251745a referenced in
AnalyticsTab without adding (proxiedNotice, proxiedNoticeText,
customOnlyCspWarningText) -- en from the source defaults, de translated.

Refs testplan REPORT.md C7, D2, D3.
This commit is contained in:
Paul Nothaft
2026-09-01 19:03:01 +02:00
parent 758dc005df
commit 413290af3e
4 changed files with 162 additions and 121 deletions
+25 -6
View File
@@ -24,12 +24,31 @@ export default defineConfig({
primaryLanguage: 'en',
// Pruning is unsafe in this codebase: a large share of keys is never visible to the
// AST extractor because it is built at runtime — t(`admin.activities.${type}`),
// t(`admin.notificationMessages.${type}`), t(`projects.status.${status}`) — or held in
// constant tables the extractor does not resolve (AdminSidebar `nameKey`,
// CrmDevelopmentPage `titleKey`/`descKey`, the crmSettings toggle map). Enabling it
// deletes ~355 live keys per locale, so removal stays a manual decision.
// Pruning is unsafe in this codebase. A large share of keys is never visible to the
// AST extractor because it is built at runtime — 82 distinct dynamic key templates in
// src, e.g. admin.activities, admin.notificationMessages, projects.status,
// accounting.expenseStatus, crmSettings — or held in constant tables the extractor does
// not resolve (AdminSidebar `nameKey`, CrmDevelopmentPage `titleKey`/`descKey`).
// Turned on as-is, it deletes 422 keys per locale, 203 of which src references.
//
// A `preservePatterns` rescue was attempted and measured: 61 globs derived
// mechanically from every dynamic template in src, plus 17 for the constant-table
// prefixes (78 patterns). That still leaves two unfixable problems:
//
// 1. 47 of the remaining 158 removals are the base form of a plural key
// (`upload.failures.title` next to `_one`/`_other`). src passes exactly those
// strings to t(), so they are referenced by any honest definition. i18next happens
// to resolve them via the `_other` suffix first, so nothing breaks today — but
// covering them needs 47 literal patterns and one more on every future {{count}}
// key, where forgetting one silently deletes a live key. That is the exact failure
// mode this flag is supposed to prevent.
// 2. Pruning is not idempotent. `extract` had to be run three times in a row before
// `extract --ci --dry-run` came back clean — each pass uncovered one further
// removal (`businessProfile.title`, then `cssTemplates.title`). i18n:ci would
// therefore fail on a correct tree until someone happened to run extract enough
// times. With pruning off the extractor settles in a single pass.
//
// Key removal stays a manual decision.
removeUnusedKeys: false,
preserveContextVariants: true,
+8
View File
@@ -20,6 +20,14 @@ i18n
fallbackLng: 'en',
debug: false,
// i18next defaults this to `true`, which makes an empty-string translation
// a *valid* lookup — it renders as blank UI instead of falling through to
// `fallbackLng`. nl/pt/ru/fr/sl/es are deliberately partial locales, so an
// empty value landing in one of them must fail safe to English rather than
// silently blanking the label. No managed locale has one today; this keeps
// it true by construction rather than by vigilance.
returnEmptyString: false,
resources,
interpolation: {
+115 -108
View File
@@ -189,12 +189,16 @@
"eventSpecific": "(Veranstaltungsspezifisch)",
"clickToUpload": "Klicken zum Hochladen oder per Drag & Drop",
"fileRequirements": "{{formats}} (max. {{sizeLimit}}MB pro Datei, {{limit}} Dateien pro Upload)",
"videoSizeLimit": "Videos: max. {{sizeLimit}}MB pro Datei",
"selectedFiles": "Ausgewählte Dateien",
"uploading": "Wird hochgeladen...",
"transferring": "Übertragung",
"processing": "Fotos werden verarbeitet...",
"processingHint": "Dateien sind hochgeladen. PicPeak erstellt jetzt Thumbnails und liest Metadaten. Sie können diese Seite verlassen — die Verarbeitung läuft im Hintergrund weiter.",
"processingProgress": "{{complete}} von {{total}} fertig",
"processingFailed_one": "{{count}} Foto konnte nicht verarbeitet werden und erscheint nicht in der Galerie.",
"processingFailed_other": "{{count}} Fotos konnten nicht verarbeitet werden und erscheinen nicht in der Galerie.",
"processingStillRunning": "Ihr Upload wird noch verarbeitet — er erscheint in Kürze in der Galerie.",
"retryFailed": "Fehlgeschlagene erneut versuchen",
"uploadComplete": "Upload abgeschlossen!",
"someFilesFailed": "Einige Dateien konnten nicht hochgeladen werden",
@@ -366,7 +370,7 @@
"title": "Workflows",
"subtitle": "Visuelle Automatisierungen Auslöser, Bedingungen, Freigaben und Aktionen.",
"new": "Neuer Workflow",
"empty": "Noch keine Workflows. Erstelle einen, um Rechnungsstellung und Buchungsschritte zu automatisieren.",
"empty": "Noch keine Workflows. Erstellen Sie einen, um Rechnungsstellung und Buchungsschritte zu automatisieren.",
"builtin": "integriert",
"triggerLabel": "Auslöser",
"enabled": "Aktiv",
@@ -390,12 +394,12 @@
},
"approvals": {
"title": "Freigaben",
"subtitle": "Workflow-Durchläufe, die auf deine Bestätigung warten.",
"empty": "Aktuell wartet nichts auf dich.",
"subtitle": "Workflow-Durchläufe, die auf Ihre Bestätigung warten.",
"empty": "Aktuell wartet nichts auf Sie.",
"confirm": "Bestätigen",
"deny": "Ablehnen",
"recorded": "Antwort gespeichert",
"defaultPrompt": "Ein Workflow benötigt deine Bestätigung.",
"defaultPrompt": "Ein Workflow benötigt Ihre Bestätigung.",
"pendingTitle": "Offene Freigaben",
"viewAll": "Alle Freigaben ansehen",
"openEntity": "{{type}} #{{id}} öffnen",
@@ -542,7 +546,7 @@
"running": "Prüfe…",
"error": "Prüfung fehlgeschlagen: {{message}}",
"scannedAt": "Zuletzt geprüft: {{when}}",
"emptyState": "In dieser Sitzung wurde noch keine Prüfung ausgeführt. Klicke auf \"Prüfung starten\", um den Dokumentbestand zu durchsuchen.",
"emptyState": "In dieser Sitzung wurde noch keine Prüfung ausgeführt. Klicken Sie auf \"Prüfung starten\", um den Dokumentbestand zu durchsuchen.",
"summary": {
"total": "Gesamt",
"verifiedOk": "Hash-verifiziert",
@@ -1128,7 +1132,7 @@
},
"footer": {
"forgetMeConfirm": "Ihr Name und Ihre Auswahl werden aus dieser Galerie entfernt.",
"forgetMe": "Vergiss mich ({{name}})",
"forgetMe": "Mich vergessen ({{name}})",
"socials": "Soziale Netzwerke"
},
"photosCount_one": "{{count}} Foto",
@@ -1310,13 +1314,13 @@
"shareWithGuestsPublic": "Teilen Sie diesen Link mit Gästen. Für diese Galerie ist kein Passwort erforderlich.",
"shortUrls": {
"title": "Gebrandete Kurz-URLs",
"description": "Erstelle einprägsame Links wie /s/sofia-abschluss, die zu dieser Galerie führen. Die Kurz-URL zeigt selbst die reiche Social-Vorschau — iMessage, Facebook, WhatsApp etc. sehen das Galerie-Foto + den Namen auch beim Einfügen des kurzen Links.",
"description": "Erstellen Sie einprägsame Links wie /s/sofia-abschluss, die zu dieser Galerie führen. Die Kurz-URL zeigt selbst die reiche Social-Vorschau — iMessage, Facebook, WhatsApp etc. sehen das Galerie-Foto + den Namen auch beim Einfügen des kurzen Links.",
"slugPlaceholder": "sofia-abschluss (optional)",
"slugHelp": "Leer lassen für automatische Generierung aus dem Galerienamen. Erlaubte Zeichen: Kleinbuchstaben, Ziffern, Bindestriche.",
"create": "Erstellen",
"created": "Kurz-URL erstellt",
"deleted": "Kurz-URL gelöscht",
"empty": "Noch keine Kurz-URLs. Erstelle oben eine, um diese Galerie mit einem einprägsamen Link zu teilen.",
"empty": "Noch keine Kurz-URLs. Erstellen Sie oben eine, um diese Galerie mit einem einprägsamen Link zu teilen.",
"hits": "{{count}} Aufrufe",
"lastHit": "zuletzt {{when}}",
"createdAt": "erstellt {{when}}",
@@ -1354,19 +1358,19 @@
"passwordReset": {
"title": "Galerie-Passwort zurücksetzen",
"newTitle": "Neues Passwort",
"description": "Lege ein neues Passwort für {{eventName}} fest oder lasse beide Felder leer, um eines automatisch zu erzeugen.",
"description": "Legen Sie ein neues Passwort für {{eventName}} fest oder lassen Sie beide Felder leer, um eines automatisch zu erzeugen.",
"newPasswordLabel": "Neues Passwort",
"placeholder": "Leer lassen für automatische Erzeugung",
"helperText": "Mindestens 6 Zeichen verwenden oder leer lassen für automatische Erzeugung",
"confirmLabel": "Passwort bestätigen",
"sendEmail": "E-Mail-Benachrichtigung senden",
"sendEmailHelp": "Den Host über die Passwortänderung informieren",
"warning": "Hinweis: Das alte Passwort funktioniert nicht mehr. Stelle sicher, dass du das neue Passwort an den Host weitergibst.",
"warning": "Hinweis: Das alte Passwort funktioniert nicht mehr. Stellen Sie sicher, dass Sie das neue Passwort an den Host weitergeben.",
"submit": "Passwort zurücksetzen",
"successHeading": "Passwort erfolgreich zurückgesetzt!",
"emailSentNote": "Eine E-Mail-Benachrichtigung wurde an den Host gesendet.",
"generatedLabel": "Automatisch generiertes Galerie-Passwort",
"saveSecurelyNote": "Wichtig: Speichere dieses Passwort sicher. Es kann nicht wiederhergestellt werden, sobald du dieses Fenster schließt.",
"saveSecurelyNote": "Wichtig: Speichern Sie dieses Passwort sicher. Es kann nicht wiederhergestellt werden, sobald Sie dieses Fenster schließen.",
"done": "Fertig",
"toastSuccess": "Passwort erfolgreich zurückgesetzt",
"toastError": "Passwort zurücksetzen fehlgeschlagen",
@@ -1469,7 +1473,7 @@
"justPublish": "Veröffentlichen",
"passwordLabel": "Galerie-Passwort",
"passwordPlaceholder": "Galerie-Passwort eingeben",
"passwordHelp": "Gib das bei der Erstellung gesetzte Passwort erneut ein (oder wähle ein neues). Die E-Mail enthält genau diesen Text; das Backend hasht es erneut, sodass die Galerie-Anmeldung weiterhin funktioniert.",
"passwordHelp": "Geben Sie das bei der Erstellung gesetzte Passwort erneut ein (oder wählen Sie ein neues). Die E-Mail enthält genau diesen Text; das Backend hasht es erneut, sodass die Galerie-Anmeldung weiterhin funktioniert.",
"errorMinLength": "Das Passwort muss mindestens 6 Zeichen lang sein.",
"notifyLabel": "Galerie-E-Mail jetzt senden",
"notifyHelp": "Abwählen, um still zu veröffentlichen — die Galerie geht online, es wird nichts versendet. Die E-Mail lässt sich später auf dieser Seite senden.",
@@ -1596,14 +1600,12 @@
"importFromSelectedFolder": "Aus ausgewähltem Ordner importieren",
"promoBanner": {
"title": "Werbebanner",
"help": "Lege fest, wie diese Galerie das Werbebanner behandelt. „Übernehmen\" nutzt die globale Vorgabe; „Eigene\" überschreibt sie für dieses Event; „Aus\" blendet das Banner für dieses Event aus.",
"help": "Legen Sie fest, wie diese Galerie das Werbebanner behandelt. „Übernehmen\" nutzt die globale Vorgabe; „Eigene\" überschreibt sie für dieses Event; „Aus\" blendet das Banner für dieses Event aus.",
"mode_inherit": "Globale Vorgabe übernehmen",
"mode_custom": "Eigene Inhalte für dieses Event",
"mode_off": "Aus (für dieses Event ausblenden)",
"placeholder": "Markdown-Inhalt (z. B. **Aktion:** [jetzt Termin buchen](https://example.com))",
"preview": "Vorschau",
"mode_managed": "Verwaltet (Upload nach PicPeak)",
"mode_reference": "Externen Ordner referenzieren"
"preview": "Vorschau"
},
"infoBanner": {
"title": "Info-Banner",
@@ -1612,14 +1614,12 @@
"mode_custom": "Eigener Text für dieses Event",
"mode_off": "Aus (für dieses Event ausblenden)",
"placeholder": "Über die Menü-Schaltfläche oben links lassen sich die Fotos filtern.",
"preview": "Vorschau",
"mode_managed": "Verwaltet (Upload nach PicPeak)",
"mode_reference": "Externen Ordner referenzieren"
"preview": "Vorschau"
},
"ogShare": {
"title": "Heldenbild als Vorschau für geteilte Links verwenden",
"help": "Beim Teilen der Galerie-URL auf WhatsApp, Facebook, Slack usw. wird das oben gewählte Heldenbild als Link-Vorschau angezeigt. Das Thumbnail wird von Link-Preview-Crawlern ohne Authentifizierung abgerufen — wer die URL teilt, macht damit faktisch dieses Bild öffentlich. Standardmäßig aus; wähle erst ein Heldenbild, das du bewusst öffentlich zeigen möchtest, bevor du diese Option aktivierst.",
"heroRequired": "Wähle zuerst oben ein Heldenbild — diese Option verwendet es als WhatsApp- / Facebook- / Slack-Vorschaubild."
"help": "Beim Teilen der Galerie-URL auf WhatsApp, Facebook, Slack usw. wird das oben gewählte Heldenbild als Link-Vorschau angezeigt. Das Thumbnail wird von Link-Preview-Crawlern ohne Authentifizierung abgerufen — wer die URL teilt, macht damit faktisch dieses Bild öffentlich. Standardmäßig aus; wählen Sie erst ein Heldenbild, das Sie bewusst öffentlich zeigen möchten, bevor Sie diese Option aktivieren.",
"heroRequired": "Wählen Sie zuerst oben ein Heldenbild — diese Option verwendet es als WhatsApp- / Facebook- / Slack-Vorschaubild."
},
"publishQuietSuccess": "Galerie veröffentlicht. Es wurde keine E-Mail versendet.",
"sendGalleryEmail": {
@@ -1649,7 +1649,8 @@
"removeAria": "{{name}} entfernen",
"placeholder": "Nach E-Mail, Name oder Firma suchen",
"searching": "Wird gesucht…",
"noResults": "Keine Treffer. Laden Sie diesen Kunden zuerst unter Kunden → Konten ein."
"noResultsCanCreate": "Keine Treffer. Legen Sie über „Neuen Kunden anlegen einen an.",
"noResultsNoPermission": "Keine Treffer, und Ihre Rolle darf keine Kunden anlegen. Bitten Sie eine Administratorin oder einen Administrator, diesen Kunden anzulegen."
}
},
"settings": {
@@ -1664,6 +1665,8 @@
"siteUrlEnvPinned": "Durch die Umgebungsvariable FRONTEND_URL festgelegt; sie überschreibt diese Einstellung. Entfernen Sie sie aus Ihrer .env (oder der Container-Umgebung) und starten Sie neu, um die Adresse hier zu verwalten.",
"defaultExpiration": "Standardablauf (Tage)",
"maxFileSize": "Max. Dateigröße (MB)",
"maxVideoSize": "Max. Videogröße (MB)",
"maxVideoSizeHelp": "Eigenes Limit pro Videodatei, damit für Fotos ein kleineres Limit gelten kann.",
"maxFilesPerUpload": "Max. Dateien pro Upload",
"maxFilesPerUploadHelp": "Maximale Anzahl an Fotos pro Upload-Vorgang (1-{{max}}).",
"maxUploadBatchSize": "Max. Upload-Paketgröße (MB)",
@@ -2030,6 +2033,9 @@
"custom": "Custom (eigenes Skript einfügen)"
},
"providerNoneInfo": "Kein externer Tracker eingebunden. Das Admin-Dashboard zeigt weiterhin Übersichtskarten + den Tagesverlauf aus PicPeaks eigenen access_logs; das Geräte-Diagramm verwendet eine grobe User-Agent-Heuristik.",
"proxiedNotice": "Wird über Ihre eigene Domain ausgeliefert",
"proxiedNoticeText": "PicPeak lädt das Tracker-Skript und leitet dessen Ereignisse über die eigene Domain weiter, deshalb ist keine Änderung an der Content-Security-Policy oder am Reverse-Proxy nötig — und Werbeblocker sehen First-Party-Anfragen. Ihr Tracker erhält weiterhin IP-Adresse und User-Agent jedes Besuchers (als X-Forwarded-For weitergereicht), Geräte- und Standort-Auswertungen funktionieren also unverändert. Die URL muss vom PicPeak-Server aus erreichbar sein und auf eine öffentliche Adresse auflösen.",
"customOnlyCspWarningText": "PicPeak liefert eine strikte CSP aus (`script-src 'self'; connect-src 'self'`). Anders als bei den Optionen Umami und Rybbit oben wird ein eingefügtes Snippet nicht über PicPeak geleitet — tragen Sie die Domain Ihres Trackers sowohl in `script-src` (zum Laden des Skripts) als auch in `connect-src` (für die gesendeten Ereignisse) Ihrer Reverse-Proxy- oder nginx-CSP-Konfiguration ein, sonst blockiert der Browser sie stillschweigend.",
"rybbitUrl": "Rybbit-URL",
"rybbitUrlHelp": "URL Ihrer Rybbit-Instanz — `https://app.rybbit.io` für SaaS oder `https://rybbit.ihre-domain.de` für selbst gehostet.",
"rybbitWebsiteId": "Site-ID",
@@ -2345,7 +2351,7 @@
},
"workflows": {
"title": "Workflows",
"description": "Visuelle Automatisierungen auf einer Canvas erstellen Auslöser, Bedingungen, Verzweigungen, Schleifen und Freigabe-Gates für Admins. Deine Mahnstufen und Buchungsschritte werden zu bearbeitbaren Abläufen. Strikt optional.",
"description": "Visuelle Automatisierungen auf einer Canvas erstellen Auslöser, Bedingungen, Verzweigungen, Schleifen und Freigabe-Gates für Admins. Ihre Mahnstufen und Buchungsschritte werden zu bearbeitbaren Abläufen. Strikt optional.",
"sidebar": "Workflows"
},
"faces": {
@@ -2360,7 +2366,7 @@
"showLogo": "Logo in der Kundenkopfzeile anzeigen",
"showLogoHint": "Verwendet das oben konfigurierte Branding-Logo.",
"showCompanyName": "Firmennamen in der Kundenkopfzeile anzeigen",
"showCompanyNameHint": "Ausblenden, wenn dein Logo den Firmennamen bereits enthält.",
"showCompanyNameHint": "Ausblenden, wenn Ihr Logo den Firmennamen bereits enthält.",
"save": "Änderungen speichern",
"saved": "Branding des Kundendashboards gespeichert",
"error": "Einstellungen konnten nicht gespeichert werden"
@@ -2445,29 +2451,29 @@
},
"mfa": {
"title": "Zwei-Faktor-Authentifizierung",
"description": "Sichere deine Admin-Anmeldung mit einem zweiten Schritt über eine Authenticator-App (TOTP).",
"notEnrolled": "Die Zwei-Faktor-Authentifizierung ist für dein Konto nicht aktiviert.",
"description": "Sichern Sie Ihre Admin-Anmeldung mit einem zweiten Schritt über eine Authenticator-App (TOTP).",
"notEnrolled": "Die Zwei-Faktor-Authentifizierung ist für Ihr Konto nicht aktiviert.",
"setUp": "Einrichten",
"setupScanInstruction": "Scanne diesen QR-Code mit deiner Authenticator-App (z. B. Google Authenticator, 1Password, Authy).",
"setupScanInstruction": "Scannen Sie diesen QR-Code mit Ihrer Authenticator-App (z. B. Google Authenticator, 1Password, Authy).",
"manualEntry": "Oder gib diesen Schlüssel manuell ein:",
"qrAlt": "QR-Code zur Zwei-Faktor-Einrichtung",
"enterCodeLabel": "Gib den 6-stelligen Code aus deiner App ein",
"enterCodeLabel": "Geben Sie den 6-stelligen Code aus Ihrer App ein",
"codePlaceholder": "123456",
"enable": "Aktivieren",
"enabledBadge": "Die Zwei-Faktor-Authentifizierung ist aktiviert.",
"recoveryCodesRemaining": "Noch {{count}} Wiederherstellungscode übrig.",
"recoveryCodesRemaining_other": "Noch {{count}} Wiederherstellungscodes übrig.",
"regenerate": "Wiederherstellungscodes neu erzeugen",
"regenerateHelp": "Gib einen aktuellen Authentifizierungscode ein, um neue Wiederherstellungscodes zu erzeugen. Deine alten Codes werden ungültig.",
"regenerateHelp": "Geben Sie einen aktuellen Authentifizierungscode ein, um neue Wiederherstellungscodes zu erzeugen. Ihre alten Codes werden ungültig.",
"regenerateConfirm": "Neu erzeugen",
"disable": "Deaktivieren",
"disablePrompt": "Gib einen aktuellen Authentifizierungs- oder Wiederherstellungscode ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren:",
"disablePrompt": "Geben Sie einen aktuellen Authentifizierungs- oder Wiederherstellungscode ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren:",
"disableConfirmTitle": "Zwei-Faktor-Authentifizierung deaktivieren?",
"disableConfirmMessage": "Für dein Konto ist bei der Anmeldung dann kein zweiter Schritt mehr erforderlich. Du kannst sie jederzeit wieder aktivieren.",
"disableConfirmMessage": "Für Ihr Konto ist bei der Anmeldung dann kein zweiter Schritt mehr erforderlich. Sie können sie jederzeit wieder aktivieren.",
"disableConfirmButton": "Deaktivieren",
"disabledToast": "Zwei-Faktor-Authentifizierung deaktiviert.",
"codeRequired": "Ein Code ist erforderlich.",
"recoveryCodesWarning": "Speichere diese Wiederherstellungscodes jetzt. Jeder kann einmal verwendet werden, falls du den Zugriff auf deine Authenticator-App verlierst. Sie werden nicht erneut angezeigt.",
"recoveryCodesWarning": "Speichern Sie diese Wiederherstellungscodes jetzt. Jeder kann einmal verwendet werden, falls Sie den Zugriff auf Ihre Authenticator-App verlieren. Sie werden nicht erneut angezeigt.",
"recoveryCodesAck": "Ich habe meine Wiederherstellungscodes an einem sicheren Ort gespeichert.",
"copy": "Kopieren",
"copied": "Kopiert",
@@ -2475,9 +2481,9 @@
"download": ".txt herunterladen",
"done": "Fertig",
"setupFailed": "Zwei-Faktor-Einrichtung konnte nicht gestartet werden. Bitte versuche es erneut.",
"enableFailed": "Zwei-Faktor-Authentifizierung konnte nicht aktiviert werden. Prüfe den Code und versuche es erneut.",
"disableFailed": "Zwei-Faktor-Authentifizierung konnte nicht deaktiviert werden. Prüfe den Code und versuche es erneut.",
"regenerateFailed": "Wiederherstellungscodes konnten nicht neu erzeugt werden. Prüfe den Code und versuche es erneut.",
"enableFailed": "Zwei-Faktor-Authentifizierung konnte nicht aktiviert werden. Prüfen Sie den Code und versuchen Sie es erneut.",
"disableFailed": "Zwei-Faktor-Authentifizierung konnte nicht deaktiviert werden. Prüfen Sie den Code und versuchen Sie es erneut.",
"regenerateFailed": "Wiederherstellungscodes konnten nicht neu erzeugt werden. Prüfen Sie den Code und versuchen Sie es erneut.",
"recoveryCodesRemaining_one": "Noch {{count}} Wiederherstellungscode übrig."
},
"sso": {
@@ -2736,7 +2742,7 @@
"previewLayout": "Vorschau-Layout",
"livePreview": "Live-Vorschau",
"pdfTypography": "PDF-Schriftart",
"pdfTypographyHelp": "Wird für Rechnungs- und Angebotsbriefköpfe verwendet. Wähle eine der mitgelieferten Schriftarten aus oder belasse den Standard, um Helvetica zu verwenden.",
"pdfTypographyHelp": "Wird für Rechnungs- und Angebotsbriefköpfe verwendet. Wählen Sie eine der mitgelieferten Schriftarten aus oder belassen Sie den Standard, um Helvetica zu verwenden.",
"pdfFontFamily": "Fließtext-Schrift",
"pdfFontFamilyDefault": "Helvetica verwenden (Standard)",
"pdfFontFamilyMissing": "nicht verfügbar",
@@ -3130,6 +3136,7 @@
"noRecentActivity": "Keine aktuellen Aktivitäten",
"photos": {
"hidden": "Versteckt",
"hiddenTooltip": "Für Gäste ausgeblendet — dieses Foto erscheint nicht in der Kundengalerie.",
"hideSelected": "Ausblenden",
"showSelected": "Einblenden",
"hiddenSuccess": "Fotos für Gäste ausgeblendet",
@@ -3152,18 +3159,18 @@
"downloads": "Downloads",
"likes": "Likes"
},
"myMarks": "Deine Markierungen",
"myMarksHelp": "Nur für dich sichtbar. Sie erscheinen nie in der Kundengalerie und werden als XMP nach Lightroom exportiert.",
"myRating": "Deine Bewertung",
"clearRating": "Deine Bewertung entfernen",
"myMarks": "Ihre Markierungen",
"myMarksHelp": "Nur für Sie sichtbar. Sie erscheinen nie in der Kundengalerie und werden als XMP nach Lightroom exportiert.",
"myRating": "Ihre Bewertung",
"clearRating": "Ihre Bewertung entfernen",
"rateStars": "Mit {{count}} Sternen bewerten",
"markError": "Markierung konnte nicht gespeichert werden",
"yourMarkColor": "Deine Markierung: {{color}}",
"yourMarkRating": "Deine Bewertung: {{count}}",
"yourMarkColor": "Ihre Markierung: {{color}}",
"yourMarkRating": "Ihre Bewertung: {{count}}",
"rateStars_one": "Mit {{count}} Stern bewerten",
"rateStars_other": "Mit {{count}} Sternen bewerten",
"yourMarkRating_one": "Deine Bewertung: {{count}}",
"yourMarkRating_other": "Deine Bewertung: {{count}}"
"yourMarkRating_one": "Ihre Bewertung: {{count}}",
"yourMarkRating_other": "Ihre Bewertung: {{count}}"
},
"events": {
"tabs": {
@@ -3488,9 +3495,9 @@
"confirmDelete": "Alle erkannten Personen und Gesichtsdaten dieser Galerie löschen? Dies kann nicht rückgängig gemacht werden. Die Fotos selbst bleiben unverändert.",
"autoCategories": "Fotos automatisch in Kategorien einsortieren",
"autoCategoriesHint": "Nutzt die Anzahl der Gesichter, um Fotos als Details, Porträts, Kleine Gruppen oder Gruppen abzulegen. Gilt für alle Galerien, füllt ausschließlich leere Kategorien und ändert niemals eine von Ihnen gesetzte.",
"sidecarUnreachable": "Der Gesichtserkennungs-Dienst unter {{url}} ist nicht erreichbar, deshalb werden die {{pending}} eingereihten Fotos nicht verarbeitet. Es geht nichts verloren — der Scan läuft von selbst weiter, sobald der Dienst wieder da ist. Starte ihn mit `docker compose --profile faces up -d`; er beendet sich sofort, wenn FACE_ML_TOKEN nicht auf denselben Wert wie im Backend gesetzt ist — einen Standardwert gibt es nicht.",
"sidecarUnauthorized": "Der Gesichtserkennungs-Dienst unter {{url}} weist unser Token zurück; Fotos werden als fehlgeschlagen markiert statt erneut versucht. Setze FACE_ML_TOKEN im Backend und im picpeak-ml-Container identisch, starte beide neu und nutze dann Neu scannen — das Token allein zu korrigieren verarbeitet die bereits fehlgeschlagenen Fotos nicht erneut.",
"sidecarRejected": "{{url}} antwortet, aber nicht wie der Gesichtserkennungs-Dienst — Fotos werden als fehlgeschlagen markiert statt erneut versucht. Prüfe, ob FACE_ML_URL auf den picpeak-ml-Container zeigt und kein Proxy dazwischenliegt, und nutze dann Neu scannen für die bereits fehlgeschlagenen Fotos.",
"sidecarUnreachable": "Der Gesichtserkennungs-Dienst unter {{url}} ist nicht erreichbar, deshalb werden die {{pending}} eingereihten Fotos nicht verarbeitet. Es geht nichts verloren — der Scan läuft von selbst weiter, sobald der Dienst wieder da ist. Starten Sie ihn mit `docker compose --profile faces up -d`; er beendet sich sofort, wenn FACE_ML_TOKEN nicht auf denselben Wert wie im Backend gesetzt ist — einen Standardwert gibt es nicht.",
"sidecarUnauthorized": "Der Gesichtserkennungs-Dienst unter {{url}} weist unser Token zurück; Fotos werden als fehlgeschlagen markiert statt erneut versucht. Setzen Sie FACE_ML_TOKEN im Backend und im picpeak-ml-Container identisch, starten Sie beide neu und nutzen Sie dann Neu scannen — das Token allein zu korrigieren verarbeitet die bereits fehlgeschlagenen Fotos nicht erneut.",
"sidecarRejected": "{{url}} antwortet, aber nicht wie der Gesichtserkennungs-Dienst — Fotos werden als fehlgeschlagen markiert statt erneut versucht. Prüfen Sie, ob FACE_ML_URL auf den picpeak-ml-Container zeigt und kein Proxy dazwischenliegt, und nutzen Sie dann Neu scannen für die bereits fehlgeschlagenen Fotos.",
"sidecarStateNow": "Aktueller Zustand des Dienstes — einige der Fehler oben können eine andere Ursache haben, aber ein erneuter Scan wird erst nach der Behebung erfolgreich sein:",
"consolidated_one": "Beim letzten Scan wurde {{count}} ähnliches Paar automatisch zusammengeführt. Prüfen Sie es unter „Personen verwalten“ — falsch Zusammengeführtes lässt sich mit „Trennen“ wieder aufteilen.",
"consolidated_other": "Beim letzten Scan wurden {{count}} ähnliche Paare automatisch zusammengeführt. Prüfen Sie sie unter „Personen verwalten“ — falsch Zusammengeführtes lässt sich mit „Trennen“ wieder aufteilen.",
@@ -3811,7 +3818,7 @@
"billing": "Rechnungen"
},
"featureOff": "Funktion aus",
"featureOffTooltip": "Die zugehörige Funktion ist derzeit deaktiviert. Du kannst die Vorlage trotzdem bearbeiten — sie wird verwendet, sobald die Funktion wieder aktiviert ist.",
"featureOffTooltip": "Die zugehörige Funktion ist derzeit deaktiviert. Sie können die Vorlage trotzdem bearbeiten — sie wird verwendet, sobald die Funktion wieder aktiviert ist.",
"subcategories": {
"gallery": "Galerien",
"admin": "Admin-Konten",
@@ -3959,7 +3966,7 @@
},
"migrationBanner": {
"title": "Die Image-Registry von PicPeak wurde verschoben",
"body": "Aktualisiere deine docker-compose.yml, um Images von {{newPath}} zu beziehen — der alte Pfad wird nicht mehr aktualisiert.",
"body": "Aktualisieren Sie Ihre docker-compose.yml, um Images von {{newPath}} zu beziehen — der alte Pfad wird nicht mehr aktualisiert.",
"link": "Migrationshinweise ansehen"
},
"passwordChange": {
@@ -4211,7 +4218,7 @@
"fiveStarsOnly": "Nur 5 Sterne",
"colorLabels": "Farbmarkierungen",
"showOnlyColor": "Nur {{color}} anzeigen",
"myColorLabels": "Deine Markierungen",
"myColorLabels": "Ihre Markierungen",
"showOnlyMyColor": "Nur meine Markierungen in {{color}} anzeigen"
},
"export": {
@@ -4235,7 +4242,7 @@
},
"markSource": "XMP-Sterne & -Farbe aus",
"markSourceClient": "Kundenauswahl",
"markSourceMine": "Deine Markierungen"
"markSourceMine": "Ihre Markierungen"
},
"slideshow": {
"adminTitle": "Live-Diashow",
@@ -4420,12 +4427,12 @@
},
"community": {
"subtitle": "Alles bereit",
"mission": "PicPeak gibt es, damit Fotografinnen und Fotografen ihre Galerien und Kundendaten selbst besitzen — auf dem eigenen Server, ohne monatliche SaaS-Gebühren. Danke, dass du es ausprobierst.",
"mission": "PicPeak gibt es, damit Fotografinnen und Fotografen ihre Galerien und Kundendaten selbst besitzen — auf dem eigenen Server, ohne monatliche SaaS-Gebühren. Danke, dass Sie es ausprobieren.",
"bugTitle": "Fehler gefunden?",
"bugDesc": "Melde ihn und wir sehen ihn uns an.",
"bugDesc": "Melden Sie ihn und wir sehen ihn uns an.",
"featureTitle": "Wunschfunktion?",
"featureDesc": "Sag uns, was PicPeak für dich besser machen würde.",
"starTitle": "Gefällt es dir? Erzähl davon",
"featureDesc": "Sagen Sie uns, was PicPeak für Sie besser machen würde.",
"starTitle": "Gefällt es Ihnen? Erzählen Sie davon",
"starDesc": "Empfiehl es anderen Fotografen, teile es und gib dem Projekt einen Stern auf GitHub.",
"supportTitle": "Entwicklung unterstützen",
"supportDesc": "Optional — spendier mir einen Kaffee, um PicPeak am Leben zu halten.",
@@ -4483,8 +4490,8 @@
"devModeHint": "Entwicklungsmodus: E-Mail: admin@example.com, Passwort: admin123",
"mfa": {
"title": "Zwei-Faktor-Authentifizierung",
"subtitle": "Gib den 6-stelligen Code aus deiner Authenticator-App ein.",
"recoverySubtitle": "Gib einen deiner Wiederherstellungscodes ein.",
"subtitle": "Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein.",
"recoverySubtitle": "Geben Sie einen Ihrer Wiederherstellungscodes ein.",
"codeLabel": "Authentifizierungscode",
"codePlaceholder": "123456",
"recoveryCodeLabel": "Wiederherstellungscode",
@@ -4493,11 +4500,11 @@
"back": "Zurück",
"useRecoveryCode": "Stattdessen Wiederherstellungscode verwenden",
"useAuthenticator": "Stattdessen Authenticator-App verwenden",
"codeRequired": "Gib deinen Authentifizierungscode ein",
"codeRequired": "Geben Sie Ihren Authentifizierungscode ein",
"invalidCode": "Ungültiger Code. Bitte versuche es erneut.",
"sessionExpired": "Deine Bestätigungssitzung ist abgelaufen. Bitte melde dich erneut an.",
"locked": "Konto wegen zu vieler Versuche vorübergehend gesperrt. Versuche es später erneut.",
"lockedRetry": "Konto vorübergehend gesperrt. Versuche es in {{seconds}} Sekunden erneut."
"sessionExpired": "Ihre Bestätigungssitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
"locked": "Konto wegen zu vieler Versuche vorübergehend gesperrt. Versuchen Sie es später erneut.",
"lockedRetry": "Konto vorübergehend gesperrt. Versuchen Sie es in {{seconds}} Sekunden erneut."
},
"ssoDivider": "oder",
"ssoSignIn": "Mit SSO anmelden",
@@ -4575,9 +4582,9 @@
"generalError": "Anmeldung fehlgeschlagen. Bitte erneut versuchen.",
"acceptedToast": "Konto bereit — bitte anmelden.",
"adminHint": "Sie suchen das Admin-Panel? Besuchen Sie /admin/login.",
"emailPlaceholder": "du@beispiel.de",
"passwordPlaceholder": "Dein Passwort",
"needHelp": "Brauchst du Hilfe?"
"emailPlaceholder": "name@beispiel.de",
"passwordPlaceholder": "Ihr Passwort",
"needHelp": "Brauchen Sie Hilfe?"
},
"acceptInvite": {
"title": "Konto einrichten",
@@ -4596,7 +4603,7 @@
"alreadyExists": "Es existiert bereits ein Konto mit dieser E-Mail. Bitte melden Sie sich stattdessen an.",
"invalidSubmission": "Konto konnte nicht erstellt werden.",
"generalError": "Konto konnte nicht erstellt werden. Bitte erneut versuchen.",
"subtitle": "Bestätige oder ergänze deine Daten. Du kannst alles später im Profil bearbeiten.",
"subtitle": "Bestätigen oder ergänzen Sie Ihre Daten. Sie können alles später im Profil bearbeiten.",
"displayName": "Anzeigename",
"section": {
"personal": "Persönlich",
@@ -4606,8 +4613,8 @@
}
},
"dashboard": {
"title": "Deine Galerien",
"subtitle": "Klicke auf eine Galerie, um sie zu öffnen. Der Download-Button bündelt alle Fotos als ZIP.",
"title": "Ihre Galerien",
"subtitle": "Klicken Sie auf eine Galerie, um sie zu öffnen. Der Download-Button bündelt alle Fotos als ZIP.",
"loadError": "Galerien konnten nicht geladen werden. Bitte erneut versuchen.",
"emptyTitle": "Noch keine Galerien",
"emptyBody": "Sobald Ihr Fotograf Sie einer Galerie zuweist, erscheint sie hier.",
@@ -4651,9 +4658,9 @@
},
"quotes": {
"title": "Angebote",
"subtitle": "Alle Angebote, die dir dein Fotograf gesendet hat. Klicke auf ein offenes Angebot, um es anzunehmen oder abzulehnen.",
"subtitle": "Alle Angebote, die Ihnen Ihr Fotograf gesendet hat. Klicken Sie auf ein offenes Angebot, um es anzunehmen oder abzulehnen.",
"empty": "Noch keine Angebote.",
"disabled": "Diese Funktion ist für dein Konto aktuell deaktiviert. Bitte wende dich an deinen Fotografen, falls du hier Angebote erwartest.",
"disabled": "Diese Funktion ist für Ihr Konto aktuell deaktiviert. Bitte wenden Sie sich an Ihren Fotografen, falls Sie hier Angebote erwarten.",
"loadError": "Angebote konnten nicht geladen werden.",
"noEventName": "—",
"openToRespond": "Öffnen zum Antworten",
@@ -4662,9 +4669,9 @@
},
"bills": {
"title": "Rechnungen",
"subtitle": "Alle Rechnungen, die dir dein Fotograf gesendet hat. Klicke auf „PDF anzeigen“, um sie herunterzuladen.",
"subtitle": "Alle Rechnungen, die Ihnen Ihr Fotograf gesendet hat. Klicken Sie auf „PDF anzeigen“, um sie herunterzuladen.",
"empty": "Noch keine Rechnungen.",
"disabled": "Diese Funktion ist für dein Konto aktuell deaktiviert. Bitte wende dich an deinen Fotografen, falls du hier Rechnungen erwartest.",
"disabled": "Diese Funktion ist für Ihr Konto aktuell deaktiviert. Bitte wenden Sie sich an Ihren Fotografen, falls Sie hier Rechnungen erwarten.",
"loadError": "Rechnungen konnten nicht geladen werden.",
"viewPdf": "PDF anzeigen",
"popupBlocked": "Erlaube Pop-ups für diese Seite, um das Rechnungs-PDF anzuzeigen.",
@@ -4701,7 +4708,7 @@
},
"profile": {
"title": "Kundenprofil",
"subtitle": "Halte Kontakt- und Rechnungsdaten aktuell — sie erscheinen auf Angeboten und Rechnungen, sobald diese Funktionen live sind.",
"subtitle": "Halten Sie Kontakt- und Rechnungsdaten aktuell — sie erscheinen auf Angeboten und Rechnungen, sobald diese Funktionen live sind.",
"savedToast": "Profil gespeichert",
"saveError": "Profil konnte nicht gespeichert werden.",
"loadError": "Profil konnte nicht geladen werden.",
@@ -4721,12 +4728,12 @@
},
"field": {
"email": "E-Mail (Login)",
"emailHint": "Wende dich an deinen Fotografen, wenn du deine Login-E-Mail ändern möchtest.",
"emailHint": "Wenden Sie sich an Ihren Fotografen, wenn Sie Ihre Login-E-Mail ändern möchten.",
"salutation": "Anrede",
"firstName": "Vorname",
"lastName": "Nachname",
"displayName": "Anzeigename",
"displayNameHint": "So begrüßen wir dich im Dashboard.",
"displayNameHint": "So begrüßen wir Sie im Dashboard.",
"phone": "Telefon",
"companyName": "Firmenname",
"vatId": "USt-IdNr.",
@@ -4753,7 +4760,7 @@
},
"resetPassword": {
"title": "Passwort zurücksetzen",
"invalidToken": "Dieser Link ist ungültig oder abgelaufen. Bitte fordere bei deinem Fotografen einen neuen an.",
"invalidToken": "Dieser Link ist ungültig oder abgelaufen. Bitte fordern Sie bei Ihrem Fotografen einen neuen an.",
"forEmail": "Neues Passwort wird gesetzt für ",
"password": "Neues Passwort",
"confirm": "Neues Passwort bestätigen",
@@ -4767,13 +4774,13 @@
},
"contracts": {
"title": "Verträge",
"disabled": "Diese Funktion ist für dein Konto aktuell deaktiviert.",
"disabled": "Diese Funktion ist für Ihr Konto aktuell deaktiviert.",
"loadError": "Verträge konnten nicht geladen werden.",
"subtitle": "Alle Verträge, die dir dein Fotograf gesendet hat. Öffne bei Verträgen, die noch auf deine Unterschrift warten, den Signatur-Link — oder lade das unterschriebene PDF herunter, sobald beide Seiten unterschrieben haben.",
"subtitle": "Alle Verträge, die Ihnen Ihr Fotograf gesendet hat. Öffnen Sie bei Verträgen, die noch auf Ihre Unterschrift warten, den Signatur-Link — oder laden Sie das unterschriebene PDF herunter, sobald beide Seiten unterschrieben haben.",
"empty": "Noch keine Verträge.",
"popupBlocked": "Erlaube Pop-ups für diese Seite, um das Vertrags-PDF herunterzuladen.",
"issued": "Ausgestellt",
"signedByYou": "Von dir unterschrieben",
"signedByYou": "Von Ihnen unterschrieben",
"counterSigned": "Gegengezeichnet",
"openSign": "Öffnen & unterschreiben",
"downloadSigned": "Unterschriebenes PDF herunterladen",
@@ -4910,7 +4917,7 @@
"invalidEmail": "Bitte geben Sie eine gültige E-Mail ein",
"showPrefill": "+ Kontaktdaten hinzufügen (optional)",
"hidePrefill": " Kontaktdaten ausblenden",
"prefillHint": "Alles, was du ausfüllst, wird auf der Anmeldeseite des Kunden vorausgefüllt — er kann es weiterhin bearbeiten."
"prefillHint": "Alles, was Sie ausfüllen, wird auf der Anmeldeseite des Kunden vorausgefüllt — er kann es weiterhin bearbeiten."
},
"invitations": {
"empty": "Keine offenen Einladungen.",
@@ -4975,7 +4982,7 @@
"button": "Passwort-Reset-E-Mail senden",
"success": "Passwort-Reset-E-Mail gesendet",
"error": "Passwort-Reset konnte nicht gesendet werden",
"inactive": "Aktiviere den Kunden, bevor du einen Reset sendest."
"inactive": "Aktivieren Sie den Kunden, bevor Sie einen Reset senden."
},
"manageEvents": "Galerien verwalten",
"quotesSection": "Angebote",
@@ -5034,12 +5041,12 @@
},
"assignedEvents": {
"title": "Zugewiesene Galerien verwalten",
"subtitle": "Wähle jede Galerie aus, auf die dieser Kunde über sein Dashboard zugreifen können soll. Eine hier entfernte Galerie wird beim nächsten Aufruf des Kunden sofort gesperrt.",
"subtitle": "Wählen Sie jede Galerie aus, auf die dieser Kunde über sein Dashboard zugreifen können soll. Eine hier entfernte Galerie wird beim nächsten Aufruf des Kunden sofort gesperrt.",
"currentLabel": "Zugewiesene Galerien",
"empty": "Noch keine Galerien zugewiesen. Suche unten, um eine hinzuzufügen.",
"empty": "Noch keine Galerien zugewiesen. Suchen Sie unten, um eine hinzuzufügen.",
"searchLabel": "Galerie hinzufügen",
"searchPlaceholder": "Nach Eventname suchen",
"searchHint": "Beginne zu tippen, um Galerien zu finden.",
"searchHint": "Beginnen Sie zu tippen, um Galerien zu finden.",
"noResults": "Keine passenden Galerien.",
"removeAria": "{{name}} entfernen",
"save": "Zuweisungen speichern",
@@ -5061,7 +5068,7 @@
"navAriaLabel": "CRM-Navigation",
"empty": {
"title": "Keine CRM-Funktionen aktiviert",
"body": "Aktiviere „Konten\" (oder eine andere CRM-Unterfunktion) unter Einstellungen → Funktionen, um loszulegen."
"body": "Aktivieren Sie „Konten\" (oder eine andere CRM-Unterfunktion) unter Einstellungen → Funktionen, um loszulegen."
},
"subnav": {
"overview": "Übersicht",
@@ -5081,7 +5088,7 @@
"navAriaLabel": "Buchhaltungs-Navigation",
"empty": {
"title": "Keine Buchhaltungsfunktionen aktiviert",
"body": "Aktiviere die Steuerliste (oder eine andere Buchhaltungs-Unterfunktion) unter Einstellungen → Funktionen, um loszulegen."
"body": "Aktivieren Sie die Steuerliste (oder eine andere Buchhaltungs-Unterfunktion) unter Einstellungen → Funktionen, um loszulegen."
},
"subnav": {
"incomingInvoices": "Eingangsrechnungen",
@@ -5103,8 +5110,8 @@
"duplikat": "Duplikat",
"abgelehnt": "Abgelehnt",
"help": {
"rebill": "Eigene Lieferantenkosten, die du an einen Kunden weiterverrechnest — meist mit Zuschlag. Wird als Kosten und als weiterverrechneter Ertrag gebucht.",
"durchlaufend": "Ein Betrag, den du nur im Namen des Kunden vorstreckst und exakt durchreichst — kein Zuschlag, MwSt-neutral (durchlaufender Posten). Kunde zuordnen, um ihn zum Selbstkostenpreis weiterzuverrechnen.",
"rebill": "Eigene Lieferantenkosten, die Sie an einen Kunden weiterverrechnen — meist mit Zuschlag. Wird als Kosten und als weiterverrechneter Ertrag gebucht.",
"durchlaufend": "Ein Betrag, den Sie nur im Namen des Kunden vorstrecken und exakt durchreichen — kein Zuschlag, MwSt-neutral (durchlaufender Posten). Kunde zuordnen, um ihn zum Selbstkostenpreis weiterzuverrechnen.",
"eigener_aufwand": "Eigene Kosten, die nicht weiterverrechnet werden. Kategorie wählen, damit der Posten richtig in der Erfolgsrechnung landet.",
"duplikat": "Duplikat einer bereits erfassten Rechnung — wird nicht verbucht.",
"abgelehnt": "Dokument ablehnen — wird nicht verbucht."
@@ -5164,7 +5171,7 @@
"passthroughCustomerHint": "Optional — einen Kunden zuordnen, um diese durchlaufende Position weiterzuverrechnen; leer lassen, um sie nur auf das Event zu buchen.",
"supplierCountry": "Lieferantenland",
"supplierCountryNone": "— unbekannt —",
"supplierCountryHint": "Setzt die Steuerbehandlung automatisch: ausserhalb deiner Vorsteuer-Länder → ausländische MwSt (nicht abziehbar)."
"supplierCountryHint": "Setzt die Steuerbehandlung automatisch: ausserhalb Ihrer Vorsteuer-Länder → ausländische MwSt (nicht abziehbar)."
}
},
"expenseStatus": {
@@ -5186,7 +5193,7 @@
"company": "Firma",
"event": "Event",
"eventId": "Event-ID",
"inboundHint": "Auf welches Event diese Kosten in Auswertungen & Steuerexport entfallen (Firma = allgemeiner Aufwand). Unabhängig davon, an wen du weiterverrechnest."
"inboundHint": "Auf welches Event diese Kosten in Auswertungen & Steuerexport entfallen (Firma = allgemeiner Aufwand). Unabhängig davon, an wen Sie weiterverrechnen."
},
"incoming": {
"triageTitle": "Eingangsrechnung kategorisieren",
@@ -5279,7 +5286,7 @@
"title": "Projektübersicht",
"subtitle": "Fasse Events zu Projekten zusammen und sieh jede E-Mail, jedes Dokument, jede Galerie und jede Stunde in einem Cockpit.",
"search": "Nach Name oder Kunde suchen…",
"empty": "Noch keine Projekte. Erstelle oben eines, oder vorhandene Events wurden automatisch gruppiert.",
"empty": "Noch keine Projekte. Erstellen Sie oben eines, oder vorhandene Events wurden automatisch gruppiert.",
"notFound": "Projekt nicht gefunden",
"backToList": "Alle Projekte",
"noCustomer": "Kein einzelner Kunde",
@@ -5407,7 +5414,7 @@
"pickPlaceholder": "— Kunde wählen —",
"emptyList": "Noch keine Kunden mit aktivierter Stundenerfassung. Aktivieren Sie „Stundenerfassung“ zuerst auf der Kundendetailseite.",
"searchPlaceholder": "Nach E-Mail oder Firma suchen…",
"customerLoggingDisabled": "Bei diesem Kunden ist die Stundenerfassung deaktiviert. Aktiviere sie auf der Kundendetailseite, um Stunden zu erfassen.",
"customerLoggingDisabled": "Bei diesem Kunden ist die Stundenerfassung deaktiviert. Aktivieren Sie sie auf der Kundendetailseite, um Stunden zu erfassen.",
"openHours": {
"title": "Offene Stunden über alle Kunden",
"subtitle": "Noch nicht abgerechnete Zeitblöcke. Oben einen Kunden wählen oder auf eine Zeile klicken, um Details zu öffnen.",
@@ -5424,7 +5431,7 @@
"warning": "Diese Werkzeuge lösen echte Nebeneffekte aus (E-Mails, Statusänderungen). Nur gegen Testdaten verwenden.",
"envDisabled": {
"title": "Entwicklungs-Tools durch Umgebungsschutz blockiert",
"body": "Setze PICPEAK_ENABLE_DEV_TOOLS=1 in der Backend-Env-Datei und starte den Server neu. Der Funktions-Schalter allein reicht nicht — dies ist eine bewusste Produktiv-Schutzschicht.",
"body": "Setzen Sie PICPEAK_ENABLE_DEV_TOOLS=1 in der Backend-Env-Datei und starten Sie den Server neu. Der Funktions-Schalter allein reicht nicht — dies ist eine bewusste Produktiv-Schutzschicht.",
"cardHint": "E-Mail-Vorlagen können nicht geladen werden, solange der Entwicklungs-Tools-Umgebungsschutz nicht geöffnet ist."
},
"paymentCheck": {
@@ -5514,7 +5521,7 @@
}
},
"ledger": {
"intro": "Dient nur dem Treuhänder-Export — picpeak führt keine doppelte Buchhaltung. Der vorbelegte Kontenplan + die MWST-Codes folgen dem Schweizer/LI KMU-Kontenrahmen; passe sie an das Setup deines Treuhänders an.",
"intro": "Dient nur dem Treuhänder-Export — picpeak führt keine doppelte Buchhaltung. Der vorbelegte Kontenplan + die MWST-Codes folgen dem Schweizer/LI KMU-Kontenrahmen; passen Sie sie an das Setup Ihres Treuhänders an.",
"settingsSaved": "Zuordnungen gespeichert.",
"saveDefaults": "Zuordnungen speichern",
"accountType": {
@@ -5581,7 +5588,7 @@
},
"export": {
"title": "Treuhänder-Export",
"intro": "Lade das Sammeljournal (Erträge + Kosten als periodengerechte Buchungssätze mit Konto- und MWST-Codes) zum Import in die Buchhaltungssoftware deines Treuhänders herunter.",
"intro": "Laden Sie das Sammeljournal (Erträge + Kosten als periodengerechte Buchungssätze mit Konto- und MWST-Codes) zum Import in die Buchhaltungssoftware Ihres Treuhänders herunter.",
"format": "Zielsoftware",
"format_generic": "Generisch (alle Spalten)",
"format_banana": "Banana — doppelte Buchhaltung",
@@ -5589,7 +5596,7 @@
"format_bexio": "bexio",
"download": "CSV herunterladen",
"failed": "Export fehlgeschlagen.",
"disclaimer": "Nur periodengerecht (Belegdaten) — Zahlungen/Bankbewegungen sind nicht enthalten. Konto- + MWST-Codes folgen deiner Kontenplan-Zuordnung. Den Import vor der Einreichung immer mit dem Treuhänder prüfen."
"disclaimer": "Nur periodengerecht (Belegdaten) — Zahlungen/Bankbewegungen sind nicht enthalten. Konto- + MWST-Codes folgen Ihrer Kontenplan-Zuordnung. Den Import vor der Einreichung immer mit dem Treuhänder prüfen."
}
},
"taxReport": {
@@ -5639,7 +5646,7 @@
"costs": "Ausgaben",
"result": "Ergebnis",
"vatPayable": "MWST-Zahllast (Umsatz- Vorsteuer)",
"vatUnconfigured": "Die MWST-Registrierung ist nicht konfiguriert, daher kann die MWST-Zahllast nicht berechnet werden. Lege sie unter Einstellungen → Buchhaltung fest."
"vatUnconfigured": "Die MWST-Registrierung ist nicht konfiguriert, daher kann die MWST-Zahllast nicht berechnet werden. Legen Sie sie unter Einstellungen → Buchhaltung fest."
},
"cost": {
"source": "Art",
@@ -6094,7 +6101,7 @@
"savedToast": "CRM-Einstellungen gespeichert.",
"title": "CRM-Einstellungen",
"subtitle": "Standardwerte für Angebote und Rechnungen.",
"emptyState": "Aktuell sind keine CRM-Funktionen aktiv. Aktiviere Angebote, Rechnungen oder Verträge unter Einstellungen → Features, damit die passenden Konfigurationen hier erscheinen.",
"emptyState": "Aktuell sind keine CRM-Funktionen aktiv. Aktivieren Sie Angebote, Rechnungen oder Verträge unter Einstellungen → Features, damit die passenden Konfigurationen hier erscheinen.",
"section": {
"quotes": "Angebote",
"invoices": "Rechnungen",
@@ -6168,7 +6175,7 @@
"crm_invoices_vat_note_text": {
"label": "MwSt.- / Freitext-Hinweis auf Rechnungen",
"placeholder": "z. B. Gemäß § 6 Abs. 1 Z 27 UStG 1994 wird keine Umsatzsteuer berechnet (Kleinunternehmer).",
"help": "Wird direkt unter der MwSt.-Zeile auf jeder Rechnungs-PDF gedruckt. Leer lassen zum Ausblenden. Bitte den genauen Wortlaut mit deinem Steuerberater abstimmen."
"help": "Wird direkt unter der MwSt.-Zeile auf jeder Rechnungs-PDF gedruckt. Leer lassen zum Ausblenden. Bitte den genauen Wortlaut mit Ihrem Steuerberater abstimmen."
},
"crm_invoices_skonto_percent_default": {
"label": "Standard-Skonto %"
@@ -6295,7 +6302,7 @@
"sectionTemplates": "Vorlagen",
"defaultLabel": "Standard (Auffang)",
"usesDefault": "Standard",
"usesDefaultTooltip": "Noch keine eigene Vorlage — dieser Anlass-Typ nutzt die Standard-Vorlage. Bearbeite + speichere hier, um eine eigene zu erstellen.",
"usesDefaultTooltip": "Noch keine eigene Vorlage — dieser Anlass-Typ nutzt die Standard-Vorlage. Bearbeiten und speichern Sie hier, um eine eigene zu erstellen.",
"willCreateOnSave": "Dieser Anlass-Typ verwendet die Standard-Vorlage. Die Felder unten sind aus der Standard-Vorlage vorbelegt; beim Speichern wird eine eigene Vorlage für diesen Anlass-Typ erstellt.",
"variablesHint": "Verfügbare Variablen: {{customer_name}}, {{event_name}}, {{event_date}}, {{event_type}}, {{days_before}}, {{business_name}} — werden beim Versand der E-Mail ersetzt.",
"subjectLabel": "Betreff",
@@ -6342,8 +6349,8 @@
"enableLabel": "In Teilzahlungen aufteilen",
"advancedToggle": "Dynamische Trigger verwenden",
"simpleToggle": "Einfache Datumsauswahl verwenden",
"simpleHint": "Jede Zeile wird an einem festen Datum versendet. Wechsle zu dynamischen Triggern für Pläne, die an das Anlassdatum oder die Lieferung gekoppelt sind.",
"advancedHint": "Wähle einen Trigger (Angebot angenommen, vor/nach Anlass, bei Lieferung, festes Datum) und einen Offset in Tagen. Trigger lösen sich neu auf, wenn sich das Anlassdatum später verschiebt.",
"simpleHint": "Jede Zeile wird an einem festen Datum versendet. Wechseln Sie zu dynamischen Triggern für Pläne, die an das Anlassdatum oder die Lieferung gekoppelt sind.",
"advancedHint": "Wählen Sie einen Trigger (Angebot angenommen, vor/nach Anlass, bei Lieferung, festes Datum) und einen Offset in Tagen. Trigger lösen sich neu auf, wenn sich das Anlassdatum später verschiebt.",
"percent": "%",
"label": "Bezeichnung",
"labelPlaceholder": "Anzahlung / vor Anlass …",
@@ -6630,8 +6637,8 @@
"send": "Senden",
"subject": "Betreff",
"optional": "optional",
"bodyHint": "Bearbeite die Nachricht frei — füge vor dem Senden an beliebiger Stelle eine Notiz ein.",
"sendsFromHint": "Wird von deiner konfigurierten Absenderadresse gesendet.",
"bodyHint": "Bearbeiten Sie die Nachricht frei — fügen Sie vor dem Senden an beliebiger Stelle eine Notiz ein.",
"sendsFromHint": "Wird von Ihrer konfigurierten Absenderadresse gesendet.",
"sentToast": "Nachricht gesendet.",
"sendFailed": "Nachricht konnte nicht gesendet werden.",
"onWrote": "Am",
@@ -6641,7 +6648,7 @@
"createNewDoc": "Neues {{label}} erstellen",
"existingDocs": "Oder ein bestehendes referenzieren",
"noExistingDocs": "Für diesen Kunden gibt es noch keine Dokumente.",
"galleryCreateOnly": "Galerien sind event-basiert — dies öffnet den Event-Editor, wo du den Kunden zuweisen kannst.",
"galleryCreateOnly": "Galerien sind event-basiert — dies öffnet den Event-Editor, wo Sie den Kunden zuweisen können.",
"syncOk": "Postfächer geprüft — {{count}} neu.",
"syncDisabled": "Eingehende E-Mails sind deaktiviert — unter Einstellungen → Funktionen aktivieren.",
"syncUnconfigured": "Zuerst ein Postfach unter Einstellungen → E-Mail konfigurieren.",
@@ -6684,7 +6691,7 @@
},
"roleEditor": {
"loading": "Rollen werden geladen…",
"subtitle": "Lege fest, was jede Rolle darf. Beginne mit einer Vorlage, indem du sie duplizierst, und passe die Berechtigungen an.",
"subtitle": "Legen Sie fest, was jede Rolle darf. Beginnen Sie mit einer Vorlage, indem Sie sie duplizieren, und passen Sie die Berechtigungen an.",
"newRole": "Neue Rolle",
"systemRole": "System",
"permCount": "{{count}} Berechtigungen",
@@ -6714,9 +6721,9 @@
"confirmDelete": {
"title": "Rolle löschen?",
"message": "Die Rolle \"{{name}}\" endgültig löschen? Dies kann nicht rückgängig gemacht werden.",
"hasUsers": "Weise die {{count}} Benutzer der Rolle \"{{name}}\" neu zu, bevor du sie löschst.",
"hasUsers_one": "Weise den {{count}} Benutzer der Rolle \"{{name}}\" neu zu, bevor du sie löschst.",
"hasUsers_other": "Weise die {{count}} Benutzer der Rolle \"{{name}}\" neu zu, bevor du sie löschst."
"hasUsers": "Weisen Sie die {{count}} Benutzer der Rolle \"{{name}}\" neu zu, bevor Sie sie löschen.",
"hasUsers_one": "Weisen Sie den {{count}} Benutzer der Rolle \"{{name}}\" neu zu, bevor Sie sie löschen.",
"hasUsers_other": "Weisen Sie die {{count}} Benutzer der Rolle \"{{name}}\" neu zu, bevor Sie sie löschen."
},
"permCount_one": "{{count}} Berechtigung",
"permCount_other": "{{count}} Berechtigungen",
+14 -7
View File
@@ -189,12 +189,16 @@
"eventSpecific": "(Event specific)",
"clickToUpload": "Click to upload or drag and drop",
"fileRequirements": "{{formats}} (max {{sizeLimit}}MB per file, {{limit}} files per upload)",
"videoSizeLimit": "Videos: max {{sizeLimit}}MB per file",
"selectedFiles": "Selected files",
"uploading": "Uploading...",
"transferring": "Transferring",
"processing": "Processing photos...",
"processingHint": "Files are uploaded. PicPeak is now generating thumbnails and reading metadata. You can leave this page — work continues in the background.",
"processingProgress": "{{complete}} of {{total}} done",
"processingFailed_one": "{{count}} photo could not be processed and will not appear in the gallery.",
"processingFailed_other": "{{count}} photos could not be processed and will not appear in the gallery.",
"processingStillRunning": "Your upload is still being processed — it will appear in the gallery shortly.",
"retryFailed": "Retry failed",
"uploadComplete": "Upload complete!",
"someFilesFailed": "Some files failed to upload",
@@ -1099,9 +1103,7 @@
"mode_custom": "Custom override for this event",
"mode_off": "Off (hide for this event)",
"placeholder": "Markdown content (e.g. **Special offer:** [book your next session](https://example.com))",
"preview": "Preview",
"mode_managed": "Managed (upload to PicPeak)",
"mode_reference": "Reference external folder"
"preview": "Preview"
},
"infoBanner": {
"title": "Info Banner",
@@ -1110,9 +1112,7 @@
"mode_custom": "Custom override for this event",
"mode_off": "Off (hide for this event)",
"placeholder": "Use the menu button in the top-left corner to filter the photos.",
"preview": "Preview",
"mode_managed": "Managed (upload to PicPeak)",
"mode_reference": "Reference external folder"
"preview": "Preview"
},
"ogShare": {
"title": "Use hero photo as social-share preview",
@@ -1147,7 +1147,8 @@
"removeAria": "Remove {{name}}",
"placeholder": "Search by email, name, or company",
"searching": "Searching…",
"noResults": "No matches. Invite this customer from Clients → Accounts first."
"noResultsCanCreate": "No matches. Use “Create new customer” to add one.",
"noResultsNoPermission": "No matches, and your role cannot create customers. Ask an administrator to add this customer."
}
},
"settings": {
@@ -1162,6 +1163,8 @@
"siteUrlEnvPinned": "Pinned by the FRONTEND_URL environment variable, which overrides this setting. Remove it from your .env (or container environment) and restart to manage the address here.",
"defaultExpiration": "Default Expiration (days)",
"maxFileSize": "Max File Size (MB)",
"maxVideoSize": "Max Video Size (MB)",
"maxVideoSizeHelp": "Separate per-file limit for video uploads, so photos can keep a smaller limit.",
"maxFilesPerUpload": "Max Files per Upload",
"maxFilesPerUploadHelp": "Maximum number of photos allowed in a single upload batch (1-{{max}}).",
"maxUploadBatchSize": "Max Upload Batch Size (MB)",
@@ -1528,6 +1531,9 @@
"custom": "Custom (paste your own script)"
},
"providerNoneInfo": "No external tracker injected. The admin dashboard still shows summary cards + the daily chart from PicPeak's own access_logs; the device-breakdown chart uses a coarse user-agent heuristic.",
"proxiedNotice": "Served from your own domain",
"proxiedNoticeText": "PicPeak loads the tracker script and forwards its events through its own domain, so no Content-Security-Policy or reverse-proxy change is needed — and ad blockers see first-party requests. Your tracker still gets each visitor's IP and user agent (forwarded as X-Forwarded-For), so device and location reporting keeps working. The URL must be reachable from the PicPeak server and resolve to a public address.",
"customOnlyCspWarningText": "PicPeak ships with a strict CSP (`script-src 'self'; connect-src 'self'`). Unlike the Umami and Rybbit options above, a pasted snippet is not proxied — add your tracker's domain to BOTH `script-src` (to load the script) and `connect-src` (for the events it sends) in your reverse-proxy or nginx CSP config, otherwise the browser silently blocks it.",
"rybbitUrl": "Rybbit URL",
"rybbitUrlHelp": "Your Rybbit instance URL — `https://app.rybbit.io` for the SaaS, or `https://rybbit.yourdomain.com` for self-hosted.",
"rybbitWebsiteId": "Site ID",
@@ -2662,6 +2668,7 @@
"noRecentActivity": "No recent activity",
"photos": {
"hidden": "Hidden",
"hiddenTooltip": "Hidden from guests — this photo is not shown in the client gallery.",
"hideSelected": "Hide",
"showSelected": "Show",
"hiddenSuccess": "Photos hidden from guests",