863 lines
28 KiB
TypeScript
863 lines
28 KiB
TypeScript
/* eslint-disable max-lines -- country data table */
|
|
/**
|
|
* Supported keyword-data countries and their data provider.
|
|
*
|
|
* Default provider is DataForSEO Labs (94 countries; source:
|
|
* https://api.dataforseo.com/v3/dataforseo_labs/locations_and_languages).
|
|
* Countries Labs does not cover are marked `googleAdsOnly` and are served by
|
|
* the DataForSEO Keywords Data API (Google Ads endpoints), which covers the
|
|
* full Google geotarget list — see specs/0004-keyword-data-source-routing.md.
|
|
* Google-Ads-only rows have no keyword difficulty or search intent.
|
|
*
|
|
* For countries with multiple Google-supported languages, we pick the
|
|
* language with the largest keyword corpus (the primary search market)
|
|
* as the default. The APIs accept a single location_code + language_code
|
|
* pair per request, so we expose one entry per country. Language codes for
|
|
* googleAdsOnly entries must exist in BOTH the Google Ads and SERP language
|
|
* lists (rank tracking shares this picker and uses the SERP API).
|
|
*
|
|
* Entries are sorted alphabetically by country name; pick US as the
|
|
* product-wide default via DEFAULT_LOCATION_CODE below.
|
|
*/
|
|
export const DEFAULT_LOCATION_CODE = 2840;
|
|
|
|
/**
|
|
* Human-readable form of a canonical DataForSEO location_name, whose segments
|
|
* are comma-separated with inconsistent spacing ("Portland-Auburn, ME,United
|
|
* States"). Trims each segment; `maxSegments` truncates for compact display
|
|
* ("Enid, Oklahoma").
|
|
*/
|
|
export function formatLocationLabel(
|
|
locationName: string,
|
|
maxSegments?: number,
|
|
): string {
|
|
const parts = locationName.split(",").map((part) => part.trim());
|
|
return (maxSegments ? parts.slice(0, maxSegments) : parts).join(", ");
|
|
}
|
|
|
|
/**
|
|
* shortLabel is a *display* label; the one entry that diverges from ISO
|
|
* 3166-1 alpha-2 is the United Kingdom ("UK" reads better, ISO is "GB").
|
|
*/
|
|
const ISO_COUNTRY_OVERRIDES: Record<string, string> = { UK: "GB" };
|
|
|
|
/**
|
|
* Lowercase ISO 3166-1 alpha-2 code for a country location_code — the format
|
|
* DataForSEO's per-country endpoints (e.g. SERP locations) require.
|
|
*/
|
|
export function getIsoCountryCode(locationCode: number): string {
|
|
const shortLabel =
|
|
LOCATION_OPTIONS.find((option) => option.code === locationCode)
|
|
?.shortLabel ?? "US";
|
|
return (ISO_COUNTRY_OVERRIDES[shortLabel] ?? shortLabel).toLowerCase();
|
|
}
|
|
|
|
type KeywordDataProvider = "labs" | "google_ads";
|
|
|
|
type LocationOption = {
|
|
code: number;
|
|
label: string;
|
|
shortLabel: string;
|
|
languageCode: string;
|
|
/** Set when DataForSEO Labs does not support this country. */
|
|
googleAdsOnly?: true;
|
|
};
|
|
|
|
export const LOCATION_OPTIONS: readonly LocationOption[] = [
|
|
{ code: 2008, label: "Albania", shortLabel: "AL", languageCode: "sq" },
|
|
{ code: 2012, label: "Algeria", shortLabel: "DZ", languageCode: "fr" },
|
|
{
|
|
code: 2020,
|
|
label: "Andorra",
|
|
shortLabel: "AD",
|
|
languageCode: "ca",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2024, label: "Angola", shortLabel: "AO", languageCode: "pt" },
|
|
{ code: 2032, label: "Argentina", shortLabel: "AR", languageCode: "es" },
|
|
{ code: 2051, label: "Armenia", shortLabel: "AM", languageCode: "hy" },
|
|
{ code: 2036, label: "Australia", shortLabel: "AU", languageCode: "en" },
|
|
{ code: 2040, label: "Austria", shortLabel: "AT", languageCode: "de" },
|
|
{ code: 2031, label: "Azerbaijan", shortLabel: "AZ", languageCode: "az" },
|
|
{
|
|
code: 2044,
|
|
label: "Bahamas",
|
|
shortLabel: "BS",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2048, label: "Bahrain", shortLabel: "BH", languageCode: "ar" },
|
|
{ code: 2050, label: "Bangladesh", shortLabel: "BD", languageCode: "bn" },
|
|
{
|
|
code: 2052,
|
|
label: "Barbados",
|
|
shortLabel: "BB",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2056, label: "Belgium", shortLabel: "BE", languageCode: "nl" },
|
|
{
|
|
code: 2084,
|
|
label: "Belize",
|
|
shortLabel: "BZ",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2068, label: "Bolivia", shortLabel: "BO", languageCode: "es" },
|
|
{
|
|
code: 2070,
|
|
label: "Bosnia and Herzegovina",
|
|
shortLabel: "BA",
|
|
languageCode: "bs",
|
|
},
|
|
{
|
|
code: 2072,
|
|
label: "Botswana",
|
|
shortLabel: "BW",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2076, label: "Brazil", shortLabel: "BR", languageCode: "pt" },
|
|
{
|
|
code: 2096,
|
|
label: "Brunei",
|
|
shortLabel: "BN",
|
|
languageCode: "ms",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2100, label: "Bulgaria", shortLabel: "BG", languageCode: "bg" },
|
|
{ code: 2854, label: "Burkina Faso", shortLabel: "BF", languageCode: "fr" },
|
|
{ code: 2116, label: "Cambodia", shortLabel: "KH", languageCode: "en" },
|
|
{ code: 2120, label: "Cameroon", shortLabel: "CM", languageCode: "fr" },
|
|
{ code: 2124, label: "Canada", shortLabel: "CA", languageCode: "en" },
|
|
{ code: 2152, label: "Chile", shortLabel: "CL", languageCode: "es" },
|
|
{ code: 2170, label: "Colombia", shortLabel: "CO", languageCode: "es" },
|
|
{ code: 2188, label: "Costa Rica", shortLabel: "CR", languageCode: "es" },
|
|
{ code: 2384, label: "Cote d'Ivoire", shortLabel: "CI", languageCode: "fr" },
|
|
{ code: 2191, label: "Croatia", shortLabel: "HR", languageCode: "hr" },
|
|
{ code: 2196, label: "Cyprus", shortLabel: "CY", languageCode: "el" },
|
|
{ code: 2203, label: "Czechia", shortLabel: "CZ", languageCode: "cs" },
|
|
{ code: 2208, label: "Denmark", shortLabel: "DK", languageCode: "da" },
|
|
{
|
|
code: 2214,
|
|
label: "Dominican Republic",
|
|
shortLabel: "DO",
|
|
languageCode: "es",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2218, label: "Ecuador", shortLabel: "EC", languageCode: "es" },
|
|
{ code: 2818, label: "Egypt", shortLabel: "EG", languageCode: "ar" },
|
|
{ code: 2222, label: "El Salvador", shortLabel: "SV", languageCode: "es" },
|
|
{ code: 2233, label: "Estonia", shortLabel: "EE", languageCode: "et" },
|
|
{
|
|
code: 2231,
|
|
label: "Ethiopia",
|
|
shortLabel: "ET",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2242,
|
|
label: "Fiji",
|
|
shortLabel: "FJ",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2246, label: "Finland", shortLabel: "FI", languageCode: "fi" },
|
|
{ code: 2250, label: "France", shortLabel: "FR", languageCode: "fr" },
|
|
{
|
|
code: 2268,
|
|
label: "Georgia",
|
|
shortLabel: "GE",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2276, label: "Germany", shortLabel: "DE", languageCode: "de" },
|
|
{ code: 2288, label: "Ghana", shortLabel: "GH", languageCode: "en" },
|
|
{ code: 2300, label: "Greece", shortLabel: "GR", languageCode: "el" },
|
|
{ code: 2320, label: "Guatemala", shortLabel: "GT", languageCode: "es" },
|
|
{
|
|
code: 2831,
|
|
label: "Guernsey",
|
|
shortLabel: "GG",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2328,
|
|
label: "Guyana",
|
|
shortLabel: "GY",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2332,
|
|
label: "Haiti",
|
|
shortLabel: "HT",
|
|
languageCode: "fr",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2340,
|
|
label: "Honduras",
|
|
shortLabel: "HN",
|
|
languageCode: "es",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2344, label: "Hong Kong", shortLabel: "HK", languageCode: "zh-TW" },
|
|
{ code: 2348, label: "Hungary", shortLabel: "HU", languageCode: "hu" },
|
|
{
|
|
code: 2352,
|
|
label: "Iceland",
|
|
shortLabel: "IS",
|
|
languageCode: "is",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2356, label: "India", shortLabel: "IN", languageCode: "en" },
|
|
{ code: 2360, label: "Indonesia", shortLabel: "ID", languageCode: "id" },
|
|
{
|
|
code: 2368,
|
|
label: "Iraq",
|
|
shortLabel: "IQ",
|
|
languageCode: "ar",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2372, label: "Ireland", shortLabel: "IE", languageCode: "en" },
|
|
{
|
|
code: 2833,
|
|
label: "Isle of Man",
|
|
shortLabel: "IM",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2376, label: "Israel", shortLabel: "IL", languageCode: "he" },
|
|
{ code: 2380, label: "Italy", shortLabel: "IT", languageCode: "it" },
|
|
{
|
|
code: 2388,
|
|
label: "Jamaica",
|
|
shortLabel: "JM",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2392, label: "Japan", shortLabel: "JP", languageCode: "ja" },
|
|
{
|
|
code: 2832,
|
|
label: "Jersey",
|
|
shortLabel: "JE",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2400, label: "Jordan", shortLabel: "JO", languageCode: "ar" },
|
|
{ code: 2398, label: "Kazakhstan", shortLabel: "KZ", languageCode: "ru" },
|
|
{ code: 2404, label: "Kenya", shortLabel: "KE", languageCode: "en" },
|
|
{
|
|
code: 2414,
|
|
label: "Kuwait",
|
|
shortLabel: "KW",
|
|
languageCode: "ar",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2417,
|
|
label: "Kyrgyzstan",
|
|
shortLabel: "KG",
|
|
languageCode: "ru",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2418,
|
|
label: "Laos",
|
|
shortLabel: "LA",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2428, label: "Latvia", shortLabel: "LV", languageCode: "lv" },
|
|
{
|
|
code: 2422,
|
|
label: "Lebanon",
|
|
shortLabel: "LB",
|
|
languageCode: "ar",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2438,
|
|
label: "Liechtenstein",
|
|
shortLabel: "LI",
|
|
languageCode: "de",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2440, label: "Lithuania", shortLabel: "LT", languageCode: "lt" },
|
|
{
|
|
code: 2442,
|
|
label: "Luxembourg",
|
|
shortLabel: "LU",
|
|
languageCode: "fr",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2450,
|
|
label: "Madagascar",
|
|
shortLabel: "MG",
|
|
languageCode: "fr",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2454,
|
|
label: "Malawi",
|
|
shortLabel: "MW",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2458, label: "Malaysia", shortLabel: "MY", languageCode: "en" },
|
|
{
|
|
code: 2462,
|
|
label: "Maldives",
|
|
shortLabel: "MV",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2470, label: "Malta", shortLabel: "MT", languageCode: "en" },
|
|
{
|
|
code: 2480,
|
|
label: "Mauritius",
|
|
shortLabel: "MU",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2484, label: "Mexico", shortLabel: "MX", languageCode: "es" },
|
|
{ code: 2498, label: "Moldova", shortLabel: "MD", languageCode: "ro" },
|
|
{ code: 2492, label: "Monaco", shortLabel: "MC", languageCode: "fr" },
|
|
{
|
|
code: 2496,
|
|
label: "Mongolia",
|
|
shortLabel: "MN",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2499,
|
|
label: "Montenegro",
|
|
shortLabel: "ME",
|
|
languageCode: "sr",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2504, label: "Morocco", shortLabel: "MA", languageCode: "ar" },
|
|
{
|
|
code: 2508,
|
|
label: "Mozambique",
|
|
shortLabel: "MZ",
|
|
languageCode: "pt",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2104,
|
|
label: "Myanmar (Burma)",
|
|
shortLabel: "MM",
|
|
languageCode: "en",
|
|
},
|
|
{
|
|
code: 2516,
|
|
label: "Namibia",
|
|
shortLabel: "NA",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2524,
|
|
label: "Nepal",
|
|
shortLabel: "NP",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2528, label: "Netherlands", shortLabel: "NL", languageCode: "nl" },
|
|
{ code: 2554, label: "New Zealand", shortLabel: "NZ", languageCode: "en" },
|
|
{ code: 2558, label: "Nicaragua", shortLabel: "NI", languageCode: "es" },
|
|
{ code: 2566, label: "Nigeria", shortLabel: "NG", languageCode: "en" },
|
|
{
|
|
code: 2807,
|
|
label: "North Macedonia",
|
|
shortLabel: "MK",
|
|
languageCode: "mk",
|
|
},
|
|
{ code: 2578, label: "Norway", shortLabel: "NO", languageCode: "nb" },
|
|
{
|
|
code: 2512,
|
|
label: "Oman",
|
|
shortLabel: "OM",
|
|
languageCode: "ar",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2586, label: "Pakistan", shortLabel: "PK", languageCode: "en" },
|
|
{
|
|
code: 2275,
|
|
label: "Palestine",
|
|
shortLabel: "PS",
|
|
languageCode: "ar",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2591, label: "Panama", shortLabel: "PA", languageCode: "es" },
|
|
{
|
|
code: 2598,
|
|
label: "Papua New Guinea",
|
|
shortLabel: "PG",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2600, label: "Paraguay", shortLabel: "PY", languageCode: "es" },
|
|
{ code: 2604, label: "Peru", shortLabel: "PE", languageCode: "es" },
|
|
{ code: 2608, label: "Philippines", shortLabel: "PH", languageCode: "en" },
|
|
{ code: 2616, label: "Poland", shortLabel: "PL", languageCode: "pl" },
|
|
{ code: 2620, label: "Portugal", shortLabel: "PT", languageCode: "pt" },
|
|
{
|
|
code: 2634,
|
|
label: "Qatar",
|
|
shortLabel: "QA",
|
|
languageCode: "ar",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2642, label: "Romania", shortLabel: "RO", languageCode: "ro" },
|
|
{
|
|
code: 2646,
|
|
label: "Rwanda",
|
|
shortLabel: "RW",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2674,
|
|
label: "San Marino",
|
|
shortLabel: "SM",
|
|
languageCode: "it",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2682, label: "Saudi Arabia", shortLabel: "SA", languageCode: "ar" },
|
|
{ code: 2686, label: "Senegal", shortLabel: "SN", languageCode: "fr" },
|
|
{ code: 2688, label: "Serbia", shortLabel: "RS", languageCode: "sr" },
|
|
{ code: 2702, label: "Singapore", shortLabel: "SG", languageCode: "en" },
|
|
{ code: 2703, label: "Slovakia", shortLabel: "SK", languageCode: "sk" },
|
|
{ code: 2705, label: "Slovenia", shortLabel: "SI", languageCode: "sl" },
|
|
{ code: 2710, label: "South Africa", shortLabel: "ZA", languageCode: "en" },
|
|
{ code: 2410, label: "South Korea", shortLabel: "KR", languageCode: "ko" },
|
|
{ code: 2724, label: "Spain", shortLabel: "ES", languageCode: "es" },
|
|
{ code: 2144, label: "Sri Lanka", shortLabel: "LK", languageCode: "en" },
|
|
{
|
|
code: 2740,
|
|
label: "Suriname",
|
|
shortLabel: "SR",
|
|
languageCode: "nl",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2752, label: "Sweden", shortLabel: "SE", languageCode: "sv" },
|
|
{ code: 2756, label: "Switzerland", shortLabel: "CH", languageCode: "de" },
|
|
{ code: 2158, label: "Taiwan", shortLabel: "TW", languageCode: "zh-TW" },
|
|
{
|
|
code: 2762,
|
|
label: "Tajikistan",
|
|
shortLabel: "TJ",
|
|
languageCode: "ru",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2834,
|
|
label: "Tanzania",
|
|
shortLabel: "TZ",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2764, label: "Thailand", shortLabel: "TH", languageCode: "th" },
|
|
{
|
|
code: 2780,
|
|
label: "Trinidad and Tobago",
|
|
shortLabel: "TT",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2788, label: "Tunisia", shortLabel: "TN", languageCode: "ar" },
|
|
{ code: 2792, label: "Turkiye", shortLabel: "TR", languageCode: "tr" },
|
|
{
|
|
code: 2795,
|
|
label: "Turkmenistan",
|
|
shortLabel: "TM",
|
|
languageCode: "ru",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2800,
|
|
label: "Uganda",
|
|
shortLabel: "UG",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2804, label: "Ukraine", shortLabel: "UA", languageCode: "uk" },
|
|
{
|
|
code: 2784,
|
|
label: "United Arab Emirates",
|
|
shortLabel: "AE",
|
|
languageCode: "en",
|
|
},
|
|
{
|
|
code: 2826,
|
|
label: "United Kingdom",
|
|
shortLabel: "UK",
|
|
languageCode: "en",
|
|
},
|
|
{ code: 2840, label: "United States", shortLabel: "US", languageCode: "en" },
|
|
{ code: 2858, label: "Uruguay", shortLabel: "UY", languageCode: "es" },
|
|
{
|
|
code: 2860,
|
|
label: "Uzbekistan",
|
|
shortLabel: "UZ",
|
|
languageCode: "ru",
|
|
googleAdsOnly: true,
|
|
},
|
|
{ code: 2862, label: "Venezuela", shortLabel: "VE", languageCode: "es" },
|
|
{ code: 2704, label: "Vietnam", shortLabel: "VN", languageCode: "vi" },
|
|
{
|
|
code: 2894,
|
|
label: "Zambia",
|
|
shortLabel: "ZM",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
{
|
|
code: 2716,
|
|
label: "Zimbabwe",
|
|
shortLabel: "ZW",
|
|
languageCode: "en",
|
|
googleAdsOnly: true,
|
|
},
|
|
] as const;
|
|
|
|
/**
|
|
* Languages selectable for rank tracking, which runs against the DataForSEO
|
|
* SERP (Google) API. This is the full set of language codes that API accepts;
|
|
* source/refresh it from the live endpoint (auth required):
|
|
* GET https://api.dataforseo.com/v3/serp/google/languages
|
|
* (Country list above comes from the sibling Labs endpoint cited at the top of
|
|
* this file: /v3/dataforseo_labs/locations_and_languages.)
|
|
*
|
|
* `code` is the DataForSEO `language_code` (authoritative); `label` is its
|
|
* `language_name`, lightly cleaned for display. Deviations from the raw
|
|
* endpoint: the deprecated `iw` Hebrew alias and the redundant `no` are
|
|
* dropped (Norway uses `nb`, which both SERP and Labs accept). Every country
|
|
* default in LOCATION_OPTIONS must appear here so the picker can show it.
|
|
*
|
|
* This is the master list. Rank tracking (SERP) offers all of it for any
|
|
* country; the Labs-backed project picker shows a per-country subset via
|
|
* getLanguageOptions() below.
|
|
*/
|
|
export const SERP_LANGUAGE_OPTIONS = [
|
|
{ code: "af", label: "Afrikaans" },
|
|
{ code: "ak", label: "Akan" },
|
|
{ code: "sq", label: "Albanian" },
|
|
{ code: "am", label: "Amharic" },
|
|
{ code: "ar", label: "Arabic" },
|
|
{ code: "hy", label: "Armenian" },
|
|
{ code: "az", label: "Azerbaijani" },
|
|
{ code: "ban", label: "Balinese" },
|
|
{ code: "eu", label: "Basque" },
|
|
{ code: "be", label: "Belarusian" },
|
|
{ code: "bn", label: "Bengali" },
|
|
{ code: "bs", label: "Bosnian" },
|
|
{ code: "bg", label: "Bulgarian" },
|
|
{ code: "my", label: "Burmese" },
|
|
{ code: "ca", label: "Catalan" },
|
|
{ code: "ceb", label: "Cebuano" },
|
|
{ code: "ny", label: "Chichewa" },
|
|
{ code: "zh-CN", label: "Chinese (Simplified)" },
|
|
{ code: "zh-TW", label: "Chinese (Traditional)" },
|
|
{ code: "hr", label: "Croatian" },
|
|
{ code: "cs", label: "Czech" },
|
|
{ code: "da", label: "Danish" },
|
|
{ code: "nl", label: "Dutch" },
|
|
{ code: "en", label: "English" },
|
|
{ code: "et", label: "Estonian" },
|
|
{ code: "ee", label: "Ewe" },
|
|
{ code: "fo", label: "Faroese" },
|
|
{ code: "fa", label: "Farsi" },
|
|
{ code: "fil", label: "Filipino" },
|
|
{ code: "fi", label: "Finnish" },
|
|
{ code: "fr", label: "French" },
|
|
{ code: "fy", label: "Frisian" },
|
|
{ code: "gaa", label: "Ga" },
|
|
{ code: "gl", label: "Galician" },
|
|
{ code: "lg", label: "Ganda" },
|
|
{ code: "ka", label: "Georgian" },
|
|
{ code: "de", label: "German" },
|
|
{ code: "el", label: "Greek" },
|
|
{ code: "gu", label: "Gujarati" },
|
|
{ code: "ht", label: "Haitian" },
|
|
{ code: "ha", label: "Hausa" },
|
|
{ code: "he", label: "Hebrew" },
|
|
{ code: "hi", label: "Hindi" },
|
|
{ code: "hu", label: "Hungarian" },
|
|
{ code: "is", label: "Icelandic" },
|
|
{ code: "bem", label: "IciBemba" },
|
|
{ code: "ig", label: "Igbo" },
|
|
{ code: "id", label: "Indonesian" },
|
|
{ code: "ga", label: "Irish" },
|
|
{ code: "it", label: "Italian" },
|
|
{ code: "ja", label: "Japanese" },
|
|
{ code: "kn", label: "Kannada" },
|
|
{ code: "kk", label: "Kazakh" },
|
|
{ code: "km", label: "Khmer" },
|
|
{ code: "rw", label: "Kinyarwanda" },
|
|
{ code: "rn", label: "Kirundi" },
|
|
{ code: "kg", label: "Kongo" },
|
|
{ code: "ko", label: "Korean" },
|
|
{ code: "mfe", label: "Kreol morisien" },
|
|
{ code: "crs", label: "Kreol Seselwa" },
|
|
{ code: "kri", label: "Krio" },
|
|
{ code: "ckb", label: "Kurdish" },
|
|
{ code: "ky", label: "Kyrgyz" },
|
|
{ code: "lo", label: "Lao" },
|
|
{ code: "lv", label: "Latvian" },
|
|
{ code: "ln", label: "Lingala" },
|
|
{ code: "lt", label: "Lithuanian" },
|
|
{ code: "ach", label: "Luo" },
|
|
{ code: "mk", label: "Macedonian" },
|
|
{ code: "mg", label: "Malagasy" },
|
|
{ code: "ms", label: "Malay" },
|
|
{ code: "ml", label: "Malayalam" },
|
|
{ code: "mt", label: "Maltese" },
|
|
{ code: "mi", label: "Maori" },
|
|
{ code: "mr", label: "Marathi" },
|
|
{ code: "mn", label: "Mongolian" },
|
|
{ code: "ne", label: "Nepali" },
|
|
{ code: "nso", label: "Northern Sotho" },
|
|
{ code: "nb", label: "Norwegian (Bokmål)" },
|
|
{ code: "nyn", label: "Nyankole" },
|
|
{ code: "om", label: "Oromo" },
|
|
{ code: "ps", label: "Pashto" },
|
|
{ code: "pcm", label: "Pidgin" },
|
|
{ code: "pl", label: "Polish" },
|
|
{ code: "pt", label: "Portuguese" },
|
|
{ code: "pt-BR", label: "Portuguese (Brazil)" },
|
|
{ code: "pt-PT", label: "Portuguese (Portugal)" },
|
|
{ code: "pa", label: "Punjabi" },
|
|
{ code: "qu", label: "Quechua" },
|
|
{ code: "ro", label: "Romanian" },
|
|
{ code: "rm", label: "Romansh" },
|
|
{ code: "ru", label: "Russian" },
|
|
{ code: "sr", label: "Serbian" },
|
|
{ code: "sr-Latn", label: "Serbian (Latin)" },
|
|
{ code: "sr-ME", label: "Serbian (Montenegro)" },
|
|
{ code: "st", label: "Sesotho" },
|
|
{ code: "sn", label: "Shona" },
|
|
{ code: "loz", label: "Silozi" },
|
|
{ code: "sd", label: "Sindhi" },
|
|
{ code: "si", label: "Sinhalese" },
|
|
{ code: "sk", label: "Slovak" },
|
|
{ code: "sl", label: "Slovenian" },
|
|
{ code: "so", label: "Somali" },
|
|
{ code: "es", label: "Spanish" },
|
|
{ code: "es-419", label: "Spanish (Latin America)" },
|
|
{ code: "sw", label: "Swahili" },
|
|
{ code: "sv", label: "Swedish" },
|
|
{ code: "tl", label: "Tagalog" },
|
|
{ code: "tg", label: "Tajik" },
|
|
{ code: "ta", label: "Tamil" },
|
|
{ code: "te", label: "Telugu" },
|
|
{ code: "th", label: "Thai" },
|
|
{ code: "ti", label: "Tigrinya" },
|
|
{ code: "to", label: "Tonga (Tonga Islands)" },
|
|
{ code: "lua", label: "Tshiluba" },
|
|
{ code: "tn", label: "Tswana" },
|
|
{ code: "tum", label: "Tumbuka" },
|
|
{ code: "tr", label: "Turkish" },
|
|
{ code: "tk", label: "Turkmen" },
|
|
{ code: "uk", label: "Ukrainian" },
|
|
{ code: "ur", label: "Urdu" },
|
|
{ code: "uz", label: "Uzbek" },
|
|
{ code: "vi", label: "Vietnamese" },
|
|
{ code: "cy", label: "Welsh" },
|
|
{ code: "wo", label: "Wolof" },
|
|
{ code: "xh", label: "Xhosa" },
|
|
{ code: "yo", label: "Yoruba" },
|
|
{ code: "zu", label: "Zulu" },
|
|
] as const;
|
|
/** Countries usable by DataForSEO Labs features (domain overview etc.). */
|
|
export const LABS_LOCATION_OPTIONS = LOCATION_OPTIONS.filter(
|
|
(option) => !option.googleAdsOnly,
|
|
);
|
|
|
|
const LOCATION_CODES = new Set<number>(
|
|
LOCATION_OPTIONS.map((option) => option.code),
|
|
);
|
|
|
|
const LABS_LOCATION_CODES = new Set<number>(
|
|
LABS_LOCATION_OPTIONS.map((option) => option.code),
|
|
);
|
|
|
|
export const LOCATIONS: Record<number, string> = Object.fromEntries(
|
|
LOCATION_OPTIONS.map((option) => [option.code, option.shortLabel]),
|
|
);
|
|
|
|
const LOCATION_LANGUAGE: Record<number, string> = Object.fromEntries(
|
|
LOCATION_OPTIONS.map((option) => [option.code, option.languageCode]),
|
|
);
|
|
|
|
const SUPPORTED_LANGUAGE_CODES = new Set<string>(
|
|
SERP_LANGUAGE_OPTIONS.map((language) => language.code),
|
|
);
|
|
|
|
export function getLanguageCode(locationCode: number): string {
|
|
return LOCATION_LANGUAGE[locationCode] ?? "en";
|
|
}
|
|
|
|
/**
|
|
* Resolves a request's market against the project's default. The pair is
|
|
* resolved together: overriding only the location snaps the language to that
|
|
* location's default language, because the project's language was chosen for
|
|
* the project's own location and may not be valid — or sensible — for the
|
|
* override (e.g. a Vietnam project querying Germany must not default to
|
|
* Vietnamese).
|
|
*/
|
|
export function resolveMarket(
|
|
args: { locationCode?: number; languageCode?: string },
|
|
project: { locationCode: number; languageCode: string },
|
|
): { locationCode: number; languageCode: string } {
|
|
const locationCode = args.locationCode ?? project.locationCode;
|
|
const languageCode =
|
|
args.languageCode ??
|
|
(locationCode === project.locationCode
|
|
? project.languageCode
|
|
: getLanguageCode(locationCode));
|
|
return { locationCode, languageCode };
|
|
}
|
|
|
|
/**
|
|
* Whether DataForSEO serves this language for this location. Only Labs
|
|
* locations have authoritative per-location language lists; Google Ads
|
|
* locations are left to the metering safety net.
|
|
*/
|
|
export function isLanguageServedForLocation(
|
|
locationCode: number,
|
|
languageCode: string,
|
|
): boolean {
|
|
if (getKeywordDataProvider(locationCode) !== "labs") return true;
|
|
return getLanguageOptions(locationCode).some(
|
|
(option) => option.code === languageCode,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolves the market for a Labs-only tool. Same as resolveMarket, except a
|
|
* project default Labs cannot serve is replaced by the United States: the
|
|
* caller never chose that market, so rejecting the call would dead-end on a
|
|
* value it can't see — and passing the pair through would spend credits on a
|
|
* task DataForSEO rejects. An explicit location is left alone, so a caller that
|
|
* names an unserved country still fails loudly on its own assert.
|
|
*/
|
|
export function resolveLabsMarket(
|
|
args: { locationCode?: number; languageCode?: string },
|
|
project: { locationCode: number; languageCode: string },
|
|
): { locationCode: number; languageCode: string } {
|
|
const projectIsServed =
|
|
getKeywordDataProvider(project.locationCode) === "labs" &&
|
|
isLanguageServedForLocation(project.locationCode, project.languageCode);
|
|
|
|
return resolveMarket(
|
|
args,
|
|
projectIsServed
|
|
? project
|
|
: { locationCode: DEFAULT_LOCATION_CODE, languageCode: "en" },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Language codes DataForSEO accepts — the master SERP_LANGUAGE_OPTIONS list.
|
|
* Callers (e.g. MCP tools) can pass an arbitrary `language_code`; an
|
|
* unsupported one is otherwise rejected by DataForSEO as an opaque *charged*
|
|
* "Invalid Field: 'language_code'." failure, so we validate against this set
|
|
* first (cost 0).
|
|
*/
|
|
export function isSupportedLanguageCode(languageCode: string): boolean {
|
|
return SUPPORTED_LANGUAGE_CODES.has(languageCode);
|
|
}
|
|
|
|
/**
|
|
* Countries where DataForSEO offers more than one language, from the Labs
|
|
* locations_and_languages endpoint (each country's default is included).
|
|
* Every other country offers just its single default (see getLanguageOptions);
|
|
* googleAdsOnly countries have no per-country language data, so they fall back
|
|
* to the default too. Keep each list's codes present in SERP_LANGUAGE_OPTIONS.
|
|
*/
|
|
const MULTI_LANGUAGE_LOCATIONS: Record<number, readonly string[]> = {
|
|
2012: ["ar", "fr"], // Algeria
|
|
2056: ["de", "fr", "nl"], // Belgium
|
|
2124: ["en", "fr"], // Canada
|
|
2196: ["el", "en"], // Cyprus
|
|
2300: ["el", "en"], // Greece
|
|
2344: ["en", "zh-TW"], // Hong Kong
|
|
2356: ["en", "hi"], // India
|
|
2360: ["en", "id"], // Indonesia
|
|
2376: ["ar", "he"], // Israel
|
|
2458: ["en", "ms"], // Malaysia
|
|
2504: ["ar", "fr"], // Morocco
|
|
2586: ["en", "ur"], // Pakistan
|
|
2608: ["en", "tl"], // Philippines
|
|
2702: ["en", "zh-CN"], // Singapore
|
|
2756: ["de", "fr", "it"], // Switzerland
|
|
2784: ["ar", "en"], // United Arab Emirates
|
|
2804: ["ru", "uk"], // Ukraine
|
|
2818: ["ar", "en"], // Egypt
|
|
2840: ["en", "es"], // United States
|
|
2704: ["en", "vi"], // Vietnam
|
|
};
|
|
|
|
/**
|
|
* Languages to offer for a location. Restricts the global SERP_LANGUAGE_OPTIONS
|
|
* list to the languages DataForSEO supports for that country, so a picker
|
|
* isn't a wall of irrelevant options.
|
|
*/
|
|
export function getLanguageOptions(
|
|
locationCode: number,
|
|
): readonly (typeof SERP_LANGUAGE_OPTIONS)[number][] {
|
|
const codes = new Set(
|
|
MULTI_LANGUAGE_LOCATIONS[locationCode] ?? [getLanguageCode(locationCode)],
|
|
);
|
|
return SERP_LANGUAGE_OPTIONS.filter((language) => codes.has(language.code));
|
|
}
|
|
|
|
/**
|
|
* The language to send to the keyword-data APIs (Labs / Google Ads) for a
|
|
* market whose language was chosen for the SERP API. SERP serves any language
|
|
* in any country — rank tracking relies on that — but the keyword-data APIs
|
|
* only serve a country's own languages and reject anything else as an opaque
|
|
* *charged* "Invalid Field: 'language_code'." task failure. Falls back to the
|
|
* country's default language.
|
|
*/
|
|
export function resolveKeywordDataLanguage(
|
|
locationCode: number,
|
|
languageCode: string,
|
|
): string {
|
|
return getLanguageOptions(locationCode).some(
|
|
(option) => option.code === languageCode,
|
|
)
|
|
? languageCode
|
|
: getLanguageCode(locationCode);
|
|
}
|
|
|
|
export function isSupportedLocationCode(locationCode: number): boolean {
|
|
return LOCATION_CODES.has(locationCode);
|
|
}
|
|
|
|
export function isLabsLocationCode(locationCode: number): boolean {
|
|
return LABS_LOCATION_CODES.has(locationCode);
|
|
}
|
|
|
|
/**
|
|
* Which DataForSEO API serves keyword data for this location. Unknown codes
|
|
* fall back to Labs so behavior for arbitrary codes is unchanged (Labs
|
|
* rejects unsupported locations with its own error).
|
|
*/
|
|
export function getKeywordDataProvider(
|
|
locationCode: number,
|
|
): KeywordDataProvider {
|
|
return LOCATION_CODES.has(locationCode) &&
|
|
!LABS_LOCATION_CODES.has(locationCode)
|
|
? "google_ads"
|
|
: "labs";
|
|
}
|