fix(i18n): render locale switch as links

Use locale-prefixed <Link> elements for EN/DE so language switching works even when client-side hydration is broken.

Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
Cursor Agent
2026-01-14 16:29:55 +00:00
parent 411806d5ce
commit f2b3f1edfd
2 changed files with 17 additions and 31 deletions

View File

@@ -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 = () => {
<div className="hidden md:flex items-center space-x-3">
<div className="flex items-center bg-white/40 border border-white/50 rounded-full overflow-hidden shadow-sm">
<button
type="button"
onClick={() => switchLocale("en")}
<Link
href={enHref}
className={`px-3 py-1.5 text-xs font-semibold transition-colors ${
locale === "en"
? "bg-stone-900 text-stone-50"
@@ -155,10 +142,9 @@ const Header = () => {
aria-label="Switch language to English"
>
EN
</button>
<button
type="button"
onClick={() => switchLocale("de")}
</Link>
<Link
href={deHref}
className={`px-3 py-1.5 text-xs font-semibold transition-colors ${
locale === "de"
? "bg-stone-900 text-stone-50"
@@ -167,7 +153,7 @@ const Header = () => {
aria-label="Sprache auf Deutsch umstellen"
>
DE
</button>
</Link>
</div>
{socialLinks.map((social) => (
<motion.a

View File

@@ -4,15 +4,15 @@ test.describe("i18n routing", () => {
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