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:
@@ -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 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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user