---
title: Locale Management
description: useLocale, useLocaleRouter, useLanguages, and LanguageSwitcher
icon: Globe
---

## useLocale

Access and update the current locale:

```tsx
import { useLocale } from '@better-i18n/use-intl'

function LocaleDisplay() {
  const { locale, setLocale, isLoading } = useLocale()

  return (
    <div>
      <p>Current locale: {locale}</p>
      <button onClick={() => setLocale('tr')}>
        Switch to Turkish
      </button>
    </div>
  )
}
```

### Return Value

| Property | Type | Description |
|----------|------|-------------|
| `locale` | `string` | Current locale code (e.g., `"en"`, `"tr"`) |
| `setLocale` | `(locale: string) => void` | Function to change the locale |
| `isLoading` | `boolean` | Whether new messages are being loaded |

### Locale Switching Flow

When you call `setLocale()`, the provider:

1. Fetches new messages from the CDN
2. Updates the React context
3. Triggers re-render with new translations
4. Calls `onLocaleChange` callback (if provided)

```tsx
function LanguageButtons() {
  const { locale, setLocale, isLoading } = useLocale()

  if (isLoading) {
    return <p>Switching language...</p>
  }

  return (
    <div className="flex gap-2">
      <button
        onClick={() => setLocale('en')}
        className={locale === 'en' ? 'font-bold' : ''}
      >
        English
      </button>
      <button
        onClick={() => setLocale('tr')}
        className={locale === 'tr' ? 'font-bold' : ''}
      >
        Türkçe
      </button>
    </div>
  )
}
```

---

## useLocaleRouter

Navigation-first hook for locale switching with TanStack Router integration. Uses `router.navigate()` instead of state updates.

### Why useLocaleRouter?

Traditional i18n libraries use `setLocale()` which changes React state but doesn't trigger router navigation:

- ❌ URL not updated
- ❌ Loaders don't re-execute
- ❌ Browser history not updated

`useLocaleRouter` solves these:

- ✅ Triggers proper SPA navigation
- ✅ Re-executes loaders (fresh messages)
- ✅ Updates URL correctly
- ✅ Works with browser history

### Usage

```tsx
import { useLocaleRouter } from '@better-i18n/use-intl'

function LanguageSwitcher() {
  const { locale, locales, navigate, isReady } = useLocaleRouter()

  if (!isReady) return <Skeleton />

  return (
    <select value={locale} onChange={(e) => navigate(e.target.value)}>
      {locales.map((loc) => (
        <option key={loc} value={loc}>{loc}</option>
      ))}
    </select>
  )
}
```

### Return Value

| Property | Type | Description |
|----------|------|-------------|
| `locale` | `string` | Current locale from URL |
| `locales` | `string[]` | Available locale codes from CDN manifest |
| `defaultLocale` | `string` | Default locale (no URL prefix) |
| `navigate` | `(locale: string) => void` | Navigate to same page with new locale |
| `localePath` | `(path: string, locale?: string) => string` | Get localized path |
| `isReady` | `boolean` | Whether languages are loaded from CDN |

### URL Strategy

The default locale has **no prefix** in the URL:

| Locale | URL |
|--------|-----|
| English (default) | `/about` |
| Turkish | `/tr/about` |
| German | `/de/about` |

This provides cleaner URLs for the primary language while supporting SEO-friendly URLs for all locales.

### Using localePath

Generate localized paths for links:

```tsx
function Navigation() {
  const { localePath, locale } = useLocaleRouter()

  return (
    <nav>
      <Link to={localePath('/about')}>About</Link>
      <Link to={localePath('/contact')}>Contact</Link>

      {/* Force a specific locale */}
      <Link to={localePath('/about', 'tr')}>About (Turkish)</Link>
    </nav>
  )
}
```

### useLocale vs useLocaleRouter

| Feature | useLocale | useLocaleRouter |
|---------|-----------|-----------------|
| **State update** | React context only | Router navigation |
| **URL update** | Manual | Automatic |
| **Loader re-execution** | No | Yes |
| **History update** | No | Yes |
| **Use case** | Simple CSR apps | TanStack Router apps |

<Callout type="tip">
  Use `useLocaleRouter` for TanStack Start/Router apps. Use `useLocale` for simple Vite apps without router integration.
</Callout>

---

## useLanguages

Fetch available languages dynamically from the CDN manifest:

