Get User Country and Region on Browser With JavaScript Only

Without Using IP Address

Detecting the country and region of the visiting user on 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 go about changing their system-auto-detected timezone nowadays (except for testing, which I have shown below), I believe it is a good enough and cheap detection method for non-critical use cases.

In this post, I explain how to gather and use essential data to find the country of the user. You can get the final JSON dictionary to map timezone cities to countries. To see how and where it can be used, check track user activity with custom script.

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 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 such as US, Russia, have multiple timezones.

With this step we have identified the region. 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 from their city, get to the country name. 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 one may ask, why can’t we use moment-timezone directly? For three reasons:

  1. We don’t want a full-fledged library in our app/website, only the relevant part (which is quite small).
  2. moment-timezone does not provide a method to find the country from the timezone (because that is not the purpose of the library).
  3. The mapping data is not entirely 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 its the city of Mozambique.

To get around these issues, we write a script as follows, and generate the dictionary of city to country mapping. Before running the script, install 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 just take the first country it has listed (zones[z].countries[0]). I have checked this and verified that the first country is the real 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 country only, and remove the region part, because city is the only thing that matters in the mapping. This helps in saving space. 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)

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

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 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:

Time Zone: Asia/Karachi
Region: Asia
City: Karachi
Country: Pakistan

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:

Time Zone:
Region:
City:
Country:

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:

Time Zone: Pacific/Auckland
Region: Pacific
City: Auckland
Country: New Zealand

See also