fix(i18n): make i18n:ci pass by fixing the extractor config

exit 1 -> exit 0.

Three findings, none of which matched the reported symptoms.

1. The two "unparseable .d.ts files" are not malformed. RestoreWizard.d.ts and
   BackupHistory.d.ts are valid declaration files sitting next to their .jsx
   implementations; i18next-cli feeds them to SWC as ordinary .ts modules with
   no ambient flag, where an uninitialised `const` is a hard syntax error. They
   should never have been scanned at all. Root cause is the input glob:
   i18next-cli passes `input` straight to `glob`, which does NOT honour
   `!`-prefixed negation inside the pattern list, so
   '!src/**/*.{test,spec,d}.{ts,tsx}' was a silent no-op and all four .d.ts
   files plus 57 test files were being scanned. Moved the exclusions to
   extract.ignore, where they take effect; the extracted key set is unchanged.

2. The "missing French keys" were not English-vs-French drift. The extractor
   wanted to add ~2771 keys to fr.json with value "" -- and src/i18n/config.ts
   does not set returnEmptyString, whose i18next default is true, so those
   empty strings would be returned as valid translations and render as blank
   UI rather than falling back to English. Filling nl/pt/ru/fr with ~11000
   empty strings would have been a worse regression than the failing check.
   The check was demanding parity for locales this project deliberately keeps
   partial, so `locales` is now ['en','de'] -- the two actually kept at parity.
   nl/pt/ru/fr join sl/es as hand-maintained partial locales on
   fallbackLng 'en'. No French was written.

3. de.json is a parity locale, and the extractor legitimately found 307 keys
   missing from both en and de (shipped t() calls never added to the locale
   files). Rather than accept 307 blank German strings these were written by
   hand: 105 are _one/_other variants derived from existing German bases with
   correct singular/plural, the rest translated against each section's register
   (Sie on admin/public-billing surfaces, du in the customer portal to match
   customer.quotes/customer.bills) reusing terms already established in de.json.
   Verified: 0 interpolation-placeholder mismatches between en and de across
   all 308 new keys, 0 empty and 0 key-shaped values remaining, and the diff is
   strictly additive (en +308, de +307, 0 removed, 0 changed).

removeUnusedKeys is now false, replacing the dead preservePatterns: []. It
wanted to delete ~355 live keys per locale across ~90 prefixes -- families
built at runtime (admin.activities.*, admin.notificationMessages.*,
projects.status.*) or held in constant tables the extractor cannot resolve
(AdminSidebar nameKey, CrmDevelopmentPage titleKey/descKey). Covering them
would need ~30 wildcards spanning most of the key space; disabling pruning is
the same behaviour, honestly stated, with the call sites named.

