From b96b32cb53db5103f328fcf96162e84aa18d3f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Wed, 25 Mar 2026 02:08:45 +0100 Subject: [PATCH 1/3] Wire up i18n in both apps Initialize i18n via initI18n() in both root.tsx files and replace all hardcoded user-facing strings with useTranslation() calls. Adds missing translation keys for planner (profiles, connection states, save flow) and journal (auth pages, route management, passkey prompts). Both English and German translations are complete. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/journal/app/root.tsx | 19 +++++--- apps/journal/app/routes/auth.login.tsx | 24 +++++----- apps/journal/app/routes/auth.register.tsx | 14 +++--- apps/journal/app/routes/home.tsx | 18 ++++---- apps/journal/app/routes/routes.$id.tsx | 12 ++--- apps/journal/app/routes/routes._index.tsx | 8 ++-- apps/planner/app/components/ExportButton.tsx | 5 ++- .../app/components/ProfileSelector.tsx | 18 +++----- .../app/components/SaveToJournalButton.tsx | 8 ++-- apps/planner/app/components/SessionView.tsx | 14 +++--- apps/planner/app/root.tsx | 15 ++++--- apps/planner/app/routes/home.tsx | 7 ++- packages/i18n/src/locales/de.ts | 41 +++++++++++++++-- packages/i18n/src/locales/en.ts | 45 ++++++++++++++++--- 14 files changed, 172 insertions(+), 76 deletions(-) diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx index 04124de..6802b50 100644 --- a/apps/journal/app/root.tsx +++ b/apps/journal/app/root.tsx @@ -2,8 +2,12 @@ import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } import type { LinksFunction } from "react-router"; import type { Route } from "./+types/root"; import * as Sentry from "@sentry/react"; +import { useTranslation } from "react-i18next"; +import { initI18n } from "@trails-cool/i18n"; import stylesheet from "@trails-cool/ui/styles.css?url"; +initI18n(); + export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }]; export function Layout({ children }: { children: React.ReactNode }) { @@ -30,18 +34,19 @@ export default function App() { export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { Sentry.captureException(error); + const { t } = useTranslation(); if (isRouteErrorResponse(error)) { return (

{error.status}

- {error.status === 404 && "Page not found"} - {error.status === 503 && "Service temporarily unavailable. Please try again later."} - {error.status !== 404 && error.status !== 503 && (error.statusText || "Something went wrong")} + {error.status === 404 && t("pageNotFound")} + {error.status === 503 && t("serviceUnavailable")} + {error.status !== 404 && error.status !== 503 && (error.statusText || t("error"))}

- Go home + {t("goHome")}
); @@ -49,12 +54,12 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { return (
-

Error

+

{t("error")}

- {error instanceof Error ? error.message : "An unexpected error occurred"} + {error instanceof Error ? error.message : t("error")}

- Go home + {t("goHome")}
); diff --git a/apps/journal/app/routes/auth.login.tsx b/apps/journal/app/routes/auth.login.tsx index c0a98c3..9698f19 100644 --- a/apps/journal/app/routes/auth.login.tsx +++ b/apps/journal/app/routes/auth.login.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; export default function LoginPage() { + const { t } = useTranslation("journal"); const [mode, setMode] = useState<"passkey" | "magic-link">("passkey"); const [email, setEmail] = useState(""); const [error, setError] = useState(null); @@ -81,7 +83,7 @@ export default function LoginPage() { return (
-

Sign In

+

{t("auth.login")}

{mode === "passkey" && (
@@ -90,7 +92,7 @@ export default function LoginPage() { disabled={loading} className="w-full rounded-md bg-blue-600 px-4 py-3 text-white hover:bg-blue-700 disabled:opacity-50" > - {loading ? "Authenticating..." : "Sign in with Passkey"} + {loading ? t("auth.authenticating") : t("auth.signInWithPasskey")}
@@ -98,7 +100,7 @@ export default function LoginPage() { onClick={() => setMode("magic-link")} className="text-sm text-gray-500 hover:text-gray-700" > - No passkey on this device? Use a magic link instead + {t("auth.useMagicLink")}
@@ -107,11 +109,11 @@ export default function LoginPage() { {mode === "magic-link" && !magicLinkSent && (

- We'll send a login link to your email. + {t("auth.magicLinkHelp")}

- {loading ? "Sending..." : "Send Magic Link"} + {loading ? t("auth.sending") : t("auth.sendMagicLink")}
@@ -137,7 +139,7 @@ export default function LoginPage() { onClick={() => setMode("passkey")} className="text-sm text-gray-500 hover:text-gray-700" > - Back to passkey login + {t("auth.backToPasskey")}
@@ -146,10 +148,10 @@ export default function LoginPage() { {magicLinkSent && (

- Check your email! We sent a login link to {email}. + {t("auth.checkEmail")} {email}.

- The link expires in 15 minutes. + {t("auth.linkExpires")}

)} @@ -159,9 +161,9 @@ export default function LoginPage() { )}

- Don't have an account?{" "} + {t("auth.noAccount")}{" "} - Register + {t("auth.register")}

diff --git a/apps/journal/app/routes/auth.register.tsx b/apps/journal/app/routes/auth.register.tsx index 8d1ca15..6db6cd3 100644 --- a/apps/journal/app/routes/auth.register.tsx +++ b/apps/journal/app/routes/auth.register.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; export default function RegisterPage() { + const { t } = useTranslation("journal"); const [email, setEmail] = useState(""); const [username, setUsername] = useState(""); const [error, setError] = useState(null); @@ -59,15 +61,15 @@ export default function RegisterPage() { return (
-

Create Account

+

{t("auth.register")}

- Register with a passkey — no password needed. + {t("auth.registerDescription")}

- {loading ? "Creating passkey..." : "Register with Passkey"} + {loading ? t("auth.creatingPasskey") : t("auth.registerWithPasskey")}

Already have an account?{" "} - Sign in + {t("auth.login")}

diff --git a/apps/journal/app/routes/home.tsx b/apps/journal/app/routes/home.tsx index a110331..196ad10 100644 --- a/apps/journal/app/routes/home.tsx +++ b/apps/journal/app/routes/home.tsx @@ -1,5 +1,6 @@ import { useState, useCallback } from "react"; import { data } from "react-router"; +import { useTranslation } from "react-i18next"; import type { Route } from "./+types/home"; import { getSessionUser } from "~/lib/auth.server"; @@ -22,6 +23,7 @@ export async function loader({ request }: Route.LoaderArgs) { export default function Home({ loaderData }: Route.ComponentProps) { const { user, showAddPasskey } = loaderData; + const { t } = useTranslation("journal"); const [addingPasskey, setAddingPasskey] = useState(false); const [passkeyDone, setPasskeyDone] = useState(false); const [error, setError] = useState(null); @@ -74,19 +76,19 @@ export default function Home({ loaderData }: Route.ComponentProps) { return (
-

trails.cool

-

Your outdoor activity journal

+

{t("title")}

+

{t("subtitle")}

{user ? (

- Welcome, {user.displayName ?? user.username} + {t("welcome")} {user.displayName ?? user.username}

{showAddPasskey && !passkeyDone && (

- Add a passkey for faster sign-in on this device. + {t("addPasskeyPrompt")}

{error &&

{error}

}
)} @@ -102,7 +104,7 @@ export default function Home({ loaderData }: Route.ComponentProps) { {passkeyDone && (

- Passkey added! You can now sign in instantly on this device. + {t("passkeyAdded")}

)} @@ -113,13 +115,13 @@ export default function Home({ loaderData }: Route.ComponentProps) { href="/auth/register" className="rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700" > - Register + {t("auth.register")} - Sign in + {t("auth.login")}
)} diff --git a/apps/journal/app/routes/routes.$id.tsx b/apps/journal/app/routes/routes.$id.tsx index 1533e95..5eb680b 100644 --- a/apps/journal/app/routes/routes.$id.tsx +++ b/apps/journal/app/routes/routes.$id.tsx @@ -1,5 +1,6 @@ import { useState, useCallback } from "react"; import { data, redirect } from "react-router"; +import { useTranslation } from "react-i18next"; import type { Route } from "./+types/routes.$id"; import { getSessionUser } from "~/lib/auth.server"; import { getRouteWithVersions, deleteRoute, updateRoute } from "~/lib/routes.server"; @@ -73,6 +74,7 @@ export function meta({ data: loaderData }: Route.MetaArgs) { export default function RouteDetailPage({ loaderData }: Route.ComponentProps) { const { route, versions, isOwner } = loaderData; + const { t } = useTranslation("journal"); const [editLoading, setEditLoading] = useState(false); const handleEditInPlanner = useCallback(async () => { @@ -104,13 +106,13 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) { disabled={editLoading} className="rounded-md bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700 disabled:opacity-50" > - {editLoading ? "Opening..." : "Edit in Planner"} + {editLoading ? t("routes.opening") : t("routes.editInPlanner")} - Edit + {t("routes.edit")} {route.hasGpx && ( - Export GPX + {t("routes.exportGpx")} )}
@@ -131,7 +133,7 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) {

{(route.distance / 1000).toFixed(1)} km

-

Distance

+

{t("routes.distance")}

)} {route.elevationGain != null && ( @@ -181,7 +183,7 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) { type="submit" className="rounded-md bg-red-50 px-4 py-2 text-sm text-red-700 hover:bg-red-100" > - Delete Route + {t("routes.delete")}
diff --git a/apps/journal/app/routes/routes._index.tsx b/apps/journal/app/routes/routes._index.tsx index 506f07f..7fdf714 100644 --- a/apps/journal/app/routes/routes._index.tsx +++ b/apps/journal/app/routes/routes._index.tsx @@ -1,4 +1,5 @@ import { data, redirect } from "react-router"; +import { useTranslation } from "react-i18next"; import type { Route } from "./+types/routes._index"; import { getSessionUser } from "~/lib/auth.server"; import { listRoutes } from "~/lib/routes.server"; @@ -26,22 +27,23 @@ export function meta(_args: Route.MetaArgs) { export default function RoutesListPage({ loaderData }: Route.ComponentProps) { const { routes } = loaderData; + const { t } = useTranslation("journal"); return (
-

My Routes

+

{t("routes.myRoutes")}

- New Route + {t("routes.new")}
{routes.length === 0 ? (

- No routes yet. Create your first route! + {t("routes.noRoutesYet")}

) : (
    diff --git a/apps/planner/app/components/ExportButton.tsx b/apps/planner/app/components/ExportButton.tsx index 43b941b..b4dff4e 100644 --- a/apps/planner/app/components/ExportButton.tsx +++ b/apps/planner/app/components/ExportButton.tsx @@ -1,10 +1,13 @@ import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; import * as Y from "yjs"; import type { YjsState } from "~/lib/use-yjs"; import { generateGpx } from "@trails-cool/gpx"; import type { TrackPoint } from "@trails-cool/gpx"; export function ExportButton({ yjs }: { yjs: YjsState }) { + const { t } = useTranslation("planner"); + const handleExport = useCallback(() => { // Get waypoints from Yjs const waypoints = yjs.waypoints.toArray().map((yMap: Y.Map) => ({ @@ -51,7 +54,7 @@ export function ExportButton({ yjs }: { yjs: YjsState }) { onClick={handleExport} className="rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200" > - Export GPX + {t("exportGpx")} ); } diff --git a/apps/planner/app/components/ProfileSelector.tsx b/apps/planner/app/components/ProfileSelector.tsx index 0f8b629..91d6ac5 100644 --- a/apps/planner/app/components/ProfileSelector.tsx +++ b/apps/planner/app/components/ProfileSelector.tsx @@ -1,19 +1,15 @@ import { useEffect, useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import type { YjsState } from "~/lib/use-yjs"; -const PROFILES = [ - { id: "trekking", label: "Hiking" }, - { id: "fastbike", label: "Cycling (fast)" }, - { id: "safety", label: "Cycling (safe)" }, - { id: "shortest", label: "Shortest" }, - { id: "car-eco", label: "Car" }, -]; +const PROFILE_IDS = ["trekking", "fastbike", "safety", "shortest", "car"] as const; interface ProfileSelectorProps { yjs: YjsState; } export function ProfileSelector({ yjs }: ProfileSelectorProps) { + const { t } = useTranslation("planner"); const [profile, setProfile] = useState("trekking"); useEffect(() => { @@ -38,7 +34,7 @@ export function ProfileSelector({ yjs }: ProfileSelectorProps) { return (
    diff --git a/apps/planner/app/components/SaveToJournalButton.tsx b/apps/planner/app/components/SaveToJournalButton.tsx index 113bb4e..af0389d 100644 --- a/apps/planner/app/components/SaveToJournalButton.tsx +++ b/apps/planner/app/components/SaveToJournalButton.tsx @@ -1,4 +1,5 @@ import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import * as Y from "yjs"; import type { YjsState } from "~/lib/use-yjs"; import { generateGpx } from "@trails-cool/gpx"; @@ -12,6 +13,7 @@ interface SaveToJournalButtonProps { } export function SaveToJournalButton({ yjs, callbackUrl, callbackToken, returnUrl }: SaveToJournalButtonProps) { + const { t } = useTranslation("planner"); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); @@ -72,13 +74,13 @@ export function SaveToJournalButton({ yjs, callbackUrl, callbackToken, returnUrl disabled={saving} className="rounded bg-green-600 px-3 py-1 text-sm text-white hover:bg-green-700 disabled:opacity-50" > - {saving ? "Saving..." : "Save to Journal"} + {saving ? t("saving") : t("saveToJournal")} - {saved && Saved!} + {saved && {t("saved")}} {error && {error}} {saved && returnUrl && ( - Return to Journal + {t("returnToJournal")} )}
    diff --git a/apps/planner/app/components/SessionView.tsx b/apps/planner/app/components/SessionView.tsx index cf02c8c..b7b6251 100644 --- a/apps/planner/app/components/SessionView.tsx +++ b/apps/planner/app/components/SessionView.tsx @@ -1,4 +1,5 @@ import { Suspense, lazy, useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { useYjs } from "~/lib/use-yjs"; import { useRouting } from "~/lib/use-routing"; import { ProfileSelector } from "~/components/ProfileSelector"; @@ -25,6 +26,7 @@ interface SessionViewProps { } export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, initialWaypoints }: SessionViewProps) { + const { t } = useTranslation("planner"); const yjs = useYjs(sessionId, initialWaypoints); const { isHost, computing, routeStats, requestRoute } = useRouting(yjs); const [highlightPosition, setHighlightPosition] = useState<[number, number] | null>(null); @@ -36,7 +38,7 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, if (!yjs) { return (
    -

    Connecting...

    +

    {t("connecting")}

    ); } @@ -45,7 +47,7 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, <>
    -

    trails.cool Planner

    +

    {t("title")}

    @@ -59,15 +61,15 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, )} {computing && ( - Computing route... + {t("computingRoute")} )} {isHost && ( - Host + {t("host")} )} - {yjs.connected ? "Connected" : "Connecting..."} · {sessionId.slice(0, 8)} + {yjs.connected ? t("connected") : t("connecting")} · {sessionId.slice(0, 8)}
    @@ -84,7 +86,7 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, - Loading map... + {t("loadingMap")}
} > diff --git a/apps/planner/app/root.tsx b/apps/planner/app/root.tsx index 2b7acf5..ff708ce 100644 --- a/apps/planner/app/root.tsx +++ b/apps/planner/app/root.tsx @@ -2,8 +2,12 @@ import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } import type { LinksFunction } from "react-router"; import type { Route } from "./+types/root"; import * as Sentry from "@sentry/react"; +import { useTranslation } from "react-i18next"; +import { initI18n } from "@trails-cool/i18n"; import stylesheet from "@trails-cool/ui/styles.css?url"; +initI18n(); + export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }]; export function Layout({ children }: { children: React.ReactNode }) { @@ -30,6 +34,7 @@ export default function App() { export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { Sentry.captureException(error); + const { t } = useTranslation(); if (isRouteErrorResponse(error)) { return ( @@ -37,9 +42,9 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {

{error.status}

- {error.status === 404 && "Page not found"} - {error.status === 503 && "Service temporarily unavailable"} - {error.status !== 404 && error.status !== 503 && (error.statusText || "Something went wrong")} + {error.status === 404 && t("pageNotFound")} + {error.status === 503 && t("serviceUnavailable")} + {error.status !== 404 && error.status !== 503 && (error.statusText || t("error"))}

@@ -49,9 +54,9 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { return (
-

Error

+

{t("error")}

- {error instanceof Error ? error.message : "An unexpected error occurred"} + {error instanceof Error ? error.message : t("error")}

diff --git a/apps/planner/app/routes/home.tsx b/apps/planner/app/routes/home.tsx index 8aa3380..de5d4e9 100644 --- a/apps/planner/app/routes/home.tsx +++ b/apps/planner/app/routes/home.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import type { Route } from "./+types/home"; export function meta(_args: Route.MetaArgs) { @@ -8,11 +9,13 @@ export function meta(_args: Route.MetaArgs) { } export default function Home() { + const { t } = useTranslation("planner"); + return (
-

trails.cool Planner

-

Collaborative route planning

+

{t("title")}

+

{t("subtitle")}

); diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 63d6037..9e86754 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -7,6 +7,9 @@ export default { 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", @@ -14,11 +17,22 @@ export default { newSession: "Neue Sitzung", saveRoute: "Route speichern", exportGpx: "GPX exportieren", - profile: "Routing-Profil", + profile: "Profil", + connecting: "Verbinde...", + loadingMap: "Karte wird geladen...", + computingRoute: "Route wird berechnet...", + host: "Host", + connected: "Verbunden", + saveToJournal: "Im Journal speichern", + saving: "Speichert...", + saved: "Gespeichert!", + returnToJournal: "Zurück zum Journal", profiles: { trekking: "Wandern", - bike: "Radfahren", + fastbike: "Radfahren (schnell)", + safety: "Radfahren (sicher)", shortest: "Kürzeste", + car: "Auto", }, elevation: { gain: "Höhenmeter aufwärts", @@ -29,16 +43,25 @@ export default { 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.", 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", + noRoutesYet: "Noch keine Routen. Erstelle deine erste Route!", + saveChanges: "Änderungen speichern", }, activities: { title: "Aktivitäten", @@ -52,8 +75,20 @@ export default { register: "Registrieren", logout: "Abmelden", email: "E-Mail", - password: "Passwort", 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.", + noAccount: "Noch kein Konto?", + registerDescription: "Registriere dich mit einem Passkey — kein Passwort nötig.", + registerWithPasskey: "Mit Passkey registrieren", + creatingPasskey: "Erstelle Passkey...", }, }, } as const; diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index bffca1f..c5b3837 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -7,6 +7,9 @@ export default { 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", @@ -14,11 +17,22 @@ export default { newSession: "New Session", saveRoute: "Save Route", exportGpx: "Export GPX", - profile: "Routing Profile", + profile: "Profile", + connecting: "Connecting...", + loadingMap: "Loading map...", + computingRoute: "Computing route...", + host: "Host", + connected: "Connected", + saveToJournal: "Save to Journal", + saving: "Saving...", + saved: "Saved!", + returnToJournal: "Return to Journal", profiles: { trekking: "Hiking", - bike: "Cycling", + fastbike: "Cycling (fast)", + safety: "Cycling (safe)", shortest: "Shortest", + car: "Car", }, elevation: { gain: "Elevation Gain", @@ -29,16 +43,25 @@ export default { 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.", 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", + noRoutesYet: "No routes yet. Create your first route!", + saveChanges: "Save Changes", }, activities: { title: "Activities", @@ -48,12 +71,24 @@ export default { createRouteFromActivity: "Create Route from Activity", }, auth: { - login: "Log In", - register: "Sign Up", + login: "Sign In", + register: "Register", logout: "Log Out", email: "Email", - password: "Password", 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.", + noAccount: "Don't have an account?", + registerDescription: "Register with a passkey — no password needed.", + registerWithPasskey: "Register with Passkey", + creatingPasskey: "Creating passkey...", }, }, } as const; From df845ec61062da001c4dc1d1852fa356797a9a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Wed, 25 Mar 2026 02:17:13 +0100 Subject: [PATCH 2/3] Fix E2E test for renamed register heading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The i18n change renamed "Create Account" to "Register" — update the E2E test to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e/journal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/journal.test.ts b/e2e/journal.test.ts index e7699a6..08f99b3 100644 --- a/e2e/journal.test.ts +++ b/e2e/journal.test.ts @@ -15,7 +15,7 @@ test.describe("Journal", () => { test("registration page renders correctly", async ({ page }) => { await page.goto("/auth/register"); - await expect(page.getByText("Create Account")).toBeVisible(); + await expect(page.getByText("Register")).toBeVisible(); await expect(page.getByLabel("Email")).toBeVisible(); await expect(page.getByLabel("Username")).toBeVisible(); await expect(page.getByRole("button", { name: /Register with Passkey/ })).toBeVisible(); From 4ecb7a93008d50a5a618206857ec12548ba1f41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Wed, 25 Mar 2026 02:26:12 +0100 Subject: [PATCH 3/3] Fix E2E: use heading role for register page check "Register" appears 3 times on the page (heading, button text, link). Use getByRole("heading") to target the h1 specifically. Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e/journal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/journal.test.ts b/e2e/journal.test.ts index 08f99b3..b2863fa 100644 --- a/e2e/journal.test.ts +++ b/e2e/journal.test.ts @@ -15,7 +15,7 @@ test.describe("Journal", () => { test("registration page renders correctly", async ({ page }) => { await page.goto("/auth/register"); - await expect(page.getByText("Register")).toBeVisible(); + await expect(page.getByRole("heading", { name: "Register" })).toBeVisible(); await expect(page.getByLabel("Email")).toBeVisible(); await expect(page.getByLabel("Username")).toBeVisible(); await expect(page.getByRole("button", { name: /Register with Passkey/ })).toBeVisible();