// Package geoflag maps server/profile names to ISO 3166-1 alpha-2 and emoji flags. package geoflag import ( "regexp" "strings" "unicode" ) var ( reBracketCC = regexp.MustCompile(`(?i)[\[(]([a-z]{2})[\])]`) rePrefixCC = regexp.MustCompile(`(?i)^([a-z]{2})(?:\s*[-_|/]\s*|\s+)`) reTokenCC = regexp.MustCompile(`(?i)(?:^|[^a-z])([a-z]{2})(?:\s*[-_|]|$)`) reEmojiFlag = regexp.MustCompile(`[\x{1F1E6}-\x{1F1FF}]{2}`) ) // FromText returns ISO2 (upper) and emoji flag, or empty if unknown. func FromText(name string) (iso2, emoji string) { name = strings.TrimSpace(name) if name == "" { return "", "" } if m := reEmojiFlag.FindString(name); m != "" { if cc := emojiToISO(m); cc != "" { return cc, m } } lower := strings.ToLower(name) for _, pair := range countryNames { if strings.Contains(lower, pair.name) { return pair.code, ISOToEmoji(pair.code) } } if m := reBracketCC.FindStringSubmatch(name); len(m) == 2 { cc := strings.ToUpper(m[1]) if Known(cc) { return cc, ISOToEmoji(cc) } } if m := rePrefixCC.FindStringSubmatch(name); len(m) == 2 { cc := strings.ToUpper(m[1]) if Known(cc) { return cc, ISOToEmoji(cc) } } if m := reTokenCC.FindStringSubmatch(name); len(m) == 2 { cc := strings.ToUpper(m[1]) if Known(cc) { return cc, ISOToEmoji(cc) } } return "", "" } // Known reports whether cc is a mapped ISO2 code. func Known(cc string) bool { cc = strings.ToUpper(strings.TrimSpace(cc)) _, ok := isoSet[cc] return ok } // ISOToEmoji converts "DE" → "🇩🇪". Invalid → "". func ISOToEmoji(cc string) string { cc = strings.ToUpper(strings.TrimSpace(cc)) if len(cc) != 2 { return "" } a, b := rune(cc[0]), rune(cc[1]) if a < 'A' || a > 'Z' || b < 'A' || b > 'Z' { return "" } return string(rune(0x1F1E6+a-'A')) + string(rune(0x1F1E6+b-'A')) } func emojiToISO(flag string) string { rs := []rune(flag) if len(rs) != 2 { return "" } a, b := rs[0]-0x1F1E6, rs[1]-0x1F1E6 if a < 0 || a > 25 || b < 0 || b > 25 { return "" } cc := string([]byte{byte('A' + a), byte('A' + b)}) if !Known(cc) { // Still return regional indicator pair as ISO guess for display. if unicode.IsLetter(rune('A' + a)) { return cc } return "" } return cc } type nameCode struct { name string code string } // Longer / more specific names first where needed. var countryNames = []nameCode{ {"netherlands", "NL"}, {"deutschland", "DE"}, {"germany", "DE"}, {"united kingdom", "GB"}, {"great britain", "GB"}, {"england", "GB"}, {"united states", "US"}, {"america", "US"}, {"finland", "FI"}, {"sweden", "SE"}, {"norway", "NO"}, {"denmark", "DK"}, {"poland", "PL"}, {"france", "FR"}, {"spain", "ES"}, {"italy", "IT"}, {"portugal", "PT"}, {"switzerland", "CH"}, {"austria", "AT"}, {"belgium", "BE"}, {"czech", "CZ"}, {"slovakia", "SK"}, {"hungary", "HU"}, {"romania", "RO"}, {"bulgaria", "BG"}, {"ukraine", "UA"}, {"russia", "RU"}, {"turkey", "TR"}, {"israel", "IL"}, {"uae", "AE"}, {"emirates", "AE"}, {"dubai", "AE"}, {"singapore", "SG"}, {"hong kong", "HK"}, {"japan", "JP"}, {"korea", "KR"}, {"taiwan", "TW"}, {"india", "IN"}, {"canada", "CA"}, {"mexico", "MX"}, {"brazil", "BR"}, {"argentina", "AR"}, {"australia", "AU"}, {"new zealand", "NZ"}, {"latvia", "LV"}, {"lithuania", "LT"}, {"estonia", "EE"}, {"moldova", "MD"}, {"georgia", "GE"}, {"armenia", "AM"}, {"kazakhstan", "KZ"}, {"uzbekistan", "UZ"}, {"serbia", "RS"}, {"croatia", "HR"}, {"slovenia", "SI"}, {"greece", "GR"}, {"cyprus", "CY"}, {"ireland", "IE"}, {"iceland", "IS"}, {"luxembourg", "LU"}, {"malaysia", "MY"}, {"thailand", "TH"}, {"vietnam", "VN"}, {"indonesia", "ID"}, {"philippines", "PH"}, {"south africa", "ZA"}, // Russian country names (lowercase; longer/more specific first). {"великобритания", "GB"}, {"нидерланды", "NL"}, {"голландия", "NL"}, {"германия", "DE"}, {"австралия", "AU"}, {"австрия", "AT"}, {"швейцария", "CH"}, {"финляндия", "FI"}, {"швеция", "SE"}, {"норвегия", "NO"}, {"дания", "DK"}, {"польша", "PL"}, {"франция", "FR"}, {"испания", "ES"}, {"италия", "IT"}, {"португалия", "PT"}, {"бельгия", "BE"}, {"чехия", "CZ"}, {"словакия", "SK"}, {"словения", "SI"}, {"венгрия", "HU"}, {"румыния", "RO"}, {"болгария", "BG"}, {"украина", "UA"}, {"россия", "RU"}, {"турция", "TR"}, {"израиль", "IL"}, {"эмираты", "AE"}, {"дубай", "AE"}, {"сингапур", "SG"}, {"гонконг", "HK"}, {"япония", "JP"}, {"корея", "KR"}, {"тайвань", "TW"}, {"индия", "IN"}, {"канада", "CA"}, {"бразилия", "BR"}, {"аргентина", "AR"}, {"латвия", "LV"}, {"литва", "LT"}, {"эстония", "EE"}, {"молдова", "MD"}, {"грузия", "GE"}, {"армения", "AM"}, {"казахстан", "KZ"}, {"узбекистан", "UZ"}, {"сербия", "RS"}, {"хорватия", "HR"}, {"греция", "GR"}, {"кипр", "CY"}, {"ирландия", "IE"}, {"исландия", "IS"}, {"люксембург", "LU"}, {"англия", "GB"}, {"сша", "US"}, {"америка", "US"}, // Частые опечатки в именах серверов на панелях (наблюдались в проде). // Longer stems first (Contains): «румьиния» before «румьиня». {"румьиния", "RO"}, {"румяння", "RO"}, {"румьиня", "RO"}, {"румыня", "RO"}, {"руминия", "RO"}, {"румыниа", "RO"}, // City → country hints (EN + RU). {"frankfurt", "DE"}, {"amsterdam", "NL"}, {"london", "GB"}, {"paris", "FR"}, {"helsinki", "FI"}, {"stockholm", "SE"}, {"oslo", "NO"}, {"copenhagen", "DK"}, {"warsaw", "PL"}, {"prague", "CZ"}, {"vienna", "AT"}, {"zurich", "CH"}, {"madrid", "ES"}, {"milan", "IT"}, {"lisbon", "PT"}, {"moscow", "RU"}, {"istanbul", "TR"}, {"tokyo", "JP"}, {"seoul", "KR"}, {"new york", "US"}, {"los angeles", "US"}, {"dallas", "US"}, {"miami", "US"}, {"chicago", "US"}, {"toronto", "CA"}, {"riga", "LV"}, {"vilnius", "LT"}, {"tallinn", "EE"}, {"bucharest", "RO"}, {"sofia", "BG"}, {"kyiv", "UA"}, {"kiev", "UA"}, {"tbilisi", "GE"}, {"almaty", "KZ"}, {"франкфурт", "DE"}, {"амстердам", "NL"}, {"лондон", "GB"}, {"париж", "FR"}, {"хельсинки", "FI"}, {"стокгольм", "SE"}, {"осло", "NO"}, {"копенгаген", "DK"}, {"варшава", "PL"}, {"прага", "CZ"}, {"вена", "AT"}, {"цюрих", "CH"}, {"мадрид", "ES"}, {"милан", "IT"}, {"москва", "RU"}, {"стамбул", "TR"}, {"токио", "JP"}, {"сеул", "KR"}, {"торонто", "CA"}, {"рига", "LV"}, {"вильнюс", "LT"}, {"таллин", "EE"}, {"бухарест", "RO"}, {"софия", "BG"}, {"киев", "UA"}, {"тбилиси", "GE"}, } var isoSet = map[string]struct{}{ "AD": {}, "AE": {}, "AF": {}, "AL": {}, "AM": {}, "AR": {}, "AT": {}, "AU": {}, "AZ": {}, "BA": {}, "BD": {}, "BE": {}, "BG": {}, "BH": {}, "BR": {}, "BY": {}, "CA": {}, "CH": {}, "CL": {}, "CN": {}, "CO": {}, "CY": {}, "CZ": {}, "DE": {}, "DK": {}, "EE": {}, "EG": {}, "ES": {}, "FI": {}, "FR": {}, "GB": {}, "GE": {}, "GR": {}, "HK": {}, "HR": {}, "HU": {}, "ID": {}, "IE": {}, "IL": {}, "IN": {}, "IQ": {}, "IR": {}, "IS": {}, "IT": {}, "JP": {}, "KG": {}, "KR": {}, "KZ": {}, "LT": {}, "LU": {}, "LV": {}, "MD": {}, "MK": {}, "MX": {}, "MY": {}, "NL": {}, "NO": {}, "NZ": {}, "PH": {}, "PK": {}, "PL": {}, "PT": {}, "RO": {}, "RS": {}, "RU": {}, "SA": {}, "SE": {}, "SG": {}, "SI": {}, "SK": {}, "TH": {}, "TJ": {}, "TM": {}, "TR": {}, "TW": {}, "UA": {}, "US": {}, "UZ": {}, "VN": {}, "ZA": {}, }