```tsx
import { useLanguages } from '@better-i18n/use-intl'

function LanguageList() {
  const { languages, isLoading } = useLanguages()

  if (isLoading) {
    return <p>Loading languages...</p>
  }

  return (
    <ul>
      {languages.map((lang) => (
        <li key={lang.code}>
          {lang.nativeName} ({lang.code})
        </li>
      ))}
    </ul>
  )
}
```

### Return Value

| Property | Type | Description |
|----------|------|-------------|
| `languages` | `LanguageOption[]` | Array of available languages |
| `isLoading` | `boolean` | Whether languages are being fetched |

### LanguageOption Type

```ts
interface LanguageOption {
  code: string       // "en", "tr", "de"
  name?: string      // "English", "Turkish", "German"
  nativeName?: string // "English", "Türkçe", "Deutsch"
  flagUrl?: string   // URL to flag image
}
```

### Dynamic Languages

Languages are fetched from your project's CDN manifest. When you add a new language in the dashboard, it automatically appears:

```
Dashboard: Add "Spanish" language
     ↓
CDN manifest updated
     ↓
useLanguages() returns updated list
     ↓
Language switcher shows "Español"
```

<Callout type="tip">
  No code change needed when you add a language from the dashboard — `useLanguages` always reflects the current CDN manifest.
</Callout>

### Avoid Static Language Arrays

A common mistake is maintaining a hardcoded list for validation or locale detection:

```ts
const SUPPORTED = ['tr', 'en', 'ar', 'de']       // [!code --]
if (!SUPPORTED.includes(lang)) return 'en'         // [!code --]
```

Use CDN languages instead:

```ts
const { languages } = await i18nReady                    // [!code ++]
const supported = languages.map(l => l.code)             // [!code ++]
if (!supported.includes(lang)) return 'en'               // [!code ++]
```

<Callout type="warn">
  Hardcoded lists break silently — adding a language in the dashboard works everywhere **except** wherever the static array was used.
</Callout>

---

## LanguageSwitcher Component

A ready-to-use language selector component:

```tsx
import { LanguageSwitcher } from '@better-i18n/use-intl'

function Header() {
  return (
    <header>
      <nav>
        <Logo />
        <LanguageSwitcher />
      </nav>
    </header>
  )
}
```

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `className` | `string` | | CSS class for the select element |
| `loadingLabel` | `string` | `"Loading..."` | Text shown while loading |
| `renderOption` | `(lang: LanguageOption) => ReactNode` | | Custom option renderer |

### Styling

```tsx
<LanguageSwitcher
  className="
    w-40 px-4 py-2
    bg-white border border-gray-200 rounded-lg
    focus:ring-2 focus:ring-blue-500
    text-sm font-medium text-gray-700
  "
/>
```

---

## Building Custom Switchers

For more control, build your own using the hooks:

### Button Group

```tsx
import { useLocale, useLanguages } from '@better-i18n/use-intl'

function ButtonGroupSwitcher() {
  const { locale, setLocale } = useLocale()
  const { languages, isLoading } = useLanguages()

  if (isLoading) return null

  return (
    <div className="flex gap-1 p-1 bg-gray-100 rounded-lg">
      {languages.map((lang) => (
        <button
          key={lang.code}
          onClick={() => setLocale(lang.code)}
          className={`
            px-3 py-1.5 rounded-md text-sm font-medium
            transition-colors
            ${locale === lang.code
              ? 'bg-white shadow text-gray-900'
              : 'text-gray-600 hover:text-gray-900'
            }
          `}
        >
          {lang.code.toUpperCase()}
        </button>
      ))}
    </div>
  )
}
```

### With Flags

```tsx
function FlagSwitcher() {
  const { locale, setLocale } = useLocale()
  const { languages, isLoading } = useLanguages()

  if (isLoading) return null

  return (
    <div className="flex gap-2">
      {languages.map((lang) => (
        <button
          key={lang.code}
          onClick={() => setLocale(lang.code)}
          className={`
            flex items-center gap-2 px-3 py-2 rounded border
            ${locale === lang.code
              ? 'bg-blue-50 border-blue-500'
              : 'border-gray-200 hover:border-gray-300'
            }
          `}
        >
          {lang.flagUrl && (
            <img
              src={lang.flagUrl}
              alt=""
              className="w-5 h-4 object-cover rounded-sm"
            />
          )}
          <span>{lang.nativeName}</span>
        </button>
      ))}
    </div>
  )
}
```

