TL;DR: Detecting the right language for each visitor involves layering browser headers, geolocation, stored preferences, and URL signals. This guide covers detection strategies, fallback chains, and persistence. Patterns from tanstackship.com.
Introduction
Showing users the wrong language is one of the fastest ways to lose international visitors. A German user landing on an English-only page has a 70% bounce rate. Effective language detection requires a multi-layered strategy with sensible fallbacks. For an overview of i18n architecture patterns, start with our i18n for SaaS: Architecture Patterns Compared.
Detection Layer Strategy
| Priority | Source | How It Works | Reliability |
|---|---|---|---|
| 1 | URL path/language prefix | /de/dashboard | Highest |
| 2 | User preference (saved) | Cookie, localStorage, DB | High |
| 3 | Browser Accept-Language | navigator.languages | Medium |
| 4 | Geo IP location | Cloudflare, MaxMind | Medium |
| 5 | Default locale | Configurable fallback | Lowest |
URL-Based Detection (Highest Priority)
// TanStack Router: locale from URL path
import { createRootRoute, createRouter, redirect } from '@tanstack/react-router'
const SUPPORTED_LOCALES = ['en', 'de', 'zh', 'ja', 'ar']
const DEFAULT_LOCALE = 'en'
// Root route with locale parameter
const rootRoute = createRootRoute({
beforeLoad: ({ params, location }) => {
const locale = params.locale
if (!locale || !SUPPORTED_LOCALES.includes(locale)) {
// Detect and redirect
const detected = detectUserLocale()
throw redirect({
to: `/${detected}${location.pathname}`,
replace: true,
})
}
// Set locale for the app
setLocale(locale)
},
})
Browser Language Detection (Middleware)
// Cloudflare Worker: Accept-Language parsing
function parseAcceptLanguage(header: string | null): string[] {
if (!header) return [DEFAULT_LOCALE]
return header
.split(',')
.map(entry => {
const [lang, quality = 'q=1'] = entry.trim().split(';')
const q = parseFloat(quality.replace('q=', '')) || 1
const locale = lang.split('-')[0] // 'en-US' → 'en'
return { locale, q }
})
.sort((a, b) => b.q - a.q)
.map(entry => entry.locale)
.filter(locale => SUPPORTED_LOCALES.includes(locale))
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const pathLocale = url.pathname.split('/')[1]
if (SUPPORTED_LOCALES.includes(pathLocale)) {
return env.ASSETS.fetch(request)
}
// Detect from Accept-Language
const acceptLang = request.headers.get('Accept-Language')
const preferredLocales = parseAcceptLanguage(acceptLang)
const detectedLocale = preferredLocales[0] || 'en'
// Redirect to detected locale path
return Response.redirect(`${url.origin}/${detectedLocale}${url.pathname}`, 302)
}
}
User Preference Persistence
// React: save and restore language preference
import { setLocale } from '~/paraglide/runtime.js'
function LanguageSwitcher() {
const switchLanguage = (locale: string) => {
// Save preference
localStorage.setItem('preferred-locale', locale)
document.cookie = `locale=${locale}; path=/; max-age=${60 * 60 * 24 * 365}`
// Switch locale (Paraglide.js)
setLocale(locale)
// Update URL without full reload
const url = new URL(window.location.href)
url.pathname = url.pathname.replace(/^\/[a-z]{2}/, `/${locale}`)
window.history.pushState({}, '', url)
}
return (
<select onChange={(e) => switchLanguage(e.target.value)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="zh">中文</option>
<option value="ja">日本語</option>
<option value="ar">العربية</option>
</select>
)
}
Detection Decision Flow
Visitor arrives
↓
URL has locale? ──Yes──→ Use URL locale
No
↓
Has saved preference? ──Yes──→ Use saved locale
No
↓
Check Accept-Language
↓
Locale supported? ──Yes──→ Use browser locale
No
↓
Geo IP detection
↓
Use default (en) ──────→ Show locale switcher prominently
Conclusion
Layer your detection strategy: URL path first, saved preference second, browser headers third, geolocation fourth. Always show a prominent language switcher — automated detection should never be the only way to change language. Cache the user's choice in a cookie and localStorage for persistence across sessions.
For SEO aspects of language detection, see the hreflang Tags Ultimate Guide. If you're building locale-specific landing pages, Building a Language-Specific Landing Page Strategy covers the approach. For running experiments across languages, check out the Multi-Language A/B Testing Guide.