Skip to content

Locale Switcher Next.js yang Nggak Bikin Visitor Tersesat

Adityo Guni Waluyo

Bikin locale switcher Next.js App Router yang preserve current path pakai usePathname dan regex swap, plus active state accessible dengan aria-current.

Ringkasan

Locale switcher yang cuma link ke homepage bikin visitor kehilangan konten pas ganti bahasa. Solusinya pakai usePathname plus regex buat swap prefix locale, jadi /en/about tetep nyampe ke /id/about. Jangan lupa aria-current buat active state, dan kalo project makin ribet, pindah aja ke next-intl.

Visitor buka halaman About di /en/about, terus klik tombol "ID" di header. Harusnya dia pindah ke /id/about, kan? Eh, yang muncul malah homepage /id. Dia kehilangan context halaman yang lagi dibaca, harus scroll balik atau klik menu lagi buat nyampe ke konten yang tadi.

Saya nemuin masalah ini pas lagi develop sebuah project company profile yang pakai Next.js App Router dengan sub-path routing buat internationalization. Waktu itu saya bikin locale switcher simpel, cuma dua link: satu ke /id, satu ke /en. Naive banget, tapi keliatan fine aja pas testing di homepage. Masalahnya baru muncul waktu visitor udah deep di suatu halaman dan pengen switch language.

Awalnya saya kira masalahnya ada di cara Next.js handle routing. Mungkin ada config yang kelewati, atau harus pakai middleware buat redirect. Saya coba beberapa approach, tapi semuanya bikin routing logic jadi ribet.

Ternyata solusinya lebih straightforward: saya perlu preserve bagian pathname setelah prefix locale, terus cuma swap prefix-nya aja pakai regex.

Regex swap: preserve path, ganti locale

Next.js App Router support i18n dengan sub-path routing, di mana special files nested di bawah app/[lang] dan parameter lang di-forward ke setiap layout dan page [1]. Masalahnya, pas kita mau bikin tombol buat ganti bahasa, kita perlu tau pathname sekarang apa, terus swap bagian depannya doang.

Di sinilah usePathname main peranan. Ini Client Component hook yang baca current pathname, karena Server Components nggak bisa baca URL [2]. Jadi saya bikin component client yang import hook ini, terus pakai regex buat swap prefix.

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

Cara kerjanya: regex /^\/(?:id|en)(?=\\/|$)/ match prefix /id atau /en di awal string, tapi cuma kalo setelahnya ada slash lain atau end of string. Bagian setelah prefix (kalo ada) di-keep, terus di-prepend dengan locale baru.

  • Pathname /en/about + next id menghasilkan /id/about
  • Pathname /en/services/web + next id menghasilkan /id/services/web
  • Pathname /en + next id menghasilkan /id

Display order di UI saya fix EN|ID, independent dari urutan config. Terus saya kasih ARIA group label per locale, misalnya "Select language" buat English group dan "Pilih bahasa" buat Indonesian group. Ini bantu screen reader announce context yang bener.

Active pill dan aria-current

Selain locale switcher, masalah lain yang sering ke-skip di navigation header adalah active state yang accessible. Banyak yang cuma pakai CSS class active atau bg-primary, tapi nggak nandain elemen yang lagi aktif ke assistive technology.

Attribute aria-current nandain current item dalam suatu set, dan cuma satu element per set yang boleh punya attribute ini [3]. Nilainya bisa page, step, true, atau false. Kalo absent atau false, element itu nggak di-expose ke assistive tech.

Buat nav links, saya pakai aria-current="page". Logic-nya simple: cek apakah pathname === item.href, kalo iya set aria-current="page", plus ganti background token biar keliatan active.

const active = pathname === item.href;

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

Buat locale toggle (yang juga semacam set tapi beda jenis), saya pakai aria-current="true" buat locale yang lagi aktif. Jadi ada dua layer of active state di header: satu buat current page dalam nav menu, satu buat current language di toggle.

Yang tricky di sini adalah active state harus re-render pas pathname berubah. Karena usePathname itu reactive hook, component otomatis update pas navigasi terjadi. Tapi pastiin component ini client component, bukan server component.

Kapan graduate ke next-intl

Approach regex swap ini works fine buat use case simpel kayak project company profile yang cuma punya dua bahasa dan routing structure-nya flat. Tapi ada batasnya.

Kalo kamu mulai nambahin banyak bahasa (kayak 5+ locales), atau punya dynamic segments yang complex (kayak /[lang]/blog/[slug]), atau perlu handle locale detection dari Accept-Language header plus cookie persistence, manual regex approach ini mulai bikin codebase messy.

Di titik itu, next-intl punya fungsi createNavigation yang nge-wrap semua API navigasi Next.js, dan semuanya otomatis handle locale [4]. Link component-nya punya prop locale yang otomatis set attribute hreflang, dan docs-nya ada contoh active-link yang pair useSelectedLayoutSegment dengan aria-current page.

Bedanya apa? Dengan createNavigation, kamu nggak perlu manual swap prefix atau track current locale di state. Library-nya handle itu semua, plus kasih type safety dan consistent API. Trade-off-nya ya tambah satu dependency dan learning curve dikit buat setup-nya.

Saya personally pilih stay dengan regex approach buat project yang simpel, karena overhead-nya minimal dan logic-nya transparent. Tapi kalo project-nya scale up atau butuh fitur i18n yang lebih advanced (kayak pluralization, number formatting, date formatting), migrate ke next-intl itu worth it.

Yang penting sih, apapun approach yang kamu pilih, pastiin locale switcher preserve current path dan active state-nya accessible. Visitor nggak harus kehilangan context cuma karena ganti bahasa, dan screen reader user harus tau page mana atau language mana yang lagi aktif.

Artikel terkait