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) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-25 02:08:45 +01:00 committed by GitHub
parent 69ba510530
commit b96b32cb53
14 changed files with 172 additions and 76 deletions

View file

@ -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<unknown>) => ({
@ -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")}
</button>
);
}

View file

@ -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 (
<div className="flex items-center gap-2">
<label htmlFor="profile" className="text-sm text-gray-600">
Profile:
{t("profile")}:
</label>
<select
id="profile"
@ -46,9 +42,9 @@ export function ProfileSelector({ yjs }: ProfileSelectorProps) {
onChange={handleChange}
className="rounded border border-gray-300 bg-white px-2 py-1 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
>
{PROFILES.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
{PROFILE_IDS.map((id) => (
<option key={id} value={id}>
{t(`profiles.${id}`)}
</option>
))}
</select>

View file

@ -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<string | null>(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")}
</button>
{saved && <span className="text-xs text-green-600">Saved!</span>}
{saved && <span className="text-xs text-green-600">{t("saved")}</span>}
{error && <span className="text-xs text-red-600">{error}</span>}
{saved && returnUrl && (
<a href={returnUrl} className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-700 hover:bg-gray-200">
Return to Journal
{t("returnToJournal")}
</a>
)}
</div>

View file

@ -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 (
<div className="flex h-full items-center justify-center">
<p className="text-gray-500">Connecting...</p>
<p className="text-gray-500">{t("connecting")}</p>
</div>
);
}
@ -45,7 +47,7 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl,
<>
<header className="flex flex-wrap items-center justify-between gap-2 border-b border-gray-200 px-4 py-2">
<div className="flex items-center gap-2 md:gap-4">
<h1 className="hidden text-lg font-semibold text-gray-900 sm:block">trails.cool Planner</h1>
<h1 className="hidden text-lg font-semibold text-gray-900 sm:block">{t("title")}</h1>
<ProfileSelector yjs={yjs} />
</div>
<div className="flex items-center gap-3">
@ -59,15 +61,15 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl,
)}
<ExportButton yjs={yjs} />
{computing && (
<span className="text-xs text-blue-600">Computing route...</span>
<span className="text-xs text-blue-600">{t("computingRoute")}</span>
)}
{isHost && (
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
Host
{t("host")}
</span>
)}
<span className="text-sm text-gray-500">
{yjs.connected ? "Connected" : "Connecting..."} · {sessionId.slice(0, 8)}
{yjs.connected ? t("connected") : t("connecting")} · {sessionId.slice(0, 8)}
</span>
</div>
</header>
@ -84,7 +86,7 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl,
<Suspense
fallback={
<div className="flex h-full items-center justify-center bg-gray-100 text-gray-500">
Loading map...
{t("loadingMap")}
</div>
}
>

View file

@ -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) {
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">{error.status}</h1>
<p className="mt-2 text-gray-600">
{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"))}
</p>
</div>
</div>
@ -49,9 +54,9 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">Error</h1>
<h1 className="text-4xl font-bold text-gray-900">{t("error")}</h1>
<p className="mt-2 text-gray-600">
{error instanceof Error ? error.message : "An unexpected error occurred"}
{error instanceof Error ? error.message : t("error")}
</p>
</div>
</div>

View file

@ -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 (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">trails.cool Planner</h1>
<p className="mt-4 text-lg text-gray-600">Collaborative route planning</p>
<h1 className="text-4xl font-bold text-gray-900">{t("title")}</h1>
<p className="mt-4 text-lg text-gray-600">{t("subtitle")}</p>
</div>
</div>
);