# Language Selector Component

> Learn how to add and customize the language selector component to let users switch languages in your application.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/translations/language-selector*

---

The `LanguageSelector` component lets users switch between available languages. It automatically displays all languages registered in your i18n settings.

## Using the Language Selector

Import and render the component anywhere in your application, passing the list of supported locales:

```tsx
import { locales } from '@kit/i18n/config';

import { LanguageSelector } from '@kit/ui/language-selector';

export function SettingsPage() {
  return (
    <div>
      <h2>Language Settings</h2>
      <LanguageSelector locales={locales} />
    </div>
  );
}
```

The component:
- Renders the locales you pass in (it returns `null` when only one locale is provided)
- Displays language names in the user's current language using `Intl.DisplayNames`
- Writes the chosen locale to the `locale` cookie and reloads so the server re-resolves the locale

## Default Placement

The language selector is already included in the personal account settings page when more than one language is configured. You'll find it at:

```
/settings → Account Settings → Language
```

If only one locale is registered in `packages/i18n/src/config.ts`, the selector is hidden automatically.

## Adding to Other Locations

### Marketing Header

Add the selector to your marketing site header:

```tsx title="apps/web/src/components/marketing/site-header.tsx"
import { locales } from '@kit/i18n/config';

import { LanguageSelector } from '@kit/ui/language-selector';

export function SiteHeader() {
  return (
    <header>
      <nav>
        {/* Navigation items */}
      </nav>

      <LanguageSelector locales={locales} />
    </header>
  );
}
```

### Footer

Add language selection to your footer:

```tsx title="apps/web/src/components/marketing/site-footer.tsx"
import { locales } from '@kit/i18n/config';

import { LanguageSelector } from '@kit/ui/language-selector';

export function SiteFooter() {
  return (
    <footer>
      <div>
        {/* Footer content */}
      </div>

      {locales.length > 1 && (
        <div className="flex items-center gap-2">
          <span className="text-sm text-muted-foreground">Language:</span>
          <LanguageSelector locales={locales} />
        </div>
      )}
    </footer>
  );
}
```

### Dashboard Sidebar

Include in the application sidebar:

```tsx title="apps/web/src/components/sidebar.tsx"
import { locales } from '@kit/i18n/config';

import { LanguageSelector } from '@kit/ui/language-selector';

export function Sidebar() {
  return (
    <aside>
      {/* Sidebar navigation */}

      <div className="mt-auto p-4">
        <LanguageSelector locales={locales} />
      </div>
    </aside>
  );
}
```

## Handling Language Changes

The `onChange` prop lets you run custom logic when the language changes:

```tsx
import { locales } from '@kit/i18n/config';

import { LanguageSelector } from '@kit/ui/language-selector';

export function LanguageSettings() {
  const handleLanguageChange = (locale: string) => {
    // Track analytics
    analytics.track('language_changed', { locale });

    // Update user preferences in database
    updateUserPreferences({ language: locale });
  };

  return (
    <LanguageSelector locales={locales} onChange={handleLanguageChange} />
  );
}
```

The `onChange` callback fires before the cookie write and reload, so keep it synchronous or use a fire-and-forget pattern for async operations.

## How Language Detection Works

The system uses cookie-based locale resolution (there is no locale URL prefix). The locale is resolved server-side on each request by the `detectLocale` server function in `apps/web/src/lib/i18n/i18n.functions.ts`.

### 1. Locale Cookie

When the user picks a language, `useChangeLocale` writes a `locale` cookie. On the next request the server reads this cookie and renders in that language.

### 2. Browser Preference (New Visitors)

When no `locale` cookie is present, `detectLocale` reads the browser's `Accept-Language` header and picks the closest supported locale.

### 3. Default Fallback

If no supported locale matches, the system falls back to `VITE_DEFAULT_LOCALE`:

```bash title=".env"
VITE_DEFAULT_LOCALE=en
```

## Configuration Options

### Adding Locales

Register supported locales in your configuration:

```tsx title="packages/i18n/src/config.ts"
export const locales: string[] = ['en', 'es', 'de', 'fr'];
```

When only one locale is registered, the language selector is hidden automatically.

## Styling the Selector

The `LanguageSelector` uses Shadcn UI's `Select` component. Customize it through your Tailwind configuration or by wrapping it:

```tsx title="components/custom-language-selector.tsx"
import { locales } from '@kit/i18n/config';

import { LanguageSelector } from '@kit/ui/language-selector';

export function CustomLanguageSelector() {
  return (
    <div className="[&_button]:w-[180px] [&_button]:bg-muted">
      <LanguageSelector locales={locales} />
    </div>
  );
}
```

For deeper customization, you can create your own selector using the `@kit/i18n` utilities:

```tsx title="components/language-dropdown.tsx"
import { useCallback, useMemo } from 'react';

import { useLocale } from 'use-intl';
import { Globe } from 'lucide-react';

import { locales } from '@kit/i18n/config';
import { useChangeLocale } from '@kit/i18n/navigation';

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { Button } from '@kit/ui/button';

export function LanguageDropdown() {
  const locale = useLocale();
  const changeLocale = useChangeLocale();

  const languageNames = useMemo(() => {
    return new Intl.DisplayNames([locale], { type: 'language' });
  }, [locale]);

  const handleLanguageChange = useCallback(
    (newLocale: string) => {
      changeLocale(newLocale);
    },
    [changeLocale],
  );

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon">
          <Globe className="h-4 w-4" />
        </Button>
      </DropdownMenuTrigger>

      <DropdownMenuContent align="end">
        {locales.map((loc) => {
          const label = languageNames.of(loc) ?? loc;
          const isActive = loc === locale;

          return (
            <DropdownMenuItem
              key={loc}
              onClick={() => handleLanguageChange(loc)}
              className={isActive ? 'bg-accent' : ''}
            >
              {label}
            </DropdownMenuItem>
          );
        })}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

## SEO Considerations

This kit uses cookie-based locale resolution, so each page keeps a single URL regardless of the active language. That means there are no per-locale URLs or automatic `hreflang` alternates to configure. The chosen locale is set in the `<html lang>` attribute server-side (from the resolved locale in the root route), which keeps the rendered page correct for crawlers.

If your project requires per-locale URLs for SEO, you would need to introduce locale-prefixed routes yourself — this is not part of the default setup.

## Testing Language Switching

To test language switching during development:

1. **Register a second locale**:
    - Add another locale (e.g., `'es'`) to `packages/i18n/src/config.ts` and create its message files
2. **Component method**:
    - Navigate to account settings or wherever you placed the selector
    - Select a different language
    - Verify the page reloads and translations change (the `locale` cookie is updated)

## Accessibility Considerations

The default `LanguageSelector` uses Shadcn UI's `Select` component which provides:

- Keyboard navigation (arrow keys, Enter, Escape)
- Screen reader announcements
- Focus management

When creating custom language selectors, ensure you include:

```tsx
<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button
      variant="ghost"
      size="icon"
      aria-label="Change language"
      aria-haspopup="listbox"
    >
      <Globe className="h-4 w-4" />
      <span className="sr-only">
        Current language: {languageNames.of(locale)}
      </span>
    </Button>
  </DropdownMenuTrigger>
  {/* ... */}
</DropdownMenu>
```

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Why does the page reload when I change the language?", "answer": "Language switching writes the chosen locale to the `locale` cookie and reloads the page. The reload lets the server re-resolve the locale and render every component with the correct translations."},
     {"question": "Can I change the language without a reload?", "answer": "The default useChangeLocale helper reloads so the server can re-render in the new locale. You could build a client-only flow that also updates the use-intl provider messages, but a reload is the simplest way to guarantee server and client agree on the locale."},
     {"question": "How do I hide the language selector for single-language apps?", "answer": "The selector returns null when it receives one or zero locales. You can also conditionally render it: {locales.length > 1 && <LanguageSelector locales={locales} />}"},
     {"question": "Can I save language preference to the user's profile?", "answer": "Yes. Use the onChange prop to save to your database when the language changes. On future visits, you can set the `locale` cookie server-side from the saved preference."},
     {"question": "Does the language selector use URL prefixes?", "answer": "No. This kit resolves the locale from the `locale` cookie server-side, so pages keep a single URL. The selector updates the cookie and reloads rather than navigating to a locale-prefixed URL."}
   ]
/%}

## Related Documentation

- [Using Translations](/docs/tanstack-supabase/translations/using-translations) - Learn how to use translations
- [Adding Translations](/docs/tanstack-supabase/translations/adding-translations) - Add new languages
- [Email Translations](/docs/tanstack-supabase/translations/email-translations) - Translate email templates
