Detecting the country and region of the visiting user on the browser is certainly possible using JavaScript alone, without using any third-party geolocation service such as ipstack. The only consideration: It may not be accurate, and depend on the timezone selected by the user on their system, which can be changed. But since hardly anybody changes their system-auto-detected timezone nowadays (except for testing, which I have shown below), it is a good enough and cheap detection method for non-critical use cases.
This post explains how to gather and use essential data to find the user’s country. You can get the final JSON dictionary to map timezone cities to countries. To see how and where you can use it, check track user activity with custom script and restrict content for users of specific countries with JavaScript only.
If you’re not interested in the intermediate steps and want the final exact code, head to section 4 at the bottom!
1. Use of Intl.DateTimeFormat()
Our first step is to find the timezone of the user’s system. All modern browsers support Intl Internationalization Methods. Calling Intl.DateTimeFormat().resolvedOptions().timeZone gives the default timezone of the runtime/system. It consists of the region, followed by a slash, and the city name. In my case, it is:
Intl.DateTimeFormat().resolvedOptions().timeZone;
// 'Asia/Karachi'
Here Asia is the region, and Karachi is the city. Note that Pakistan has only one timezone “Asia/Karachi”, and although I am connecting from Lahore city, it will be the same. Bigger countries like the US and Russia have multiple time zones.
With this step, we have identified the region. So now we need to find the country from the city.
2. Generate a Dictionary to Map All Timezones City Names to Countries
Our next step is to find all the timezones and get to the country name from their city. That is, generate a dictionary that does this mapping.
For this, we create a script and use moment-timezone. moment-timezone has the list of timezones and their countries' data in “moment-timezone/data/meta/latest.json”. Now, why can’t we use moment-timezone directly? For three reasons:
- We don’t want a full-fledged library in our app/website, only the relevant part (relatively small).
- moment-timezone does not provide a method to find the country from the timezone (because that is not the library’s purpose).
- The mapping data is only partially accurate. For instance “Asia/Riyadh” zone is mapped to Saudi Arabia, Kuwait, and Yemen, whereas “Riyadh” is the city of Saudi Arabia. There is another one, “Africa/Maputo,” which is found in 8 countries, while it’s the city of Mozambique.
We write a script to get around these issues and generate the city-to-country mapping dictionary. Before running the script, install the moment-timezone in your project:
npm install moment-timezone
Then run this script:
const { countries, zones } = require("moment-timezone/data/meta/latest.json");
const timeZoneToCountry = {};
Object.keys(zones).forEach(z => {
timeZoneToCountry[z] = countries[zones[z].countries[0]].name;
});
console.log(JSON.stringify(timeZoneToCountry, null, 2))
The result (424 keys in timeZoneToCountry
the object, truncated for brevity):
{
"Europe/Andorra": "Andorra",
"Asia/Dubai": "United Arab Emirates",
"Asia/Kabul": "Afghanistan",
"Europe/Tirane": "Albania",
"Asia/Yerevan": "Armenia",
"Antarctica/Casey": "Antarctica",
"Antarctica/Davis": "Antarctica",
"Antarctica/Mawson": "Antarctica",
"Antarctica/Palmer": "Antarctica",
"Antarctica/Rothera": "Antarctica",
"Antarctica/Troll": "Antarctica",
"Antarctica/Vostok": "Antarctica",
"America/Argentina/Buenos_Aires": "Argentina",
"America/Argentina/Cordoba": "Argentina",
"America/Argentina/Salta": "Argentina",
"America/Argentina/Jujuy": "Argentina",
"America/Argentina/Tucuman": "Argentina",
"America/Argentina/Catamarca": "Argentina",
"America/Argentina/La_Rioja": "Argentina",
"America/Argentina/San_Juan": "Argentina",
"America/Argentina/Mendoza": "Argentina",
"America/Argentina/San_Luis": "Argentina",
"America/Argentina/Rio_Gallegos": "Argentina",
"America/Argentina/Ushuaia": "Argentina",
"Pacific/Pago_Pago": "Samoa (American)",
"Europe/Vienna": "Austria",
"Australia/Lord_Howe": "Australia",
"Antarctica/Macquarie": "Australia",
// .
// .
// .
"America/Montserrat": "Montserrat",
"Africa/Blantyre": "Malawi",
"Africa/Niamey": "Niger",
"Asia/Muscat": "Oman",
"Africa/Kigali": "Rwanda",
"Atlantic/St_Helena": "St Helena",
"Europe/Ljubljana": "Slovenia",
"Arctic/Longyearbyen": "Svalbard & Jan Mayen",
"Europe/Bratislava": "Slovakia",
"Africa/Freetown": "Sierra Leone",
"Europe/San_Marino": "San Marino",
"Africa/Dakar": "Senegal",
"Africa/Mogadishu": "Somalia",
"America/Lower_Princes": "St Maarten (Dutch)",
"Africa/Mbabane": "Eswatini (Swaziland)",
"Africa/Lome": "Togo",
"America/Port_of_Spain": "Trinidad & Tobago",
"Africa/Dar_es_Salaam": "Tanzania",
"Africa/Kampala": "Uganda",
"Pacific/Midway": "US minor outlying islands",
"Europe/Vatican": "Vatican City",
"America/St_Vincent": "St Vincent",
"America/Tortola": "Virgin Islands (UK)",
"America/St_Thomas": "Virgin Islands (US)",
"Asia/Aden": "Yemen",
"Indian/Mayotte": "Mayotte",
"Africa/Lusaka": "Zambia",
"Africa/Harare": "Zimbabwe"
}
Note that for a given zone, we take the first country it has listed (zones[z].countries[0]
). I have checked this and verified that the first country is the actual country of a particular zone (the combination of region/city). For instance, “Asia/Riyadh” has Saudia Arabia as the first country in the array. Same for “Africa/Maputo” and Mozambique:
{
"Asia/Riyadh": {
"name": "Asia/Riyadh",
"countries": [
"SA", // code for Saudi Arabia
"AQ",
"KW",
"YE"
],
},
// ...
"Africa/Maputo": {
"name": "Africa/Maputo",
"countries": [
"MZ", // code for Mozambique
"BI",
"BW",
"CD",
"MW",
"RW",
"ZM",
"ZW"
],
},
}
3. Reduce the Size of the Dictionary
Now we have the mapping, for good measure, we can reduce it to the city to the country only and remove the region because the city is the only thing that matters in the mapping. It helps in saving space. So change the above script to:
const { countries, zones } = require("moment-timezone/data/meta/latest.json");
const timeZoneCityToCountry = {};
Object.keys(zones).forEach(z => {
const cityArr = z.split("/");
const city = cityArr[cityArr.length-1];
timeZoneCityToCountry[city] = countries[zones[z].countries[0]].name;
});
console.log(timeZoneToCountry)
I just found out that choosing any timezone in India returns “Calcutta” instead of “Kolkata” (as found in the moment-timezone). So I have manually added the key “Calcutta” in the JSON that can be downloaded below. You should double-check the countries you’re interested in, and if they’re missing, add them manually.
The result will be as follows (truncated):
{
"Andorra": "Andorra",
"Dubai": "United Arab Emirates",
"Kabul": "Afghanistan",
"Tirane": "Albania",
"Yerevan": "Armenia",
"Casey": "Antarctica",
"Davis": "Antarctica",
"Mawson": "Antarctica",
"Palmer": "Antarctica",
"Rothera": "Antarctica",
"Troll": "Antarctica",
"Vostok": "Antarctica",
"Buenos_Aires": "Argentina",
"Cordoba": "Argentina",
"Salta": "Argentina",
"Jujuy": "Argentina",
"Tucuman": "Argentina",
"Catamarca": "Argentina",
"La_Rioja": "Argentina",
"San_Juan": "Argentina",
"Mendoza": "Argentina",
"San_Luis": "Argentina",
"Rio_Gallegos": "Argentina",
"Ushuaia": "Argentina",
"Pago_Pago": "Samoa (American)",
"Vienna": "Austria",
"Lord_Howe": "Australia",
"Macquarie": "Australia",
"Hobart": "Australia",
"Melbourne": "Australia",
"Sydney": "Australia",
// .
// .
// .
"Bratislava": "Slovakia",
"Freetown": "Sierra Leone",
"San_Marino": "San Marino",
"Dakar": "Senegal",
"Mogadishu": "Somalia",
"Lower_Princes": "St Maarten (Dutch)",
"Mbabane": "Eswatini (Swaziland)",
"Lome": "Togo",
"Port_of_Spain": "Trinidad & Tobago",
"Dar_es_Salaam": "Tanzania",
"Kampala": "Uganda",
"Midway": "US minor outlying islands",
"Vatican": "Vatican City",
"St_Vincent": "St Vincent",
"Tortola": "Virgin Islands (UK)",
"St_Thomas": "Virgin Islands (US)",
"Aden": "Yemen",
"Mayotte": "Mayotte",
"Lusaka": "Zambia",
"Harare": "Zimbabwe"
}
4. Get the Region and Country of the Browser User (The Final Code)
Now the dictionary timeZoneCityToCountry
is available (you can copy or download this complete JSON dictionary), we can find the region and country of the visiting user as follows:
// include timeZoneCityToCountry via a script, require, or import
var timeZoneCityToCountry = {
"Andorra": "Andorra",
"Dubai": "United Arab Emirates",
"Kabul": "Afghanistan",
"Tirane": "Albania",
"Yerevan": "Armenia",
"Casey": "Antarctica",
"Davis": "Antarctica",
"Mawson": "Antarctica",
"Palmer": "Antarctica",
"Rothera": "Antarctica",
"Troll": "Antarctica",
"Vostok": "Antarctica",
"Buenos_Aires": "Argentina",
"Cordoba": "Argentina",
"Salta": "Argentina",
"Jujuy": "Argentina",
"Tucuman": "Argentina",
"Catamarca": "Argentina",
"La_Rioja": "Argentina",
"San_Juan": "Argentina",
"Mendoza": "Argentina",
"San_Luis": "Argentina",
"Rio_Gallegos": "Argentina",
"Ushuaia": "Argentina",
"Pago_Pago": "Samoa (American)",
"Vienna": "Austria",
"Lord_Howe": "Australia",
// .
// .
// .
"Bratislava": "Slovakia",
"Freetown": "Sierra Leone",
"San_Marino": "San Marino",
"Dakar": "Senegal",
"Mogadishu": "Somalia",
"Lower_Princes": "St Maarten (Dutch)",
"Mbabane": "Eswatini (Swaziland)",
"Lome": "Togo",
"Port_of_Spain": "Trinidad & Tobago",
"Dar_es_Salaam": "Tanzania",
"Kampala": "Uganda",
"Midway": "US minor outlying islands",
"Vatican": "Vatican City",
"St_Vincent": "St Vincent",
"Tortola": "Virgin Islands (UK)",
"St_Thomas": "Virgin Islands (US)",
"Aden": "Yemen",
"Mayotte": "Mayotte",
"Lusaka": "Zambia",
"Harare": "Zimbabwe"
};
var userRegion;
var userCity;
var userCountry;
var userTimeZone;
if (Intl) {
userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
var tzArr = userTimeZone.split("/");
userRegion = tzArr[0];
userCity = tzArr[tzArr.length - 1];
userCountry = timeZoneCityToCountry[userCity];
}
console.log("Time Zone:", TimeZone);
console.log("Region:", userRegion);
console.log("City:", userCity);
console.log("Country:", userCountry);
The result should be as follows:
5. Test it Live
If your browser supports Intl
, you should see data based on your system settings below (else N/A). To see it changed to another location, open the settings and select another time zone, then refresh this web page:
On Macbook, the time zone can be updated by the following steps:
- Open System Preferences.
- Go to Date & Time.
- Select Time Zone.
- Uncheck “Set time zone automatically using current location”.
- Select some other country/region other than your current time zone.
- Reload this web page to see the new country above.
Below, I’ve followed the same steps to select Wellington - New Zealand.
On reloading the page, the new data will reflect New Zealand’s data:
See also
- SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
- Yup Date Format Validation With Moment JS
- Yup Number Validation: Allow Empty String
- Exactly Same Query Behaving Differently in Mongo Client and Mongoose
- JavaScript Unit Testing JSON Schema Validation
- Reduce JS Size With Constant Strings
- JavaScript SDK