### With Router Integration

```tsx
import { useLocaleRouter, useLanguages } from '@better-i18n/use-intl'

function RouterLanguageSwitcher() {
  const { locale, navigate, isReady } = useLocaleRouter()
  const { languages } = useLanguages()

  if (!isReady) {
    return <div className="h-10 w-24 bg-gray-200 animate-pulse rounded" />
  }

  return (
    <div className="flex items-center gap-2">
      {languages.map((lang) => (
        <button
          key={lang.code}
          onClick={() => navigate(lang.code)}
          className={`
            flex items-center gap-2 px-3 py-2 rounded
            ${locale === lang.code
              ? 'bg-blue-100 border-blue-500'
              : 'bg-white border-gray-200'
            } border
          `}
        >
          {lang.flagUrl && (
            <img src={lang.flagUrl} alt="" className="w-5 h-4 object-cover rounded-sm" />
          )}
          <span>{lang.nativeName}</span>
        </button>
      ))}
    </div>
  )
}
```

## LocaleDropdown

A fully-featured, accessible dropdown component for locale switching — with flag emojis, native language names, keyboard navigation, and customizable rendering.

<LocaleDropdownDemo />

### Installation

<Tabs items={["TanStack / Vite", "Next.js", "Remix"]}>
<Tab value="TanStack / Vite">
```bash
bun add @better-i18n/use-intl
```
</Tab>
<Tab value="Next.js">
```bash
bun add @better-i18n/next
```
</Tab>
<Tab value="Remix">
```bash
bun add @better-i18n/remix
```
</Tab>
</Tabs>

### Basic Usage

<Tabs items={["TanStack / Vite", "Next.js", "Remix"]}>
<Tab value="TanStack / Vite">
Zero-config — reads locale and languages from context automatically:

```tsx
import { LocaleDropdown } from '@better-i18n/use-intl'

function Header() {
  return (
    <header>
      <nav>
        <Logo />
        <LocaleDropdown />
      </nav>
    </header>
  )
}
```

Selecting a locale triggers `useLocaleRouter().navigate()` — a full TanStack Router navigation with loader re-execution.
</Tab>
<Tab value="Next.js">
Requires `config` and `locale` props:

```tsx
'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>
      <nav>
        <Logo />
        <LocaleDropdown config={i18n.config} locale={locale} />
      </nav>
    </header>
  );
}
```

<Callout type="info">
  Import from `@better-i18n/next/client` — this is the RSC-safe client entrypoint. No `transpilePackages` needed.
</Callout>
</Tab>
<Tab value="Remix">
```tsx
import { LocaleDropdown } from '@better-i18n/remix/react';

function Header({ locale }: { locale: string }) {
  return (
    <header>
      <nav>
        <Logo />
        <LocaleDropdown />
      </nav>
    </header>
  );
}
```

Selecting a locale navigates to the locale-prefixed URL using `useNavigate()` from react-router.
</Tab>
</Tabs>

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `"styled" \| "unstyled"` | `"styled"` | Styled includes all inline styles + CSS custom properties. Unstyled gives full control. |
| `className` | `string` | — | CSS class for root wrapper |
| `triggerClassName` | `string` | — | CSS class for trigger button |
| `menuClassName` | `string` | — | CSS class for dropdown menu |
| `showFlag` | `boolean` | `true` | Show flag emoji/image |
| `showNativeName` | `boolean` | `true` | Show native language name (e.g., "Türkçe") |
| `showLocaleCode` | `boolean` | `true` | Show locale code (e.g., "TR") |
| `renderTrigger` | `(ctx) => ReactNode` | — | Custom trigger renderer |
| `renderItem` | `(ctx) => ReactNode` | — | Custom item renderer |

**Next.js only:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `config` | `I18nConfig` | Required | Config from `createI18n()` |
| `locale` | `string` | Required | Current locale |
| `languages` | `LanguageOption[]` | — | Pre-fetched languages (skips client CDN fetch) |

**Remix only:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `defaultLocale` | `string` | `"en"` | Default locale for URL prefix logic |

### Variants

<Tabs items={["Styled (default)", "Unstyled"]}>
<Tab value="Styled (default)">
Includes all inline styles, skeleton loading animation, and CSS custom property support. Drop-in ready:

```tsx
<LocaleDropdown />
```
</Tab>
<Tab value="Unstyled">
No styles applied — data attributes remain for targeting. Full control via className props:

