diff --git a/frontend/app/[locale]/layout.tsx b/frontend/app/[locale]/layout.tsx
new file mode 100644
index 0000000..11ec10b
--- /dev/null
+++ b/frontend/app/[locale]/layout.tsx
@@ -0,0 +1,53 @@
+'use client';
+
+import { useEffect, useState, type ReactNode } from 'react';
+import { useRouter } from 'next/navigation';
+import { I18nProvider } from '@/lib/i18n';
+import { ToastProvider } from '@/components/ui/Toast';
+import { Navigation } from '@/components/Navigation';
+import { isAuthenticated } from '@/lib/api';
+
+export default function LocaleLayout({
+ children,
+ params,
+}: {
+ children: ReactNode;
+ params: { locale: string };
+}) {
+ const router = useRouter();
+ const [checked, setChecked] = useState(false);
+
+ // Auth guard: redirect to /login if no token is present
+ useEffect(() => {
+ if (!isAuthenticated()) {
+ router.replace('/login');
+ return;
+ }
+ setChecked(true);
+ }, [router]);
+
+ // Show nothing while redirecting (avoids flashing protected content)
+ if (!checked) {
+ return (
+
+ );
+ }
+
+ const locale = params.locale === 'en' ? 'en' : 'de';
+
+ return (
+
+
+
+
+ {/* Main content area — offset for sidebar on desktop */}
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/frontend/components/Navigation.tsx b/frontend/components/Navigation.tsx
new file mode 100644
index 0000000..5ecd351
--- /dev/null
+++ b/frontend/components/Navigation.tsx
@@ -0,0 +1,162 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { useI18n } from '@/lib/i18n';
+import { UserMenu } from '@/components/UserMenu';
+
+interface NavItem {
+ href: string;
+ labelKey: string;
+ icon: React.ReactNode;
+}
+
+const navItems: NavItem[] = [
+ {
+ href: '/de/fahrzeuge',
+ labelKey: 'nav.vehicles',
+ icon: (
+
+ ),
+ },
+ {
+ href: '/de/kontakte',
+ labelKey: 'nav.contacts',
+ icon: (
+
+ ),
+ },
+ {
+ href: '/de/verkauf',
+ labelKey: 'nav.sales',
+ icon: (
+
+ ),
+ },
+ {
+ href: '/de/ki-copilot',
+ labelKey: 'nav.ki-copilot',
+ icon: (
+
+ ),
+ },
+ {
+ href: '/de/ocr',
+ labelKey: 'nav.ocr',
+ icon: (
+
+ ),
+ },
+ {
+ href: '/de/retouch',
+ labelKey: 'nav.retouch',
+ icon: (
+
+ ),
+ },
+];
+
+export function Navigation() {
+ const pathname = usePathname();
+ const { t } = useI18n();
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ // Close mobile sidebar on route change
+ useEffect(() => {
+ setMobileOpen(false);
+ }, [pathname]);
+
+ function isActive(href: string): boolean {
+ // Extract the path segment after locale (e.g. /de/fahrzeuge -> fahrzeuge)
+ const segments = href.split('/'); // ['', 'de', 'fahrzeuge']
+ const section = segments[2]; // 'fahrzeuge'
+ if (!section) return false;
+ return pathname.includes(`/${section}`);
+ }
+
+ return (
+ <>
+ {/* Mobile header bar with hamburger */}
+
+
+ {t('nav.appName')}
+
+
+
+ {/* Mobile overlay */}
+ {mobileOpen && (
+ setMobileOpen(false)}
+ data-testid="mobile-overlay"
+ />
+ )}
+
+ {/* Sidebar */}
+
+
+ {/* Mobile spacer so content starts below header */}
+
+ >
+ );
+}
diff --git a/frontend/components/UserMenu.tsx b/frontend/components/UserMenu.tsx
new file mode 100644
index 0000000..d10621e
--- /dev/null
+++ b/frontend/components/UserMenu.tsx
@@ -0,0 +1,92 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import { useRouter } from 'next/navigation';
+import { clearTokens } from '@/lib/api';
+import { useI18n } from '@/lib/i18n';
+
+/**
+ * Decodes the email from a JWT access token stored in localStorage.
+ * Returns null if no token is present or the payload cannot be parsed.
+ */
+function decodeEmailFromToken(token: string | null): string | null {
+ if (!token) return null;
+ const parts = token.split('.');
+ if (parts.length !== 3) return null;
+ try {
+ const payload = JSON.parse(atob(parts[1]));
+ return payload.email || payload.sub || null;
+ } catch {
+ return null;
+ }
+}
+
+export function UserMenu() {
+ const router = useRouter();
+ const { t } = useI18n();
+ const [email, setEmail] = useState
(null);
+ const [open, setOpen] = useState(false);
+ const menuRef = useRef(null);
+
+ useEffect(() => {
+ const token = localStorage.getItem('access_token');
+ setEmail(decodeEmailFromToken(token));
+ }, []);
+
+ useEffect(() => {
+ function handleClickOutside(e: MouseEvent) {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ setOpen(false);
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ function handleLogout() {
+ clearTokens();
+ router.push('/login');
+ }
+
+ return (
+
+
+ {open && (
+
+
+
{t('nav.settings')}
+
{email || '—'}
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/messages/de.json b/frontend/messages/de.json
index 146148e..fea554e 100644
--- a/frontend/messages/de.json
+++ b/frontend/messages/de.json
@@ -14,6 +14,11 @@
"nav.sales": "Verkäufe",
"nav.settings": "Einstellungen",
"nav.logout": "Abmelden",
+ "nav.ki-copilot": "KI-Copilot",
+ "nav.ocr": "OCR",
+ "nav.retouch": "Bildretusche",
+ "nav.menu": "Menü",
+ "nav.appName": "ERP Nutzfahrzeuge",
"common.save": "Speichern",
"common.cancel": "Abbrechen",
"common.delete": "Löschen",
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index b9484ae..c90343b 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -14,6 +14,11 @@
"nav.sales": "Sales",
"nav.settings": "Settings",
"nav.logout": "Logout",
+ "nav.ki-copilot": "AI Copilot",
+ "nav.ocr": "OCR",
+ "nav.retouch": "Image Retouch",
+ "nav.menu": "Menu",
+ "nav.appName": "ERP Commercial Vehicles",
"common.save": "Save",
"common.cancel": "Cancel",
"common.delete": "Delete",