Skip to content
better-i18n.com
本页内容

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 #

PropertyTypeDescription
localestringCurrent locale code (e.g., "en", "tr")
setLocale(locale: string) => voidFunction to change the locale
isLoadingbooleanWhether 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:

  • Not supported: URL not updated
  • Not supported: Loaders don't re-execute
  • Not supported: Browser history not updated

useLocaleRouter solves these:

  • Supported: Triggers proper SPA navigation
  • Supported: Re-executes loaders (fresh messages)
  • Supported: Updates URL correctly
  • Supported: 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 #

PropertyTypeDescription
localestringCurrent locale from URL
localesstring[]Available locale codes from CDN manifest
defaultLocalestringDefault locale (no URL prefix)
navigate(locale: string) => voidNavigate to same page with new locale
localePath(path: string, locale?: string) => stringGet localized path
isReadybooleanWhether languages are loaded from CDN

URL Strategy #

The default locale has no prefix in the URL:

LocaleURL
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 #

FeatureuseLocaleuseLocaleRouter
State updateReact context onlyRouter navigation
URL updateManualAutomatic
Loader re-executionNoYes
History updateNoYes
Use caseSimple CSR appsTanStack Router apps

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 #

PropertyTypeDescription
languagesLanguageOption[]Array of available languages
isLoadingbooleanWhether languages are being fetched

LanguageOption Type #

TypeScript
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:

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

Avoid Static Language Arrays #

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

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

Use CDN languages instead:

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

LanguageSwitcher Component #

A ready-to-use language selector component:

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

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

      </nav>
    </header>
  )
}

Props #

PropTypeDefaultDescription
classNamestringCSS class for the select element
loadingLabelstring"Loading..."Text shown while loading
renderOption(lang: LanguageOption) => ReactNodeCustom 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.

Installation #

bun add @better-i18n/use-intl

Basic Usage #

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.

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>
  );
}

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.

Props #

PropTypeDefaultDescription
variant"styled" | "unstyled""styled"Styled includes all inline styles + CSS custom properties. Unstyled gives full control.
classNamestringCSS class for root wrapper
triggerClassNamestringCSS class for trigger button
menuClassNamestringCSS class for dropdown menu
showFlagbooleantrueShow flag emoji/image
showNativeNamebooleantrueShow native language name (e.g., "Türkçe")
showLocaleCodebooleantrueShow locale code (e.g., "TR")
renderTrigger(ctx) => ReactNodeCustom trigger renderer
renderItem(ctx) => ReactNodeCustom item renderer

Next.js only:

PropTypeDefaultDescription
configI18nConfigRequiredConfig from createI18n()
localestringRequiredCurrent locale
languagesLanguageOption[]Pre-fetched languages (skips client CDN fetch)

Remix only:

PropTypeDefaultDescription
defaultLocalestring"en"Default locale for URL prefix logic

Variants #

Styled (default)

Includes all inline styles, skeleton loading animation, and CSS custom property support. Drop-in ready:

TSX
<LocaleDropdown />

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"
/>

CSS Custom Properties #

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

PropertyDefaultDescription
--better-locale-trigger-padding6px 10pxTrigger padding
--better-locale-trigger-radius8pxTrigger border-radius
--better-locale-trigger-border1px solid transparentTrigger border
--better-locale-trigger-bgtransparentTrigger background
--better-locale-text#374151Text color
--better-locale-border#e5e7ebMenu border + skeleton
--better-locale-menu-bg#ffffffMenu background
--better-locale-active-bg#f9fafbActive item background
--better-locale-hover-bg#f3f4f6Hovered item background
--better-locale-code-text#9ca3afLocale code + globe icon color
--better-locale-accentcurrentColorCheckmark 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:

AttributeElementDescription
data-better-locale-dropdownRoot <div>Always present
data-better-locale-triggerTrigger <button>Always present
data-better-locale-menu<ul>Present when open
data-better-locale-itemEach <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:

KeyAction
/ Enter / SpaceOpen menu, focus first item
/ Navigate items (wraps around)
Home / EndJump to first / last item
Enter / SpaceSelect focused item
EscapeClose menu