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
nullwhen only one locale is provided) - Displays language names in the user's current language using
Intl.DisplayNames - Writes the chosen locale to the
localecookie 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 → LanguageIf 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> );}Footer
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.
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:
VITE_DEFAULT_LOCALE=enConfiguration 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:
- Register a second locale:
- Add another locale (e.g.,
'es') topackages/i18n/src/config.tsand create its message files
- Add another locale (e.g.,
- Component method:
- Navigate to account settings or wherever you placed the selector
- Select a different language
- Verify the page reloads and translations change (the
localecookie 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?
Can I change the language without a reload?
How do I hide the language selector for single-language apps?
Can I save language preference to the user's profile?
Does the language selector use URL prefixes?
Related Documentation
- Using Translations - Learn how to use translations
- Adding Translations - Add new languages
- Email Translations - Translate email templates