trails/packages/i18n/src/locales/en.ts
Ullrich Schäfer 4e2bfb0bbe Redesign navbar with avatar dropdown and mobile drawer
Stream C from docs/information-architecture.md. The navbar's account
cluster (<username> + Settings + Logout) was three controls for the
same concept; the cluster now collapses behind an avatar dropdown.
On mobile, all primary nav and account controls move into a
hamburger drawer so the top bar stays at logo + bell + hamburger
without wrapping at small viewports.

- New apps/journal/app/components/Avatar.tsx — initials-only avatar
  (no image-upload story yet; the component is the single place to
  add image fallback when one lands).
- New apps/journal/app/components/AccountDropdown.tsx — click the
  avatar trigger to reveal Profile / Settings / Log Out menuitems.
  Click-outside + Escape-to-close. role="menu" + role="menuitem" for
  a11y.
- New apps/journal/app/components/MobileNavMenu.tsx — hamburger
  trigger + slide-out drawer at md and below. Contains all primary
  nav (Feed, Explore, Routes, Activities, Notifications) plus the
  account cluster (Profile, Settings, Log Out). Auto-closes on
  navigation; locks body scroll while open.
- apps/journal/app/root.tsx — refactor NavBar:
  * Brand + primary nav links hide at md and below (drawer covers
    them).
  * Right cluster: bell on every viewport; avatar dropdown on
    desktop, hamburger on mobile.
  * Loader exposes user.displayName so Avatar can compute initials.
  * Drops the now-unused Form import; Form moved into the
    dropdown/drawer components.

E2E tests updated:
- e2e/auth.test.ts — logout helper opens the avatar dropdown via
  aria-label = displayName||username, then clicks the Log Out
  menuitem. Username assertions in nav switch from getByText to
  getByRole("button", {name: username}) (the avatar button).
- e2e/settings.test.ts — "settings link visible in nav" opens the
  avatar dropdown first, then asserts the Settings menuitem.

