Better I18NBetter I18N
Next.js

Client-Side Features

Client-side components and hooks for instant locale switching in Next.js

Client-side features allow instant locale switching without a full page reload or server round-trip. When BetterI18nProvider is present in the tree, locale changes trigger a CDN fetch and client re-render — no navigation needed.

BetterI18nProvider

Wrap your Root Layout with BetterI18nProvider to enable instant client-side locale switching via useSetLocale.

app/[locale]/layout.tsx
import { BetterI18nProvider } from '@better-i18n/next/client';
import { i18n } from '@/i18n.config';

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params;
  const messages = await i18n.getMessages(locale);

  return (
    <BetterI18nProvider config={i18n.config} locale={locale} messages={messages}>
      {children}
    </BetterI18nProvider>
  );
}

BetterI18nProvider enables instant client-side locale switching via useSetLocale — no page navigation or router refresh needed.

Props

PropTypeDefaultDescription
configI18nConfigRequiredThe i18n config from createI18n()
localestringFrom cookieOverride active locale
messagesMessagesPre-loaded SSR messages
timeZonestringconfig.timeZoneIANA timezone
nowDatePin "now" for SSR hydration consistency

useSetLocale()

Switches the active locale. Behavior depends on whether BetterI18nProvider is present in the tree.

With BetterI18nProvider (instant)

When the provider is in the tree, calling setLocale triggers an instant CDN fetch and client re-render. No page navigation or router.refresh() is needed.

'use client';

import { useSetLocale } from '@better-i18n/next/client';

function LanguageSwitcher() {
  const setLocale = useSetLocale();

  return (
    <button onClick={() => setLocale('tr')}>
      Switch to Turkish
    </button>
  );
}

Without BetterI18nProvider (soft refresh)

When called outside a provider, useSetLocale updates the locale cookie and calls router.refresh() to trigger a soft server re-render.

'use client';

import { useSetLocale } from '@better-i18n/next/client';
import { i18n } from '@/i18n.config';

function LanguageSwitcher() {
  // Pass config explicitly when no provider is in the tree
  const setLocale = useSetLocale({ config: i18n.config });

  return (
    <select onChange={(e) => setLocale(e.target.value)}>
      <option value="en">English</option>
      <option value="tr">Türkçe</option>
    </select>
  );
}

Behavior Comparison

ModeTriggerResult
With BetterI18nProvidersetLocale('tr')Instant CDN fetch + client re-render
Without providersetLocale('tr')Cookie update + router.refresh()
With provider + localePrefix: "never"setLocale('tr')Instant CDN fetch + cookie update (URL unchanged)
Without provider + localePrefix: "never"setLocale('tr')Cookie update + router.refresh() (URL unchanged)

With localePrefix: "never", the URL never changes during locale switching — only the cookie and rendered content update. This makes it ideal for dashboards and SaaS apps where the URL should remain stable across languages.


useManifestLanguages(config)

Fetches available languages with full metadata on the client. Includes request deduplication.

'use client';

import { useManifestLanguages } from '@better-i18n/next/client';
import { i18n } from '@/i18n.config';

function LanguageSwitcher() {
  const { languages, isLoading } = useManifestLanguages(i18n.config);
  const setLocale = useSetLocale();

  if (isLoading) return <select disabled><option>Loading...</option></select>;

  return (
    <select onChange={(e) => setLocale(e.target.value)}>
      {languages.map((lang) => (
        <option key={lang.code} value={lang.code}>
          {lang.nativeName || lang.name || lang.code}
        </option>
      ))}
    </select>
  );
}

Return Value

PropertyTypeDescription
languagesLanguageOption[]Available languages from manifest
isLoadingbooleantrue while fetching
errorError | nullError if fetch failed

LocaleDropdown

A fully-featured, accessible locale switcher dropdown for Next.js. Import from @better-i18n/next/client — the RSC-safe client entrypoint.

'use client';

import { LocaleDropdown } from '@better-i18n/next/client';
import { useLocale } from 'next-intl';
import { i18n } from '@/i18n.config';

function Header() {
  const locale = useLocale();

  return (
    <header>
      <LocaleDropdown config={i18n.config} locale={locale} />
    </header>
  );
}

Props

PropTypeDefaultDescription
configI18nConfigRequiredConfig from createI18n()
localestringRequiredCurrent locale from useLocale()
languagesLanguageOption[]Pre-fetched languages (skips client CDN fetch)
variant"styled" | "unstyled""styled"Styled or unstyled mode
showFlagbooleantrueShow flag emoji/image
showNativeNamebooleantrueShow native language name
showLocaleCodebooleantrueShow locale code
renderTrigger(ctx) => ReactNodeCustom trigger renderer
renderItem(ctx) => ReactNodeCustom item renderer

Locale Switching Behavior

ContextBehavior
Inside BetterI18nProviderInstant CDN fetch + client re-render — no navigation
Without providerCookie update + router.refresh() — soft server re-render

SSR-Optimized Example

Pass languages from the server to skip the client-side CDN fetch:

app/[locale]/layout.tsx
import { BetterI18nProvider } from '@better-i18n/next/client';
import { i18n } from '@/i18n.config';

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params;
  const [messages, languages] = await Promise.all([
    i18n.getMessages(locale),
    i18n.getLanguages(),
  ]);

  return (
    <BetterI18nProvider
      config={i18n.config}
      locale={locale}
      messages={messages}
    >
      <Header locale={locale} languages={languages} />
      {children}
    </BetterI18nProvider>
  );
}
components/header.tsx
'use client';

import { LocaleDropdown } from '@better-i18n/next/client';
import { i18n } from '@/i18n.config';
import type { LanguageOption } from '@better-i18n/core';

function Header({ locale, languages }: { locale: string; languages: LanguageOption[] }) {
  return (
    <header>
      <LocaleDropdown config={i18n.config} locale={locale} languages={languages} />
    </header>
  );
}

No transpilePackages configuration needed — @better-i18n/next ships pre-compiled in dist/.

For full props reference, variants, CSS custom properties, and custom rendering — see LocaleDropdown.

On this page