```tsx
<LocaleDropdown
  variant="unstyled"
  className="relative inline-block"
  triggerClassName="flex items-center gap-2 px-3 py-2 rounded-lg border"
  menuClassName="absolute mt-1 w-full rounded-lg border bg-white shadow-lg"
/>
```
</Tab>
</Tabs>

### CSS Custom Properties

Override these on any parent element to theme the styled variant:

| Property | Default | Description |
|----------|---------|-------------|
| `--better-locale-trigger-padding` | `6px 10px` | Trigger padding |
| `--better-locale-trigger-radius` | `8px` | Trigger border-radius |
| `--better-locale-trigger-border` | `1px solid transparent` | Trigger border |
| `--better-locale-trigger-bg` | `transparent` | Trigger background |
| `--better-locale-text` | `#374151` | Text color |
| `--better-locale-border` | `#e5e7eb` | Menu border + skeleton |
| `--better-locale-menu-bg` | `#ffffff` | Menu background |
| `--better-locale-active-bg` | `#f9fafb` | Active item background |
| `--better-locale-hover-bg` | `#f3f4f6` | Hovered item background |
| `--better-locale-code-text` | `#9ca3af` | Locale code + globe icon color |
| `--better-locale-accent` | `currentColor` | Checkmark color |

```css
/* Dark theme example */
.dark [data-better-locale-dropdown] {
  --better-locale-text: #e5e7eb;
  --better-locale-border: #374151;
  --better-locale-menu-bg: #1f2937;
  --better-locale-active-bg: #374151;
  --better-locale-hover-bg: #2d3748;
  --better-locale-code-text: #6b7280;
}
```

### Data Attributes

Use these for CSS targeting with the `unstyled` variant:

| Attribute | Element | Description |
|-----------|---------|-------------|
| `data-better-locale-dropdown` | Root `<div>` | Always present |
| `data-better-locale-trigger` | Trigger `<button>` | Always present |
| `data-better-locale-menu` | `<ul>` | Present when open |
| `data-better-locale-item` | Each `<li>` | Every menu item |
| `data-active` | `<li>` | Current locale item |
| `data-focused` | `<li>` | Keyboard-focused item |

### Custom Rendering

#### renderTrigger

Replaces the trigger button content. The wrapper still handles click, keyboard, and ARIA:

```tsx
<LocaleDropdown
  renderTrigger={({ language, isOpen, isLoading, flag, label }) => (
    <div className={`my-trigger ${isOpen ? 'open' : ''}`}>
      {isLoading ? (
        <span>Loading...</span>
      ) : (
        <>
          {flag.type === 'emoji' && <span>{flag.emoji}</span>}
          <span>{label}</span>
        </>
      )}
    </div>
  )}
/>
```

#### renderItem

Replaces each item's content. The `<li>` still handles click, `role="option"`, and data attributes:

```tsx
<LocaleDropdown
  renderItem={({ language, isActive, flag, label }) => (
    <div className={`flex items-center gap-2 ${isActive ? 'font-bold' : ''}`}>
      {flag.type === 'emoji' && <span>{flag.emoji}</span>}
      <span>{label}</span>
      {isActive && <span>✓</span>}
    </div>
  )}
/>
```

### Expo (useLocaleSwitcher)

On React Native, use the `useLocaleSwitcher` hook instead of the dropdown component:

```tsx
import { useLocaleSwitcher } from '@better-i18n/expo'

function LanguagePicker() {
  const { languages, currentLocale, switchLocale, isLoading } = useLocaleSwitcher()

  return (
    <FlatList
      data={languages}
      renderItem={({ item }) => (
        <Pressable onPress={() => switchLocale(item.code)}>
          <Text style={item.code === currentLocale ? styles.active : undefined}>
            {item.nativeName}
          </Text>
        </Pressable>
      )}
    />
  )
}
```

### Keyboard Navigation

The dropdown supports full keyboard navigation:

| Key | Action |
|-----|--------|
| `↓` / `Enter` / `Space` | Open menu, focus first item |
| `↓` / `↑` | Navigate items (wraps around) |
| `Home` / `End` | Jump to first / last item |
| `Enter` / `Space` | Select focused item |
| `Escape` | Close menu |

---

## Related

- [Provider](/frameworks/provider) - Configure locale handling
- [Translations](/frameworks/translations) - Access translations
- [TanStack Routing](/frameworks/tanstack-start/routing) - Path-based routing setup