Refs testplan REPORT.md #22 (Part 1.3.03).
This commit is contained in:
Paul Nothaft
2026-09-01 17:07:07 +02:00
parent d16137bb2a
commit 5dbb43549c
3 changed files with 817 additions and 107 deletions
+21 -6
View File
@@ -3,19 +3,34 @@ import { typescriptPlugin } from "./scripts/i18nextExtractionHelper";
export default defineConfig({
locales: ['en', 'de', 'nl', 'pt', 'ru', 'fr'],
// Only the locales that are kept at full key parity are managed by the extractor.
// nl/pt/ru/fr/sl/es are deliberately partial and rely on `fallbackLng: 'en'`; letting
// the extractor own them would fill each file with ~2700 empty-string values, and
// i18next's default `returnEmptyString: true` renders those as blank UI instead of
// falling back to English.
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.{test,spec,d}.{ts,tsx}'],
input: ['src/**/*.{ts,tsx,js,jsx}'],
// `glob` (used by i18next-cli) ignores `!`-prefixed entries inside `input`,
// so exclusions have to live here or they are silently no-ops.
ignore: [
'src/**/*.{test,spec}.{ts,tsx,js,jsx}',
'src/**/__tests__/**',
'src/**/*.d.ts',
],
output: 'src/i18n/locales/{{language}}.json',
defaultNS: false,
primaryLanguage: 'en',
removeUnusedKeys: true,
// Dynamic keys to preserve (e.g.: t(`errors.${code}`))
preservePatterns: [],
// 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.
removeUnusedKeys: false,
preserveContextVariants: true,
+397 -50
View File
@@ -72,7 +72,23 @@
"confirmCancelInvitation": {
"title": "Einladung abbrechen",
"message": "Sind Sie sicher, dass Sie die Einladung für {{email}} abbrechen möchten?"
}
},
"userActivated": "Benutzer erfolgreich reaktiviert",
"activateUserError": "Fehler beim Reaktivieren des Benutzers",
"userDeleted": "Benutzer erfolgreich gelöscht",
"deleteUserError": "Fehler beim Löschen des Benutzers",
"activateUser": "Benutzer reaktivieren",
"deleteUser": "Benutzer endgültig löschen",
"confirmActivate": {
"title": "Benutzer reaktivieren?",
"message": "{{name}} reaktivieren? Die Anmeldung ist danach sofort wieder möglich."
},
"confirmDelete": {
"title": "Benutzer endgültig löschen?",
"message": "{{name}} endgültig löschen? Dies kann nicht rückgängig gemacht werden. Ausstehende Einladungen und API-Token werden entfernt; anderswo erstellte Datensätze bleiben erhalten, verlieren aber die Zuordnung."
},
"activate": "Reaktivieren",
"delete": "Endgültig löschen"
},
"systemHealth": {
"title": "Systemzustand",
@@ -159,7 +175,13 @@
"off": "aus",
"clear": "Zurücksetzen",
"saved": "Gespeichert",
"saveFailed": "Speichern fehlgeschlagen"
"saveFailed": "Speichern fehlgeschlagen",
"menu": "Menü",
"deleted": "Gelöscht.",
"actions": "Aktionen",
"copyFailed": "Kopieren in die Zwischenablage fehlgeschlagen.",
"searching": "Wird gesucht…",
"language": "Sprache"
},
"upload": {
"photoCategory": "Fotokategorie",
@@ -184,7 +206,11 @@
"kindProcessing": "Verarbeitung fehlgeschlagen",
"transferReason": "Übertragung fehlgeschlagen",
"unknownReason": "Unbekannter Fehler",
"unknownFile": "Unbekannte Datei"
"unknownFile": "Unbekannte Datei",
"toast_one": "{{count}} Datei konnte nicht hochgeladen werden siehe Liste unten.",
"toast_other": "{{count}} Dateien konnten nicht hochgeladen werden siehe Liste unten.",
"title_one": "{{count}} Datei konnte nicht hochgeladen werden",
"title_other": "{{count}} Dateien konnten nicht hochgeladen werden"
},
"replaceByName": "Vorhandene Fotos mit gleichem Namen ersetzen",
"uploadPhotos": "Fotos hochladen",
@@ -290,7 +316,9 @@
"recipients": "E-Mail-Adressen der Empfänger",
"recipientsPlaceholder": "[email protected], [email protected]",
"recipientsCount": "{{count}} Empfänger — jeder erhält den Download-Link",
"recipientsHint": "Mehrere Adressen durch Kommas trennen. Jeder Empfänger erhält den Download-Link."
"recipientsHint": "Mehrere Adressen durch Kommas trennen. Jeder Empfänger erhält den Download-Link.",
"recipientsCount_one": "{{count}} Empfänger — er erhält den Download-Link",
"recipientsCount_other": "{{count}} Empfänger — jeder erhält den Download-Link"
},
"picker": {
"title": "Bilder aus anderen Events auswählen",
@@ -302,7 +330,9 @@
"alreadyAdded": "Hinzugefügt",
"selectedCount": "{{count}} ausgewählt",
"addSelected": "Auswahl hinzufügen",
"lightbox": "Lightbox"
"lightbox": "Lightbox",
"selectedCount_one": "{{count}} ausgewählt",
"selectedCount_other": "{{count}} ausgewählt"
},
"public": {
"notFoundTitle": "Link nicht gefunden",
@@ -313,7 +343,9 @@
"limitBody": "Dieser Transfer hat die maximale Anzahl an Downloads erreicht.",
"availableUntil": "Verfügbar bis {{date}}",
"fileSummary": "{{count}} Dateien",
"downloadFile": "Herunterladen"
"downloadFile": "Herunterladen",
"fileSummary_one": "{{count}} Datei",
"fileSummary_other": "{{count}} Dateien"
},
"upload": {
"unavailableTitle": "Upload-Link nicht verfügbar",
@@ -325,7 +357,9 @@
"limits": "Bis zu {{files}} Dateien, je {{mb}} MB",
"tooBig": "Jede Datei darf höchstens {{mb}} MB groß sein",
"send": "{{count}} Dateien hochladen",
"failed": "Upload fehlgeschlagen. Bitte erneut versuchen."
"failed": "Upload fehlgeschlagen. Bitte erneut versuchen.",
"send_one": "{{count}} Datei hochladen",
"send_other": "{{count}} Dateien hochladen"
}
},
"workflows": {
@@ -469,7 +503,27 @@
"backup": {
"picpeak": {
"restoreIntro": "Laden Sie eine .picpeak-Datei von dieser oder einer anderen Instanz hoch. Die Wiederherstellung eines SQLite-Backups auf einer PostgreSQL-Instanz wird unterstützt (Upgrade-Pfad); ansonsten müssen die Datenbank-Engines übereinstimmen.",
"crossEngineNote": "Engine-übergreifende Wiederherstellung: Ein SQLite-Backup wurde auf diese PostgreSQL-Instanz übernommen."
"crossEngineNote": "Engine-übergreifende Wiederherstellung: Ein SQLite-Backup wurde auf diese PostgreSQL-Instanz übernommen.",
"downloadFailed": "Die Backup-Datei konnte nicht erstellt werden.",
"title": "Portables Backup (.picpeak)",
"intro": "Laden Sie eine einzelne, in sich geschlossene Datei herunter und laden Sie sie auf einer anderen Instanz wieder hoch, um diese zu klonen — alles direkt im Browser.",
"includePhotos": "Original-Galeriefotos einschließen (größere Datei)",
"secretsWarning": "Diese Datei enthält Geheimnisse im Klartext (E-Mail-Passwort, Admin-Zugangsdaten, API-Schlüssel). Bewahren Sie sie sicher auf und übertragen Sie sie nur über vertrauenswürdige Kanäle.",
"download": ".picpeak herunterladen",
"externalPathsFailed": "Backup wiederhergestellt, aber die externen Fotopfade konnten nicht umgestellt werden — diese Originale laden erst nach einem erneuten Versuch.",
"restoreDone": "Backup wiederhergestellt.",
"reloginRequired": "Wiederherstellung abgeschlossen — bitte melden Sie sich erneut an.",
"restoreFailed": "Wiederherstellung fehlgeschlagen.",
"restoreTitle": "Aus einer .picpeak-Datei wiederherstellen",
"chooseFile": ".picpeak-Datei auswählen…",
"restoreSummary": "{{tables}} Tabellen und {{files}} Dateien wiederhergestellt.",
"externalPathsFailedDetail": "Die externen Fotopfade wurden nicht umgestellt, deshalb ist derzeit kein referenziertes Original erreichbar. Führen Sie die Wiederherstellung oder die ausstehenden Migrationen erneut aus, sobald die Ursache behoben ist.",
"externalMediaNote": "Dieses Backup verweist auf eine externe Medienbibliothek. Stellen Sie sicher, dass das Routing für externe Medien auf dieser Instanz eingerichtet ist.",
"externalMediaLink": "Einrichtungsanleitung",
"reload": "App neu laden",
"confirmTitle": "Die Wiederherstellung löscht alle aktuellen Daten",
"confirmBody": "Dies ersetzt ALLE Daten dieser Instanz dauerhaft durch das hochgeladene Backup — ausgenommen Ihr aktuelles Konto. Das kann nicht rückgängig gemacht werden.",
"confirmRestore": "Löschen & wiederherstellen"
},
"title": "Backup-Verwaltung",
"subtitle": "Verwalten Sie System-Backups, konfigurieren Sie automatisierte Backups und stellen Sie vorherige Backups wieder her.",
@@ -623,6 +677,15 @@
"excellent": "Ausgezeichnet",
"good": "Gut",
"critical": "Kritisch"
},
"lastSuccessful": "Letztes erfolgreiches Backup",
"lastAttempt": "Letzter Versuch",
"zombieRuns_one": "{{count}} Backup läuft seit über 30 Min. — es könnte abgestürzt sein, ohne abzuschließen",
"zombieRuns_other": "{{count}} Backups laufen seit über 30 Min. — sie könnten abgestürzt sein, ohne abzuschließen",
"status": {
"completed": "Abgeschlossen",
"failed": "Fehlgeschlagen",
"running": "Läuft"
}
},
"configuration": {
@@ -700,7 +763,8 @@
"messages": {
"requiredFields": "Bitte füllen Sie alle erforderlichen Felder aus",
"connectionSuccess": "Verbindungstest erfolgreich!",
"connectionFailed": "Verbindungstest fehlgeschlagen"
"connectionFailed": "Verbindungstest fehlgeschlagen",
"invalidCron": "Bitte geben Sie einen gültigen Cron-Ausdruck ein (5 Felder)"
},
"testingConnection": "Teste Verbindung...",
"saveSettings": "Konfiguration speichern",
@@ -782,7 +846,15 @@
"encrypted": "Verschlüsseltes Backup",
"encryptedMessage": "Sie müssen die Verschlüsselungs-Passphrase angeben, um dieses Backup wiederherzustellen.",
"enterPassphrase": "Verschlüsselungs-Passphrase eingeben",
"at": "um"
"at": "um",
"filesOnlyHint": "Dieses Backup enthält keinen Datenbank-Dump — eine Wiederherstellung stellt die Datenbank NICHT wieder her (CRM-Daten, Kunden, Angebote, Rechnungen und Verträge sind danach leer).",
"filesOnlyBadge": "Keine DB",
"corruptHint": "Die Manifest-Datei ist nicht lesbar — das Backup könnte unvollständig oder beschädigt sein.",
"corruptBadge": "Beschädigt",
"filesOnlyWarning": {
"title": "Das gewählte Backup enthält keinen Datenbank-Dump",
"message": "Die Wiederherstellung dieses Backups stellt Dateien (Fotos, PDFs) wieder her, die Datenbank — inklusive Admin-Benutzer, Kunden, Angebote, Rechnungen, Verträge und Einstellungen — jedoch NICHT. Wählen Sie ein anderes Backup mit Datenbank-Dump, falls vorhanden, oder fahren Sie nur fort, wenn Sie ausschließlich die Dateien möchten."
}
},
"restoreTypes": {
"full": {
@@ -857,7 +929,12 @@
"title": "Wiederherstellung erfolgreich abgeschlossen",
"message": "Ihre Daten wurden wiederhergestellt. Bitte überprüfen Sie, ob alles korrekt funktioniert.",
"verifyIntegrity": "Dokumentintegrität jetzt prüfen"
}
},
"failedSubtitle": "Wiederherstellung fehlgeschlagen — siehe Fehler unten. Das Ziel wurde auf den Stand vor der Wiederherstellung zurückgesetzt.",
"idle": "Derzeit läuft keine Wiederherstellung.",
"errorTitle": "Wiederherstellung nicht abgeschlossen",
"errorUnknown": "Keine Fehlermeldung aufgezeichnet.",
"rolledBack": "Das Sicherheits-Backup von vor der Wiederherstellung wurde für den Rollback verwendet. Das Ziel befindet sich im Zustand vor der Wiederherstellung — ein erneuter Versuch ist gefahrlos möglich, sobald das Problem oben behoben ist."
},
"actions": {
"back": "Zurück",
@@ -1084,7 +1161,17 @@
"noMatches": "Niemand passt zu diesem Namen.",
"privacyNote": "Personen werden automatisch innerhalb dieser Galerie erkannt. Es werden keine Daten an externe Dienste gesendet.",
"inThisPhoto": "Auf diesem Foto:",
"downloadThese": "Diese {{count}} herunterladen"
"downloadThese": "Diese {{count}} herunterladen",
"unnamedCount_one": "{{count}} Foto",
"unnamedCount_other": "{{count}} Fotos",
"collapsedSummary_one": "{{count}} Person gefunden",
"collapsedSummary_other": "{{count}} Personen gefunden",
"showAll_one": "Alle {{count}} anzeigen",
"showAll_other": "Alle {{count}} anzeigen",
"matchCount_one": "{{count}} von {{total}} Fotos",
"matchCount_other": "{{count}} von {{total}} Fotos",
"downloadThese_one": "Dieses {{count}} Foto herunterladen",
"downloadThese_other": "Diese {{count}} Fotos herunterladen"
},
"colorFilter": "Farbe",
"filterByColor": "Nur {{color}} anzeigen",
@@ -1094,7 +1181,13 @@
"backToGallery": "Alle Fotos",
"downloadFolder": "Ordner herunterladen ({{count}})",
"downloadFolderCapped": "Erste {{limit}} von {{total}} herunterladen",
"downloadEverything": "Alle Fotos herunterladen"
"downloadEverything": "Alle Fotos herunterladen",
"downloadFolder_one": "Ordner herunterladen ({{count}})",
"downloadFolder_other": "Ordner herunterladen ({{count}})",
"folderPhotoCount_one": "{{count}} Foto",
"folderPhotoCount_other": "{{count}} Fotos",
"preparingProgress_one": "{{count}} Foto verpackt",
"preparingProgress_other": "{{count}} Fotos verpackt"
},
"categories": {
"title": "Fotokategorien",
@@ -1232,7 +1325,9 @@
"errors": {
"slugTaken": "Diese Kurz-URL ist bereits vergeben — versuche stattdessen {{suggested}}.",
"invalidSlug": "Kurz-URLs müssen aus Kleinbuchstaben, Ziffern und Bindestrichen bestehen (164 Zeichen)."
}
},
"hits_one": "{{count}} Aufruf",
"hits_other": "{{count}} Aufrufe"
},
"resetGalleryPassword": "Galerie-Passwort zurücksetzen",
"resendCreationEmail": "Erstellungs-E-Mail erneut senden",
@@ -1277,7 +1372,9 @@
"toastError": "Passwort zurücksetzen fehlgeschlagen",
"toastCopied": "Passwort in Zwischenablage kopiert",
"errorMinLength": "Das Passwort muss mindestens 6 Zeichen lang sein",
"errorMismatch": "Passwörter stimmen nicht überein"
"errorMismatch": "Passwörter stimmen nicht überein",
"hide": "Ausblenden",
"show": "Einblenden"
},
"galleryExpiration": "Galerie-Ablauf",
"daysAfterEvent": "Tage nach dem Veranstaltungsdatum",
@@ -1377,7 +1474,9 @@
"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.",
"descriptionQuiet": "Durch das Veröffentlichen wird \"{{eventName}}\" zugänglich. Es wird keine E-Mail versendet — Sie können sie später auf dieser Seite senden.",
"descriptionWhatsapp": "Durch das Veröffentlichen wird \"{{eventName}}\" zugänglich. Sofern WhatsApp eingerichtet ist, wird der Kunde dort benachrichtigt."
"descriptionWhatsapp": "Durch das Veröffentlichen wird \"{{eventName}}\" zugänglich. Sofern WhatsApp eingerichtet ist, wird der Kunde dort benachrichtigt.",
"descriptionAssignedAccount_one": "Die Galerie \"{{eventName}}\" wird zugänglich gemacht. Das zugewiesene Kundenkonto wird per E-Mail (in seiner Sprache) benachrichtigt, dass sie verfügbar ist.",
"descriptionAssignedAccount_other": "Die Galerie \"{{eventName}}\" wird zugänglich gemacht. Die zugewiesenen Kundenkonten werden per E-Mail (in ihrer Sprache) benachrichtigt, dass sie verfügbar ist."
},
"duplicateEvent": "Galerie duplizieren",
"duplicateDialog": {
@@ -1502,7 +1601,9 @@
"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"
"preview": "Vorschau",
"mode_managed": "Verwaltet (Upload nach PicPeak)",
"mode_reference": "Externen Ordner referenzieren"
},
"infoBanner": {
"title": "Info-Banner",
@@ -1511,7 +1612,9 @@
"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"
"preview": "Vorschau",
"mode_managed": "Verwaltet (Upload nach PicPeak)",
"mode_reference": "Externen Ordner referenzieren"
},
"ogShare": {
"title": "Heldenbild als Vorschau für geteilte Links verwenden",
@@ -1539,6 +1642,14 @@
"listHeading": "Zu archivierende Veranstaltungen:",
"submit_one": "{{count}} Veranstaltung archivieren",
"submit_other": "{{count}} Veranstaltungen archivieren"
},
"customerPicker": {
"help": "Hier hinzugefügte Kunden können sich unter /customer/login anmelden und diese Galerie ansehen, ohne das Galerie-Passwort einzugeben.",
"label": "Kundenkonten",
"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."
}
},
"settings": {
@@ -1647,7 +1758,8 @@
"capacityRequiredForAvailable": "Geben Sie zuerst eine Gesamtkapazität an, bevor Sie verfügbaren Speicher setzen.",
"availableExceedsCapacity": "Der verfügbare Speicher darf die Gesamtkapazität nicht überschreiten.",
"storageUsage": "Speichernutzung",
"unlimited": "Unbegrenzt"
"unlimited": "Unbegrenzt",
"catalogMeasurement": "Katalogisierte Größe — die Objekte liegen im konfigurierten S3-Bucket, nicht auf dieser Festplatte"
},
"security": {
"title": "Sicherheit",
@@ -1948,7 +2060,9 @@
"uniqueVisitors": "Eindeutige Besucher nach IP",
"deviceTypes": "Gerätetypen aus User-Agents",
"privacy": "Datenschutz",
"privacyText": "IP-Adressen werden aus Datenschutzgründen gehasht. Es werden keine persönlichen Daten gespeichert. Analysedaten werden 90 Tage aufbewahrt."
"privacyText": "IP-Adressen werden aus Datenschutzgründen gehasht. Es werden keine persönlichen Daten gespeichert. Analysedaten werden 90 Tage aufbewahrt.",
"umamiApiKey": "API-Schlüssel",
"umamiApiKeyHelp": "Optional. Nur für das Geräte-Diagramm im Analyse-Dashboard erforderlich. Erstellen in Umami → Settings → Profile → API Keys. Nach dem Speichern maskiert als •••••••• hinterlegt — den maskierten Wert stehen lassen, um den bestehenden Schlüssel zu behalten."
},
"groups": {
"general": "Allgemein",
@@ -2363,7 +2477,8 @@
"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."
"regenerateFailed": "Wiederherstellungscodes konnten nicht neu erzeugt werden. Prüfe den Code und versuche es erneut.",
"recoveryCodesRemaining_one": "Noch {{count}} Wiederherstellungscode übrig."
},
"sso": {
"title": "Single Sign-On (OIDC)",
@@ -2444,7 +2559,9 @@
"noneToFill": "Alle Fotos haben bereits ein Aufnahmedatum",
"resultSuccess": "Letzter Lauf: {{success}} aktualisiert, {{noExif}} ohne gefundenes Datum, {{failed}} nicht erreichbar",
"skipped": "{{count}} Foto(s) wurden während des Laufs anderweitig geändert und daher nicht aktualisiert.",
"description": "Trägt „Aufnahmedatum\" aus den EXIF-Daten nach, für Fotos die vor dieser Auswertung importiert wurden. Externe Importe haben nie eines gespeichert, dadurch sortieren diese Galerien nach Importreihenfolge statt nach Aufnahmezeit."
"description": "Trägt „Aufnahmedatum\" aus den EXIF-Daten nach, für Fotos die vor dieser Auswertung importiert wurden. Externe Importe haben nie eines gespeichert, dadurch sortieren diese Galerien nach Importreihenfolge statt nach Aufnahmezeit.",
"skipped_one": "{{count}} Foto wurde während des Laufs anderweitig geändert und daher nicht aktualisiert.",
"skipped_other": "{{count}} Fotos wurden während des Laufs anderweitig geändert und daher nicht aktualisiert."
},
"orientationBackfill": {
"title": "Fotoausrichtung",
@@ -2452,8 +2569,12 @@
"button": "Fotoausrichtung korrigieren",
"running": "Ausrichtung wird geprüft...",
"resultSuccess": "Letzter Lauf: {{checked}} geprüft, {{corrected}} korrigiert, {{requeued}} erneut zur Gesichtserkennung eingereiht, {{failed}} nicht erreichbar",
"staleTiers": "{{count}} zwischengespeicherte Größe(n) konnten nicht gelöscht werden und liefern weiterhin die alte Ausrichtung — bitte erneut ausführen, sobald der Speicher beschreibbar ist."
}
"staleTiers": "{{count}} zwischengespeicherte Größe(n) konnten nicht gelöscht werden und liefern weiterhin die alte Ausrichtung — bitte erneut ausführen, sobald der Speicher beschreibbar ist.",
"staleTiers_one": "{{count}} zwischengespeicherte Größe konnte nicht gelöscht werden und liefert weiterhin die alte Ausrichtung — bitte erneut ausführen, sobald der Speicher beschreibbar ist.",
"staleTiers_other": "{{count}} zwischengespeicherte Größen konnten nicht gelöscht werden und liefern weiterhin die alte Ausrichtung — bitte erneut ausführen, sobald der Speicher beschreibbar ist."
},
"saved": "Einstellungen gespeichert",
"saveError": "Einstellungen konnten nicht gespeichert werden"
},
"branding": {
"title": "Branding & Themen",
@@ -2818,7 +2939,25 @@
"copyAllCommands": "Alle Befehle kopieren",
"completeChecklist": "Checkliste vor dem Update ausfüllen",
"error": "Fehler beim Laden der Update-Anweisungen"
}
},
"viewDetails": "Versionshinweise und Update-Anleitung ansehen",
"registryMoved": {
"title": "Bezug aus der stillgelegten Image-Registry",
"body": "Dieses Update erreicht Sie erst, wenn Sie den Image-Pfad in der docker-compose.yml auf {{newPath}} ändern. Der alte Pfad antwortet weiterhin, deshalb wirkt `docker compose pull` erfolgreich, liefert aber denselben eingefrorenen Build.",
"link": "Migrationshinweise ansehen"
},
"copyFailed": "Kopieren in die Zwischenablage fehlgeschlagen.",
"modalTitle": "Update verfügbar",
"modalSubtitle": "v{{current}} → v{{latest}}",
"howToUpgrade": "So aktualisieren Sie",
"detectedEnv": "Erkannte Umgebung: {{env}}",
"copyCommand": "Befehl kopieren",
"releaseNotes": "Versionshinweise",
"changelogError": "Versionshinweise konnten nicht geladen werden. Bitte die Release-Seiten direkt auf GitHub prüfen.",
"noReleases": "Keine Versionshinweise verfügbar.",
"viewOnGitHub": "Auf GitHub ansehen",
"noNotes": "Keine Versionshinweise hinterlegt.",
"dismissUntilNext": "Bis zur nächsten Version ausblenden"
},
"notifications": "Benachrichtigungen",
"markAllRead": "Alle als gelesen markieren",
@@ -2966,7 +3105,11 @@
"feedbackModerated": "Feedback moderiert",
"feedbackSettingsUpdated": "Feedback-Einstellungen aktualisiert für {{eventName}}",
"wordFilterAdded": "Wortfilter hinzugefügt: {{word}}",
"whatsappConfigUpdated": "WhatsApp-Konfiguration aktualisiert"
"whatsappConfigUpdated": "WhatsApp-Konfiguration aktualisiert",
"bulkDeleteCompleted_one": "Massen-Löschung abgeschlossen: {{count}} Event entfernt",
"bulkDeleteCompleted_other": "Massen-Löschung abgeschlossen: {{count}} Events entfernt",
"bulkArchiveCompleted_one": "Massen-Archivierung abgeschlossen: {{count}} Event archiviert",
"bulkArchiveCompleted_other": "Massen-Archivierung abgeschlossen: {{count}} Events archiviert"
},
"notificationToasts": {
"markedAllRead": "Alle Benachrichtigungen als gelesen markiert",
@@ -3016,7 +3159,11 @@
"rateStars": "Mit {{count}} Sternen bewerten",
"markError": "Markierung konnte nicht gespeichert werden",
"yourMarkColor": "Deine Markierung: {{color}}",
"yourMarkRating": "Deine Bewertung: {{count}}"
"yourMarkRating": "Deine Bewertung: {{count}}",
"rateStars_one": "Mit {{count}} Stern bewerten",
"rateStars_other": "Mit {{count}} Sternen bewerten",
"yourMarkRating_one": "Deine Bewertung: {{count}}",
"yourMarkRating_other": "Deine Bewertung: {{count}}"
},
"events": {
"tabs": {
@@ -3260,7 +3407,10 @@
"email_queue_flushed": "E-Mail-Warteschlange geleert",
"email_template_created": "E-Mail-Vorlage erstellt: {{template_key}}",
"event_duplicated": "Event dupliziert aus {{source_event_name}}",
"whatsapp_config_updated": "WhatsApp-Konfiguration aktualisiert"
"whatsapp_config_updated": "WhatsApp-Konfiguration aktualisiert",
"feature_flags_summary_one": "{{count}} Funktion aktualisiert: {{summary}}",
"feature_flags_summary_other": "{{count}} Funktionen aktualisiert: {{summary}}",
"feature_flags_updated": "Funktionseinstellungen aktualisiert"
},
"people": {
"title": "Personen in dieser Galerie",
@@ -3303,7 +3453,15 @@
"suggestionScore": "{{percent}}% ähnlich",
"suggestionAccept": "Dieselbe Person",
"suggestionReject": "Nicht dieselbe",
"suggestionDismissed": "Getrennt gelassen"
"suggestionDismissed": "Getrennt gelassen",
"merged_one": "Personen zusammengeführt",
"merged_other": "Personen zusammengeführt",
"splitSelected_one": "{{count}} ausgewählt",
"splitSelected_other": "{{count}} ausgewählt",
"suggestionsHeading_one": "Das könnte dieselbe Person sein. Die Gruppierung war sich nicht sicher genug, um sie von selbst zusammenzuführen.",
"suggestionsHeading_other": "Das könnte dieselbe Person sein. Die Gruppierung war sich nicht sicher genug, um sie von selbst zusammenzuführen.",
"selectedCount_one": "{{count}} ausgewählt",
"selectedCount_other": "{{count}} ausgewählt"
},
"faces": {
"manage": "Personen verwalten",
@@ -3335,11 +3493,21 @@
"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.",
"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."
"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.",
"queued_one": "{{count}} Foto wird nach Personen durchsucht…",
"queued_other": "{{count}} Fotos werden nach Personen durchsucht…",
"visibleToGuests_one": "({{count}} für Gäste sichtbar)",
"visibleToGuests_other": "({{count}} für Gäste sichtbar)",
"failed_one": "{{count}} fehlgeschlagen",
"failed_other": "{{count}} fehlgeschlagen"
},
"catalogedMedia": "{{size}} katalogisiert",
"storageUnavailable": "nicht verfügbar",
"catalogedMediaOnly": "katalogisiert — Objekte liegen in S3"
"catalogedMediaOnly": "katalogisiert — Objekte liegen in S3",
"viewReleaseNotes": "Versionshinweise auf GitHub ansehen",
"expandSidebar": "Seitenleiste ausklappen",
"collapseSidebar": "Seitenleiste einklappen",
"viewOnGithub": "PicPeak auf GitHub ansehen"
},
"acceptInvitation": {
"title": "Einladung annehmen",
@@ -3524,7 +3692,11 @@
"noFolders": "Der Server hat keine Ordner zurückgegeben.",
"detectFailed": "Ordner konnten nicht erkannt werden.",
"requiredFields": "Host, Port und Benutzername sind erforderlich.",
"savedToast": "Einstellungen für eingehende E-Mails gespeichert."
"savedToast": "Einstellungen für eingehende E-Mails gespeichert.",
"pollOk_one": "Postfach geprüft — {{count}} neue E-Mail übernommen.",
"pollOk_other": "Postfach geprüft — {{count}} neue E-Mails übernommen.",
"foldersDetected_one": "{{count}} Ordner gefunden.",
"foldersDetected_other": "{{count}} Ordner gefunden."
},
"received": {
"tab": "Empfangene E-Mails",
@@ -3563,7 +3735,11 @@
"created": "Eingereiht",
"sent": "Gesendet",
"event": "Anlass"
}
},
"retries_one": "{{count}} Versuch",
"retries_other": "{{count}} Versuche",
"pagination_one": "Seite {{page}} von {{total}} · {{count}} E-Mail",
"pagination_other": "Seite {{page}} von {{total}} · {{count}} E-Mails"
},
"commonSmtpSettings": "Häufige SMTP-Einstellungen:",
"editTemplate": "Vorlage bearbeiten",
@@ -4519,7 +4695,9 @@
"label": "Filter",
"all": "Alle",
"countAll": "{{count}} insgesamt",
"countFiltered": "{{visible}} von {{total}}"
"countFiltered": "{{visible}} von {{total}}",
"countAll_one": "{{count}} insgesamt",
"countAll_other": "{{count}} insgesamt"
},
"profile": {
"title": "Kundenprofil",
@@ -4586,6 +4764,20 @@
"successToast": "Passwort aktualisiert. Bitte einloggen.",
"invalidSubmission": "Passwort konnte nicht aktualisiert werden.",
"generalError": "Passwort konnte nicht aktualisiert werden. Bitte erneut versuchen."
},
"contracts": {
"title": "Verträge",
"disabled": "Diese Funktion ist für dein 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.",
"empty": "Noch keine Verträge.",
"popupBlocked": "Erlaube Pop-ups für diese Seite, um das Vertrags-PDF herunterzuladen.",
"issued": "Ausgestellt",
"signedByYou": "Von dir unterschrieben",
"counterSigned": "Gegengezeichnet",
"openSign": "Öffnen & unterschreiben",
"downloadSigned": "Unterschriebenes PDF herunterladen",
"download": "PDF herunterladen"
}
},
"customers": {
@@ -4651,7 +4843,9 @@
"error": {
"noRate": "Für diesen Kunden ist kein Stundensatz hinterlegt. Geben Sie einen Satz-Override ein, hinterlegen Sie einen Satz beim Kunden oder konfigurieren Sie in den Einstellungen einen installationsweiten Standardsatz.",
"createFailed": "Eintrag konnte nicht erfasst werden"
}
},
"unbilledCount_one": "{{count}} offener Eintrag, total {{total}}",
"unbilledCount_other": "{{count}} offene Einträge, total {{total}}"
},
"create": {
"openButton": "Passiven Kunden erstellen",
@@ -4791,7 +4985,8 @@
"billsSection": "Rechnungen",
"noBills": "Noch keine Rechnungen für diesen Kunden.",
"rebillsSection": "Weiterverrechnungen & Durchlaufposten",
"noRebills": "Noch keine weiterverrechneten oder durchlaufenden Lieferantenrechnungen für diesen Kunden."
"noRebills": "Noch keine weiterverrechneten oder durchlaufenden Lieferantenrechnungen für diesen Kunden.",
"companyName": "Firmenname"
},
"billing": {
"section": "Abrechnungsrhythmus",
@@ -4853,7 +5048,11 @@
"addedN": "{{count}} hinzugefügt",
"removedN": "{{count}} entfernt",
"error": "Zuweisungen konnten nicht aktualisiert werden",
"clearSearchAria": "Suche löschen"
"clearSearchAria": "Suche löschen",
"addedN_one": "{{count}} hinzugefügt",
"addedN_other": "{{count}} hinzugefügt",
"removedN_one": "{{count}} entfernt",
"removedN_other": "{{count}} entfernt"
}
},
"clients": {
@@ -5008,7 +5207,9 @@
"billPending": "Verrechnen",
"bundledToast": "{{count}} Weiterverrechnung zu einer Rechnung gebündelt.",
"bundledToast_other": "{{count}} Weiterverrechnungen zu einer Rechnung gebündelt.",
"amountRequired": "Rechnungsbetrag vor der Weiterverrechnung eingeben (0 ist erlaubt)."
"amountRequired": "Rechnungsbetrag vor der Weiterverrechnung eingeben (0 ist erlaubt).",
"bundledToast_one": "{{count}} Weiterverrechnung zu einer Rechnung gebündelt.",
"pendingCount_one": "{{count}} Posten"
},
"expense": {
"kind": "Art",
@@ -5150,7 +5351,9 @@
"status": {
"active": "Aktiv",
"archived": "Archiviert"
}
},
"eventCount_one": "{{count}} Event",
"eventCount_other": "{{count}} Events"
},
"calendar": {
"pageTitle": "Kalender",
@@ -5193,7 +5396,8 @@
"featureOffToast": "Stundenerfassung ist deaktiviert.",
"lockedTooltip": "Bereits abgerechnet — Rechnung stornieren, um zu bearbeiten.",
"noDescription": "Keine Beschreibung.",
"untitledCustomer": "Stunden"
"untitledCustomer": "Stunden",
"customerLoggingDisabled": "Bei diesem Kunden ist die Stundenerfassung deaktiviert. Aktivieren Sie sie auf der Detailseite des Kunden, um Stunden zu erfassen."
}
},
"hoursLogging": {
@@ -5335,7 +5539,9 @@
"ledger_account_default_expense": "Standard-Aufwandkonto",
"ledger_account_mileage": "Kilometer-Konto",
"ledger_account_per_diem": "Spesenpauschalen-Konto",
"ledger_account_rebilled_revenue": "Weiterverrechnete Spesen (Ertrag)"
"ledger_account_rebilled_revenue": "Weiterverrechnete Spesen (Ertrag)",
"ledger_vat_map": "MWST-Code nach steuerlicher Behandlung (Kosten)",
"ledger_output_vat_map": "MWST-Code nach Umsatzsatz"
},
"vatMap": {
"title": "MWST-Code nach steuerlicher Behandlung (Kosten)"
@@ -5463,7 +5669,10 @@
"reference": "Referenz",
"party": "Kunde / Lieferant",
"tax": "Steuer"
}
},
"cancelledFootnote_one": "{{count}} stornierte Rechnung — Betrag nicht in den Summen enthalten (für lückenlose Nummernfolge dargestellt).",
"cancelledFootnote_other": "{{count}} stornierte Rechnungen — Beträge nicht in den Summen enthalten (für lückenlose Nummernfolge dargestellt).",
"costsErrorTitle": "Kosten konnten nicht geladen werden"
},
"quotes": {
"status": {
@@ -5575,7 +5784,11 @@
"convertToContract": "In Vertrag umwandeln",
"confirmConvertToContract": "Aus diesem Angebot einen Vertrag entwerfen? Kunde und Administrator unterzeichnen beide, bevor Anlass / Rechnung erstellt werden.",
"convertedToContractToast": "Vertrag aus diesem Angebot entworfen.",
"contractAlreadyLinkedToast": "Aus diesem Angebot wurde bereits ein Vertrag entworfen."
"contractAlreadyLinkedToast": "Aus diesem Angebot wurde bereits ein Vertrag entworfen.",
"pagination_one": "Seite {{page}} von {{total}} · {{count}} Angebot",
"pagination_other": "Seite {{page}} von {{total}} · {{count}} Angebote",
"convertedToInvoiceToast_one": "{{count}} Rechnung aus diesem Angebot erstellt",
"convertedToInvoiceToast_other": "{{count}} Rechnungen aus diesem Angebot erstellt"
},
"crm": {
"lineItems": {
@@ -5600,6 +5813,9 @@
"detailsPlaceholder": "Optionale Notizen — Kleingedrucktes, Paketinhalte, Bedingungen…",
"autoTotaledHint": "Summe automatisch aus den Unterpositionen berechnet",
"autoTotaledNote": "= Σ Unterpositionen"
},
"customerPicker": {
"search": "Kunde nach E-Mail oder Firma suchen…"
}
},
"bills": {
@@ -5770,7 +5986,9 @@
"modePassthrough": "durchlaufend",
"modeRebill": "Weiterverrechnung",
"noProofFile": "Keine gespeicherte Belegdatei",
"sendWithCount": "Mit {{count}} Beleg(en) senden"
"sendWithCount": "Mit {{count}} Beleg(en) senden",
"sendWithCount_one": "Mit {{count}} Beleg senden",
"sendWithCount_other": "Mit {{count}} Belegen senden"
}
},
"businessProfile": {
@@ -6051,7 +6269,13 @@
"changeWithin": "Sie können Ihre Antwort innerhalb von {{minutes}} Minuten ändern.",
"intro": "Vielen Dank für Ihre Anfrage. Bitte prüfen Sie das folgende Angebot und antworten Sie unten.",
"accept": "Angebot annehmen",
"decline": "Angebot ablehnen"
"decline": "Angebot ablehnen",
"tosRequiredError": "Bitte bestätigen Sie „Ich akzeptiere die Allgemeinen Geschäftsbedingungen“, bevor Sie das Angebot annehmen — oder klicken Sie auf „Ablehnen“.",
"submitting": "Ihre Antwort wird erfasst…",
"tosCheckbox": "Ich habe die Allgemeinen Geschäftsbedingungen gelesen und akzeptiere sie.",
"tosLink": "Vollständige AGB lesen",
"preselectAcceptHint": "Sie sind dem Link „Annehmen“ aus der E-Mail gefolgt — klicken Sie zur Bestätigung auf die Schaltfläche unten.",
"preselectDeclineHint": "Sie sind dem Link „Ablehnen“ aus der E-Mail gefolgt — klicken Sie zur Bestätigung auf die Schaltfläche unten."
},
"reminderTemplates": {
"title": "Erinnerungs-E-Mails vor dem Anlass",
@@ -6171,7 +6395,9 @@
"issueDate": "Ausgestellt",
"status": "Status"
},
"pagination": "Seite {{page}} von {{total}} · {{count}} Verträge"
"pagination": "Seite {{page}} von {{total}} · {{count}} Verträge",
"pagination_one": "Seite {{page}} von {{total}} · {{count}} Vertrag",
"pagination_other": "Seite {{page}} von {{total}} · {{count}} Verträge"
},
"detail": {
"previewPdf": "PDF-Vorschau",
@@ -6253,7 +6479,9 @@
"signedNamePlaceholder": "Ihr vollständiger Name",
"uploadError": "Upload fehlgeschlagen",
"uploadSigned": "Signiertes PDF hochladen",
"uploadedToast": "Signiertes PDF hochgeladen."
"uploadedToast": "Signiertes PDF hochgeladen.",
"convertedToInvoiceToast_one": "{{count}} Rechnung aus diesem Vertrag erstellt",
"convertedToInvoiceToast_other": "{{count}} Rechnungen aus diesem Vertrag erstellt"
},
"editor": {
"back": "Zurück zur Liste",
@@ -6419,7 +6647,9 @@
"syncUnconfigured": "Zuerst ein Postfach unter Einstellungen → E-Mail konfigurieren.",
"syncBusy": "Es läuft bereits eine Synchronisierung.",
"syncFailed": "Synchronisierung fehlgeschlagen.",
"actionFailed": "Aktion fehlgeschlagen."
"actionFailed": "Aktion fehlgeschlagen.",
"syncOk_one": "Postfächer geprüft — {{count}} neu.",
"syncOk_other": "Postfächer geprüft — {{count}} neu."
},
"rebills": {
"createInvoice": "Rechnung aus Weiterverrechnungen erstellen",
@@ -6448,7 +6678,9 @@
"hours": "offene Stunden",
"rebills": "offene Weiterverrechnungen",
"hoursOnly": "Nur die Stunden",
"rebillsOnly": "Nur die Weiterverrechnungen"
"rebillsOnly": "Nur die Weiterverrechnungen",
"body_one": "Dieser Kunde hat außerdem {{count}} {{label}}. Zur selben Rechnung hinzufügen? Sie bleiben eine separate Gruppe — Stunden und Weiterverrechnungen werden nie in einer Position vermischt.",
"body_other": "Dieser Kunde hat außerdem {{count}} {{label}}. Zur selben Rechnung hinzufügen? Sie bleiben eine separate Gruppe — Stunden und Weiterverrechnungen werden nie in einer Position vermischt."
},
"roleEditor": {
"loading": "Rollen werden geladen…",
@@ -6482,7 +6714,122 @@
"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": "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."
},
"permCount_one": "{{count}} Berechtigung",
"permCount_other": "{{count}} Berechtigungen",
"userCount_one": "{{count}} Benutzer",
"userCount_other": "{{count}} Benutzer",
"selectedCount_one": "{{count}} ausgewählt",
"selectedCount_other": "{{count}} ausgewählt"
},
"paymentCheck": {
"missingToken": "Token fehlt",
"alreadyUsed": "Dieser Link wurde bereits verwendet (Aktion: {{action}}). Wenn Sie eine weitere Zahlung erfassen möchten, öffnen Sie die Rechnung im Admin-Bereich.",
"expired": "Dieser Link ist abgelaufen. Öffnen Sie die Rechnung im Admin-Bereich, um die Zahlung manuell zu erfassen.",
"loadError": "Die Rechnung konnte nicht geladen werden. Der Link ist möglicherweise ungültig.",
"partialInvalid": "Bitte einen positiven Betrag eingeben.",
"partialTooHigh": "Der Betrag darf den offenen Gesamtbetrag ({{max}}) nicht überschreiten.",
"submitError": "Die Aktion konnte nicht erfasst werden.",
"title": "Zahlung bestätigen",
"subtitle": "Wählen Sie aus, was zu dieser Rechnung eingegangen ist. Die Auswahl wird protokolliert und die passende Erinnerung automatisch eingereiht.",
"field": {
"invoice": "Rechnung",
"customer": "Kunde",
"issued": "Ausgestellt",
"due": "Fällig",
"total": "Gesamt",
"outstanding": "Offen",
"paid": "Bereits bezahlt",
"lateFee": "Mahngebühr"
},
"action": {
"paidFull": "Vollständig bezahlt",
"paidFullHelp": "Den gesamten offenen Betrag ({{amount}}) als eingegangen markieren. Es wird keine Erinnerung versendet.",
"paidSkonto": "Mit Skonto bezahlt",
"paidSkontoHelp": "Der Kunde hat innerhalb der Skontofrist bezahlt. {{amount}} ({{percent}}% Abzug) als bezahlt erfassen.",
"partial": "Teilweise bezahlt",
"partialHelp": "Den eingegangenen Betrag erfassen und anschließend die Zahlungserinnerung für den Restbetrag einreihen.",
"partialAmount": "Eingegangener Betrag",
"partialMax": "Max.: {{max}}",
"unpaid": "Noch nicht bezahlt",
"unpaidHelp": "Nichts eingegangen. Die Zahlungserinnerung an den Kunden wird eingereiht{{fee}}.",
"unpaidWithFee": " (mit Mahngebühr ab der zweiten Erinnerung)"
},
"submitting": "Wird erfasst…",
"submit": "Bestätigen",
"result": {
"title": "Aktion erfasst",
"paid": "Rechnung {{n}} als vollständig bezahlt markiert.",
"paidSkonto": "Rechnung {{n}} als mit Skonto bezahlt markiert.",
"partial": "Teilzahlung für Rechnung {{n}} erfasst. Die Zahlungserinnerung für den Restbetrag wurde eingereiht.",
"unpaidMax": "Als unbezahlt erfasst. Die höchste Mahnstufe ist bereits erreicht — bitte klären Sie den Fall direkt mit dem Kunden.",
"unpaid": "Als unbezahlt erfasst. Zahlungserinnerung eingereiht (Stufe {{lvl}}).",
"close": "Sie können diesen Tab schließen."
}
},
"publicContract": {
"errorAccept": "Bitte bestätigen Sie das Kontrollkästchen zur Annahme.",
"errorName": "Bitte geben Sie Ihren Namen ein.",
"errorSignatureRequired": "Für diesen Vertrag ist eine gezeichnete Unterschrift erforderlich.",
"notFoundTitle": "Vertrag nicht verfügbar",
"notFoundBody": "Dieser Signatur-Link ist ungültig oder abgelaufen. Bitte wenden Sie sich an den Absender.",
"fallbackTitle": "Vertrag",
"signed": {
"title": "Vielen Dank — der Vertrag ist unterschrieben.",
"by": "Unterschrieben von",
"counterBy": "Gegengezeichnet von",
"auditTitle": "Signatur-Prüfprotokoll",
"auditBody": "Für Ihre Unterlagen. Speichern Sie diese Angaben oder machen Sie einen Screenshot — mit dem SHA-256-Hash können Sie später belegen, dass das heruntergeladene PDF exakt dem von uns ausgestellten entspricht (Datei erneut hashen und vergleichen).",
"contractNumber": "Vertrag",
"signedAt": "Unterschrieben am",
"ipAddress": "IP bei Unterzeichnung",
"counterSignedAt": "Gegengezeichnet am",
"signedSha": "SHA-256 des unterschriebenen PDFs",
"unsignedSha": "SHA-256 des Original-PDFs"
},
"downloadSigned": "Unterschriebenes PDF herunterladen",
"download": "PDF herunterladen",
"signTitle": "Diesen Vertrag unterschreiben",
"nameField": "Ihr vollständiger Name",
"signaturePromptRequired": "Zeichnen Sie Ihre Unterschrift",
"signaturePrompt": "Zeichnen Sie Ihre Unterschrift (optional)",
"clearSignature": "Löschen",
"acceptCheckbox": "Ich habe diesen Vertrag gelesen und erkläre mich mit seinen Bedingungen einverstanden.",
"submit": "Vertrag unterschreiben",
"uploadAlternative": "Oder ein handschriftlich unterschriebenes PDF hochladen",
"uploadHint": "Unterschreiben Sie den ausgedruckten Vertrag von Hand, scannen Sie ihn ein und laden Sie das PDF hier hoch.",
"uploadButton": "Unterschriebenes PDF hochladen"
},
"crmOverview": {
"title": "CRM-Übersicht",
"loadError": "CRM-Kennzahlen konnten nicht geladen werden. Prüfen Sie, ob Sie die Berechtigung bills.view oder quotes.view besitzen und ob das Backend auf dem aktuellen Stand ist.",
"revenue": {
"month": "Umsatz · letzte 30 Tage",
"quarter": "Umsatz · letzte 90 Tage",
"yearCalendar": "Umsatz · dieses Jahr",
"year": "Umsatz · letzte 365 Tage",
"toggleHint": "Tippen, um den Zeitraum zu wechseln"
},
"outstanding": "Offene Zahlungen",
"outstandingSub_one": "{{count}} Rechnung offen",
"outstandingSub_other": "{{count}} Rechnungen offen",
"quotes": {
"title": "Angebote"
},
"viewAll": "Alle anzeigen",
"invoices": {
"title": "Rechnungen"
}
},
"users": {
"roles": {
"viewer": "Betrachter",
"editor": "Bearbeiter",
"admin": "Admin",
"super_admin": "Super-Admin"
}
}
}
+399 -51
View File
@@ -72,7 +72,23 @@
"confirmCancelInvitation": {
"title": "Cancel Invitation",
"message": "Are you sure you want to cancel the invitation for {{email}}?"
}
},
"userActivated": "User reactivated successfully",
"activateUserError": "Failed to reactivate user",
"userDeleted": "User deleted successfully",
"deleteUserError": "Failed to delete user",
"activateUser": "Reactivate user",
"deleteUser": "Delete user permanently",
"confirmActivate": {
"title": "Reactivate user?",
"message": "Reactivate {{name}}? They will be able to log in again immediately."
},
"confirmDelete": {
"title": "Delete user permanently?",
"message": "Permanently delete {{name}}? This cannot be undone. Their pending invitations and API tokens will be removed; records they created elsewhere will be kept but de-attributed."
},
"activate": "Reactivate",
"delete": "Delete permanently"
},
"systemHealth": {
"title": "System health",
@@ -159,7 +175,13 @@
"off": "off",
"clear": "Clear",
"saved": "Saved",
"saveFailed": "Save failed"
"saveFailed": "Save failed",
"menu": "Menu",
"deleted": "Deleted.",
"actions": "Actions",
"copyFailed": "Could not copy to clipboard",
"searching": "Searching…",
"language": "Language"
},
"upload": {
"photoCategory": "Photo Category",
@@ -184,7 +206,11 @@
"kindProcessing": "Processing failed",
"transferReason": "Transfer failed",
"unknownReason": "Unknown error",
"unknownFile": "Unknown file"
"unknownFile": "Unknown file",
"toast_one": "{{count}} file(s) could not be uploaded — see the list below.",
"toast_other": "{{count}} file(s) could not be uploaded — see the list below.",
"title_one": "{{count}} file(s) could not be uploaded",
"title_other": "{{count}} file(s) could not be uploaded"
},
"replaceByName": "Replace existing photos with same name",
"uploadPhotos": "Upload Photos",
@@ -290,7 +316,9 @@
"recipients": "Recipient email addresses",
"recipientsPlaceholder": "[email protected], [email protected]",
"recipientsCount": "{{count}} recipient(s) — each gets the download link",
"recipientsHint": "Separate multiple addresses with commas. Each recipient gets the download link."
"recipientsHint": "Separate multiple addresses with commas. Each recipient gets the download link.",
"recipientsCount_one": "{{count}} recipient(s) — each gets the download link",
"recipientsCount_other": "{{count}} recipient(s) — each gets the download link"
},
"picker": {
"title": "Select images from other events",
@@ -302,7 +330,9 @@
"alreadyAdded": "Added",
"selectedCount": "{{count}} selected",
"addSelected": "Add selected",
"lightbox": "Lightbox"
"lightbox": "Lightbox",
"selectedCount_one": "{{count}} selected",
"selectedCount_other": "{{count}} selected"
},
"public": {
"notFoundTitle": "Link not found",
@@ -313,7 +343,9 @@
"limitBody": "This transfer has reached its maximum number of downloads.",
"availableUntil": "Available until {{date}}",
"fileSummary": "{{count}} files",
"downloadFile": "Download"
"downloadFile": "Download",
"fileSummary_one": "{{count}} files",
"fileSummary_other": "{{count}} files"
},
"upload": {
"unavailableTitle": "Upload link unavailable",
@@ -325,7 +357,9 @@
"limits": "Up to {{files}} files, {{mb}} MB each",
"tooBig": "Each file must be {{mb}} MB or smaller",
"send": "Upload {{count}} files",
"failed": "Upload failed. Please try again."
"failed": "Upload failed. Please try again.",
"send_one": "Upload {{count}} files",
"send_other": "Upload {{count}} files"
}
},
"workflows": {
@@ -625,7 +659,17 @@
"noMatches": "No one matches that name.",
"privacyNote": "People are detected automatically inside this gallery. Nothing is sent to any external service.",
"inThisPhoto": "In this photo:",
"downloadThese": "Download these {{count}}"
"downloadThese": "Download these {{count}}",
"unnamedCount_one": "{{count}} photo",
"unnamedCount_other": "{{count}} photos",
"collapsedSummary_one": "{{count}} person found",
"collapsedSummary_other": "{{count}} people found",
"showAll_one": "Show all {{count}}",
"showAll_other": "Show all {{count}}",
"matchCount_one": "{{count}} of {{total}} photos",
"matchCount_other": "{{count}} of {{total}} photos",
"downloadThese_one": "Download this {{count}} photo",
"downloadThese_other": "Download these {{count}}"
},
"colorFilter": "Color",
"filterByColor": "Show only {{color}}",
@@ -635,7 +679,13 @@
"backToGallery": "All photos",
"downloadFolder": "Download folder ({{count}})",
"downloadFolderCapped": "Download first {{limit}} of {{total}}",
"downloadEverything": "Download all photos"
"downloadEverything": "Download all photos",
"downloadFolder_one": "Download folder ({{count}})",
"downloadFolder_other": "Download folder ({{count}})",
"folderPhotoCount_one": "{{count}} photos",
"folderPhotoCount_other": "{{count}} photos",
"preparingProgress_one": "{{count}} photos packaged",
"preparingProgress_other": "{{count}} photos packaged"
},
"categories": {
"title": "Photo Categories",
@@ -771,7 +821,9 @@
"errors": {
"slugTaken": "That short URL is already taken — try {{suggested}} instead.",
"invalidSlug": "Short URLs must be lowercase letters, digits, and hyphens (164 chars)."
}
},
"hits_one": "{{count}} hits",
"hits_other": "{{count}} hits"
},
"resetGalleryPassword": "Reset Gallery Password",
"resendCreationEmail": "Resend Creation Email",
@@ -816,7 +868,9 @@
"toastError": "Failed to reset password",
"toastCopied": "Password copied to clipboard",
"errorMinLength": "Password must be at least 6 characters",
"errorMismatch": "Passwords do not match"
"errorMismatch": "Passwords do not match",
"hide": "Hide",
"show": "Show"
},
"themeAndStyle": "Theme & Style",
"galleryExpiration": "Gallery Expiration",
@@ -907,7 +961,9 @@
"notifyLabel": "Send the gallery email now",
"notifyHelp": "Uncheck to publish quietly — the gallery goes live and nothing is sent. You can send the email later from this page.",
"descriptionQuiet": "Publishing \"{{eventName}}\" makes the gallery accessible. No email will be sent — you can send it later from this page.",
"descriptionWhatsapp": "Publishing \"{{eventName}}\" makes the gallery accessible. If WhatsApp is configured, the customer is notified there."
"descriptionWhatsapp": "Publishing \"{{eventName}}\" makes the gallery accessible. If WhatsApp is configured, the customer is notified there.",
"descriptionAssignedAccount_one": "Publishing \"{{eventName}}\" makes the gallery accessible. The assigned customer account(s) will be notified by email (in their language) that it is available.",
"descriptionAssignedAccount_other": "Publishing \"{{eventName}}\" makes the gallery accessible. The assigned customer account(s) will be notified by email (in their language) that it is available."
},
"duplicateEvent": "Duplicate gallery",
"duplicateDialog": {
@@ -1043,7 +1099,9 @@
"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"
"preview": "Preview",
"mode_managed": "Managed (upload to PicPeak)",
"mode_reference": "Reference external folder"
},
"infoBanner": {
"title": "Info Banner",
@@ -1052,7 +1110,9 @@
"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"
"preview": "Preview",
"mode_managed": "Managed (upload to PicPeak)",
"mode_reference": "Reference external folder"
},
"ogShare": {
"title": "Use hero photo as social-share preview",
@@ -1080,6 +1140,14 @@
"listHeading": "Events to be archived:",
"submit_one": "Archive {{count}} event",
"submit_other": "Archive {{count}} events"
},
"customerPicker": {
"help": "Customers added here can log in at /customer/login and view this gallery without entering the per-event password.",
"label": "Customer accounts",
"removeAria": "Remove {{name}}",
"placeholder": "Search by email, name, or company",
"searching": "Searching…",
"noResults": "No matches. Invite this customer from Clients → Accounts first."
}
},
"settings": {
@@ -1188,7 +1256,8 @@
"capacityRequiredForAvailable": "Enter a total capacity before setting available space.",
"availableExceedsCapacity": "Available space cannot exceed the total capacity.",
"storageUsage": "Storage Usage",
"unlimited": "Unlimited"
"unlimited": "Unlimited",
"catalogMeasurement": "Catalogued size — objects live in the configured S3 bucket, not on this disk"
},
"security": {
"title": "Security",
@@ -1489,7 +1558,9 @@
"uniqueVisitors": "Unique visitors by IP",
"deviceTypes": "Device types from user agents",
"privacy": "Privacy",
"privacyText": "IP addresses are hashed for privacy. No personal data is stored. Analytics data is retained for 90 days."
"privacyText": "IP addresses are hashed for privacy. No personal data is stored. Analytics data is retained for 90 days.",
"umamiApiKey": "API key",
"umamiApiKeyHelp": "Optional. Required only for the device-breakdown chart on the Analytics dashboard. Generate in Umami → Settings → Profile → API Keys. Stored masked as •••••••• once saved — leave the masked value to keep the existing key."
},
"groups": {
"general": "General",
@@ -1904,7 +1975,8 @@
"setupFailed": "Could not start two-factor setup. Please try again.",
"enableFailed": "Could not enable two-factor authentication. Check the code and try again.",
"disableFailed": "Could not disable two-factor authentication. Check the code and try again.",
"regenerateFailed": "Could not regenerate recovery codes. Check the code and try again."
"regenerateFailed": "Could not regenerate recovery codes. Check the code and try again.",
"recoveryCodesRemaining_one": "{{count}} recovery code remaining."
},
"sso": {
"title": "Single Sign-On (OIDC)",
@@ -1985,7 +2057,9 @@
"noneToFill": "All photos already have a capture date",
"resultSuccess": "Last run: {{success}} updated, {{noExif}} with no date found, {{failed}} unreachable",
"skipped": "{{count}} photo(s) were changed by something else while the run was reading them and were not updated.",
"description": "Backfill \"Date Taken\" from EXIF for photos imported before capture dates were read. External/reference imports never recorded one, so their galleries sort by import order instead of when the photos were taken."
"description": "Backfill \"Date Taken\" from EXIF for photos imported before capture dates were read. External/reference imports never recorded one, so their galleries sort by import order instead of when the photos were taken.",
"skipped_one": "{{count}} photo(s) were changed by something else while the run was reading them and were not updated.",
"skipped_other": "{{count}} photo(s) were changed by something else while the run was reading them and were not updated."
},
"orientationBackfill": {
"title": "Photo Orientation",
@@ -1993,8 +2067,12 @@
"button": "Fix Photo Orientation",
"running": "Checking orientation...",
"resultSuccess": "Last run: {{checked}} checked, {{corrected}} corrected, {{requeued}} requeued for face scanning, {{failed}} unreachable",
"staleTiers": "{{count}} cached size(s) could not be deleted and will keep serving the old orientation — re-run once storage is writable."
}
"staleTiers": "{{count}} cached size(s) could not be deleted and will keep serving the old orientation — re-run once storage is writable.",
"staleTiers_one": "{{count}} cached size(s) could not be deleted and will keep serving the old orientation — re-run once storage is writable.",
"staleTiers_other": "{{count}} cached size(s) could not be deleted and will keep serving the old orientation — re-run once storage is writable."
},
"saved": "Settings saved",
"saveError": "Failed to save settings"
},
"analytics": {
"title": "Analytics Dashboard",
@@ -2391,7 +2469,25 @@
"copyAllCommands": "Copy All Commands",
"completeChecklist": "Complete the checklist before updating",
"error": "Failed to load update instructions"
}
},
"viewDetails": "View release notes and upgrade instructions",
"registryMoved": {
"title": "Pulling from the retired image registry",
"body": "This update will not arrive until you change the image path in docker-compose.yml to {{newPath}}. The old path still responds, so `docker compose pull` appears to succeed while serving the same frozen build.",
"link": "See migration notes"
},
"copyFailed": "Could not copy to clipboard",
"modalTitle": "Update available",
"modalSubtitle": "v{{current}} → v{{latest}}",
"howToUpgrade": "How to upgrade",
"detectedEnv": "Detected environment: {{env}}",
"copyCommand": "Copy command",
"releaseNotes": "Release notes",
"changelogError": "Could not load release notes. Check the release pages directly on GitHub.",
"noReleases": "No release notes available.",
"viewOnGitHub": "View on GitHub",
"noNotes": "No release notes provided.",
"dismissUntilNext": "Dismiss until next version"
},
"notifications": "Notifications",
"markAllRead": "Mark all read",
@@ -2539,7 +2635,11 @@
"feedbackModerated": "Feedback moderated",
"feedbackSettingsUpdated": "Feedback settings updated for {{eventName}}",
"wordFilterAdded": "Word filter added: {{word}}",
"whatsappConfigUpdated": "WhatsApp configuration updated"
"whatsappConfigUpdated": "WhatsApp configuration updated",
"bulkDeleteCompleted_one": "Bulk delete completed: {{count}} event removed",
"bulkDeleteCompleted_other": "Bulk delete completed: {{count}} events removed",
"bulkArchiveCompleted_one": "Bulk archive completed: {{count}} event archived",
"bulkArchiveCompleted_other": "Bulk archive completed: {{count}} events archived"
},
"notificationToasts": {
"markedAllRead": "All notifications marked as read",
@@ -2591,7 +2691,11 @@
"rateStars": "Rate {{count}} stars",
"markError": "Failed to save your mark",
"yourMarkColor": "Your mark: {{color}}",
"yourMarkRating": "Your rating: {{count}}"
"yourMarkRating": "Your rating: {{count}}",
"rateStars_one": "Rate {{count}} stars",
"rateStars_other": "Rate {{count}} stars",
"yourMarkRating_one": "Your rating: {{count}}",
"yourMarkRating_other": "Your rating: {{count}}"
},
"events": {
"tabs": {
@@ -2835,7 +2939,10 @@
"email_queue_flushed": "Email queue flushed",
"email_template_created": "Email template created: {{template_key}}",
"event_duplicated": "Event duplicated from {{source_event_name}}",
"whatsapp_config_updated": "WhatsApp configuration updated"
"whatsapp_config_updated": "WhatsApp configuration updated",
"feature_flags_summary_one": "{{count}} features updated: {{summary}}",
"feature_flags_summary_other": "{{count}} features updated: {{summary}}",
"feature_flags_updated": "Feature settings updated"
},
"people": {
"title": "People in this gallery",
@@ -2878,7 +2985,15 @@
"suggestionScore": "{{percent}}% alike",
"suggestionAccept": "Same person",
"suggestionReject": "Not the same",
"suggestionDismissed": "Kept separate"
"suggestionDismissed": "Kept separate",
"merged_one": "People merged",
"merged_other": "People merged",
"splitSelected_one": "{{count}} selected",
"splitSelected_other": "{{count}} selected",
"suggestionsHeading_one": "These might be the same person. Grouping was not confident enough to merge them on its own.",
"suggestionsHeading_other": "These might be the same person. Grouping was not confident enough to merge them on its own.",
"selectedCount_one": "{{count}} selected",
"selectedCount_other": "{{count}} selected"
},
"faces": {
"manage": "Manage people",
@@ -2910,11 +3025,21 @@
"sidecarRejected": "{{url}} answered, but not like the face-detection service — photos are being marked failed rather than retried. Check FACE_ML_URL points at the picpeak-ml container and that nothing is proxying that address, then use Re-scan for the photos that already failed.",
"sidecarStateNow": "Service state right now — some of the failures above may have a different cause, but a re-scan will not succeed until this is fixed:",
"consolidated_one": "Grouping merged {{count}} look-alike pair automatically after the last scan. Open Manage people to check it — anything merged wrongly can be separated again with Split.",
"consolidated_other": "Grouping merged {{count}} look-alike pairs automatically after the last scan. Open Manage people to check them — anything merged wrongly can be separated again with Split."
"consolidated_other": "Grouping merged {{count}} look-alike pairs automatically after the last scan. Open Manage people to check them — anything merged wrongly can be separated again with Split.",
"queued_one": "Scanning {{count}} photo for people…",
"queued_other": "Scanning {{count}} photos for people…",
"visibleToGuests_one": "({{count}} shown to guests)",
"visibleToGuests_other": "({{count}} shown to guests)",
"failed_one": "{{count}} failed",
"failed_other": "{{count}} failed"
},
"catalogedMedia": "{{size}} catalogued",
"storageUnavailable": "unavailable",
"catalogedMediaOnly": "catalogued — objects live in S3"
"catalogedMediaOnly": "catalogued — objects live in S3",
"viewReleaseNotes": "View release notes on GitHub",
"expandSidebar": "Expand sidebar",
"collapseSidebar": "Collapse sidebar",
"viewOnGithub": "View PicPeak on GitHub"
},
"acceptInvitation": {
"title": "Accept Invitation",
@@ -3080,7 +3205,11 @@
"noFolders": "No folders returned by the server.",
"detectFailed": "Could not detect folders.",
"requiredFields": "Host, port and username are required.",
"savedToast": "Incoming mail settings saved."
"savedToast": "Incoming mail settings saved.",
"pollOk_one": "Checked mailbox — {{count}} new email(s) ingested.",
"pollOk_other": "Checked mailbox — {{count}} new email(s) ingested.",
"foldersDetected_one": "{{count}} folders found.",
"foldersDetected_other": "{{count}} folders found."
},
"received": {
"tab": "Received emails",
@@ -3119,7 +3248,11 @@
"created": "Queued",
"sent": "Sent",
"event": "Event"
}
},
"retries_one": "{{count}} retries",
"retries_other": "{{count}} retries",
"pagination_one": "Page {{page}} of {{total}} · {{count}} emails",
"pagination_other": "Page {{page}} of {{total}} · {{count}} emails"
},
"commonSmtpSettings": "Common SMTP Settings:",
"editTemplate": "Edit Template",
@@ -3370,7 +3503,27 @@
"backup": {
"picpeak": {
"restoreIntro": "Upload a .picpeak taken from this or another instance. Restoring a SQLite backup onto a PostgreSQL instance is supported (the upgrade path); other engine combinations must match.",
"crossEngineNote": "Cross-engine restore: a SQLite backup was converted onto this PostgreSQL instance."
"crossEngineNote": "Cross-engine restore: a SQLite backup was converted onto this PostgreSQL instance.",
"downloadFailed": "Could not create the backup file.",
"title": "Portable backup (.picpeak)",
"intro": "Download a single self-contained file, then upload it on another instance to clone this one — all through the browser.",
"includePhotos": "Include original gallery photos (larger file)",
"secretsWarning": "This file contains secrets in plain text (email password, admin credentials, API keys). Store it securely and only transfer it over trusted channels.",
"download": "Download .picpeak",
"externalPathsFailed": "Backup restored, but external photo paths could not be converted — those originals will not load until this is retried.",
"restoreDone": "Backup restored.",
"reloginRequired": "Restore complete — please sign in again.",
"restoreFailed": "Restore failed.",
"restoreTitle": "Restore from a .picpeak",
"chooseFile": "Choose .picpeak file…",
"restoreSummary": "{{tables}} tables and {{files}} files restored.",
"externalPathsFailedDetail": "External photo paths were not converted, so every referenced original is currently unreachable. Re-run the restore or the pending migrations once the cause is resolved.",
"externalMediaNote": "This backup references an external-media library. Make sure external-media routing is configured on this instance.",
"externalMediaLink": "Setup guide",
"reload": "Reload app",
"confirmTitle": "Restore will delete all current data",
"confirmBody": "This permanently replaces ALL data on this instance with the uploaded backup, except your current account. This cannot be undone.",
"confirmRestore": "Delete & restore"
},
"title": "Backup Management",
"subtitle": "Manage system backups, configure automated backups, and restore from previous backups.",
@@ -3524,6 +3677,15 @@
"good": "Good",
"warning": "Warning",
"critical": "Critical"
},
"lastSuccessful": "Last successful backup",
"lastAttempt": "Last attempt",
"zombieRuns_one": "{{count}} backup(s) running >30min — may have crashed without completing",
"zombieRuns_other": "{{count}} backup(s) running >30min — may have crashed without completing",
"status": {
"completed": "Completed",
"failed": "Failed",
"running": "Running"
}
},
"configuration": {
@@ -3602,7 +3764,8 @@
"messages": {
"requiredFields": "Please fill in all required fields",
"connectionSuccess": "Connection test successful!",
"connectionFailed": "Connection test failed"
"connectionFailed": "Connection test failed",
"invalidCron": "Please enter a valid cron expression (5 fields)"
},
"testingConnection": "Testing connection...",
"saveSettings": "Save Configuration",
@@ -3683,7 +3846,15 @@
"encrypted": "Encrypted Backup",
"encryptedMessage": "You'll need to provide the encryption passphrase to restore this backup.",
"enterPassphrase": "Enter encryption passphrase",
"at": "at"
"at": "at",
"filesOnlyHint": "This backup has no database dump — restoring it will NOT recover the database (CRM data, customers, quotes, invoices, contracts will be empty after restore).",
"filesOnlyBadge": "No DB",
"corruptHint": "The manifest file is unreadable — the backup may be incomplete or damaged.",
"corruptBadge": "Corrupt",
"filesOnlyWarning": {
"title": "Selected backup has no database dump",
"message": "Restoring this backup will recover files (photos, PDFs) but the database — including admin users, customers, quotes, invoices, contracts, and settings — will NOT come back. Pick a different backup if you have one with a database dump, or proceed only if files-only is what you want."
}
},
"restoreTypes": {
"full": {
@@ -3758,7 +3929,12 @@
"title": "Restore Completed Successfully",
"message": "Your data has been restored. Please verify everything is working correctly.",
"verifyIntegrity": "Verify document integrity now"
}
},
"failedSubtitle": "Restore failed — see error below. Destination has been rolled back to its pre-restore state.",
"idle": "No restore in progress.",
"errorTitle": "Restore did not complete",
"errorUnknown": "No error message recorded.",
"rolledBack": "Pre-restore safety backup was used to roll back. Destination is in its pre-restore state — safe to retry once the issue above is resolved."
},
"actions": {
"back": "Back",
@@ -4519,7 +4695,9 @@
"label": "Filter",
"all": "All",
"countAll": "{{count}} total",
"countFiltered": "{{visible}} of {{total}}"
"countFiltered": "{{visible}} of {{total}}",
"countAll_one": "{{count}} total",
"countAll_other": "{{count}} total"
},
"profile": {
"title": "Customer profile",
@@ -4586,6 +4764,20 @@
"successToast": "Password updated. Please log in.",
"invalidSubmission": "Could not update your password.",
"generalError": "Could not update your password. Please try again."
},
"contracts": {
"title": "Contracts",
"disabled": "This feature is currently disabled for your account.",
"loadError": "Could not load contracts.",
"subtitle": "Every contract your photographer has sent you. Open the signing link on awaiting-signature contracts, or download the signed PDF once both parties have signed.",
"empty": "No contracts yet.",
"popupBlocked": "Allow pop-ups for this site to download the contract PDF.",
"issued": "Issued",
"signedByYou": "Signed by you",
"counterSigned": "Counter-signed",
"openSign": "Open & sign",
"downloadSigned": "Download signed PDF",
"download": "Download PDF"
}
},
"customers": {
@@ -4651,7 +4843,9 @@
"error": {
"noRate": "No hourly rate set for this customer. Enter a rate override, set a rate on the customer, or configure an install-wide default in Settings.",
"createFailed": "Failed to log entry"
}
},
"unbilledCount_one": "{{count}} unbilled entries totaling {{total}}",
"unbilledCount_other": "{{count}} unbilled entries totaling {{total}}"
},
"create": {
"openButton": "Create passive customer",
@@ -4791,7 +4985,8 @@
"noBills": "No invoices for this customer yet.",
"manageEvents": "Manage galleries",
"rebillsSection": "Re-bills & passthrough",
"noRebills": "No re-billed or passed-through supplier invoices for this customer yet."
"noRebills": "No re-billed or passed-through supplier invoices for this customer yet.",
"companyName": "Company name"
},
"billing": {
"section": "Billing cadence",
@@ -4853,7 +5048,11 @@
"addedN": "{{count}} added",
"removedN": "{{count}} removed",
"error": "Could not update assignments",
"clearSearchAria": "Clear search"
"clearSearchAria": "Clear search",
"addedN_one": "{{count}} added",
"addedN_other": "{{count}} added",
"removedN_one": "{{count}} removed",
"removedN_other": "{{count}} removed"
}
},
"clients": {
@@ -5008,7 +5207,9 @@
"billPending": "Bill these",
"bundledToast": "Bundled {{count}} re-bill into one invoice.",
"bundledToast_other": "Bundled {{count}} re-bills into one invoice.",
"amountRequired": "Enter the invoice amount before re-billing (0 is allowed)."
"amountRequired": "Enter the invoice amount before re-billing (0 is allowed).",
"bundledToast_one": "Bundled {{count}} re-bill(s) into one invoice.",
"pendingCount_one": "{{count}} item(s)"
},
"expense": {
"kind": "Type",
@@ -5150,7 +5351,9 @@
"status": {
"active": "Active",
"archived": "Archived"
}
},
"eventCount_one": "{{count}} events",
"eventCount_other": "{{count}} events"
},
"calendar": {
"pageTitle": "Calendar",
@@ -5193,7 +5396,8 @@
"featureOffToast": "Hour logging is disabled.",
"lockedTooltip": "Already billed — Storno the invoice to edit.",
"noDescription": "No description.",
"untitledCustomer": "Hours"
"untitledCustomer": "Hours",
"customerLoggingDisabled": "This customer has hour logging disabled. Enable it on the customer's detail page to log hours."
}
},
"hoursLogging": {
@@ -5335,7 +5539,9 @@
"ledger_account_default_expense": "Default expense account",
"ledger_account_mileage": "Mileage account",
"ledger_account_per_diem": "Per-diem account",
"ledger_account_rebilled_revenue": "Re-billed expenses (revenue)"
"ledger_account_rebilled_revenue": "Re-billed expenses (revenue)",
"ledger_vat_map": "VAT code by tax treatment (costs)",
"ledger_output_vat_map": "VAT code by revenue rate"
},
"vatMap": {
"title": "VAT code by tax treatment (costs)"
@@ -5463,7 +5669,10 @@
"reference": "Reference",
"party": "Customer / supplier",
"tax": "Tax"
}
},
"cancelledFootnote_one": "{{count}} cancelled invoice(s) — amounts excluded from totals (shown for audit-trail continuity).",
"cancelledFootnote_other": "{{count}} cancelled invoice(s) — amounts excluded from totals (shown for audit-trail continuity).",
"costsErrorTitle": "Costs could not be loaded"
},
"quotes": {
"errors": {
@@ -5575,7 +5784,11 @@
"convertToContract": "Convert to contract",
"confirmConvertToContract": "Draft a contract from this quote? The customer + admin will both sign before event / invoice creation.",
"convertedToContractToast": "Contract drafted from this quote.",
"contractAlreadyLinkedToast": "A contract was already drafted from this quote."
"contractAlreadyLinkedToast": "A contract was already drafted from this quote.",
"pagination_one": "Page {{page}} of {{total}} · {{count}} quotes",
"pagination_other": "Page {{page}} of {{total}} · {{count}} quotes",
"convertedToInvoiceToast_one": "{{count}} invoice(s) created from this quote",
"convertedToInvoiceToast_other": "{{count}} invoice(s) created from this quote"
},
"crm": {
"lineItems": {
@@ -5600,6 +5813,9 @@
"detailsPlaceholder": "Optional notes — fine print, package inclusions, conditions…",
"autoTotaledHint": "Total auto-computed from sub-items below",
"autoTotaledNote": "= sum of sub-items"
},
"customerPicker": {
"search": "Search customer by email or company…"
}
},
"bills": {
@@ -5701,7 +5917,8 @@
"ccPdfPickFromAdmins": "Pick from admins:",
"paymentTermHelp": "Net days + Skonto for this invoice. Leave blank to inherit from the source quote or the global CRM defaults.",
"skontoDisabled": "Disable Skonto for this invoice (suppresses the early-payment-discount block on the PDF and the \"Paid with Skonto\" buttons in the admin email / record-payment dialog).",
"sourceContract": "From contract"
"sourceContract": "From contract",
"status": "Status"
},
"import": "Import historical",
"importTitle": "Import historical invoice",
@@ -5768,7 +5985,9 @@
"modePassthrough": "passthrough",
"modeRebill": "re-bill",
"noProofFile": "No stored proof file",
"sendWithCount": "Send with {{count}} proof(s)"
"sendWithCount": "Send with {{count}} proof(s)",
"sendWithCount_one": "Send with {{count}} proof(s)",
"sendWithCount_other": "Send with {{count}} proof(s)"
}
},
"businessProfile": {
@@ -6049,7 +6268,13 @@
"changeWithin": "You can change your response until {{at}}.",
"intro": "Please accept or decline this quote:",
"accept": "Accept quote",
"decline": "Decline"
"decline": "Decline",
"tosRequiredError": "Please tick \"I accept the Terms of Service\" before accepting the quote, or click Decline to refuse.",
"submitting": "Recording your response…",
"tosCheckbox": "I have read and accept the Terms of Service.",
"tosLink": "Read the full Terms",
"preselectAcceptHint": "You followed the \"Accept\" link from the email — click the button below to confirm.",
"preselectDeclineHint": "You followed the \"Decline\" link from the email — click the button below to confirm."
},
"reminderTemplates": {
"title": "Pre-event reminder emails",
@@ -6169,7 +6394,9 @@
"issueDate": "Issued",
"status": "Status"
},
"pagination": "Page {{page}} of {{total}} · {{count}} contracts"
"pagination": "Page {{page}} of {{total}} · {{count}} contracts",
"pagination_one": "Page {{page}} of {{total}} · {{count}} contracts",
"pagination_other": "Page {{page}} of {{total}} · {{count}} contracts"
},
"detail": {
"previewPdf": "Preview PDF",
@@ -6251,7 +6478,9 @@
"signedNamePlaceholder": "Your full name",
"uploadError": "Upload failed",
"uploadSigned": "Upload signed PDF",
"uploadedToast": "Signed PDF uploaded."
"uploadedToast": "Signed PDF uploaded.",
"convertedToInvoiceToast_one": "{{count}} invoice(s) created from this contract",
"convertedToInvoiceToast_other": "{{count}} invoice(s) created from this contract"
},
"editor": {
"back": "Back to list",
@@ -6417,7 +6646,9 @@
"syncUnconfigured": "Configure a mailbox under Settings → Email first.",
"syncBusy": "A sync is already running.",
"syncFailed": "Sync failed.",
"actionFailed": "Action failed."
"actionFailed": "Action failed.",
"syncOk_one": "Checked mailboxes — {{count}} new.",
"syncOk_other": "Checked mailboxes — {{count}} new."
},
"rebills": {
"createInvoice": "Create invoice from re-bills",
@@ -6446,7 +6677,9 @@
"hours": "open hours",
"rebills": "open re-bills",
"hoursOnly": "Just the hours",
"rebillsOnly": "Just the re-bills"
"rebillsOnly": "Just the re-bills",
"body_one": "This customer also has {{count}} {{label}}. Add them to the same invoice? They stay as a separate group — hours and re-bills are never mixed into one line.",
"body_other": "This customer also has {{count}} {{label}}. Add them to the same invoice? They stay as a separate group — hours and re-bills are never mixed into one line."
},
"roleEditor": {
"loading": "Loading roles…",
@@ -6480,7 +6713,122 @@
"confirmDelete": {
"title": "Delete role?",
"message": "Permanently delete the \"{{name}}\" role? This cannot be undone.",
"hasUsers": "Reassign the {{count}} user(s) holding \"{{name}}\" before deleting it."
"hasUsers": "Reassign the {{count}} user(s) holding \"{{name}}\" before deleting it.",
"hasUsers_one": "Reassign the {{count}} user(s) holding \"{{name}}\" before deleting it.",
"hasUsers_other": "Reassign the {{count}} user(s) holding \"{{name}}\" before deleting it."
},
"permCount_one": "{{count}} permissions",
"permCount_other": "{{count}} permissions",
"userCount_one": "{{count}} users",
"userCount_other": "{{count}} users",
"selectedCount_one": "{{count}} selected",
"selectedCount_other": "{{count}} selected"
},
"paymentCheck": {
"missingToken": "Missing token",
"alreadyUsed": "This link has already been used (action: {{action}}). If you need to record another payment, open the invoice in admin.",
"expired": "This link has expired. Open the invoice in admin to record the payment manually.",
"loadError": "Could not load invoice. The link may be invalid.",
"partialInvalid": "Enter a positive amount.",
"partialTooHigh": "Amount cannot exceed the outstanding total ({{max}}).",
"submitError": "Could not record action.",
"title": "Confirm payment",
"subtitle": "Select what was received for this invoice. The choice is logged and the appropriate reminder is queued automatically.",
"field": {
"invoice": "Invoice",
"customer": "Customer",
"issued": "Issued",
"due": "Due",
"total": "Total",
"outstanding": "Outstanding",
"paid": "Already paid",
"lateFee": "Late fee"
},
"action": {
"paidFull": "Paid in full",
"paidFullHelp": "Mark the entire outstanding amount ({{amount}}) as received. No reminder is sent.",
"paidSkonto": "Paid with Skonto",
"paidSkontoHelp": "Customer settled within the Skonto window. Record {{amount}} ({{percent}}% discount) as paid.",
"partial": "Partially paid",
"partialHelp": "Log the amount received, then queue the customer reminder for the remainder.",
"partialAmount": "Amount received",
"partialMax": "Max: {{max}}",
"unpaid": "Not paid yet",
"unpaidHelp": "Nothing received. The customer reminder will be queued{{fee}}.",
"unpaidWithFee": " (with late fee at second reminder)"
},
"submitting": "Recording…",
"submit": "Confirm",
"result": {
"title": "Action recorded",
"paid": "Invoice {{n}} marked as paid in full.",
"paidSkonto": "Invoice {{n}} marked as paid with Skonto applied.",
"partial": "Partial payment logged for invoice {{n}}. Customer reminder queued for the remainder.",
"unpaidMax": "Recorded as unpaid. Maximum reminder level already reached — handle this customer offline.",
"unpaid": "Recorded as unpaid. Customer reminder queued (level {{lvl}}).",
"close": "You can close this tab."
}
},
"publicContract": {
"errorAccept": "Please tick the acceptance box.",
"errorName": "Please enter your name.",
"errorSignatureRequired": "A drawn signature is required for this contract.",
"notFoundTitle": "Contract not available",
"notFoundBody": "This signing link is invalid or expired. Please contact the sender.",
"fallbackTitle": "Contract",
"signed": {
"title": "Thank you — the contract is signed.",
"by": "Signed by",
"counterBy": "Counter-signed by",
"auditTitle": "Signing audit trail",
"auditBody": "For your records. Save or screenshot this — the SHA-256 hash lets you prove later that the PDF you downloaded is exactly what we issued (re-hash the file you have and compare).",
"contractNumber": "Contract",
"signedAt": "Signed at",
"ipAddress": "IP at signing",
"counterSignedAt": "Counter-signed at",
"signedSha": "Signed PDF SHA-256",
"unsignedSha": "Original PDF SHA-256"
},
"downloadSigned": "Download signed PDF",
"download": "Download PDF",
"signTitle": "Sign this contract",
"nameField": "Your full name",
"signaturePromptRequired": "Draw your signature",
"signaturePrompt": "Draw your signature (optional)",
"clearSignature": "Clear",
"acceptCheckbox": "I have read this contract and agree to be bound by its terms.",
"submit": "Sign contract",
"uploadAlternative": "Or upload a wet-signed PDF",
"uploadHint": "Sign the printed contract by hand, scan it, and upload the PDF here.",
"uploadButton": "Upload signed PDF"
},
"crmOverview": {
"title": "CRM overview",
"loadError": "Could not load CRM stats. Check that you have bills.view or quotes.view permission and that the backend is on the latest build.",
"revenue": {
"month": "Revenue · last 30 days",
"quarter": "Revenue · last 90 days",
"yearCalendar": "Revenue · this year",
"year": "Revenue · last 365 days",
"toggleHint": "Tap to switch window"
},
"outstanding": "Outstanding payments",
"outstandingSub_one": "{{count}} invoice(s) unpaid",
"outstandingSub_other": "{{count}} invoice(s) unpaid",
"quotes": {
"title": "Quotes"
},
"viewAll": "View all",
"invoices": {
"title": "Invoices"
}
},
"users": {
"roles": {
"viewer": "Viewer",
"editor": "Editor",
"admin": "Admin",
"super_admin": "Super Admin"
}
}
}