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/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;