trails/packages/i18n/src/locales/de.ts
Ullrich Schäfer f16e80a2eb
Prompt users with stale terms_version to re-accept
The three pre-legal-disclaimer users (ullrich, pistazie, nelli) have
NULL terms_version, and any future Terms update would leave every
existing user in the same state. Close the loop now that we have
version storage by redirecting any logged-in user whose
users.terms_version doesn't match the currently-published
TERMS_VERSION to a dedicated acceptance page.

Changes:
- auth.server: new recordTermsAcceptance(userId, version) helper that
  writes both terms_accepted_at and terms_version.
- root loader: if the session user has a stale or NULL terms_version,
  throw redirect("/auth/accept-terms?returnTo=<pathname>") unless the
  request is already on an allow-listed path
  (/auth/accept-terms, /auth/logout, /legal/*) so Terms are reachable
  and logout works.
- New route /auth/accept-terms (GET renders the prompt, POST records
  acceptance and bounces to a sanitised returnTo). Same-origin check
  on returnTo to avoid open-redirect abuse. Logout button is provided
  as an escape hatch.
- i18n: new auth.reaccept.* keys for EN and DE.
- Spec: new Requirement + five scenarios (redirect, allow-list,
  successful re-accept, missing consent, returnTo sanitisation).

No action on the three legacy users is required beyond what they'll
experience on their next visit — the gate takes care of it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:06:16 +02:00

328 lines
12 KiB
TypeScript

export default {
common: {
save: "Speichern",
cancel: "Abbrechen",
delete: "Löschen",
edit: "Bearbeiten",
close: "Schließen",
loading: "Wird geladen...",
error: "Etwas ist schiefgelaufen",
goHome: "Zur Startseite",
pageNotFound: "Seite nicht gefunden",
serviceUnavailable: "Dienst vorübergehend nicht verfügbar. Bitte versuche es später erneut.",
},
planner: {
title: "trails.cool Planer",
subtitle: "Gemeinsame Routenplanung",
home: "Startseite",
newSession: "Neue Sitzung",
saveRoute: "Route speichern",
exportGpx: "GPX exportieren",
exportRoute: "Route exportieren",
exportRouteDesc: "GPX-Track für jede App",
exportPlan: "Plan exportieren",
exportPlanDesc: "Mit Wegpunkten und Sperrzonen",
exportDays: "Tagesetappen exportieren",
exportDaysDesc: "Eine GPX-Datei pro Tag",
"undo.tooltip": "Rückgängig (Strg+Z)",
"redo.tooltip": "Wiederholen (Strg+Umschalt+Z)",
importGpx: "GPX importieren",
importGpxError: "GPX-Datei konnte nicht gelesen werden. Bitte Dateiformat prüfen.",
dropGpxHere: "GPX-Datei hier ablegen",
replaceRouteConfirm: "Aktuelle Route durch importierte GPX ersetzen?",
profile: "Profil",
connecting: "Verbinde...",
loadingMap: "Karte wird geladen...",
computingRoute: "Route wird berechnet...",
routeError: "Routenberechnung fehlgeschlagen. Bitte versuche es erneut.",
noRouteFound: "Keine Route gefunden. Versuche den Wegpunkt auf einen Weg zu verschieben.",
host: "Host",
connected: "Verbunden",
saveToJournal: "Im Journal speichern",
saving: "Speichert...",
saved: "Gespeichert!",
returnToJournal: "Zurück zum Journal",
profiles: {
trekking: "Wandern",
fastbike: "Radfahren (schnell)",
safety: "Radfahren (sicher)",
shortest: "Kürzeste",
car: "Auto",
},
participants: {
you: "(du)",
host: "Host",
editName: "Klicke, um deinen Namen zu ändern",
joined: "{{name}} ist beigetreten",
left: "{{name}} hat die Sitzung verlassen",
},
sidebar: {
waypoints: "Wegpunkte",
notes: "Notizen",
},
multiDay: {
dayLabel: "Tag {{n}}",
dayCount: "{{count}} Tage",
dayCount_one: "1 Tag",
overnight: "Übernachtung",
markOvernight: "Als Übernachtung markieren",
removeOvernight: "Übernachtung entfernen",
distance: "Distanz",
ascent: "Anstieg",
descent: "Abstieg",
},
poi: {
toggle: "Sehenswürdigkeiten",
title: "Sehenswürdigkeiten",
loading: "Laden...",
zoomIn: "Näher heranzoomen für POIs",
rateLimited: "POI-Daten vorübergehend nicht verfügbar",
error: "POIs konnten nicht geladen werden",
drinkingWater: "Trinkwasser",
shelter: "Unterstand",
camping: "Camping",
food: "Essen & Trinken",
groceries: "Lebensmittel",
bikeInfra: "Fahrrad-Infrastruktur",
accommodation: "Unterkunft",
viewpoints: "Aussichtspunkte",
toilets: "Toiletten",
},
noGoAreas: {
draw: "Sperrgebiet zeichnen",
cancel: "Sperrgebiet abbrechen",
hint: "Rechtsklick auf ein Sperrgebiet zum Löschen",
},
notes: {
placeholder: "Notizen für diese Sitzung hinzufügen...",
},
colorMode: {
label: "Routenfarbe",
plain: "Standard",
elevation: "Höhe",
surface: "Untergrund",
grade: "Steigung",
highway: "Straßentyp",
maxspeed: "Tempolimit",
smoothness: "Fahrbahnqualität",
tracktype: "Wegtyp",
cycleway: "Radweg",
bikeroute: "Radroute",
surfaceLegend: "Farbe nach Straßenbelag",
surfaceUnavailable: "Untergrunddaten für dieses Profil nicht verfügbar",
},
rateLimitExceeded: "Zu viele Anfragen. Bitte versuche es später erneut.",
elevation: {
gain: "Höhenmeter aufwärts",
loss: "Höhenmeter abwärts",
profile: "Höhenprofil",
grade: "Steigungsprofil",
highway: "Straßentypenprofil",
maxspeed: "Tempolimitprofil",
smoothness: "Fahrbahnqualitätsprofil",
tracktype: "Wegtypenprofil",
cycleway: "Radwegprofil",
bikeroute: "Radroutenprofil",
low: "Tief",
high: "Hoch",
},
landing: {
startPlanning: "Planung starten",
heroDescription: "Plane Wander- und Radrouten gemeinsam in Echtzeit. Kein Konto nötig.",
featureCollaborative: "Echtzeit-Zusammenarbeit",
featureCollaborativeDesc: "Wegpunkte gemeinsam bearbeiten — Änderungen werden sofort synchronisiert.",
featureRouting: "Intelligentes Routing",
featureRoutingDesc: "BRouter berechnet optimale Routen für Wandern, Radfahren oder Autofahren mit Höhenprofil.",
featureElevation: "Höhenprofil",
featureElevationDesc: "Das vollständige Höhenprofil deiner Route mit Auf- und Abstiegsstatistiken.",
featureExport: "GPX-Export",
featureExportDesc: "Exportiere deine Route als GPX für jedes GPS-Gerät oder jede App.",
featureNoAccount: "Kein Konto nötig",
featureNoAccountDesc: "Sofort loslegen. Teile einen Link und jeder kann deiner Sitzung beitreten.",
saveRoutes: "Routen speichern?",
saveRoutesDesc: "Erstelle ein kostenloses Konto auf trails.cool, um Routen zu speichern, Aktivitäten zu verfolgen und mehr.",
goToJournal: "Zu trails.cool",
footer: {
imprint: "Impressum",
privacy: "Datenschutz",
terms: "Nutzungsbedingungen",
source: "Quellcode",
builtWith: "Erstellt mit BRouter und OpenStreetMap",
alpha: "Alpha-Version",
},
},
},
journal: {
title: "trails.cool",
subtitle: "Dein Outdoor-Aktivitäten-Tagebuch",
welcome: "Willkommen,",
addPasskeyPrompt: "Füge einen Passkey hinzu, um dich schneller auf diesem Gerät anzumelden.",
addPasskey: "Passkey hinzufügen",
settingUp: "Wird eingerichtet...",
passkeyAdded: "Passkey hinzugefügt! Du kannst dich jetzt sofort auf diesem Gerät anmelden.",
passkeyStatus: "{{count}} Passkey registriert",
passkeyStatus_other: "{{count}} Passkeys registriert",
routes: {
title: "Routen",
myRoutes: "Meine Routen",
new: "Neue Route",
edit: "Route bearbeiten",
editInPlanner: "Im Planer bearbeiten",
opening: "Öffne...",
importGpx: "GPX importieren",
exportGpx: "GPX exportieren",
delete: "Route löschen",
distance: "Strecke",
elevationGain: "Höhenmeter",
dayBreakdown: "Tagesübersicht",
dayLabel: "Tag {{n}}",
noRoutesYet: "Noch keine Routen. Erstelle deine erste Route!",
noMapPreview: "Keine Kartenvorschau",
saveChanges: "Änderungen speichern",
},
activities: {
title: "Aktivitäten",
new: "Neue Aktivität",
duration: "Dauer",
linkToRoute: "Mit Route verknüpfen",
createRouteFromActivity: "Route aus Aktivität erstellen",
delete: "Aktivität löschen",
deleteConfirm: "Möchtest du diese Aktivität wirklich löschen?",
importedFrom: "Importiert von {{provider}}",
},
settings: {
title: "Einstellungen",
profile: {
title: "Profil",
displayName: "Anzeigename",
bio: "Bio",
saved: "Profil gespeichert.",
},
security: {
title: "Sicherheit",
description: "Verwalte deine Passkeys zum Anmelden.",
addPasskey: "Passkey hinzufügen",
passkey: "Passkey",
thisDevice: "Dieses Gerät",
securityKey: "Sicherheitsschlüssel",
bluetooth: "Bluetooth",
phoneOrTablet: "Telefon oder Tablet",
addedOn: "Hinzugefügt am {{date}}",
noPasskeys: "Keine Passkeys registriert. Du kannst dich per Magic Link anmelden.",
deleteConfirm: "Möchtest du diesen Passkey wirklich löschen?",
deleteLastWarning: "Dies ist dein einziger Passkey. Du musst Magic Links zum Anmelden verwenden. Trotzdem löschen?",
},
account: {
title: "Konto",
changeEmail: "Ändern",
newEmailPlaceholder: "Neue E-Mail-Adresse",
sendVerification: "Bestätigen",
verificationSent: "Bestätigungs-E-Mail gesendet. Prüfe deinen Posteingang.",
dangerZone: "Gefahrenzone",
deleteDescription: "Lösche dein Konto und alle zugehörigen Daten unwiderruflich.",
deleteAccount: "Konto löschen",
deleteConfirmPrompt: "Gib \"{{username}}\" ein, um die Löschung zu bestätigen:",
confirmDelete: "Endgültig löschen",
},
services: {
title: "Verbundene Dienste",
connect: "Verbinden",
disconnect: "Trennen",
connectedAs: "Verbunden (ID: {{id}})",
},
},
sync: {
import: "Importieren",
importFrom: "Import von {{provider}}",
imported: "Importiert",
noWorkouts: "Keine Workouts gefunden.",
noGps: "Kein GPS",
noGpsTooltip: "{{provider}} stellt für diese Aktivität keine Routendaten bereit",
importing: "Importiere...",
importAll: "Alle importieren ({{count}})",
importingProgress: "Importiere {{current}} von {{total}}...",
previous: "Zurück",
next: "Weiter",
},
nav: {
routes: "Routen",
activities: "Aktivitäten",
login: "Anmelden",
register: "Registrieren",
profile: "Profil",
settings: "Einstellungen",
logout: "Abmelden",
},
alpha: {
message: "trails.cool befindet sich in früher Entwicklung — deine Daten können zurückgesetzt werden.",
},
footer: {
imprint: "Impressum",
privacy: "Datenschutz",
terms: "Nutzungsbedingungen",
source: "Quellcode",
alpha: "Alpha-Version",
},
auth: {
login: "Anmelden",
register: "Registrieren",
logout: "Abmelden",
email: "E-Mail",
username: "Benutzername",
signInWithPasskey: "Mit Passkey anmelden",
authenticating: "Authentifiziere...",
useMagicLink: "Kein Passkey auf diesem Gerät? Nutze stattdessen einen Magic Link",
magicLinkHelp: "Wir senden dir einen Anmeldelink per E-Mail.",
sendMagicLink: "Magic Link senden",
sending: "Wird gesendet...",
backToPasskey: "Zurück zur Passkey-Anmeldung",
checkEmail: "Prüfe deine E-Mails! Wir haben einen Anmeldelink gesendet an",
linkExpires: "Der Link ist 15 Minuten gültig.",
codeHelp: "Oder gib den 6-stelligen Code aus der E-Mail ein:",
verifyCode: "Code bestätigen",
termsBefore: "Ich habe die ",
termsLink: "Nutzungsbedingungen",
termsAfter: " gelesen und akzeptiere sie.",
termsRequired: "Du musst die Nutzungsbedingungen akzeptieren.",
noAccount: "Noch kein Konto?",
alreadyHaveAccount: "Bereits ein Konto?",
handleWillBe: "Dein Handle wird {{handle}} sein",
passkeyNotFound: "Für diese Seite wurde kein Passkey gefunden. Registriere ein neues Konto oder nutze stattdessen einen Magic Link.",
reaccept: {
heading: "Aktualisierte Nutzungsbedingungen",
bodyUpdated: "Die Nutzungsbedingungen wurden aktualisiert (von Version {{from}} auf {{to}}). Bitte lies und akzeptiere sie, um trails.cool weiter zu nutzen.",
bodyNew: "Ab sofort erfassen wir, welche Version der Nutzungsbedingungen du akzeptiert hast. Bitte lies die aktuelle Version ({{version}}) und akzeptiere sie, um trails.cool weiter zu nutzen.",
submit: "Akzeptieren und fortfahren",
logoutInstead: "Stattdessen abmelden",
},
registerDescription: "Registriere dich mit einem Passkey — kein Passwort nötig.",
registerWithPasskey: "Mit Passkey registrieren",
creatingPasskey: "Erstelle Passkey...",
passkeyNotSupported: "Dein Browser unterstützt keine Passkeys. Bitte verwende einen anderen Browser zur Registrierung.",
passkeyNotSupportedRegister: "Dein Browser unterstützt keine Passkeys. Du kannst dich stattdessen per Magic Link registrieren und später einen Passkey hinzufügen.",
registerWithMagicLink: "Per Magic Link registrieren",
},
},
mobile: {
routes: {
empty: "Noch keine Routen",
emptyHint: "Erstelle deine erste Route im Planner",
retry: "Erneut versuchen",
},
activities: {
empty: "Noch keine Aktivitäten",
emptyHint: "Zeichne deine erste Aktivität auf oder importiere sie",
},
profile: {
logOut: "Abmelden",
switchServer: "Server wechseln",
switchServerConfirm: "Du wirst abgemeldet und lokale Daten werden gelöscht. Fortfahren?",
},
login: {
subtitle: "Bei deinem Journal anmelden",
signIn: "Anmelden",
connectDifferentServer: "Mit anderem Server verbinden",
serverUrl: "Server-URL",
},
},
} as const;