Quellcode durchsuchen

feat(i18n): translate user-facing text to German across multiple components

Code_Uwe vor 4 Wochen
Ursprung
Commit
8f1d0f3554

+ 2 - 2
app/(protected)/[branch]/search/page.jsx

@@ -14,8 +14,8 @@ export default async function BranchSearchPage({ params }) {
 
 	return (
 		<PlaceholderPage
-			title="Search"
-			description="Search placeholder. Real search UI will be implemented in a later ticket."
+			title="Suche"
+			description="Platzhalter für die Suche. Die echte Suche wird in einem späteren Ticket umgesetzt."
 			params={resolvedParams}
 		/>
 	);

+ 3 - 3
app/(protected)/page.jsx

@@ -13,11 +13,11 @@ import PlaceholderPage from "@/components/placeholders/PlaceholderPage";
 export default function ProtectedEntryPage() {
 	return (
 		<PlaceholderPage
-			title="Entry"
-			description='This is the protected entry route ("/"). Later it will redirect based on the session.'
+			title="Übersicht"
+			description='Dies ist der geschützte Einstieg ("/"). Später wird hier abhängig von der Sitzung weitergeleitet.'
 		>
 			<div className="space-y-2 text-sm text-muted-foreground">
-				<p>Try these URLs manually:</p>
+				<p>Testen Sie diese URLs manuell:</p>
 				<ul className="list-disc pl-5">
 					<li>
 						<code className="rounded bg-muted px-1 py-0.5">/login</code>

+ 4 - 8
app/(public)/login/page.jsx

@@ -1,9 +1,3 @@
-// ---------------------------------------------------------------------------
-// Folder: app/(public)/login
-// File: page.jsx
-// Relative Path: app/(public)/login/page.jsx
-// ---------------------------------------------------------------------------
-
 import LoginForm from "@/components/auth/LoginForm";
 import { parseLoginParams } from "@/lib/frontend/authRedirect";
 
@@ -31,13 +25,15 @@ export default async function LoginPage({ searchParams }) {
 				<h1 className="text-2xl font-semibold tracking-tight">
 					RHL Lieferscheine
 				</h1>
-				<p className="text-sm text-muted-foreground">Sign in to continue</p>
+				<p className="text-sm text-muted-foreground">
+					Bitte melden Sie sich an, um fortzufahren.
+				</p>
 			</div>
 
 			<LoginForm reason={reason} nextPath={next} />
 
 			<p className="text-center text-xs text-muted-foreground">
-				For support, contact your administrator.
+				Bei Fragen wenden Sie sich an Ihren Administrator.
 			</p>
 		</div>
 	);

+ 2 - 2
components/app-shell/AppShell.test.js

@@ -41,8 +41,8 @@ describe("components/app-shell/AppShell", () => {
 		// TopNav brand
 		expect(html).toContain("RHL Lieferscheine");
 
-		// Sidebar placeholder content
-		expect(html).toContain("Sidebar");
+		// Sidebar placeholder heading (German)
+		expect(html).toContain("Seitenleiste");
 
 		// Rendered children
 		expect(html).toContain("Child content");

+ 9 - 17
components/app-shell/SidebarPlaceholder.jsx

@@ -3,35 +3,27 @@ import React from "react";
 /**
  * SidebarPlaceholder
  *
- * Why we reserve a sidebar area even for branch users:
- * - Admin/dev will later need a branch selector.
- * - Branch users can still benefit from navigation and filters:
- *   - quick jump (year/month/day)
- *   - search filters (archive toggle, date range)
- *   - recent/favorites
+ * Reserved sidebar area for future navigation/filter UI.
  *
- * RHL-019:
- * - Placeholder only (no logic yet).
- *
- * Test/runtime note:
- * - See AppShell.jsx for details why we import React explicitly.
+ * UX rule:
+ * - All user-facing text must be German.
  */
 export default function SidebarPlaceholder() {
 	return (
 		<div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
 			<div className="space-y-3">
 				<div>
-					<p className="text-sm font-medium">Sidebar</p>
+					<p className="text-sm font-medium">Seitenleiste</p>
 					<p className="text-xs text-muted-foreground">
-						Navigation & filters will live here.
+						Navigation und Filter werden hier später angezeigt.
 					</p>
 				</div>
 
 				<ul className="space-y-1 text-xs text-muted-foreground">
-					<li>• Branch context (label or selector)</li>
-					<li>• Explorer navigation (year/month/day)</li>
-					<li>• Search filters (archive, date range, ...)</li>
-					<li>• Shortcuts (recent, favorites)</li>
+					<li>• Niederlassung (Label oder Auswahl)</li>
+					<li>• Explorer-Navigation (Jahr/Monat/Tag)</li>
+					<li>• Suchfilter (Archiv, Zeitraum, …)</li>
+					<li>• Schnellzugriffe (Zuletzt geöffnet, Favoriten)</li>
 				</ul>
 			</div>
 		</div>

+ 8 - 15
components/app-shell/TopNav.jsx

@@ -1,11 +1,6 @@
-// ---------------------------------------------------------------------------
-// Folder: components/app-shell
-// File: TopNav.jsx
-// Relative Path: components/app-shell/TopNav.jsx
-// ---------------------------------------------------------------------------
-
 import React from "react";
 import Link from "next/link";
+
 import { Button } from "@/components/ui/button";
 import UserStatus from "@/components/app-shell/UserStatus";
 import LogoutButton from "@/components/auth/LogoutButton";
@@ -14,13 +9,11 @@ import LogoutButton from "@/components/auth/LogoutButton";
  * TopNav
  *
  * RHL-020:
- * - UserStatus now displays real session info (via AuthContext).
- * - Logout button is now functional (calls apiClient.logout + redirects to /login).
+ * - UserStatus displays session info (via AuthContext).
+ * - Logout button is functional (calls apiClient.logout + redirects to /login).
  *
- * Notes:
- * - Theme toggle is still a placeholder in this ticket.
- * - We keep this component server-renderable for stability and SSR tests.
- *   LogoutButton is a client component, but it does not require Next router hooks.
+ * UX rule:
+ * - All user-facing text must be German.
  */
 export default function TopNav() {
 	return (
@@ -31,7 +24,7 @@ export default function TopNav() {
 						RHL Lieferscheine
 					</Link>
 					<span className="text-xs text-muted-foreground">
-						App shell scaffold
+						Lieferschein-Explorer
 					</span>
 				</div>
 
@@ -43,9 +36,9 @@ export default function TopNav() {
 						size="sm"
 						disabled
 						aria-disabled="true"
-						title="Theme toggle will be added later"
+						title="Design-Umschaltung kommt später"
 					>
-						Theme
+						Design
 					</Button>
 
 					<LogoutButton />

+ 16 - 20
components/app-shell/UserStatus.jsx

@@ -1,9 +1,3 @@
-// ---------------------------------------------------------------------------
-// Folder: components/app-shell
-// File: UserStatus.jsx
-// Relative Path: components/app-shell/UserStatus.jsx
-// ---------------------------------------------------------------------------
-
 "use client";
 
 import React from "react";
@@ -18,30 +12,32 @@ import { useAuth } from "@/components/auth/authContext";
  * Data source:
  * - AuthContext (provided by components/auth/AuthProvider.jsx)
  *
- * Behavior:
- * - unknown (no provider): "Not loaded" (keeps SSR tests stable)
- * - loading: "Loading..."
- * - authenticated: show role + optional branchId
- * - unauthenticated: "Signed out" (should be rare because we redirect)
- * - error: "Error"
+ * UX rule:
+ * - All user-facing text must be German.
  */
 export default function UserStatus() {
 	const { status, user } = useAuth();
 
-	let text = "Not loaded";
+	function formatRole(role) {
+		if (role === "branch") return "Niederlassung";
+		if (role === "admin") return "Admin";
+		if (role === "dev") return "Entwicklung";
+		return role ? String(role) : "Unbekannt";
+	}
+
+	let text = "Nicht geladen";
 
-	if (status === "loading") text = "Loading...";
+	if (status === "loading") text = "Lädt…";
 	if (status === "authenticated" && user) {
-		// We only have userId/role/branchId from /api/auth/me.
-		// Keep this minimal and non-personal.
-		text = user.branchId ? `${user.role} (${user.branchId})` : `${user.role}`;
+		const roleLabel = formatRole(user.role);
+		text = user.branchId ? `${roleLabel} (${user.branchId})` : roleLabel;
 	}
-	if (status === "unauthenticated") text = "Signed out";
-	if (status === "error") text = "Error";
+	if (status === "unauthenticated") text = "Abgemeldet";
+	if (status === "error") text = "Fehler";
 
 	return (
 		<div className="hidden items-center gap-2 md:flex">
-			<span className="text-xs text-muted-foreground">User:</span>
+			<span className="text-xs text-muted-foreground">Benutzer:</span>
 			<span className="text-xs">{text}</span>
 		</div>
 	);

+ 11 - 14
components/auth/LoginForm.jsx

@@ -1,9 +1,3 @@
-// ---------------------------------------------------------------------------
-// Folder: components/auth
-// File: LoginForm.jsx
-// Relative Path: components/auth/LoginForm.jsx
-// ---------------------------------------------------------------------------
-
 "use client";
 
 import React from "react";
@@ -41,6 +35,9 @@ import {
  * NOTE:
  * - Password is NOT normalized. It remains case-sensitive.
  *
+ * UX rule:
+ * - All user-facing text must be German.
+ *
  * @param {{ reason: string|null, nextPath: string|null }} props
  */
 export default function LoginForm({ reason, nextPath }) {
@@ -68,7 +65,7 @@ export default function LoginForm({ reason, nextPath }) {
 		const p = password; // do NOT normalize password
 
 		if (!u || !p) {
-			setErrorMessage("Please enter username and password.");
+			setErrorMessage("Bitte Benutzername und Passwort eingeben.");
 			return;
 		}
 
@@ -87,9 +84,9 @@ export default function LoginForm({ reason, nextPath }) {
 	return (
 		<Card>
 			<CardHeader>
-				<CardTitle>Sign in</CardTitle>
+				<CardTitle>Anmeldung</CardTitle>
 				<CardDescription>
-					Enter your credentials to access the delivery note browser.
+					Bitte geben Sie Ihre Zugangsdaten ein, um die Lieferscheine zu öffnen.
 				</CardDescription>
 			</CardHeader>
 
@@ -103,14 +100,14 @@ export default function LoginForm({ reason, nextPath }) {
 
 				{errorMessage ? (
 					<Alert variant="destructive">
-						<AlertTitle>Login error</AlertTitle>
+						<AlertTitle>Anmeldung fehlgeschlagen</AlertTitle>
 						<AlertDescription>{errorMessage}</AlertDescription>
 					</Alert>
 				) : null}
 
 				<form onSubmit={onSubmit} className="space-y-4">
 					<div className="grid gap-2">
-						<Label htmlFor="username">Username</Label>
+						<Label htmlFor="username">Benutzername</Label>
 						<Input
 							id="username"
 							name="username"
@@ -125,12 +122,12 @@ export default function LoginForm({ reason, nextPath }) {
 								setUsername(e.target.value.toLowerCase());
 							}}
 							disabled={isSubmitting}
-							placeholder="e.g. branchuser"
+							placeholder="z. B. branchuser"
 						/>
 					</div>
 
 					<div className="grid gap-2">
-						<Label htmlFor="password">Password</Label>
+						<Label htmlFor="password">Passwort</Label>
 						<Input
 							id="password"
 							name="password"
@@ -145,7 +142,7 @@ export default function LoginForm({ reason, nextPath }) {
 
 					<CardFooter className="p-0">
 						<Button type="submit" className="w-full" disabled={isSubmitting}>
-							{isSubmitting ? "Signing in..." : "Sign in"}
+							{isSubmitting ? "Anmeldung…" : "Anmelden"}
 						</Button>
 					</CardFooter>
 				</form>

+ 5 - 8
components/auth/LogoutButton.jsx

@@ -1,9 +1,3 @@
-// ---------------------------------------------------------------------------
-// Folder: components/auth
-// File: LogoutButton.jsx
-// Relative Path: components/auth/LogoutButton.jsx
-// ---------------------------------------------------------------------------
-
 "use client";
 
 import React from "react";
@@ -24,6 +18,9 @@ import { Button } from "@/components/ui/button";
  * - Some unit tests render AppShell via react-dom/server without Next.js runtime.
  * - Using window.location inside the click handler avoids needing router context
  *   during server rendering (handler is not invoked in SSR tests).
+ *
+ * UX rule:
+ * - All user-facing text must be German.
  */
 export default function LogoutButton() {
 	const [isLoggingOut, setIsLoggingOut] = React.useState(false);
@@ -56,9 +53,9 @@ export default function LogoutButton() {
 			disabled={isLoggingOut}
 			aria-disabled={isLoggingOut ? "true" : "false"}
 			onClick={handleLogout}
-			title={isLoggingOut ? "Logging out..." : "Logout"}
+			title={isLoggingOut ? "Abmeldung läuft…" : "Abmelden"}
 		>
-			{isLoggingOut ? "Logging out..." : "Logout"}
+			{isLoggingOut ? "Abmeldung…" : "Abmelden"}
 		</Button>
 	);
 }

+ 4 - 5
components/placeholders/PlaceholderPage.jsx

@@ -3,9 +3,8 @@
  *
  * A small, reusable component to render consistent placeholder pages.
  *
- * Goals:
- * - Avoid duplicating layout code across many route pages.
- * - Make it easy to replace placeholders with real UI later.
+ * UX rule:
+ * - All user-facing text must be German.
  */
 export default function PlaceholderPage({
 	title,
@@ -24,7 +23,7 @@ export default function PlaceholderPage({
 
 			<div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
 				<div className="space-y-3">
-					<p className="text-sm font-medium">Route params</p>
+					<p className="text-sm font-medium">Routenparameter</p>
 
 					{params ? (
 						<pre className="overflow-auto rounded-md bg-muted p-3 text-xs">
@@ -32,7 +31,7 @@ export default function PlaceholderPage({
 						</pre>
 					) : (
 						<p className="text-xs text-muted-foreground">
-							No params for this route.
+							Keine Parameter für diese Route.
 						</p>
 					)}
 

+ 24 - 16
components/system/ForbiddenView.jsx

@@ -20,11 +20,10 @@ import {
 /**
  * ForbiddenView (RHL-021)
  *
- * A reusable UI block for "you are authenticated, but not allowed here".
+ * Reusable UI block for "authenticated, but not allowed here".
  *
- * It relies on AuthContext when available to render the best CTA:
- * - branch users -> link to their own branch root
- * - admin/dev -> link to home (later: could link to a branch list)
+ * UX rule:
+ * - All user-facing text must be German.
  */
 export default function ForbiddenView({ attemptedBranch = null }) {
 	const { status, user } = useAuth();
@@ -32,42 +31,51 @@ export default function ForbiddenView({ attemptedBranch = null }) {
 	const isAuthed = status === "authenticated" && user;
 	const isBranchUser = isAuthed && user.role === "branch" && user.branchId;
 
-	const primaryHref = isBranchUser ? branchPath(user.branchId) : homePath();
-	const primaryLabel = isBranchUser ? "Go to my branch" : "Go to home";
+	const overviewHref = homePath();
+
+	const primaryHref = isBranchUser ? branchPath(user.branchId) : overviewHref;
+	const primaryLabel = isBranchUser
+		? "Zu meiner Niederlassung"
+		: "Zur Übersicht";
+
+	const showSecondaryOverview = isBranchUser;
 
 	return (
 		<Card>
 			<CardHeader>
-				<CardTitle>Access denied</CardTitle>
+				<CardTitle>Zugriff verweigert</CardTitle>
 				<CardDescription>
-					You are not allowed to access this resource.
+					Sie haben keine Berechtigung, diese Ressource zu öffnen.
 				</CardDescription>
 			</CardHeader>
 
 			<CardContent className="space-y-4">
 				<Alert variant="destructive">
-					<AlertTitle>Forbidden</AlertTitle>
+					<AlertTitle>Keine Berechtigung</AlertTitle>
 					<AlertDescription>
 						{attemptedBranch ? (
 							<span>
-								Your account is not permitted to access branch{" "}
-								<strong>{attemptedBranch}</strong>.
+								Ihr Konto darf die Niederlassung{" "}
+								<strong>{attemptedBranch}</strong> nicht öffnen.
 							</span>
 						) : (
-							<span>Your account is not permitted to access this page.</span>
+							<span>Ihr Konto darf diese Seite nicht öffnen.</span>
 						)}
 					</AlertDescription>
 				</Alert>
 
 				<p className="text-sm text-muted-foreground">
-					If you believe this is a mistake, please contact your administrator.
+					Wenn Sie glauben, dass das ein Fehler ist, wenden Sie sich an Ihren
+					Administrator.
 				</p>
 			</CardContent>
 
 			<CardFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end">
-				<Button variant="outline" asChild>
-					<Link href={homePath()}>Home</Link>
-				</Button>
+				{showSecondaryOverview ? (
+					<Button variant="outline" asChild>
+						<Link href={overviewHref}>Zur Übersicht</Link>
+					</Button>
+				) : null}
 
 				<Button asChild>
 					<Link href={primaryHref}>{primaryLabel}</Link>

+ 9 - 7
components/system/NotFoundView.jsx

@@ -20,7 +20,9 @@ import {
  * NotFoundView (RHL-021)
  *
  * Used by (protected)/not-found.jsx.
- * Keeps UX consistent for invalid route params and unknown pages in the protected area.
+ *
+ * UX rule:
+ * - All user-facing text must be German.
  */
 export default function NotFoundView() {
 	const { status, user } = useAuth();
@@ -34,27 +36,27 @@ export default function NotFoundView() {
 	return (
 		<Card>
 			<CardHeader>
-				<CardTitle>Not found</CardTitle>
+				<CardTitle>Nicht gefunden</CardTitle>
 				<CardDescription>
-					The page or resource you requested does not exist.
+					Die angeforderte Seite oder Ressource wurde nicht gefunden.
 				</CardDescription>
 			</CardHeader>
 
 			<CardContent className="space-y-2">
 				<p className="text-sm text-muted-foreground">
-					This can happen when route parameters are invalid (e.g.
-					year/month/day) or the URL is mistyped.
+					Dies kann passieren, wenn Routenparameter ungültig sind (z. B.
+					Jahr/Monat/Tag) oder die URL falsch eingegeben wurde.
 				</p>
 			</CardContent>
 
 			<CardFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end">
 				<Button variant="outline" asChild>
-					<Link href={homePath()}>Home</Link>
+					<Link href={homePath()}>Zur Übersicht</Link>
 				</Button>
 
 				{ownBranchHref ? (
 					<Button asChild>
-						<Link href={ownBranchHref}>Go to my branch</Link>
+						<Link href={ownBranchHref}>Zu meiner Niederlassung</Link>
 					</Button>
 				) : null}
 			</CardFooter>