Language Selector Component | Next.js Supabase SaaS Kit
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:
import { LanguageSelector } from '@kit/ui/language-selector';export function SettingsPage() { return ( <div> <h2>Language Settings</h2> <LanguageSelector /> </div> );}The component:
- Reads available languages from your i18n configuration
- Displays language names in the user's current language using
Intl.DisplayNames - Navigates to the equivalent URL with the new locale prefix
- The new locale is persisted via URL-based routing
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:
/home/settings → Account Settings → LanguageIf only one locale is registered in packages/i18n/src/locales.tsx, the selector is hidden automatically.
Adding to Other Locations
Marketing Header
Add the selector to your marketing site header:
import { LanguageSelector } from '@kit/ui/language-selector';import { routing } from '@kit/i18n/routing';export function SiteHeader() { const showLanguageSelector = routing.locales.length > 1; return ( <header> <nav> {/* Navigation items */} </nav> {showLanguageSelector && ( <LanguageSelector /> )} </header> );}Footer
Add language selection to your footer:
import { LanguageSelector } from '@kit/ui/language-selector';import { routing } from '@kit/i18n/routing';export function SiteFooter() { return ( <footer> <div> {/* Footer content */} </div> {routing.locales.length > 1 && ( <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">Language:</span> <LanguageSelector /> </div> )} </footer> );}Dashboard Sidebar
Include in the application sidebar:
import { LanguageSelector } from '@kit/ui/language-selector';import { routing } from '@kit/i18n/routing';export function Sidebar() { return ( <aside> {/* Sidebar navigation */} <div className="mt-auto p-4"> {routing.locales.length > 1 && ( <LanguageSelector /> )} </div> </aside> );}Handling Language Changes
The onChange prop lets you run custom logic when the language changes:
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 onChange={handleLanguageChange} /> );}The onChange callback fires before navigation, so keep it synchronous or use a fire-and-forget pattern for async operations.
How Language Detection Works
The system uses URL-based locale routing powered by next-intl middleware.
1. URL Prefix
The locale is determined by the URL path prefix:
/en/home → English/es/home → Spanish/de/home → German2. Browser Preference (New Visitors)
When a user visits the root URL (/), the middleware checks the browser's Accept-Language header and redirects to the matching locale:
User visits / → Accept-Language: es → Redirect to /es/3. Default Fallback
If no matching locale is found, the system redirects to NEXT_PUBLIC_DEFAULT_LOCALE:
NEXT_PUBLIC_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 { LanguageSelector } from '@kit/ui/language-selector';export function CustomLanguageSelector() { return ( <div className="[&_button]:w-[180px] [&_button]:bg-muted"> <LanguageSelector /> </div> );}For deeper customization, you can create your own selector using next-intl navigation utilities:
'use client';import { useCallback, useMemo } from 'react';import { useLocale } from 'next-intl';import { useRouter, usePathname } from '@kit/i18n/navigation';import { Globe } from 'lucide-react';import { routing } from '@kit/i18n/routing';import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from '@kit/ui/dropdown-menu';import { Button } from '@kit/ui/button';export function LanguageDropdown() { const locale = useLocale(); const router = useRouter(); const pathname = usePathname(); const languageNames = useMemo(() => { return new Intl.DisplayNames([locale], { type: 'language' }); }, [locale]); const handleLanguageChange = useCallback( (newLocale: string) => { router.replace(pathname, { locale: newLocale }); }, [router, pathname], ); return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon"> <Globe className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {routing.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
With URL-based locale routing, each language variant has its own URL, which is optimal for SEO. The next-intl middleware automatically handles hreflang alternate links.
For additional control, you can add explicit alternates in your metadata:
import { routing } from '@kit/i18n/routing';export function generateMetadata() { const baseUrl = 'https://yoursite.com'; return { alternates: { languages: Object.fromEntries( routing.locales.map((lang) => [lang, `${baseUrl}/${lang}`]) ), }, };}Testing Language Switching
To test language switching during development:
- URL method:
- Navigate directly to a URL with the locale prefix (e.g.,
/es/home) - Verify translations appear correctly
- Navigate directly to a URL with the locale prefix (e.g.,
- Component method:
- Navigate to account settings or wherever you placed the selector
- Select a different language
- Verify the URL updates with the new locale prefix and translations change
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 navigate when I change the language?
Can I change the language without navigation?
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 work with URL-based routing?
Upgrading from v2
In v2, language switching was cookie-based — changing language set a lang cookie and reloaded the page. In v3, language switching uses URL-based routing via next-intl middleware. Key differences:
- Locale is determined by URL prefix (
/en/,/es/) instead of alangcookie - Language change navigates to a new URL instead of
i18n.changeLanguage()+ reload languagesfrom~/lib/i18n/i18n.settingsis nowrouting.localesfrom@kit/i18n/routinguseTranslationfromreact-i18nextis nowuseTranslations/useLocalefromnext-intl- No custom middleware needed —
next-intlprovides URL-based routing natively
For the full migration guide, see Upgrading from v2 to v3.
Related Documentation
- Using Translations - Learn how to use translations
- Adding Translations - Add new languages
- Email Translations - Translate email templates