Skip to content

A Next.js Locale Switcher That Doesn't Lose Your Visitor

Adityo Guni Waluyo

Build a Next.js App Router locale switcher that preserves the current path with usePathname and a regex swap, plus an accessible active state.

TL;DR

A naive locale switcher sent users to the locale homepage instead of the same page. The fix: a small client component that swaps the /en or /id prefix while preserving the rest of the pathname, plus aria-current for the active state. Stick with this for simple sites; adopt next-intl once routing grows complex.

On the first deploy of a company profile site, I opened a browser tab and clicked the "ID" button in the header while on the /en/about page. The expectation was clear: switch to /id/about. The reality? I landed on /id, aka the homepage. Visitors immediately lost their place. My initial locale switcher was very naive, just linking to the locale root without considering the path behind it.

At first, I thought this was a Next.js routing configuration or middleware issue that wasn't correctly catching the prefix. I tried tinkering with middleware.ts, adding complex regex for forced redirects. The result was nil, or it even caused a dizzying redirect loop.

Wrong guess at the middleware

Turns out, the problem wasn't in the middleware. In Next.js App Router, i18n with sub-path routing means special files are nested under app/[lang], and the lang parameter is forwarded to every layout and page [1]. So, there's only one key: just swap the locale prefix, and leave the rest of the pathname intact.

Since usePathname is a client hook and Server Components cannot read the URL directly [2], I built a small client component to handle this. The logic is simple: take the current pathname, then replace the /en or /id prefix with the target locale.

function swapLocale(pathname, next) {
  return '/' + next + pathname.replace(/^\/(?:id|en)(?=\\/|$)/, '');
}

This function checks if the pathname starts with /id or /en followed by a slash or the end of the string, then replaces it. If the pathname is /en/about, called with next = 'id', the result is instantly /id/about. Clean and predictable.

A feature often forgotten is the accessible active state. Using aria-current is important because it marks the currently active item in a set, one per set. Its value can be page, step, true, or false. If absent or false, it means it's not exposed to screen readers [3].

I use a simple pattern: compare the pathname from the hook with the href of the menu item. If they match, I inject aria-current="page".

const active = pathname === item.href;

return (
  <Link
    href={item.href}
    aria-current={active ? 'page' : undefined}
    className={active ? 'bg-accent' : 'bg-transparent'}
  >
    {item.label}
  </Link>
);

For the locale toggle, which is a different kind of set, I use aria-current="true" for the currently active locale.

When to switch libraries

When should this manual approach be abandoned? Honestly, for a simple two-locale site, I stick with this regex approach. There's no need to add a dependency that bloats the bundle.

But if the project starts needing features like complex nested routing, or structured translation message management per component, it's time to use a library. next-intl has createNavigation which wraps Link, redirect, usePathname, useRouter, and getPathname with automatic locale handling [4]. The locale prop in its Link component also automatically sets hreflang. Their documentation even suggests pairing useSelectedLayoutSegment with aria-current page in the active-link example, which aligns with the pattern I built manually.

Sometimes the best solution isn't the most sophisticated one, but the one that best fits the scale of the problem.

Related articles

Next.js App Router Locale Switcher Without an i18n Library · Adityo GW