i18n: Du-Ansprache und Einladungstexte in Locales auslagern

Deutsche UI-Texte und Beta-Flyer auf informelles Deutsch umstellen;
hardcodierte Strings aus InvitationAcceptance in de/en.json verschieben.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-30 14:18:47 +02:00
co-authored by Cursor
parent 8bcfb97e98
commit caf7d81ac9
5 changed files with 159 additions and 116 deletions
+31 -48
View File
@@ -57,8 +57,6 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
const autoAcceptStarted = useRef(false) const autoAcceptStarted = useRef(false)
const isDe = i18n.language.startsWith('de')
const sessionReady = (): boolean => { const sessionReady = (): boolean => {
return !!(getActiveMasterKey() && localStorage.getItem('active_userid')) return !!(getActiveMasterKey() && localStorage.getItem('active_userid'))
} }
@@ -83,19 +81,16 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
setLogbookKey(hexToBuffer(hexKey)) setLogbookKey(hexToBuffer(hexKey))
} catch (err) { } catch (err) {
console.error('Invalid key in URL fragment:', err) console.error('Invalid key in URL fragment:', err)
setError(isDe setError(t('invitation.error_invalid_key'))
? 'Der Einladungslink ist kryptografisch ungültig (Schlüssel fehlerhaft).'
: 'The invitation link is cryptographically invalid (corrupted key).')
} }
} else { } else {
setError(isDe setError(t('invitation.error_missing_key'))
? 'Der Einladungslink enthält keinen Entschlüsselungsschlüssel (#key=...). Bitte den vollständigen Link vom Eigner verwenden.'
: 'The invitation link is missing the decryption key (#key=...). Please use the complete link from the owner.')
} }
const rand = Math.floor(1000 + Math.random() * 9000) const rand = Math.floor(1000 + Math.random() * 9000)
setRegUsername(`CrewSkipper_${rand}`) setRegUsername(`CrewSkipper_${rand}`)
}, [isDe]) // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only: URL parsing and default username
}, [])
useEffect(() => { useEffect(() => {
if (token && logbookKey) { if (token && logbookKey) {
@@ -110,14 +105,12 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
const res = await fetch(`/api/collaboration/invite-details?token=${token}`) const res = await fetch(`/api/collaboration/invite-details?token=${token}`)
if (res.status === 410) { if (res.status === 410) {
setError(isDe setError(t('invitation.error_expired'))
? 'Diese Einladung ist abgelaufen (48 Stunden gültig).'
: 'This invitation link has expired (valid for 48 hours only).')
return return
} }
if (!res.ok) { if (!res.ok) {
throw new Error(isDe ? 'Einladungstoken ungültig.' : 'Failed to verify invitation token.') throw new Error(t('invitation.error_invalid_token'))
} }
const details = await res.json() const details = await res.json()
@@ -130,7 +123,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
setDecryptedTitle(title) setDecryptedTitle(title)
} catch (err: any) { } catch (err: any) {
console.error('Failed to load invitation details:', err) console.error('Failed to load invitation details:', err)
setError(err.message || (isDe ? 'Einladungsdetails konnten nicht geladen werden.' : 'Invitation details could not be retrieved.')) setError(err.message || t('invitation.error_load_failed'))
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -141,9 +134,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
const activeUserId = localStorage.getItem('active_userid') const activeUserId = localStorage.getItem('active_userid')
if (!masterKey || !activeUserId) { if (!masterKey || !activeUserId) {
autoAcceptStarted.current = false autoAcceptStarted.current = false
setError(isDe setError(t('invitation.error_incomplete_session'))
? 'Sitzung unvollständig — bitte erneut anmelden (Benutzer-ID fehlt).'
: 'Incomplete session — please log in again (user ID missing).')
setIsLoggedIn(false) setIsLoggedIn(false)
return return
} }
@@ -194,12 +185,12 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
onAccepted(logbookId, decryptedTitle) onAccepted(logbookId, decryptedTitle)
} catch (err: any) { } catch (err: any) {
console.error('Accepting invitation failed:', err) console.error('Accepting invitation failed:', err)
setError(err.message || (isDe ? 'Beitritt fehlgeschlagen.' : 'Acceptance failed.')) setError(err.message || t('invitation.error_accept_failed'))
autoAcceptStarted.current = false autoAcceptStarted.current = false
} finally { } finally {
setAccepting(false) setAccepting(false)
} }
}, [logbookId, logbookKey, token, rawEncryptedTitle, decryptedTitle, onAccepted, isDe]) }, [logbookId, logbookKey, token, rawEncryptedTitle, decryptedTitle, onAccepted, t])
useEffect(() => { useEffect(() => {
if (loading || accepting || autoAcceptStarted.current) return if (loading || accepting || autoAcceptStarted.current) return
@@ -235,7 +226,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
if (resolvedUser) setUsername(resolvedUser) if (resolvedUser) setUsername(resolvedUser)
setShowRecoveryFallback(true) setShowRecoveryFallback(true)
} catch (err: any) { } catch (err: any) {
setAuthError(err.message || (isDe ? 'Passkey-Anmeldung fehlgeschlagen.' : 'Passkey authentication failed.')) setAuthError(err.message || t('invitation.error_login_failed'))
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -247,9 +238,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
const resolvedUser = (username.trim() || encryptedPayloads.username || '').trim() const resolvedUser = (username.trim() || encryptedPayloads.username || '').trim()
if (!resolvedUser) { if (!resolvedUser) {
setAuthError(isDe setAuthError(t('invitation.error_username_missing'))
? 'Benutzername konnte nicht ermittelt werden — bitte erneut anmelden.'
: 'Could not determine username — please try logging in again.')
return return
} }
@@ -285,7 +274,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
setRecoveryPhrase(result.recoveryPhrase) setRecoveryPhrase(result.recoveryPhrase)
} }
} catch (err: any) { } catch (err: any) {
setAuthError(err.message || (isDe ? 'Registrierung fehlgeschlagen.' : 'Registration failed.')) setAuthError(err.message || t('invitation.error_register_failed'))
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -344,7 +333,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
/> />
<div className="auth-actions mt-4"> <div className="auth-actions mt-4">
<button type="button" className="btn secondary" onClick={() => setShowRecoveryFallback(false)}> <button type="button" className="btn secondary" onClick={() => setShowRecoveryFallback(false)}>
{isDe ? 'Zurück' : 'Back'} {t('auth.back')}
</button> </button>
<button type="submit" className="btn primary" disabled={loading}> <button type="submit" className="btn primary" disabled={loading}>
{t('auth.decrypt_logbook')} {t('auth.decrypt_logbook')}
@@ -361,12 +350,10 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
<div className="auth-card glass"> <div className="auth-card glass">
<div className="auth-header"> <div className="auth-header">
<Ship className="auth-icon accent spin" size={48} /> <Ship className="auth-icon accent spin" size={48} />
<h2>{accepting ? (isDe ? 'Beitritt...' : 'Joining...') : (isDe ? 'Einladung wird geprüft...' : 'Checking Invitation...')}</h2> <h2>{accepting ? t('invitation.loading_joining') : t('invitation.loading_checking')}</h2>
</div> </div>
<p className="recovery-warning"> <p className="recovery-warning">
{accepting {accepting ? t('invitation.loading_unlocking') : t('invitation.loading_retrieving_key')}
? (isDe ? 'Logbuch wird freigeschaltet und synchronisiert...' : 'Unlocking logbook and syncing data...')
: (isDe ? 'Lade Verschlüsselungsschlüssel...' : 'Retrieving encryption key...')}
</p> </p>
</div> </div>
) )
@@ -377,12 +364,12 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
<div className="auth-card glass"> <div className="auth-card glass">
<div className="auth-header"> <div className="auth-header">
<AlertTriangle className="auth-icon warn" size={48} /> <AlertTriangle className="auth-icon warn" size={48} />
<h2>{isDe ? 'Einladungsfehler' : 'Invitation Error'}</h2> <h2>{t('invitation.error_title')}</h2>
</div> </div>
<p className="recovery-warning" style={{ color: '#ef4444' }}>{error}</p> <p className="recovery-warning" style={{ color: '#ef4444' }}>{error}</p>
<div className="auth-actions mt-6"> <div className="auth-actions mt-6">
<button className="btn primary" onClick={onCancel} style={{ width: '100%' }}> <button className="btn primary" onClick={onCancel} style={{ width: '100%' }}>
{isDe ? 'Zurück zum Start' : 'Back to Dashboard'} {t('invitation.back_to_start')}
</button> </button>
</div> </div>
</div> </div>
@@ -393,18 +380,18 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
<div className="auth-card glass"> <div className="auth-card glass">
<div className="auth-header"> <div className="auth-header">
<ShieldCheck className="auth-icon success" size={48} style={{ color: '#10b981' }} /> <ShieldCheck className="auth-icon success" size={48} style={{ color: '#10b981' }} />
<h2>{isDe ? 'Logbuch-Einladung' : 'Logbook Invitation'}</h2> <h2>{t('invitation.title')}</h2>
</div> </div>
<div style={{ textAlign: 'center', margin: '20px 0', padding: '16px', background: 'rgba(255,255,255,0.03)', borderRadius: '12px' }}> <div style={{ textAlign: 'center', margin: '20px 0', padding: '16px', background: 'rgba(255,255,255,0.03)', borderRadius: '12px' }}>
<p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#64748b', textTransform: 'uppercase' }}> <p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#64748b', textTransform: 'uppercase' }}>
{isDe ? 'Einladung von' : 'INVITED BY'} {t('invitation.invited_by')}
</p> </p>
<p style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: 600, color: '#f1f5f9' }}> <p style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: 600, color: '#f1f5f9' }}>
Skipper {ownerUsername} Skipper {ownerUsername}
</p> </p>
<p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#64748b', textTransform: 'uppercase' }}> <p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#64748b', textTransform: 'uppercase' }}>
{isDe ? 'Schiff / Logbuch' : 'VESSEL / LOGBOOK'} {t('invitation.vessel_logbook')}
</p> </p>
<p style={{ margin: 0, fontSize: '20px', fontWeight: 700, color: '#fbbf24' }}> <p style={{ margin: 0, fontSize: '20px', fontWeight: 700, color: '#fbbf24' }}>
{decryptedTitle} {decryptedTitle}
@@ -414,12 +401,10 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
{isLoggedIn ? ( {isLoggedIn ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', width: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', width: '100%' }}>
<p style={{ fontSize: '14px', color: '#94a3b8', textAlign: 'center', lineHeight: '145%' }}> <p style={{ fontSize: '14px', color: '#94a3b8', textAlign: 'center', lineHeight: '145%' }}>
{isDe {t('invitation.signed_in_preparing', { username })}
? `Angemeldet als ${username}. Beitritt wird vorbereitet...`
: `Signed in as ${username}. Preparing to join...`}
</p> </p>
<button className="btn primary" onClick={handleAccept} disabled={accepting} style={{ width: '100%' }}> <button className="btn primary" onClick={handleAccept} disabled={accepting} style={{ width: '100%' }}>
{accepting ? (isDe ? 'Beitritt...' : 'Joining...') : (isDe ? 'Erneut beitreten' : 'Join again')} {accepting ? t('invitation.loading_joining') : t('invitation.join_again')}
<ArrowRight size={16} /> <ArrowRight size={16} />
</button> </button>
</div> </div>
@@ -428,27 +413,25 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
{loginMode === 'options' && ( {loginMode === 'options' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<p style={{ fontSize: '13.5px', color: '#94a3b8', textAlign: 'center', lineHeight: '145%' }}> <p style={{ fontSize: '13.5px', color: '#94a3b8', textAlign: 'center', lineHeight: '145%' }}>
{isDe {t('invitation.login_or_register_hint')}
? 'Melden Sie sich an oder registrieren Sie ein Konto, um dem Logbuch beizutreten.'
: 'Sign in or register an account to join this logbook.'}
</p> </p>
<button className="btn primary" onClick={handleLogin} disabled={loading} style={{ width: '100%', padding: '14px' }}> <button className="btn primary" onClick={handleLogin} disabled={loading} style={{ width: '100%', padding: '14px' }}>
<LogIn size={16} /> <LogIn size={16} />
{isDe ? 'Mit Passkey anmelden' : 'Log In with Passkey'} {t('auth.login')}
</button> </button>
<div style={{ display: 'flex', alignItems: 'center', margin: '8px 0' }}> <div style={{ display: 'flex', alignItems: 'center', margin: '8px 0' }}>
<div style={{ flex: 1, height: '1px', background: 'rgba(255,255,255,0.08)' }} /> <div style={{ flex: 1, height: '1px', background: 'rgba(255,255,255,0.08)' }} />
<span style={{ padding: '0 10px', fontSize: '12px', color: '#64748b' }}> <span style={{ padding: '0 10px', fontSize: '12px', color: '#64748b' }}>
{isDe ? 'ODER NEU REGISTRIEREN' : 'OR SIGN UP'} {t('invitation.or_sign_up')}
</span> </span>
<div style={{ flex: 1, height: '1px', background: 'rgba(255,255,255,0.08)' }} /> <div style={{ flex: 1, height: '1px', background: 'rgba(255,255,255,0.08)' }} />
</div> </div>
<button className="btn secondary" onClick={() => setLoginMode('register')} style={{ width: '100%' }}> <button className="btn secondary" onClick={() => setLoginMode('register')} style={{ width: '100%' }}>
<UserPlus size={16} /> <UserPlus size={16} />
{isDe ? 'Neues Crew-Konto erstellen' : 'Register New Crew Account'} {t('invitation.register_crew_account')}
</button> </button>
</div> </div>
)} )}
@@ -457,7 +440,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
<form onSubmit={handleRegister} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> <form onSubmit={handleRegister} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div className="input-group"> <div className="input-group">
<label style={{ display: 'block', fontSize: '13px', color: '#94a3b8', marginBottom: '6px' }}> <label style={{ display: 'block', fontSize: '13px', color: '#94a3b8', marginBottom: '6px' }}>
{isDe ? 'Benutzername' : 'Username'} {t('invitation.username_label')}
</label> </label>
<input <input
type="text" type="text"
@@ -469,10 +452,10 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
</div> </div>
<div className="auth-actions"> <div className="auth-actions">
<button type="button" className="btn secondary" onClick={() => setLoginMode('options')}> <button type="button" className="btn secondary" onClick={() => setLoginMode('options')}>
{isDe ? 'Zurück' : 'Back'} {t('auth.back')}
</button> </button>
<button type="submit" className="btn primary" disabled={!regUsername.trim() || loading}> <button type="submit" className="btn primary" disabled={!regUsername.trim() || loading}>
{isDe ? 'Passkey erstellen' : 'Create Passkey'} {t('invitation.create_passkey')}
</button> </button>
</div> </div>
</form> </form>
@@ -485,7 +468,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
<div className="auth-footer" style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '16px', marginTop: '24px' }}> <div className="auth-footer" style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '16px', marginTop: '24px' }}>
<button className="btn-icon-text" onClick={toggleLanguage}> <button className="btn-icon-text" onClick={toggleLanguage}>
<Languages size={18} /> <Languages size={18} />
{isDe ? 'English' : 'Deutsch'} {i18n.language.startsWith('de') ? t('invitation.switch_language_en') : t('invitation.switch_language_de')}
</button> </button>
</div> </div>
</div> </div>
+95 -65
View File
@@ -22,43 +22,43 @@
"quick_login": "Schnell-Login", "quick_login": "Schnell-Login",
"forget_account": "Account auf diesem Gerät vergessen", "forget_account": "Account auf diesem Gerät vergessen",
"not_user": "Nicht {{name}}?", "not_user": "Nicht {{name}}?",
"recovery_title": "Ihr Wiederherstellungsschlüssel", "recovery_title": "Dein Wiederherstellungsschlüssel",
"recovery_warning": "WICHTIG: Schreiben Sie diese 12 Wörter auf. Wenn Sie Ihren Passkey und diese Wörter verlieren, können Ihre Daten nicht wiederhergestellt werden.", "recovery_warning": "WICHTIG: Schreib diese 12 Wörter auf. Wenn du deinen Passkey und diese Wörter verlierst, können deine Daten nicht wiederhergestellt werden.",
"confirm_recovery": "Ich habe die Wörter aufgeschrieben", "confirm_recovery": "Ich habe die Wörter aufgeschrieben",
"status_logged_in": "Angemeldet", "status_logged_in": "Angemeldet",
"status_logged_out": "Abgemeldet", "status_logged_out": "Abgemeldet",
"copied": "Kopiert!", "copied": "Kopiert!",
"copy_phrase": "Schlüssel kopieren", "copy_phrase": "Schlüssel kopieren",
"enter_recovery": "Wiederherstellungsschlüssel eingeben", "enter_recovery": "Wiederherstellungsschlüssel eingeben",
"recovery_fallback_warning": "Ihr Passkey wurde erfolgreich authentifiziert, aber Ihr Gerät unterstützt keine hardwarebasierte Schlüsselableitung. Geben Sie Ihren 12-Wörter-Wiederherstellungsschlüssel ein, um Ihr Logbuch zu entschlüsseln.", "recovery_fallback_warning": "Dein Passkey wurde erfolgreich authentifiziert, aber dein Gerät unterstützt keine hardwarebasierte Schlüsselableitung. Gib deinen 12-Wörter-Wiederherstellungsschlüssel ein, um dein Logbuch zu entschlüsseln.",
"recovery_placeholder": "Geben Sie Ihren aus 12 Wörtern bestehenden Wiederherstellungsschlüssel getrennt durch Leerzeichen ein...", "recovery_placeholder": "Gib deinen aus 12 Wörtern bestehenden Wiederherstellungsschlüssel getrennt durch Leerzeichen ein...",
"back": "Zurück", "back": "Zurück",
"decrypting": "Entschlüsselung...", "decrypting": "Entschlüsselung...",
"decrypt_logbook": "Logbuch entschlüsseln", "decrypt_logbook": "Logbuch entschlüsseln",
"error_incorrect_recovery": "Falscher Wiederherstellungsschlüssel. Entschlüsselung fehlgeschlagen.", "error_incorrect_recovery": "Falscher Wiederherstellungsschlüssel. Entschlüsselung fehlgeschlagen.",
"error_decryption_failed": "Entschlüsselung fehlgeschlagen. Bitte überprüfen Sie Ihren Wiederherstellungsschlüssel.", "error_decryption_failed": "Entschlüsselung fehlgeschlagen. Bitte überprüfe deinen Wiederherstellungsschlüssel.",
"or_register": "oder Registrieren", "or_register": "oder Registrieren",
"explore_demo": "Demo ohne Account erkunden", "explore_demo": "Demo ohne Account erkunden",
"username_placeholder": "Benutzername / Skippername", "username_placeholder": "Benutzername / Skippername",
"processing": "Verarbeitung...", "processing": "Verarbeitung...",
"help": "Hilfe", "help": "Hilfe",
"setup_pin_title": "Lokale PIN einrichten (Optional)", "setup_pin_title": "Lokale PIN einrichten (Optional)",
"setup_pin_warning": "Da Ihr Gerät keine direkte Passkey-Schlüsselableitung unterstützt, müssten Sie andernfalls bei jedem Login auf diesem Gerät Ihren 12-Wörter-Schlüssel eingeben. Richten Sie eine lokale PIN ein, um das zu vermeiden.", "setup_pin_warning": "Da dein Gerät keine direkte Passkey-Schlüsselableitung unterstützt, müsstest du andernfalls bei jedem Login auf diesem Gerät deinen 12-Wörter-Schlüssel eingeben. Richte eine lokale PIN ein, um das zu vermeiden.",
"pin_placeholder": "Z.B. 123456", "pin_placeholder": "Z.B. 123456",
"pin_label": "Lokaler PIN-Code (4-8 Ziffern)", "pin_label": "Lokaler PIN-Code (4-8 Ziffern)",
"save_pin": "PIN speichern & Fortfahren", "save_pin": "PIN speichern & Fortfahren",
"skip_pin": "Überspringen & recovery verwenden", "skip_pin": "Überspringen & recovery verwenden",
"enter_pin_title": "Mit PIN entschlüsseln", "enter_pin_title": "Mit PIN entschlüsseln",
"enter_pin_warning": "Geben Sie Ihre lokale PIN ein, um den Entschlüsselungsschlüssel auf diesem Gerät freizuschalten.", "enter_pin_warning": "Gib deine lokale PIN ein, um den Entschlüsselungsschlüssel auf diesem Gerät freizuschalten.",
"enter_pin_placeholder": "Geben Sie Ihre PIN ein...", "enter_pin_placeholder": "Gib deine PIN ein...",
"decrypt_with_pin": "Entschlüsseln", "decrypt_with_pin": "Entschlüsseln",
"use_recovery_instead": "Stattdessen Wiederherstellungsschlüssel verwenden", "use_recovery_instead": "Stattdessen Wiederherstellungsschlüssel verwenden",
"error_incorrect_pin": "Falsche PIN. Entschlüsselung fehlgeschlagen." "error_incorrect_pin": "Falsche PIN. Entschlüsselung fehlgeschlagen."
}, },
"pwa": { "pwa": {
"title": "App installieren", "title": "App installieren",
"generic_benefit": "Installieren Sie Kapteins Daagbok auf Ihrem Gerät für schnelleren Zugriff, Offline-Nutzung und dauerhafte Datenspeicherung.", "generic_benefit": "Installiere Kapteins Daagbok auf deinem Gerät für schnelleren Zugriff, Offline-Nutzung und dauerhafte Datenspeicherung.",
"ios_instructions": "Auf dem iPad/iPhone: Fügen Sie die App zum Home-Bildschirm hinzu, damit Ihre Logbuchdaten geschützt bleiben und die App wie eine native App startet.", "ios_instructions": "Auf dem iPad/iPhone: Füge die App zum Home-Bildschirm hinzu, damit deine Logbuchdaten geschützt bleiben und die App wie eine native App startet.",
"ios_step_share": "Teilen-Symbol in der Safari-Leiste antippen", "ios_step_share": "Teilen-Symbol in der Safari-Leiste antippen",
"ios_step_add": "„Zum Home-Bildschirm“ wählen", "ios_step_add": "„Zum Home-Bildschirm“ wählen",
"install_now": "Jetzt installieren", "install_now": "Jetzt installieren",
@@ -102,7 +102,7 @@
"saved": "Schiffsdaten erfolgreich gespeichert!", "saved": "Schiffsdaten erfolgreich gespeichert!",
"loading": "Schiffsdaten werden geladen...", "loading": "Schiffsdaten werden geladen...",
"sails_list": "Besegelung (vorhandene Segel)", "sails_list": "Besegelung (vorhandene Segel)",
"sails_help": "Tragen Sie hier die Segel ein, die an Eurem Schiff zur Verfügung stehen (z. B. Großsegel, Genua, Fock).", "sails_help": "Trag hier die Segel ein, die an deinem Schiff zur Verfügung stehen (z. B. Großsegel, Genua, Fock).",
"add_sail": "Segel hinzufügen", "add_sail": "Segel hinzufügen",
"sail_name_placeholder": "z. B. Großsegel", "sail_name_placeholder": "z. B. Großsegel",
"no_sails": "Keine Segel hinterlegt.", "no_sails": "Keine Segel hinterlegt.",
@@ -159,19 +159,19 @@
"sign_lock_notice": "Nach der Unterschrift sind Änderungen am Logbucheintrag (außer Fotos) nicht möglich, ohne dass Skipper und Crew erneut unterschreiben müssen.", "sign_lock_notice": "Nach der Unterschrift sind Änderungen am Logbucheintrag (außer Fotos) nicht möglich, ohne dass Skipper und Crew erneut unterschreiben müssen.",
"sign_lock_active": "Dieser Eintrag ist unterschrieben. Änderungen am Logbuch (außer Fotos) entfernen Skipper- und Crew-Unterschrift automatisch.", "sign_lock_active": "Dieser Eintrag ist unterschrieben. Änderungen am Logbuch (außer Fotos) entfernen Skipper- und Crew-Unterschrift automatisch.",
"sign_lock_warning_title": "Unterschrift bestätigen", "sign_lock_warning_title": "Unterschrift bestätigen",
"sign_lock_warning": "Nach dem Unterschreiben sind Änderungen am Logbucheintrag (außer Fotos) nicht mehr möglich, ohne dass Skipper und Crew erneut unterschreiben müssen.\n\nMöchten Sie fortfahren?", "sign_lock_warning": "Nach dem Unterschreiben sind Änderungen am Logbucheintrag (außer Fotos) nicht mehr möglich, ohne dass Skipper und Crew erneut unterschreiben müssen.\n\nMöchtest du fortfahren?",
"sign_proceed": "Unterschreiben", "sign_proceed": "Unterschreiben",
"sign_cancel": "Abbrechen", "sign_cancel": "Abbrechen",
"sign_cleared_re_sign_title": "Unterschriften entfernt", "sign_cleared_re_sign_title": "Unterschriften entfernt",
"sign_cleared_re_sign": "Der Logbucheintrag wurde geändert. Skipper- und Crew-Unterschrift wurden entfernt. Bitte erneut unterschreiben.", "sign_cleared_re_sign": "Der Logbucheintrag wurde geändert. Skipper- und Crew-Unterschrift wurden entfernt. Bitte erneut unterschreiben.",
"no_entries": "Keine Logbucheinträge für diese Yacht gefunden. Erstellen Sie Ihren ersten Reisetag!", "no_entries": "Keine Logbucheinträge für diese Yacht gefunden. Erstelle deinen ersten Reisetag!",
"back_to_list": "Zurück zur Journal-Liste", "back_to_list": "Zurück zur Journal-Liste",
"save": "Logbuchseite speichern", "save": "Logbuchseite speichern",
"saving": "Wird gespeichert...", "saving": "Wird gespeichert...",
"saved": "Logbuchseite erfolgreich gespeichert!", "saved": "Logbuchseite erfolgreich gespeichert!",
"loading": "Journal wird geladen...", "loading": "Journal wird geladen...",
"delete_entry": "Tag löschen", "delete_entry": "Tag löschen",
"delete_confirm": "Sind Sie sicher, dass Sie diesen Reisetag unwiderruflich löschen möchten?", "delete_confirm": "Bist du sicher, dass du diesen Reisetag unwiderruflich löschen möchtest?",
"carry_over_tanks_title": "Daten vom Vortag übernehmen?", "carry_over_tanks_title": "Daten vom Vortag übernehmen?",
"carry_over_tanks_confirm": "Start-Hafen, Frischwasser- und Kraftstoff-Morgenstände vom letzten Reisetag übernehmen?\n\nStart-Hafen: {{departure}}\nFrischwasser: {{fw}} L\nKraftstoff: {{fuel}} L", "carry_over_tanks_confirm": "Start-Hafen, Frischwasser- und Kraftstoff-Morgenstände vom letzten Reisetag übernehmen?\n\nStart-Hafen: {{departure}}\nFrischwasser: {{fw}} L\nKraftstoff: {{fuel}} L",
"carry_over_tanks_yes": "Übernehmen", "carry_over_tanks_yes": "Übernehmen",
@@ -207,16 +207,16 @@
"photo_btn": "Foto aufnehmen / Hochladen", "photo_btn": "Foto aufnehmen / Hochladen",
"photo_processing": "Wird verarbeitet...", "photo_processing": "Wird verarbeitet...",
"no_photos": "Noch keine Fotos an diesen Reisetag angehängt.", "no_photos": "Noch keine Fotos an diesen Reisetag angehängt.",
"photo_delete_confirm": "Sind Sie sicher, dass Sie dieses Foto unwiderruflich löschen möchten?", "photo_delete_confirm": "Bist du sicher, dass du dieses Foto unwiderruflich löschen möchtest?",
"confirm_yes": "Ja", "confirm_yes": "Ja",
"confirm_no": "Nein", "confirm_no": "Nein",
"track_upload_title": "GPS-Track (Datei)", "track_upload_title": "GPS-Track (Datei)",
"track_upload_points": "Punkte", "track_upload_points": "Punkte",
"gps_tracking_btn_gpx": "Track-Datei herunterladen", "gps_tracking_btn_gpx": "Track-Datei herunterladen",
"gps_track_upload_help": "Ziehen Sie eine GPX-, KML- oder GeoJSON-Datei hierher oder klicken Sie zum Auswählen", "gps_track_upload_help": "Zieh eine GPX-, KML- oder GeoJSON-Datei hierher oder klicke zum Auswählen",
"gps_track_upload_btn": "GPS-Track hochladen", "gps_track_upload_btn": "GPS-Track hochladen",
"gps_track_delete": "Track-Datei löschen", "gps_track_delete": "Track-Datei löschen",
"gps_track_delete_confirm": "Sind Sie sicher, dass Sie diese Track-Datei dauerhaft löschen möchten?", "gps_track_delete_confirm": "Bist du sicher, dass du diese Track-Datei dauerhaft löschen möchtest?",
"track_distance": "GPS-Strecke (sm)", "track_distance": "GPS-Strecke (sm)",
"track_speed_max": "Max. Geschwindigkeit (kn)", "track_speed_max": "Max. Geschwindigkeit (kn)",
"track_speed_avg": "Ø Geschwindigkeit (kn)", "track_speed_avg": "Ø Geschwindigkeit (kn)",
@@ -230,33 +230,33 @@
"share_unsupported": "Teilen wird auf diesem Gerät nicht unterstützt. Datei wurde stattdessen heruntergeladen.", "share_unsupported": "Teilen wird auf diesem Gerät nicht unterstützt. Datei wurde stattdessen heruntergeladen.",
"invite_crew": "Crew einladen", "invite_crew": "Crew einladen",
"invite_link_copied": "Einladungslink in die Zwischenablage kopiert!", "invite_link_copied": "Einladungslink in die Zwischenablage kopiert!",
"invite_link_desc": "Teilen Sie diesen Link mit Crewmitgliedern, um ihnen Schreibrechte für dieses Logbuch zu gewähren.", "invite_link_desc": "Teile diesen Link mit Crewmitgliedern, um ihnen Schreibrechte für dieses Logbuch zu gewähren.",
"collaborators_list": "Mitglieder / Crew", "collaborators_list": "Mitglieder / Crew",
"revoke": "Entfernen", "revoke": "Entfernen",
"revoke_confirm": "Sind Sie sicher, dass Sie diesem Crewmitglied den Zugriff entziehen möchten?", "revoke_confirm": "Bist du sicher, dass du diesem Crewmitglied den Zugriff entziehen möchtest?",
"invite_role": "Rolle", "invite_role": "Rolle",
"invite_expires": "Link ist 48 Stunden lang gültig" "invite_expires": "Link ist 48 Stunden lang gültig"
}, },
"dashboard": { "dashboard": {
"title": "Ihre Logbücher", "title": "Deine Logbücher",
"subtitle": "Wählen Sie ein Logbuch aus oder erstellen Sie ein neues, um Ihre Reisen zu verwalten.", "subtitle": "Wähle ein Logbuch aus oder erstelle ein neues, um deine Reisen zu verwalten.",
"create_btn": "Logbuch erstellen", "create_btn": "Logbuch erstellen",
"new_logbook_placeholder": "Name des Logbuchs oder der Yacht", "new_logbook_placeholder": "Name des Logbuchs oder der Yacht",
"logout": "Abmelden", "logout": "Abmelden",
"logged_in_as": "Angemeldet als {{name}}", "logged_in_as": "Angemeldet als {{name}}",
"delete_confirm": "Sind Sie sicher, dass Sie dieses Logbuch unwiderruflich löschen möchten? Alle lokalen Daten und Server-Kopien werden vernichtet.\n\nTipp: Erstellen Sie vorher unter Einstellungen → Backup & Wiederherstellung eine Sicherungskopie (.daagbok.json), falls Sie die Daten später behalten möchten.", "delete_confirm": "Bist du sicher, dass du dieses Logbuch unwiderruflich löschen möchtest? Alle lokalen Daten und Server-Kopien werden vernichtet.\n\nTipp: Erstelle vorher unter Einstellungen → Backup & Wiederherstellung eine Sicherungskopie (.daagbok.json), falls du die Daten später behalten möchtest.",
"no_logbooks": "Keine Logbücher gefunden. Erstellen Sie Ihr erstes Logbuch, um zu beginnen!", "no_logbooks": "Keine Logbücher gefunden. Erstelle dein erstes Logbuch, um zu beginnen!",
"loading": "Logbücher werden geladen...", "loading": "Logbücher werden geladen...",
"status_synced": "Synchronisiert", "status_synced": "Synchronisiert",
"status_local": "Nur lokaler Cache", "status_local": "Nur lokaler Cache",
"delete_btn": "Logbuch löschen", "delete_btn": "Logbuch löschen",
"section_owned": "Meine Logbücher", "section_owned": "Meine Logbücher",
"section_shared": "Geteilte Logbücher", "section_shared": "Geteilte Logbücher",
"section_shared_hint": "Sie wurden als Crew-Mitglied eingeladen. Skipper-Profil und Einstellungen gehören dem Eigner.", "section_shared_hint": "Du wurdest als Crew-Mitglied eingeladen. Skipper-Profil und Einstellungen gehören dem Eigner.",
"role_owner": "Eigenes Logbuch", "role_owner": "Eigenes Logbuch",
"role_owner_hint": "Sie sind Eigner und Skipper dieses Logbuchs", "role_owner_hint": "Du bist Eigner und Skipper dieses Logbuchs",
"role_crew": "Crew-Zugang", "role_crew": "Crew-Zugang",
"role_crew_hint": "Eingeladenes Logbuch — Sie können als Crew mitarbeiten und signieren", "role_crew_hint": "Eingeladenes Logbuch — du kannst als Crew mitarbeiten und signieren",
"role_read": "Nur Lesen", "role_read": "Nur Lesen",
"role_read_hint": "Geteiltes Logbuch — nur Ansicht, keine Bearbeitung" "role_read_hint": "Geteiltes Logbuch — nur Ansicht, keine Bearbeitung"
}, },
@@ -281,11 +281,11 @@
"save_member": "Mitglied speichern", "save_member": "Mitglied speichern",
"saved": "Skipper-Profil erfolgreich gespeichert!", "saved": "Skipper-Profil erfolgreich gespeichert!",
"loading": "Crew-Dateien werden geladen...", "loading": "Crew-Dateien werden geladen...",
"delete_confirm": "Sind Sie sicher, dass Sie dieses Crew-Mitglied entfernen möchten?" "delete_confirm": "Bist du sicher, dass du dieses Crew-Mitglied entfernen möchtest?"
}, },
"deviation": { "deviation": {
"title": "Ablenkungstabelle (Compass Deviation)", "title": "Ablenkungstabelle (Compass Deviation)",
"subtitle": "Tragen Sie die Magnetkompass-Ablenkung (Abl.) für Kurse (MgK) von 000° bis 360° in 10°-Schritten ein.", "subtitle": "Trag die Magnetkompass-Ablenkung (Abl.) für Kurse (MgK) von 000° bis 360° in 10°-Schritten ein.",
"heading": "MgK", "heading": "MgK",
"deviation": "Ablenkung", "deviation": "Ablenkung",
"save": "Kalibrierungsgitter speichern", "save": "Kalibrierungsgitter speichern",
@@ -295,18 +295,18 @@
}, },
"settings": { "settings": {
"title": "Systemeinstellungen", "title": "Systemeinstellungen",
"subtitle": "Konfigurieren Sie externe Integrationen und Anmeldedaten.", "subtitle": "Konfiguriere externe Integrationen und Anmeldedaten.",
"owm_title": "Wetter-Integration", "owm_title": "Wetter-Integration",
"owm_key": "OpenWeatherMap API-Schlüssel", "owm_key": "OpenWeatherMap API-Schlüssel",
"save": "Konfiguration speichern", "save": "Konfiguration speichern",
"saving": "Wird gespeichert...", "saving": "Wird gespeichert...",
"saved": "Einstellungen erfolgreich gespeichert!", "saved": "Einstellungen erfolgreich gespeichert!",
"key_help": "Optional: eigener OpenWeatherMap-API-Schlüssel. Ohne Eintrag wird der serverseitige Schlüssel aus der Betreiber-Konfiguration verwendet.", "key_help": "Optional: eigener OpenWeatherMap-API-Schlüssel. Ohne Eintrag wird der serverseitige Schlüssel aus der Betreiber-Konfiguration verwendet.",
"no_key": "Kein OpenWeatherMap-API-Schlüssel verfügbar. Hinterlegen Sie einen eigenen Schlüssel in den Einstellungen oder kontaktieren Sie den Betreiber.", "no_key": "Kein OpenWeatherMap-API-Schlüssel verfügbar. Hinterlege einen eigenen Schlüssel in den Einstellungen oder kontaktiere den Betreiber.",
"weather_success": "Wetterdaten erfolgreich abgerufen!", "weather_success": "Wetterdaten erfolgreich abgerufen!",
"weather_error": "Wetterdatenabruf fehlgeschlagen. Überprüfen Sie den API-Schlüssel und die Verbindung.", "weather_error": "Wetterdatenabruf fehlgeschlagen. Überprüfe den API-Schlüssel und die Verbindung.",
"weather_date_mismatch": "Wetterdaten können nur für den heutigen Tag ({{today}}) abgerufen werden. Dieser Logbucheintrag ist auf den {{date}} datiert.", "weather_date_mismatch": "Wetterdaten können nur für den heutigen Tag ({{today}}) abgerufen werden. Dieser Logbucheintrag ist auf den {{date}} datiert.",
"gps_error": "Bitte geben Sie einen Ort an oder ermitteln Sie die GPS-Koordinaten.", "gps_error": "Bitte gib einen Ort an oder ermittle die GPS-Koordinaten.",
"theme_title": "Design-Anpassung", "theme_title": "Design-Anpassung",
"theme_label": "Design-Stil der App", "theme_label": "Design-Stil der App",
"theme_auto": "Automatisch (OS-Erkennung)", "theme_auto": "Automatisch (OS-Erkennung)",
@@ -319,36 +319,36 @@
"color_scheme_light": "Hell", "color_scheme_light": "Hell",
"color_scheme_dark": "Dunkel", "color_scheme_dark": "Dunkel",
"share_title": "Logbuch teilen (Schreibgeschützt)", "share_title": "Logbuch teilen (Schreibgeschützt)",
"share_desc": "Aktivieren Sie diese Option, um einen öffentlichen, schreibgeschützten Link zu erstellen. Jeder mit dem Link kann Ihre Reisen, Yacht-Profile und Besatzung ansehen. Die Verschlüsselungsschlüssel werden niemals an den Server übertragen (sie bleiben im Hash-Teil der URL).", "share_desc": "Aktiviere diese Option, um einen öffentlichen, schreibgeschützten Link zu erstellen. Jeder mit dem Link kann deine Reisen, Yacht-Profile und Besatzung ansehen. Die Verschlüsselungsschlüssel werden niemals an den Server übertragen (sie bleiben im Hash-Teil der URL).",
"share_enable": "Öffentlichen Link aktivieren", "share_enable": "Öffentlichen Link aktivieren",
"share_copied": "Link kopiert!", "share_copied": "Link kopiert!",
"share_copy_btn": "Link kopieren", "share_copy_btn": "Link kopieren",
"danger_zone_title": "Gefahrenzone", "danger_zone_title": "Gefahrenzone",
"danger_zone_desc": "Durch das Löschen Ihres Kontos werden alle Ihre Passkeys, Logbücher, Schiffsdaten, Crew-Profile, Reiseeinträge und E2E-Schlüssel unwiderruflich gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", "danger_zone_desc": "Durch das Löschen deines Kontos werden alle deine Passkeys, Logbücher, Schiffsdaten, Crew-Profile, Reiseeinträge und E2E-Schlüssel unwiderruflich gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
"delete_account_btn": "Konto unwiderruflich löschen", "delete_account_btn": "Konto unwiderruflich löschen",
"delete_account_confirm_title": "Konto löschen?", "delete_account_confirm_title": "Konto löschen?",
"delete_account_confirm_desc": "Sind Sie absolut sicher, dass Sie Ihr Konto und alle zugehörigen Logbücher und E2E-verschlüsselten Daten unwiderruflich löschen möchten?", "delete_account_confirm_desc": "Bist du absolut sicher, dass du dein Konto und alle zugehörigen Logbücher und E2E-verschlüsselten Daten unwiderruflich löschen möchtest?",
"delete_account_confirm_yes": "Ja, Konto und alle Daten löschen", "delete_account_confirm_yes": "Ja, Konto und alle Daten löschen",
"delete_account_confirm_no": "Abbrechen", "delete_account_confirm_no": "Abbrechen",
"delete_account_failed": "Konto konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.", "delete_account_failed": "Konto konnte nicht gelöscht werden. Bitte versuche es erneut.",
"deleting_account": "Konto wird gelöscht…", "deleting_account": "Konto wird gelöscht…",
"tour_title": "App-Tour", "tour_title": "App-Tour",
"tour_desc": "Lassen Sie sich erneut durch die wichtigsten Bereiche der App führen.", "tour_desc": "Lass dich erneut durch die wichtigsten Bereiche der App führen.",
"tour_restart": "Tour erneut starten", "tour_restart": "Tour erneut starten",
"push_title": "Push-Benachrichtigungen", "push_title": "Push-Benachrichtigungen",
"push_desc": "Als Logbuch-Eigner werden Sie benachrichtigt, wenn eingeladene Crewmitglieder Änderungen synchronisieren. Es werden keine Inhalte im Klartext übermittelt.", "push_desc": "Als Logbuch-Eigner wirst du benachrichtigt, wenn eingeladene Crewmitglieder Änderungen synchronisieren. Es werden keine Inhalte im Klartext übermittelt.",
"push_enable": "Bei Crew-Änderungen benachrichtigen", "push_enable": "Bei Crew-Änderungen benachrichtigen",
"push_active": "Push-Benachrichtigungen sind auf diesem Gerät aktiv.", "push_active": "Push-Benachrichtigungen sind auf diesem Gerät aktiv.",
"push_unsupported": "Push-Benachrichtigungen werden in diesem Browser nicht unterstützt.", "push_unsupported": "Push-Benachrichtigungen werden in diesem Browser nicht unterstützt.",
"push_denied_hint": "Benachrichtigungen sind blockiert. Erlauben Sie sie in den Browser- oder Geräteeinstellungen.", "push_denied_hint": "Benachrichtigungen sind blockiert. Erlaube sie in den Browser- oder Geräteeinstellungen.",
"push_ios_install_hint": "Auf dem iPhone/iPad: App zum Home-Bildschirm hinzufügen (iOS 16.4+), um Push zu nutzen.", "push_ios_install_hint": "Auf dem iPhone/iPad: App zum Home-Bildschirm hinzufügen (iOS 16.4+), um Push zu nutzen.",
"push_error": "Push-Benachrichtigungen konnten nicht aktiviert werden.", "push_error": "Push-Benachrichtigungen konnten nicht aktiviert werden.",
"backup_title": "Backup & Wiederherstellung", "backup_title": "Backup & Wiederherstellung",
"backup_desc": "Vollständiges verschlüsseltes Backup dieses Logbuchs (Einträge, Fotos, GPS-Tracks, Crew, Schiff). Mit Backup-Passphrase geschützt — für Restore auf diesem oder einem neuen Account.", "backup_desc": "Vollständiges verschlüsseltes Backup dieses Logbuchs (Einträge, Fotos, GPS-Tracks, Crew, Schiff). Mit Backup-Passphrase geschützt — für Restore auf diesem oder einem neuen Account.",
"backup_export_title": "Backup erstellen", "backup_export_title": "Backup erstellen",
"backup_export_desc": "Lädt alle lokalen Daten als .daagbok.json herunter. Bewahren Sie Datei und Passphrase getrennt und sicher auf.", "backup_export_desc": "Lädt alle lokalen Daten als .daagbok.json herunter. Bewahre Datei und Passphrase getrennt und sicher auf.",
"backup_restore_title": "Backup wiederherstellen", "backup_restore_title": "Backup wiederherstellen",
"backup_restore_desc": "Stellt ein Backup in Ihrem aktuellen Account wieder her — auch nach Registrierung eines neuen Accounts.", "backup_restore_desc": "Stellt ein Backup in deinem aktuellen Account wieder her — auch nach Registrierung eines neuen Accounts.",
"backup_passphrase": "Backup-Passphrase", "backup_passphrase": "Backup-Passphrase",
"backup_passphrase_placeholder": "Mindestens 8 Zeichen", "backup_passphrase_placeholder": "Mindestens 8 Zeichen",
"backup_passphrase_confirm": "Passphrase bestätigen", "backup_passphrase_confirm": "Passphrase bestätigen",
@@ -368,7 +368,7 @@
"backup_invalid_json": "Die Datei ist keine gültige JSON-Datei.", "backup_invalid_json": "Die Datei ist keine gültige JSON-Datei.",
"backup_invalid_format": "Unbekanntes oder veraltetes Backup-Format.", "backup_invalid_format": "Unbekanntes oder veraltetes Backup-Format.",
"backup_not_owner": "Nur der Logbuch-Eigner kann Backups erstellen.", "backup_not_owner": "Nur der Logbuch-Eigner kann Backups erstellen.",
"backup_not_authenticated": "Bitte melden Sie sich an, um ein Backup wiederherzustellen.", "backup_not_authenticated": "Bitte melde dich an, um ein Backup wiederherzustellen.",
"backup_id_conflict": "Ein Logbuch mit dieser ID existiert bereits.", "backup_id_conflict": "Ein Logbuch mit dieser ID existiert bereits.",
"backup_overwrite_confirm": "Das vorhandene Logbuch mit gleicher ID wird ersetzt. Fortfahren?", "backup_overwrite_confirm": "Das vorhandene Logbuch mit gleicher ID wird ersetzt. Fortfahren?",
"backup_new_id_confirm": "Das Backup als neues Logbuch mit neuer ID importieren?", "backup_new_id_confirm": "Das Backup als neues Logbuch mit neuer ID importieren?",
@@ -380,13 +380,13 @@
}, },
"disclaimer": { "disclaimer": {
"title": "Wichtige Hinweise", "title": "Wichtige Hinweise",
"intro": "Bitte lesen Sie die folgenden Hinweise, bevor Sie Kapteins Daagbok nutzen.", "intro": "Bitte lies die folgenden Hinweise, bevor du Kapteins Daagbok nutzt.",
"e2e_title": "Ende-zu-Ende-Verschlüsselung", "e2e_title": "Ende-zu-Ende-Verschlüsselung",
"e2e_body": "Ihre Logbuchdaten werden Ende-zu-Ende verschlüsselt. Nur Sie bzw. Personen mit Ihrem Schlüssel können die Inhalte lesen. Auf dem Server werden ausschließlich verschlüsselte Daten gespeichert.", "e2e_body": "Deine Logbuchdaten werden Ende-zu-Ende verschlüsselt. Nur du bzw. Personen mit deinem Schlüssel können die Inhalte lesen. Auf dem Server werden ausschließlich verschlüsselte Daten gespeichert.",
"pwa_title": "Progressive Web App (PWA)", "pwa_title": "Progressive Web App (PWA)",
"pwa_body": "Kapteins Daagbok läuft als Progressive Web App in Ihrem Browser und kann auf Ihrem Gerät installiert werden ähnlich wie eine native App, ohne App-Store.", "pwa_body": "Kapteins Daagbok läuft als Progressive Web App in deinem Browser und kann auf deinem Gerät installiert werden ähnlich wie eine native App, ohne App-Store.",
"storage_title": "Lokale Speicherung & Synchronisation", "storage_title": "Lokale Speicherung & Synchronisation",
"storage_body": "Ihre Daten werden lokal auf Ihrem Gerät zwischengespeichert (IndexedDB). Bei aktiver Internetverbindung werden Änderungen mit dem Server synchronisiert. Ohne Verbindung können Sie weiterarbeiten; die Synchronisation erfolgt später.", "storage_body": "Deine Daten werden lokal auf deinem Gerät zwischengespeichert (IndexedDB). Bei aktiver Internetverbindung werden Änderungen mit dem Server synchronisiert. Ohne Verbindung kannst du weiterarbeiten; die Synchronisation erfolgt später.",
"free_title": "Kostenlos & werbefrei", "free_title": "Kostenlos & werbefrei",
"free_body": "Kapteins Daagbok ist kostenlos und enthält keine Werbung.", "free_body": "Kapteins Daagbok ist kostenlos und enthält keine Werbung.",
"liability_title": "Haftungsausschluss", "liability_title": "Haftungsausschluss",
@@ -401,24 +401,24 @@
"feedback": { "feedback": {
"button_title": "Feedback senden", "button_title": "Feedback senden",
"title": "Feedback", "title": "Feedback",
"intro": "Teilen Sie Fehler, Ideen oder allgemeines Feedback. Ihre Nachricht wird über einen sicheren Benachrichtigungskanal an das Projektteam gesendet.", "intro": "Teile Fehler, Ideen oder allgemeines Feedback. Deine Nachricht wird über einen sicheren Benachrichtigungskanal an das Projektteam gesendet.",
"category_label": "Kategorie", "category_label": "Kategorie",
"category_general": "Allgemein", "category_general": "Allgemein",
"category_bug": "Fehler melden", "category_bug": "Fehler melden",
"category_feature": "Feature-Wunsch", "category_feature": "Feature-Wunsch",
"contact_label": "E-Mail (optional)", "contact_label": "E-Mail (optional)",
"contact_placeholder": "ihre@email.beispiel", "contact_placeholder": "deine@email.beispiel",
"message_label": "Nachricht", "message_label": "Nachricht",
"message_placeholder": "Beschreiben Sie Ihr Feedback…", "message_placeholder": "Beschreib dein Feedback…",
"send": "Senden", "send": "Senden",
"sending": "Wird gesendet…", "sending": "Wird gesendet…",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"success": "Vielen Dank! Ihr Feedback wurde gesendet.", "success": "Vielen Dank! Dein Feedback wurde gesendet.",
"error_send": "Feedback konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.", "error_send": "Feedback konnte nicht gesendet werden. Bitte versuche es später erneut.",
"error_invalid_email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.", "error_invalid_email": "Bitte gib eine gültige E-Mail-Adresse ein.",
"error_not_configured": "Feedback ist auf diesem Server nicht verfügbar.", "error_not_configured": "Feedback ist auf diesem Server nicht verfügbar.",
"error_rate_limited": "Zu viele Feedback-Nachrichten in kurzer Zeit. Bitte warten Sie einige Minuten.", "error_rate_limited": "Zu viele Feedback-Nachrichten in kurzer Zeit. Bitte warte einige Minuten.",
"error_spam": "Diese Nachricht konnte nicht gesendet werden. Bitte formulieren Sie sie anders." "error_spam": "Diese Nachricht konnte nicht gesendet werden. Bitte formuliere sie anders."
}, },
"demo": { "demo": {
"logbook_title": "Demo-Logbuch Ostsee", "logbook_title": "Demo-Logbuch Ostsee",
@@ -427,6 +427,36 @@
"cta_register": "Account erstellen", "cta_register": "Account erstellen",
"back_to_login": "Zur Anmeldung" "back_to_login": "Zur Anmeldung"
}, },
"invitation": {
"error_invalid_key": "Der Einladungslink ist kryptografisch ungültig (Schlüssel fehlerhaft).",
"error_missing_key": "Der Einladungslink enthält keinen Entschlüsselungsschlüssel (#key=...). Bitte den vollständigen Link vom Eigner verwenden.",
"error_expired": "Diese Einladung ist abgelaufen (48 Stunden gültig).",
"error_invalid_token": "Einladungstoken ungültig.",
"error_load_failed": "Einladungsdetails konnten nicht geladen werden.",
"error_incomplete_session": "Sitzung unvollständig — bitte erneut anmelden (Benutzer-ID fehlt).",
"error_accept_failed": "Beitritt fehlgeschlagen.",
"error_login_failed": "Passkey-Anmeldung fehlgeschlagen.",
"error_username_missing": "Benutzername konnte nicht ermittelt werden — bitte erneut anmelden.",
"error_register_failed": "Registrierung fehlgeschlagen.",
"loading_joining": "Beitritt...",
"loading_checking": "Einladung wird geprüft...",
"loading_unlocking": "Logbuch wird freigeschaltet und synchronisiert...",
"loading_retrieving_key": "Lade Verschlüsselungsschlüssel...",
"error_title": "Einladungsfehler",
"back_to_start": "Zurück zum Start",
"title": "Logbuch-Einladung",
"invited_by": "Einladung von",
"vessel_logbook": "Schiff / Logbuch",
"signed_in_preparing": "Angemeldet als {{username}}. Beitritt wird vorbereitet...",
"join_again": "Erneut beitreten",
"login_or_register_hint": "Melde dich an oder registriere ein Konto, um dem Logbuch beizutreten.",
"or_sign_up": "ODER NEU REGISTRIEREN",
"register_crew_account": "Neues Crew-Konto erstellen",
"username_label": "Benutzername",
"create_passkey": "Passkey erstellen",
"switch_language_en": "English",
"switch_language_de": "Deutsch"
},
"stats": { "stats": {
"title": "Statistik", "title": "Statistik",
"subtitle": "Strecken, Verbrauch und Antriebsart auf einen Blick", "subtitle": "Strecken, Verbrauch und Antriebsart auf einen Blick",
@@ -469,19 +499,19 @@
"steps": { "steps": {
"welcome": { "welcome": {
"title": "Willkommen an Bord!", "title": "Willkommen an Bord!",
"body": "Wir haben ein Demo-Logbuch mit drei Reisetagen in der Kieler Förde für Sie angelegt. Die Beispieleinträge können Sie jederzeit löschen, wenn Sie mit dem eigenen Logbuch starten möchten. Diese kurze Tour zeigt Ihnen die wichtigsten Funktionen." "body": "Wir haben ein Demo-Logbuch mit drei Reisetagen in der Kieler Förde für dich angelegt. Die Beispieleinträge kannst du jederzeit löschen, wenn du mit dem eigenen Logbuch starten möchtest. Diese kurze Tour zeigt dir die wichtigsten Funktionen."
}, },
"welcome_public": { "welcome_public": {
"title": "Willkommen an Bord!", "title": "Willkommen an Bord!",
"body": "Erkunden Sie unser Demo-Logbuch mit drei Reisetagen in der Kieler Förde ganz ohne Account. Diese kurze Tour zeigt Ihnen Schiffsdaten, Crew und Logbucheinträge." "body": "Erkunde unser Demo-Logbuch mit drei Reisetagen in der Kieler Förde ganz ohne Account. Diese kurze Tour zeigt dir Schiffsdaten, Crew und Logbucheinträge."
}, },
"nav_logs": { "nav_logs": {
"title": "Logbucheinträge", "title": "Logbucheinträge",
"body": "Hier verwalten Sie Ihre Reisetage Abfahrt, Ziel, Wetter, Tankstände und GPS-Tracks." "body": "Hier verwaltest du deine Reisetage Abfahrt, Ziel, Wetter, Tankstände und GPS-Tracks."
}, },
"entry_list": { "entry_list": {
"title": "Ihre Reisetage", "title": "Deine Reisetage",
"body": "Jede Karte steht für einen Reisetag. Tippen Sie auf einen Eintrag, um Details zu sehen oder zu bearbeiten." "body": "Jede Karte steht für einen Reisetag. Tippe auf einen Eintrag, um Details zu sehen oder zu bearbeiten."
}, },
"entry_open": { "entry_open": {
"title": "Reisetag öffnen", "title": "Reisetag öffnen",
@@ -489,27 +519,27 @@
}, },
"entry_track": { "entry_track": {
"title": "GPS-Track", "title": "GPS-Track",
"body": "Laden Sie GPX-Dateien hoch oder sehen Sie bereits gespeicherte Routen auf der Karte inklusive Distanz und Geschwindigkeit." "body": "Lade GPX-Dateien hoch oder sieh bereits gespeicherte Routen auf der Karte inklusive Distanz und Geschwindigkeit."
}, },
"nav_vessel": { "nav_vessel": {
"title": "Schiffsdaten", "title": "Schiffsdaten",
"body": "Hinterlegen Sie Name, Maße und technische Daten Ihrer Yacht einmal ausfüllen, für alle Reisetage verfügbar." "body": "Hinterlege Name, Maße und technische Daten deiner Yacht einmal ausfüllen, für alle Reisetage verfügbar."
}, },
"nav_crew": { "nav_crew": {
"title": "Crew-Liste", "title": "Crew-Liste",
"body": "Verwalten Sie Besatzungsmitglieder und weisen Sie sie später Reisetagen zu." "body": "Verwalte Besatzungsmitglieder und weise sie später Reisetagen zu."
}, },
"nav_stats": { "nav_stats": {
"title": "Statistik-Dashboard", "title": "Statistik-Dashboard",
"body": "Hier sehen Sie Reisedistanzen, Verbrauch, Routenkarten und Antriebsanteile automatisch aus Ihren Logbucheinträgen berechnet." "body": "Hier siehst du Reisedistanzen, Verbrauch, Routenkarten und Antriebsanteile automatisch aus deinen Logbucheinträgen berechnet."
}, },
"nav_feedback": { "nav_feedback": {
"title": "Feedback senden", "title": "Feedback senden",
"body": "Über dieses Formular können Sie Fehler, Ideen oder allgemeines Feedback direkt an das Projektteam schicken auch nach der Tour jederzeit über das Symbol oben rechts." "body": "Über dieses Formular kannst du Fehler, Ideen oder allgemeines Feedback direkt an das Projektteam schicken auch nach der Tour jederzeit über das Symbol oben rechts."
}, },
"finish": { "finish": {
"title": "Alles klar!", "title": "Alles klar!",
"body": "Sie landen gleich im Statistik-Dashboard. Die Tour können Sie jederzeit unter Einstellungen erneut starten. Gute Fahrt!" "body": "Du landest gleich im Statistik-Dashboard. Die Tour kannst du jederzeit unter Einstellungen erneut starten. Gute Fahrt!"
} }
} }
} }
+30
View File
@@ -427,6 +427,36 @@
"cta_register": "Create account", "cta_register": "Create account",
"back_to_login": "Back to login" "back_to_login": "Back to login"
}, },
"invitation": {
"error_invalid_key": "The invitation link is cryptographically invalid (corrupted key).",
"error_missing_key": "The invitation link is missing the decryption key (#key=...). Please use the complete link from the owner.",
"error_expired": "This invitation link has expired (valid for 48 hours only).",
"error_invalid_token": "Failed to verify invitation token.",
"error_load_failed": "Invitation details could not be retrieved.",
"error_incomplete_session": "Incomplete session — please log in again (user ID missing).",
"error_accept_failed": "Acceptance failed.",
"error_login_failed": "Passkey authentication failed.",
"error_username_missing": "Could not determine username — please try logging in again.",
"error_register_failed": "Registration failed.",
"loading_joining": "Joining...",
"loading_checking": "Checking Invitation...",
"loading_unlocking": "Unlocking logbook and syncing data...",
"loading_retrieving_key": "Retrieving encryption key...",
"error_title": "Invitation Error",
"back_to_start": "Back to Dashboard",
"title": "Logbook Invitation",
"invited_by": "INVITED BY",
"vessel_logbook": "VESSEL / LOGBOOK",
"signed_in_preparing": "Signed in as {{username}}. Preparing to join...",
"join_again": "Join again",
"login_or_register_hint": "Sign in or register an account to join this logbook.",
"or_sign_up": "OR SIGN UP",
"register_crew_account": "Register New Crew Account",
"username_label": "Username",
"create_passkey": "Create Passkey",
"switch_language_en": "English",
"switch_language_de": "Deutsch"
},
"stats": { "stats": {
"title": "Statistics", "title": "Statistics",
"subtitle": "Routes, consumption and propulsion at a glance", "subtitle": "Routes, consumption and propulsion at a glance",
+3 -3
View File
@@ -241,7 +241,7 @@
</header> </header>
<p class="intro"> <p class="intro">
Führen Sie Ihr Bordlogbuch digital: Reisetage, GPS-Tracks, Crew und Schiffsdaten — Führe dein Bordlogbuch digital: Reisetage, GPS-Tracks, Crew und Schiffsdaten —
<strong>End-to-End-verschlüsselt</strong>, als App installierbar und <strong>End-to-End-verschlüsselt</strong>, als App installierbar und
<strong>auch offline</strong> auf See nutzbar. <strong>auch offline</strong> auf See nutzbar.
</p> </p>
@@ -258,10 +258,10 @@
</section> </section>
<section class="beta-box"> <section class="beta-box">
<h2>Beta-Phase — Ihr Feedback zählt</h2> <h2>Beta-Phase — Dein Feedback zählt</h2>
<p> <p>
Kapteins Daagbok ist ein <strong>privates Hobbyprojekt ohne Gewinnabsicht</strong>. Kapteins Daagbok ist ein <strong>privates Hobbyprojekt ohne Gewinnabsicht</strong>.
Als Beta-Tester helfen Sie, die App für Skipper und Crew im Alltag zu verbessern — Als Beta-Tester hilfst du, die App für Skipper und Crew im Alltag zu verbessern —
Rückmeldungen sind ausdrücklich willkommen. Rückmeldungen sind ausdrücklich willkommen.
</p> </p>
</section> </section>
Binary file not shown.