Language Selector Component

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

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:

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:

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

Add language selection to your footer:

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:

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:

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.

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:

VITE_DEFAULT_LOCALE=en

Configuration Options

Adding Locales

Register supported locales in your configuration:

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:

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:

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:

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

Frequently Asked Questions

Why does the page reload when I change the language?
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.
Can I change the language without a reload?
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.
How do I hide the language selector for single-language apps?
The selector returns null when it receives one or zero locales. You can also conditionally render it: {locales.length > 1 && <LanguageSelector locales={locales} />}
Can I save language preference to the user's profile?
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.
Does the language selector use URL prefixes?
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.