i18n: nav.openMenu, nav.closeMenu keys for the hamburger trigger
labels (EN + DE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:56:20 +02:00

486 lines
17 KiB
TypeScript

export default {
common: {
save: "Save",
cancel: "Cancel",
delete: "Delete",
edit: "Edit",
close: "Close",
loading: "Loading...",
error: "Something went wrong",
goHome: "Go home",
pageNotFound: "Page not found",
serviceUnavailable: "Service temporarily unavailable. Please try again later.",
},
planner: {
title: "trails.cool Planner",
subtitle: "Collaborative route planning",
home: "Home",
newSession: "New Session",
saveRoute: "Save Route",
exportGpx: "Export GPX",
exportRoute: "Export Route",
exportRouteDesc: "Clean GPX track for any app",
exportPlan: "Export Plan",
exportPlanDesc: "Includes waypoints and no-go areas",
exportDays: "Export Day Segments",
exportDaysDesc: "One GPX file per day",
"undo.tooltip": "Undo (Ctrl+Z)",
"redo.tooltip": "Redo (Ctrl+Shift+Z)",
importGpx: "Import GPX",
importGpxError: "Could not read GPX file. Please check the file format.",
dropGpxHere: "Drop GPX file here",
replaceRouteConfirm: "Replace current route with imported GPX?",
profile: "Profile",
connecting: "Connecting...",
loadingMap: "Loading map...",
computingRoute: "Computing route...",
routeError: "Route calculation failed. Please try again.",
noRouteFound: "No route found. Try moving the waypoint to a road or path.",
host: "Host",
connected: "Connected",
saveToJournal: "Save to Journal",
saving: "Saving...",
saved: "Saved!",
returnToJournal: "Return to Journal",
profiles: {
trekking: "Hiking",
fastbike: "Cycling (fast)",
safety: "Cycling (safe)",
shortest: "Shortest",
car: "Car",
},
participants: {
you: "(you)",
host: "Host",
editName: "Click to edit your name",
joined: "{{name}} joined",
left: "{{name}} left",
},
sidebar: {
waypoints: "Waypoints",
notes: "Notes",
},
multiDay: {
dayLabel: "Day {{n}}",
dayCount: "{{count}} days",
dayCount_one: "1 day",
overnight: "Overnight",
markOvernight: "Mark as overnight stop",
removeOvernight: "Remove overnight stop",
distance: "Distance",
ascent: "Ascent",
descent: "Descent",
},
poi: {
toggle: "Points of Interest",
title: "Points of Interest",
loading: "Loading...",
zoomIn: "Zoom in to see POIs",
rateLimited: "POI data temporarily unavailable",
error: "Failed to load POIs",
drinkingWater: "Drinking water",
shelter: "Shelter",
camping: "Camping",
food: "Food & drink",
groceries: "Groceries",
bikeInfra: "Bike infrastructure",
accommodation: "Accommodation",
viewpoints: "Viewpoints",
toilets: "Toilets",
},
noGoAreas: {
draw: "Draw no-go area",
cancel: "Cancel no-go area",
hint: "Right-click a no-go area to delete it",
},
notes: {
placeholder: "Add notes for this session...",
},
colorMode: {
label: "Route Color",
plain: "Plain",
elevation: "Elevation",
surface: "Surface",
grade: "Grade",
highway: "Road Type",
maxspeed: "Speed Limit",
smoothness: "Smoothness",
tracktype: "Track Type",
cycleway: "Cycleway",
bikeroute: "Bike Route",
surfaceLegend: "Color by road surface type",
surfaceUnavailable: "Surface data not available for this profile",
},
rateLimitExceeded: "Too many requests. Please try again later.",
elevation: {
gain: "Elevation Gain",
loss: "Elevation Loss",
profile: "Elevation Profile",
grade: "Grade Profile",
highway: "Road Type Profile",
maxspeed: "Speed Limit Profile",
smoothness: "Smoothness Profile",
tracktype: "Track Type Profile",
cycleway: "Cycleway Profile",
bikeroute: "Bike Route Profile",
low: "Low",
high: "High",
},
landing: {
startPlanning: "Start Planning",
heroDescription: "Plan hiking and cycling routes together in real-time. No account needed.",
featureCollaborative: "Real-Time Collaboration",
featureCollaborativeDesc: "Edit waypoints together — changes sync instantly across all participants.",
featureRouting: "Smart Routing",
featureRoutingDesc: "BRouter computes optimal routes for hiking, cycling, or driving with elevation awareness.",
featureElevation: "Elevation Profile",
featureElevationDesc: "See the full elevation profile of your route with gain and loss statistics.",
featureExport: "GPX Export",
featureExportDesc: "Export your route as GPX for use in any GPS device or app.",
featureNoAccount: "No Account Needed",
featureNoAccountDesc: "Start planning immediately. Share a link and anyone can join your session.",
saveRoutes: "Want to save your routes?",
saveRoutesDesc: "Create a free account on trails.cool to keep your routes, track activities, and more.",
goToJournal: "Go to trails.cool",
footer: {
imprint: "Imprint",
privacy: "Privacy",
terms: "Terms",
source: "Source Code",
builtWith: "Built with BRouter and OpenStreetMap",
alpha: "Alpha version",
},
},
},
journal: {
title: "trails.cool",
subtitle: "Your outdoor activity journal",
welcome: "Welcome,",
addPasskeyPrompt: "Add a passkey for faster sign-in on this device.",
addPasskey: "Add Passkey",
settingUp: "Setting up...",
passkeyAdded: "Passkey added! You can now sign in instantly on this device.",
passkeyStatus: "{{count}} passkey registered",
passkeyStatus_other: "{{count}} passkeys registered",
home: {
heroTitle: "Federated outdoor journal",
heroSubtitle: "Plan routes, record activities, own your data.",
tryPlannerPrefix: "Or ",
tryPlannerLink: "try the Planner without an account",
tryPlannerSuffix: " →",
exploreLink: "Browse who's here →",
marketing: {
planner: {
title: "Plan routes together",
body: "Sketch hiking and cycling routes collaboratively in real time. No account needed.",
},
journal: {
title: "Record activities",
body: "Log your rides, hikes, and walks. Keep them private, share with friends, or publish to the world.",
},
federation: {
title: "Federated by design",
body: "Your Journal instance talks to other trails.cool servers via ActivityPub. Follow friends anywhere.",
},
ownership: {
title: "Your data, always",
body: "No ads, no tracking, no data sales. Export everything as GPX or JSON any time. MIT licensed and self-hostable.",
},
},
feed: {
heading: "Recent activity",
empty: "No public activities yet.",
},
dashboardEmpty: "No activities yet. Record or import your first one from the Activities tab.",
poweredBy: "Powered by trails.cool — about the project",
},
routes: {
title: "Routes",
myRoutes: "My Routes",
new: "New Route",
edit: "Edit Route",
editInPlanner: "Edit in Planner",
opening: "Opening...",
importGpx: "Import GPX",
exportGpx: "Export GPX",
delete: "Delete Route",
distance: "Distance",
elevationGain: "Elevation Gain",
dayBreakdown: "Day Breakdown",
dayLabel: "Day {{n}}",
noRoutesYet: "No routes yet. Create your first route!",
noMapPreview: "No map preview",
saveChanges: "Save Changes",
empty: {
heading: "This route is empty",
bodyOwner: "You haven't planned any waypoints yet. Open it in the Planner to sketch a path, or upload a GPX file.",
bodyViewer: "This route doesn't have any waypoints yet.",
openInPlanner: "Open in Planner",
uploadGpx: "or upload a GPX file",
},
visibility: {
label: "Visibility",
private: "Private",
unlisted: "Unlisted (only people with the link)",
public: "Public (anyone on the internet)",
privateHelp: "Only you can see this route.",
unlistedHelp: "Anyone with the URL can view it; it won't appear on your public profile.",
publicHelp: "Visible to anyone, including logged-out visitors; listed on your public profile.",
},
},
profile: {
ownNote: "This is your profile — visitors see only what you've marked public.",
privateNote: "Your profile is set to private (locked). Visitors see a stub with a Request-to-follow button; only accepted followers see your public content.",
goToSettings: "Go to settings",
noPublicRoutes: "No public routes yet.",
noPublicActivities: "No public activities yet.",
lockedBadge: "Private",
lockedTitle: "This profile is private. Follow requests need approval.",
privateStub: {
heading: "This profile is private",
bodyAuth: "Send a follow request to see their public routes and activities. They'll need to approve it.",
bodyAnon: "Sign in and request to follow them to see their public routes and activities.",
},
},
notifications: {
title: "Notifications",
empty: "No notifications yet.",
markAllRead: "Mark all read",
loadOlder: "Load older",
someone: "Someone",
tabs: {
activity: "Activity",
requests: "Requests",
},
summary: {
followRequestReceived: "{{name}} requested to follow you",
followReceived: "{{name}} started following you",
followRequestApproved: "{{name}} accepted your follow request",
activityPublished: "{{owner}} posted “{{activity}}”",
},
},
social: {
follow: "Follow",
unfollow: "Unfollow",
requestToFollow: "Request to follow",
pendingCancel: "Requested",
requests: {
title: "Follow requests",
empty: "No pending follow requests.",
approve: "Approve",
reject: "Reject",
receivedAt: "Requested {{date}}",
},
followers: {
label: "Followers",
heading: "Followers of {{user}}",
count: "{{count}} follower",
count_other: "{{count}} followers",
empty: "Nobody yet.",
},
following: {
label: "Following",
heading: "{{user}} is following",
count: "Following {{count}}",
count_other: "Following {{count}}",
empty: "Not following anyone yet.",
},
prevPage: "Previous",
nextPage: "Next",
pageOfTotal: "Page {{page}} of {{totalPages}}",
feed: {
title: "Feed",
heading: "Following",
empty: "You're not following anyone yet. Browse profiles and tap Follow to start building your feed.",
seePublic: "Or browse the instance public feed →",
toggle: {
followed: "Following",
public: "Public",
},
public: {
heading: "Public",
empty: "No public activities on this instance yet.",
},
},
},
demo: {
badge: "🐕 Demo account",
},
activities: {
title: "Activities",
new: "New Activity",
duration: "Duration",
linkToRoute: "Link to Route",
createRouteFromActivity: "Create Route from Activity",
delete: "Delete Activity",
deleteConfirm: "Are you sure you want to delete this activity?",
importedFrom: "Imported from {{provider}}",
visibility: {
label: "Visibility",
private: "Private",
unlisted: "Unlisted (only people with the link)",
public: "Public (anyone on the internet)",
},
},
settings: {
title: "Settings",
nav: {
profile: "Profile",
account: "Account",
security: "Security",
connections: "Connected services",
},
profile: {
title: "Profile",
displayName: "Display Name",
bio: "Bio",
saved: "Profile saved.",
visibility: {
label: "Profile visibility",
public: "Public",
publicHelp: "Anyone can view your profile and your public content. Follows auto-accept.",
private: "Private (locked)",
privateHelp: "Visitors see a stub with a Request-to-follow button. Only accepted followers see your public content. You manage requests in the Requests tab on /notifications.",
},
},
security: {
title: "Security",
description: "Manage your passkeys for signing in.",
addPasskey: "Add Passkey",
passkey: "Passkey",
thisDevice: "This device",
securityKey: "Security key",
bluetooth: "Bluetooth",
phoneOrTablet: "Phone or tablet",
addedOn: "Added {{date}}",
noPasskeys: "No passkeys registered. You can sign in with magic links.",
deleteConfirm: "Are you sure you want to delete this passkey?",
deleteLastWarning: "This is your only passkey. You will need to use magic links to sign in. Delete anyway?",
},
account: {
title: "Account",
changeEmail: "Change",
newEmailPlaceholder: "New email address",
sendVerification: "Verify",
verificationSent: "Verification email sent. Check your inbox.",
dangerZone: "Danger Zone",
deleteDescription: "Permanently delete your account and all associated data.",
deleteAccount: "Delete Account",
deleteConfirmPrompt: "Type \"{{username}}\" to confirm deletion:",
confirmDelete: "Permanently Delete",
},
services: {
title: "Connected Services",
connect: "Connect",
disconnect: "Disconnect",
connectedAs: "Connected (ID: {{id}})",
},
},
sync: {
import: "Import",
importFrom: "Import from {{provider}}",
imported: "Imported",
noWorkouts: "No workouts found.",
noGps: "No GPS",
noGpsTooltip: "{{provider}} does not provide route data for this activity",
importing: "Importing...",
importAll: "Import all ({{count}})",
importingProgress: "Importing {{current}} of {{total}}...",
previous: "Previous",
next: "Next",
},
nav: {
routes: "Routes",
activities: "Activities",
explore: "Explore",
login: "Sign In",
register: "Register",
profile: "Profile",
settings: "Settings",
logout: "Log Out",
openMenu: "Open menu",
closeMenu: "Close menu",
},
explore: {
heading: "Explore",
empty: "Nobody on this instance has a public profile yet.",
activeRecently: {
heading: "Active recently",
},
directory: {
heading: "Everyone",
},
},
alpha: {
message: "trails.cool is in early development — your data may be reset.",
},
footer: {
imprint: "Imprint",
privacy: "Privacy",
terms: "Terms",
source: "Source Code",
alpha: "Alpha version",
},
auth: {
login: "Sign In",
register: "Register",
logout: "Log Out",
email: "Email",
username: "Username",
signInWithPasskey: "Sign in with Passkey",
authenticating: "Authenticating...",
useMagicLink: "No passkey on this device? Use a magic link instead",
magicLinkHelp: "We'll send a login link to your email.",
sendMagicLink: "Send Magic Link",
sending: "Sending...",
backToPasskey: "Back to passkey login",
checkEmail: "Check your email! We sent a login link to",
linkExpires: "The link expires in 15 minutes.",
codeHelp: "Or enter the 6-digit code from the email:",
verifyCode: "Verify Code",
termsBefore: "I have read and accept the ",
termsLink: "Terms of Service",
termsAfter: ".",
termsRequired: "You must accept the Terms of Service to register.",
noAccount: "Don't have an account?",
alreadyHaveAccount: "Already have an account?",
handleWillBe: "Your handle will be {{handle}}",
passkeyNotFound: "No passkey found for this site. Register a new account or use a magic link instead.",
reaccept: {
heading: "Updated Terms of Service",
bodyUpdated: "The Terms of Service have been updated (from version {{from}} to {{to}}). Please review and accept to continue using trails.cool.",
bodyNew: "We now record which version of the Terms of Service you've accepted. Please review the current version ({{version}}) and accept to continue using trails.cool.",
submit: "Accept and continue",
logoutInstead: "Log out instead",
},
registerDescription: "Register with a passkey — no password needed.",
registerWithPasskey: "Register with Passkey",
creatingPasskey: "Creating passkey...",
passkeyNotSupported: "Your browser does not support passkeys. Please use a different browser to register.",
passkeyNotSupportedRegister: "Your browser doesn't support passkeys. You can register with a magic link instead and add a passkey later from a supported browser.",
registerWithMagicLink: "Register with Magic Link",
},
},
mobile: {
routes: {
empty: "No routes yet",
emptyHint: "Create your first route in the Planner",
retry: "Retry",
},
activities: {
empty: "No activities yet",
emptyHint: "Record or import your first activity",
},
profile: {
logOut: "Log Out",
switchServer: "Switch Server",
switchServerConfirm: "This will log you out and clear local data. Continue?",
},
login: {
subtitle: "Sign in to your Journal",
signIn: "Sign In",
connectDifferentServer: "Connect to a different server",
serverUrl: "Server URL",
},
},
} as const;