Every way to detect a user’s locale (from best to worst)

Introduction

At Lingo.dev, we’re often asked about how to detect a user’s locale. This is an important aspect of internationalization (i18n), but some of the most intuitive options are actually the worst.

This post walks through every metho…


This content originally appeared on DEV Community and was authored by David Turnbull

Introduction

At Lingo.dev, we're often asked about how to detect a user's locale. This is an important aspect of internationalization (i18n), but some of the most intuitive options are actually the worst.

This post walks through every method developers use to detect locale, ranked from most reliable to least. Some work. Some guess. Some fail spectacularly.

TL;DR

  • Start with explicit choices. Check the user's profile setting first. Then check the URL. Then check stored preferences.
  • When you have nothing explicit, negotiate from browser settings. Use navigator.languages on the client and Accept-Language on the server.
  • Use IP geolocation and timezone only for regional formatting like currency and date formats, never for language selection.
  • Always show a visible language switcher. Save the user's choice immediately. Never make them search for how to change languages. Never override their explicit choice with automatic detection.

1. User selected locale in account settings

When a signed-in user picks a locale in your settings, that choice is final. Use it everywhere. Sync it across all their devices. Never override it with automatic detection.

This is the user telling you exactly what they want. Listen to them.

const user = await db.users.findById(session.userId);
const locale = user?.locale ?? null;

Save their choice immediately when they change it. Make the language switcher easy to find. Let them change their mind anytime.

2. URL locale in path, subdomain, or query parameter

URLs are explicit and shareable. When someone visits /fr-CA/pricing or fr.example.com, they chose that locale. Honor it for that request and remember it.

const u = new URL(location.href);
const pathSegment = u.pathname.split('/').filter(Boolean)[0];
const subdomain = location.hostname.split('.')[0];
const query = u.searchParams.get('lang') || u.searchParams.get('locale');

const supported = ['en', 'en-GB', 'fr', 'fr-CA', 'de'];
const locale = [pathSegment, subdomain, query].find(v => supported.includes(v)) || null;

When you need to handle deep links, ask the user if they want to switch languages. Don't force a redirect.

3. Stored client preference in cookies or localStorage

Remember what guests choose between visits. This works well for returning visitors on the same device.

const fromCookie = document.cookie.match(/(?:^|;\s*)locale=([^;]+)/)?.[1] ?? null;
const fromStorage = localStorage.getItem('locale');
const locale = fromStorage || fromCookie || null;

Shared devices create problems here. A family computer or library terminal will show the last person's choice. After someone signs in, their profile preference should override this.

4. Browser language preferences from navigator.languages

The browser tells you which languages the user prefers, in order. Match this list against what your app supports.

const preferred = navigator.languages || [navigator.language];
const supported = ['fr-CA', 'fr', 'en', 'de'];

const exactMatch = preferred.find(lang => supported.includes(lang));
const languageMatch = preferred
  .map(lang => lang.split('-')[0])
  .find(lang => supported.includes(lang));

const locale = exactMatch || languageMatch || null;

Use this when you have nothing more explicit. Never override a URL parameter or profile choice with browser preferences.

5. Accept-Language HTTP header on server

On the first request, the server receives an Accept-Language header. This contains the same preference list with quality weights.

const acceptLanguage = req.headers['accept-language'] || '';
const preferences = acceptLanguage
  .split(',')
  .map(part => {
    const [tag, quality = '1'] = part.trim().split(';q=');
    return { tag: tag.toLowerCase(), quality: Number(quality) };
  })
  .sort((a, b) => b.quality - a.quality)
  .map(pref => pref.tag);

const supported = ['en', 'en-gb', 'fr', 'fr-ca', 'de'];
const exactMatch = preferences.find(tag => supported.includes(tag));
const languageMatch = preferences
  .map(tag => tag.split('-')[0])
  .find(tag => supported.includes(tag));

const locale = exactMatch || languageMatch || null;

This matters for server-side rendering and edge routing. Use it to set a sensible default. Once the user makes a choice, stick with their choice.

6. Single language from navigator.language

This gives you one language instead of a list. The API is simpler but less precise.

const browserLanguage = navigator.language || '';
const locale = browserLanguage.split('-')[0] || null;

