Whether we’re developing a simple blog, or a sophisticated, modern single-page application (SPA), oftentimes, when considering i18n in a web application, we hit an important question: how do we detect a user’s language preference? This is important because we always want to provide the best user experience, and if the user has defined a set of preferred languages in his or her browser, we want to do our best to present our content in those preferred languages.
In this article, we’ll go through three different ways of detecting a user’s locale: through the browser’s
navigator.languages (on the client) object, through the
Accept-Language HTTP header (on the server), and through geolocation using the user’s IP address (on the server).
Modern browsers provide a
navigator.languages object that we can use to get all the preferred languages the user has set in his or her browser.
The language settings in Firefox
Given the settings above, if we were to open the Firefox console and check the value of
navigator.languages, we would get the following:
The codes for the locales match the ones in our browser settings
getBrowserLocales() checks the
navigator.languages array, falling back on
navigator.language if the array isn’t available. It’s worth noting that in some browsers, like Chrome,
navigator.language will be the UI language, which is likely the language the operating system is set to. This is different than
navigator.languages, which has the user-set preferred languages in the browser itself.
✋🏽 Heads Up » If you’re supporting Internet Explorer, you will need to use the
navigaor.browserLanguageproperties. Of course, you will also need to replace all instances of
varin the code above.
Our function also has a convenient
languageCodeOnly option, which will trim off the country codes of locales before it returns them. This can be handy when our app isn’t really handling the regional nuances of a language, e.g. we just have one version of English content.
With languageCodeOnly: true, we get the languages without countries
Accept-Language HTTP Header
If the user sets his or her language preferences in a modern browser, the browser will, in turn, send an HTTP header that relays these language preferences to the server with each request. This is the
Accept-Language header, and it often looks something like this:
The header lists the user’s preferred languages, with a weight defined by a
q value, given to each. When an explicit
q value is not specified, a default of
1.0 is assumed. So, in the above header value, the client is indicating that the user prefers Canadian English (with a weight of
q = 1.0), then Egyptian Arabic (with a weight of
q = 0.5).
We can use this standard HTTP header to determine the user’s preferred locales. Let’s write a class called
HttpAcceptLanguageHeaderLocaleDetector to do this. We’ll use PHP here, but you can use any language you like; the
Accept-Language header should be the same (or similar enough) in all environments.
This long bit of code is actually not very complicated. In the only public method,
detect(), our class does the following:
- Gets the raw string value of the
- Uses the helper method
getWeightedLocales()to parse the header string into an array that looks like
[['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]].
- Uses the helper method
sortLocalesByWeight()to sort the above array from highest to lowest
- Plucks the
localevalues from the sorted array, returning an array that looks like
We can now use our new class to get a nice, consumable array of locale codes based on the
Accept-Language HTTP header.
Server-side: Geolocation by IP Address
Accept-Language header won’t be present in the requests to our server. In these cases we might want to use the user’s IP address to determine the user’s country, and infer the locale or language from this country.
✋🏽 Heads Up » Geolocation should be used as a last resort when detecting the user’s locale, as it can often lead to an incorrect locale determination. For example, if we see that our user is coming from Canada, do we assume that his or her preferred language is English or French? Both are formal and widely-used languages in the country. And, of course, the user could belong to an Arabic-speaking minority, or be a Spanish-speaking visitor.
Using MaxMind for Geolocation
In order to determine the user’s country by the request’s IP address, we’ll use the MaxMind PHP API and the MaxMind geolocation database. MaxMind is a company that offers a few IP-related products, and among them are two that are of interest to us here:
- The GeoIP2 Databases — these are MaxMind’s commercial geolocation databases and are low-latency and subscription-based. You may want to upgrade to these if you want more up-to-date or faster databases.
- The GeoLite2 Databases — these are MaxMind’s free geolocation databases, and while reportedly less accurate than their commercial counterparts, they’re more than enough to get started with. We’ll be using a GeoLite2 database here. Do note that you will need to credit Maxmind on your public web page and link back to their site if you use one of their free databases.
To install the database, just sign up for a free MaxMind account. You’ll receive an email with a sign-in link. Follow the link and sign-in. Once you do, you should land on your Account Summary page.
Click the Download Databases link on the Account Summary page
This will take you to a page with the list of free GeoList2 databases. Grab the country binary database from there.
We want the country binary database for our purposes
Place the file you downloaded somewhere in your project.
We’ll also need the MaxMind PHP API to work with the database. We can install that with Composer.
Peter Kahl’s Country-to-Locale Package
We’ll need one more package before we get to our code. In order to determine the locales or languages of a country, we’ll use Peter Kahl’s
country-to-locale package. We can install it using Composer as well.
The IP Address Locale Detector Class
With our setup in place, we can get to our own class,
Our class is relatively straightforward. Much like
HttpAcceptLanguageHeaderLocaleDetector, it has one public method,
detect(), which does the following:
- Get the request’s IP Address from the global
- Feeds this IP address to the MaxMind database
countrymethod, which attempts to geolocate a country based on the IP address.
- Uses Peter Kahl’s
locale::country2locale()to get the languages of the given country.
- Normalizes the acquired locales, so that
- Returns the locales it normalized as an array, e.g.
📖 Go Deeper » The MaxMind
Readerhas many more methods. Check out the official API documentation if you want to dive a bit deeper into the info available in the MaxMind databases.
Server-side: Cascading Locale Detection
Given the two server-side detection strategies we covered above, we can write a little
detect_user_locales() function that can attempt the HTTP header strategy first.
If HTTP Header detection fails,
detect_user_locales() will try IP geolocation detection. If the latter bears no fruit, the function will fall back on some default locale.
If handled carefully, detecting the user’s locale can help provide a better user experience in our web apps. Thankfully, the
navigator.languages object and
Accept-Langauge HTTP header are available to reduce our guesswork when it comes to locale detection. If you and your team are working on an internationalized web app, check out Phrase for a professional, developer-friendly i18n platform. Featuring a flexible CLI and API, translation syncing with Github and Bitbucket integration, over-the-air (OTA) translations, and much, much more, Phrase has your i18n covered so you can focus on your business logic. Check out all of Phrase’s features, and sign up for a free 14-day trial.