package main import "strings" // regionFromLocale extracts the ISO 3166-1 alpha-2 region from a BCP-47 locale // tag such as the value of navigator.language ("en-AU" -> "AU", "zh-Hans-CN" -> // "CN"). The first subtag is the language unless it is itself an uppercase // 2-letter region (a bare "AU"). Returns "" when no region subtag is present // ("en" -> ""). func regionFromLocale(locale string) string { locale = strings.TrimSpace(locale) if locale == "" { return "" } parts := strings.FieldsFunc(locale, func(r rune) bool { return r == '-' || r == '_' }) for i, part := range parts { if len(part) != 2 || !isASCIILetters(part) { continue // language (handled below), script ("Hans"), or variant } // Subtag 0 is the language unless it is already an uppercase region. if i == 0 && part != strings.ToUpper(part) { continue } return strings.ToUpper(part) } return "" } // isASCIILetters reports whether s is non-empty and all ASCII letters. func isASCIILetters(s string) bool { if s == "" { return false } for _, r := range s { if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') { return false } } return true } // tzRegions maps IANA timezones to ISO 3166-1 alpha-2 regions for the common, // unambiguous cases. The device timezone is a stronger locator than the UI // locale (an en-US browser in Australia still reports Australia/*), so it is // preferred. Ambiguous zones (most America/*) are intentionally omitted — those // fall through to the locale. Australia/* is matched by prefix in // regionFromTimezone. var tzRegions = map[string]string{ "Pacific/Auckland": "NZ", "Pacific/Chatham": "NZ", "Europe/London": "GB", "Europe/Dublin": "IE", "Europe/Paris": "FR", "Europe/Berlin": "DE", "Europe/Madrid": "ES", "Europe/Rome": "IT", "Europe/Amsterdam": "NL", "Europe/Brussels": "BE", "Europe/Zurich": "CH", "Europe/Stockholm": "SE", "Europe/Oslo": "NO", "Europe/Copenhagen": "DK", "Asia/Singapore": "SG", "Asia/Kolkata": "IN", "Asia/Calcutta": "IN", "Asia/Tokyo": "JP", "Asia/Hong_Kong": "HK", "Asia/Dubai": "AE", "Asia/Shanghai": "CN", "Asia/Seoul": "KR", "Africa/Johannesburg": "ZA", } // regionFromTimezone maps an IANA timezone (navigator-detected, e.g. // "Australia/Perth") to a region for the common cases. Returns "" for // unknown/ambiguous zones (including the UTC fallback some browsers report). func regionFromTimezone(tz string) string { tz = strings.TrimSpace(tz) if tz == "" { return "" } if strings.HasPrefix(tz, "Australia/") { return "AU" } return tzRegions[tz] } // resolveRegion picks the ISO region used to pre-select the phone country // combobox and to seed E.164 normalization when the visitor has not explicitly // picked a country. It prefers the timezone region (strongest locator), then the // browser locale region, validated against the known country list. Returns "" // when neither is known — the combobox then has no default and normalizePhone // only handles already-international numbers. func resolveRegion(locale, timezone string) string { for _, r := range []string{regionFromTimezone(timezone), regionFromLocale(locale)} { if r != "" { if _, ok := countryByCode(r); ok { return r } } } return "" } // countryByCode returns the country with the given ISO alpha-2 code, or false. func countryByCode(code string) (Country, bool) { code = strings.ToUpper(strings.TrimSpace(code)) for _, c := range countries { if c.Code == code { return c, true } } return Country{}, false } // countryFlag returns the emoji flag for a 2-letter ISO region code by mapping // each letter to its Unicode regional-indicator symbol. "" for invalid codes. func countryFlag(code string) string { code = strings.ToUpper(strings.TrimSpace(code)) if len(code) != 2 || !isASCIILetters(code) { return "" } r := []rune(code) return string(rune(0x1F1E6+(r[0]-'A'))) + string(rune(0x1F1E6+(r[1]-'A'))) } // countryLabel renders the human label shown in the combobox and its pre-fill, // e.g. "🇦🇺 Australia (+61)". Returns "" when the code is unknown. func countryLabel(code string) string { c, ok := countryByCode(code) if !ok { return "" } if flag := countryFlag(c.Code); flag != "" { return flag + " " + c.Name + " (" + c.Dial + ")" } return c.Name + " (" + c.Dial + ")" }