diff --git a/frontend/i18next.config.ts b/frontend/i18next.config.ts index 70080284..e19a497f 100644 --- a/frontend/i18next.config.ts +++ b/frontend/i18next.config.ts @@ -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, diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index d635b04a..611afb37 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -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": "anna@example.com, ben@example.com", "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 (1–64 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" } } } diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 5dbe103c..1e4bb129 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -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": "anna@example.com, ben@example.com", "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 (1–64 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" } } }