/* 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 = { 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( LOCATION_OPTIONS.map((option) => option.code), ); const LABS_LOCATION_CODES = new Set( LABS_LOCATION_OPTIONS.map((option) => option.code), ); export const LOCATIONS: Record = Object.fromEntries( LOCATION_OPTIONS.map((option) => [option.code, option.shortLabel]), ); const LOCATION_LANGUAGE: Record = Object.fromEntries( LOCATION_OPTIONS.map((option) => [option.code, option.languageCode]), ); const SUPPORTED_LANGUAGE_CODES = new Set( 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 = { 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"; }