From f2b3f1edfd0083cf79a5c20b8b2370d78014dfa1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Jan 2026 16:29:55 +0000 Subject: [PATCH] fix(i18n): render locale switch as links Use locale-prefixed elements for EN/DE so language switching works even when client-side hydration is broken. Co-authored-by: dennis --- app/components/Header.tsx | 40 +++++++++++++-------------------------- e2e/i18n.spec.ts | 8 ++++---- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 441230f..62e1ea7 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -6,14 +6,14 @@ import { Menu, X, Mail } from "lucide-react"; import { SiGithub, SiLinkedin } from "react-icons/si"; import Link from "next/link"; import { useLocale, useTranslations } from "next-intl"; -import { usePathname, useRouter } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; const Header = () => { const [isOpen, setIsOpen] = useState(false); const [scrolled, setScrolled] = useState(false); const locale = useLocale(); const pathname = usePathname(); - const router = useRouter(); + const searchParams = useSearchParams(); const t = useTranslations("nav"); const isHome = pathname === `/${locale}` || pathname === `/${locale}/`; @@ -44,23 +44,11 @@ const Header = () => { { icon: Mail, href: "mailto:contact@dk0.dev", label: "Email" }, ]; - const switchLocale = (nextLocale: string) => { - try { - const pathWithoutLocale = pathname.replace(new RegExp(`^/${locale}`), "") || ""; - const hash = typeof window !== "undefined" ? window.location.hash : ""; - // Rely on middleware to persist NEXT_LOCALE cookie. - // Use a hard navigation for maximum reliability (also fixes cases where - // client-side router navigation can be prevented by runtime errors). - const target = `/${nextLocale}${pathWithoutLocale}${hash}`; - if (typeof window !== "undefined") { - window.location.assign(target); - return; - } - router.push(target); - } catch { - // ignore - } - }; + const pathWithoutLocale = pathname.replace(new RegExp(`^/${locale}`), "") || ""; + const qs = searchParams.toString(); + const query = qs ? `?${qs}` : ""; + const enHref = `/en${pathWithoutLocale}${query}`; + const deHref = `/de${pathWithoutLocale}${query}`; // Always render to prevent flash, but use opacity transition @@ -144,9 +132,8 @@ const Header = () => {
- - +
{socialLinks.map((social) => ( { test("language switcher navigates between locales", async ({ page }) => { await page.goto("/en", { waitUntil: "domcontentloaded" }); - // Buttons have aria-labels; click the DE switcher. - const deButton = page.getByRole("button", { name: "Sprache auf Deutsch umstellen" }); - if (await deButton.count()) { + // Locale switchers are links (work even without hydration) + const deLink = page.getByRole("link", { name: "Sprache auf Deutsch umstellen" }); + if (await deLink.count()) { // Verify an EN label is present before switching (nav.home) await expect(page.getByRole("link", { name: "Home" })).toBeVisible(); await Promise.all([ page.waitForURL(/\/de(\/|$)/, { timeout: 30000 }), - deButton.click(), + deLink.click(), ]); // Verify the nav label updates after switching