Prefer navigator.languages when you can. Fall back to this when you need something simple.

7. Locale from Intl.DateTimeFormat resolution

The JavaScript runtime resolves what locale it will use for formatting. This tells you how dates and numbers will appear by default.

const resolved = new Intl.DateTimeFormat().resolvedOptions().locale;
const language = resolved.split('-')[0];

Use this for formatting defaults, not for content translation. The locale for formatting numbers might differ from the language someone wants to read.

8. OAuth profile language from identity providers

Some OAuth providers include locale in user profiles. Google and other identity providers sometimes share this data.

const profile = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
  headers: { Authorization: `Bearer ${accessToken}` }
}).then(response => response.json());

const locale = profile.locale ? profile.locale.split('-')[0] : null;

Treat this as a hint after login, not a requirement. The user might want different settings in your app than in their Google account.

9. Country code top-level domain

A .fr domain suggests France, not necessarily the French language. Belgians and Swiss people speak French too. Germans and Austrians both speak German.

const tld = location.hostname.split('.').pop();
const countryHint = {
  fr: 'FR',
  de: 'DE',
  es: 'ES',
  uk: 'GB'
}[tld] || null;

Use this as a region hint for currency and formatting. Don't use it to guess content language. Many countries are multilingual.

10. Country from IP geolocation

Mapping IP addresses to countries helps with currency and regional formats. It fails for language detection.

const geoData = await fetch('https://ipapi.co/json/').then(r => r.json());
const country = (geoData.country_code || '').toUpperCase();

VPNs break this immediately. People travel with laptops. Many countries speak multiple languages. Use IP geolocation only as a last resort for regional formatting, never for language selection.

11. Timezone-based inference

Timezone data helps format dates correctly. It says nothing reliable about language preferences.

const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

Someone in America/Toronto might prefer English or French. Someone in Europe/Brussels might want Dutch, French, or German. VPNs and travel make this even less reliable.

12. User agent string parsing

Extracting locale from the User-Agent header breaks constantly. The format varies across browsers. Privacy changes are removing this data entirely.

const userAgent = req.headers['user-agent'] || '';
const match = userAgent.match(/; ([a-z]{2}-[A-Z]{2})\)/);
const locale = match ? match[1].split('-')[0] : null;

Don't build anything on top of this. It will break. Modern browsers are actively reducing what the user agent reveals.

Join our Hackathon 🚀

We're sponsoring the Hack this Fall 2025 hackathon and giving away some prizes to the most creative uses of Lingo.dev. To learn more, join our Discord channel.


This content originally appeared on DEV Community and was authored by David Turnbull


Print Share Comment Cite Upload Translate Updates
APA

David Turnbull | Sciencx (2025-11-05T19:15:44+00:00) Every way to detect a user’s locale (from best to worst). Retrieved from https://www.scien.cx/2025/11/05/every-way-to-detect-a-users-locale-from-best-to-worst/

MLA
" » Every way to detect a user’s locale (from best to worst)." David Turnbull | Sciencx - Wednesday November 5, 2025, https://www.scien.cx/2025/11/05/every-way-to-detect-a-users-locale-from-best-to-worst/
HARVARD
David Turnbull | Sciencx Wednesday November 5, 2025 » Every way to detect a user’s locale (from best to worst)., viewed ,<https://www.scien.cx/2025/11/05/every-way-to-detect-a-users-locale-from-best-to-worst/>
VANCOUVER
David Turnbull | Sciencx - » Every way to detect a user’s locale (from best to worst). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/05/every-way-to-detect-a-users-locale-from-best-to-worst/
CHICAGO
" » Every way to detect a user’s locale (from best to worst)." David Turnbull | Sciencx - Accessed . https://www.scien.cx/2025/11/05/every-way-to-detect-a-users-locale-from-best-to-worst/
IEEE
" » Every way to detect a user’s locale (from best to worst)." David Turnbull | Sciencx [Online]. Available: https://www.scien.cx/2025/11/05/every-way-to-detect-a-users-locale-from-best-to-worst/. [Accessed: ]
rf:citation
» Every way to detect a user’s locale (from best to worst) | David Turnbull | Sciencx | https://www.scien.cx/2025/11/05/every-way-to-detect-a-users-locale-from-best-to-worst/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.