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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
config | I18nConfig | Required | The i18n config from createI18n() |
locale | string | From cookie | Override active locale |
messages | Messages | — | Pre-loaded SSR messages |
timeZone | string | config.timeZone | IANA timezone |
now | Date | — | Pin "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
| Mode | Trigger | Result |
|---|---|---|
With BetterI18nProvider | setLocale('tr') | Instant CDN fetch + client re-render |
| Without provider | setLocale('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
| Property | Type | Description |
|---|---|---|
languages | LanguageOption[] | Available languages from manifest |
isLoading | boolean | true while fetching |
error | Error | null | Error 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
| Prop | Type | Default | Description |
|---|---|---|---|
config | I18nConfig | Required | Config from createI18n() |
locale | string | Required | Current locale from useLocale() |
languages | LanguageOption[] | — | Pre-fetched languages (skips client CDN fetch) |
variant | "styled" | "unstyled" | "styled" | Styled or unstyled mode |
showFlag | boolean | true | Show flag emoji/image |
showNativeName | boolean | true | Show native language name |
showLocaleCode | boolean | true | Show locale code |
renderTrigger | (ctx) => ReactNode | — | Custom trigger renderer |
renderItem | (ctx) => ReactNode | — | Custom item renderer |
Locale Switching Behavior
| Context | Behavior |
|---|---|
Inside BetterI18nProvider | Instant CDN fetch + client re-render — no navigation |
| Without provider | Cookie update + router.refresh() — soft server re-render |
SSR-Optimized Example
Pass languages from the server to skip the client-side CDN fetch:
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>
);
}'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.