diff --git a/address.go b/address.go index 2990adc..a29d62a 100644 --- a/address.go +++ b/address.go @@ -33,7 +33,9 @@ func (a Address) IsEmpty() bool { // Format represents an address format. type Format struct { + Locale Locale Layout string + LocalLayout string Required []Field SublocalityType SublocalityType LocalityType LocalityType @@ -42,6 +44,7 @@ type Format struct { PostalCodePattern string ShowRegionID bool Regions map[string]string + LocalRegions map[string]string } // IsRequired returns whether the given field is required. @@ -84,6 +87,33 @@ func (f Format) CheckPostalCode(postalCode string) bool { return rx.MatchString(postalCode) } +// SelectLayout selects the correct layout for the given locale. +func (f Format) SelectLayout(locale Locale) string { + if f.LocalLayout != "" && f.useLocalData(locale) { + return f.LocalLayout + } + return f.Layout +} + +// SelectRegions selects the correct regions for the given locale. +func (f Format) SelectRegions(locale Locale) map[string]string { + if len(f.LocalRegions) > 0 && f.useLocalData(locale) { + return f.LocalRegions + } + return f.Regions +} + +// useLocalData returns whether local data should be used for the given locale. +func (f Format) useLocalData(locale Locale) bool { + if locale.Script == "Latn" { + // Allow locales to opt out of local data. E.g: zh-Latn. + return false + } + // Scripts are not compared, matching libaddressinput behavior. This means + // that zh-Hant data will be shown to zh-Hans users, and vice-versa. + return locale.Language == f.Locale.Language +} + // CheckCountryCode checks whether the given country code is valid. // // An empty country code is considered valid. diff --git a/address_test.go b/address_test.go index fecc18b..578f899 100644 --- a/address_test.go +++ b/address_test.go @@ -121,6 +121,83 @@ func TestFormat_CheckPostalCode(t *testing.T) { } } +func TestFormat_SelectLayout(t *testing.T) { + tests := []struct { + countryCode string + locale string + wantLocal bool + }{ + // China ("zh"). + {"CN", "en", false}, + {"CN", "ja", false}, + {"CN", "zh-Latn", false}, + {"CN", "zh", true}, + {"CN", "zh-Hant", true}, + // Hong Kong ("zh-Hant"). + {"HK", "en", false}, + {"HK", "ja", false}, + {"HK", "zh-Latn", false}, + {"HK", "zh", true}, + {"HK", "zh-Hant", true}, + // Serbia (no local layout defined). + {"RS", "en", false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + locale := address.NewLocale(tt.locale) + format := address.GetFormat(tt.countryCode) + got := format.SelectLayout(locale) + want := format.Layout + if tt.wantLocal { + want = format.LocalLayout + } + + if got != want { + t.Errorf("got %v, want %v", got, want) + } + }) + } +} + +func TestFormat_SelectRegions(t *testing.T) { + tests := []struct { + countryCode string + locale string + wantLocal bool + }{ + // China ("zh"). + {"CN", "en", false}, + {"CN", "ja", false}, + {"CN", "zh-Latn", false}, + {"CN", "zh", true}, + {"CN", "zh-Hant", true}, + // Hong Kong ("zh-Hant"). + {"HK", "en", false}, + {"HK", "ja", false}, + {"HK", "zh-Latn", false}, + {"HK", "zh", true}, + {"HK", "zh-Hant", true}, + // Serbia (no local regions defined). + {"RS", "en", false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + format := address.GetFormat(tt.countryCode) + locale := address.NewLocale(tt.locale) + got := format.SelectRegions(locale) + want := format.Regions + if tt.wantLocal { + want = format.LocalRegions + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } + }) + } +} + func TestCheckCountryCode(t *testing.T) { tests := []struct { countryCode string diff --git a/formats.go b/formats.go index ff84e53..5db94ce 100644 --- a/formats.go +++ b/formats.go @@ -19,6 +19,7 @@ var formats = map[string]Format{ PostalCodePattern: "AD[1-7]0\\d", }, "AE": { + Locale: Locale{Language: "ar"}, Layout: "%1\n%2\n%R", Required: []Field{FieldLine1, FieldRegion}, RegionType: RegionTypeEmirate, @@ -27,6 +28,11 @@ var formats = map[string]Format{ "FU": "Fujairah", "RK": "Ras Al Khaimah", "SH": "Sharjah", "UQ": "Umm Al Quwain", }, + LocalRegions: map[string]string{ + "AZ": "أبو ظبي", "SH": "الشارقة", "FU": "الفجيرة", + "UQ": "ام القيوين", "DU": "دبي", "RK": "رأس الخيمة", + "AJ": "عجمان", + }, }, "AF": { Layout: "%1\n%2\n%L\n%P", @@ -48,6 +54,7 @@ var formats = map[string]Format{ PostalCodePattern: "\\d{4}", }, "AM": { + Locale: Locale{Language: "hy"}, Layout: "%1\n%2\n%P\n%L\n%R", Required: []Field{FieldLine1, FieldLocality}, PostalCodePattern: "(?:37)?\\d{4}", @@ -57,6 +64,12 @@ var formats = map[string]Format{ "SH": "Shirak", "SU": "Syunik", "TV": "Tavush", "VD": "Vayots Dzor", "ER": "Yerevan", }, + LocalRegions: map[string]string{ + "AG": "Արագածոտն", "AR": "Արարատ", "AV": "Արմավիր", + "GR": "Գեղարքունիք", "ER": "Երևան", "LO": "Լոռի", + "KT": "Կոտայք", "SH": "Շիրակ", "SU": "Սյունիք", + "VD": "Վայոց ձոր", "TV": "Տավուշ", + }, }, "AR": { Layout: "%1\n%2\n%P %L\n%R", @@ -189,6 +202,7 @@ var formats = map[string]Format{ PostalCodePattern: "\\d{6}", }, "CA": { + Locale: Locale{Language: "fr"}, Layout: "%1\n%2\n%L %R %P", Required: []Field{FieldLine1, FieldLocality, FieldRegion, FieldPostalCode}, PostalCodePattern: "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d", @@ -200,6 +214,13 @@ var formats = map[string]Format{ "PE": "Prince Edward Island", "QC": "Quebec", "SK": "Saskatchewan", "YT": "Yukon", }, + LocalRegions: map[string]string{ + "AB": "Alberta", "BC": "Colombie-Britannique", "MB": "Manitoba", + "NB": "Nouveau-Brunswick", "NL": "Terre-Neuve-et-Labrador", "NT": "Territoires du Nord-Ouest", + "NS": "Nouvelle-Écosse", "NU": "Nunavut", "ON": "Ontario", + "PE": "Île-du-Prince-Édouard", "QC": "Québec", "SK": "Saskatchewan", + "YT": "Yukon", + }, }, "CC": { Layout: "%1\n%2\n%L %R %P", @@ -224,7 +245,9 @@ var formats = map[string]Format{ }, }, "CN": { + Locale: Locale{Language: "zh"}, Layout: "%1\n%2\n%S\n%L\n%R, %P", + LocalLayout: "%P\n%R%L%S\n%1\n%2", Required: []Field{FieldLine1, FieldLocality, FieldRegion, FieldPostalCode}, SublocalityType: SublocalityTypeDistrict, PostalCodePattern: "\\d{6}", @@ -242,6 +265,20 @@ var formats = map[string]Format{ "XJ": "Xinjiang Weiwu'er Zizhiqu", "XZ": "Xizang Zizhiqu", "YN": "Yunnan Sheng", "ZJ": "Zhejiang Sheng", }, + LocalRegions: map[string]string{ + "AH": "安徽省", "MO": "澳门", "BJ": "北京市", + "CQ": "重庆市", "FJ": "福建省", "GS": "甘肃省", + "GD": "广东省", "GX": "广西", "GZ": "贵州省", + "HI": "海南省", "HE": "河北省", "HA": "河南省", + "HL": "黑龙江省", "HB": "湖北省", "HN": "湖南省", + "JL": "吉林省", "JS": "江苏省", "JX": "江西省", + "LN": "辽宁省", "NM": "内蒙古", "NX": "宁夏", + "QH": "青海省", "SD": "山东省", "SX": "山西省", + "SN": "陕西省", "SH": "上海市", "SC": "四川省", + "TW": "台湾", "TJ": "天津市", "XZ": "西藏", + "HK": "香港", "XJ": "新疆", "YN": "云南省", + "ZJ": "浙江省", + }, }, "CO": { Layout: "%1\n%2\n%L, %R, %P", @@ -341,6 +378,7 @@ var formats = map[string]Format{ }, }, "EG": { + Locale: Locale{Language: "ar"}, Layout: "%1\n%2\n%L\n%R\n%P", Required: []Field{FieldLine1, FieldLocality}, PostalCodePattern: "\\d{5}", @@ -355,6 +393,17 @@ var formats = map[string]Format{ "KN": "Qena", "BA": "Red Sea", "SHR": "Sharqia", "SHG": "Sohag", "JS": "South Sinai", "SUZ": "Suez", }, + LocalRegions: map[string]string{ + "ASN": "أسوان", "AST": "أسيوط", "ALX": "الإسكندرية", + "IS": "الإسماعيلية", "LX": "الأقصر", "BA": "البحر الأحمر", + "BH": "البحيرة", "GZ": "الجيزة", "DK": "الدقهلية", + "SUZ": "السويس", "SHR": "الشرقية", "GH": "الغربية", + "FYM": "الفيوم", "C": "القاهرة", "KB": "القليوبية", + "MNF": "المنوفية", "MN": "المنيا", "WAD": "الوادي الجديد", + "BNS": "بني سويف", "PTS": "بورسعيد", "JS": "جنوب سيناء", + "DT": "دمياط", "SHG": "سوهاج", "SIN": "شمال سيناء", + "KN": "قنا", "KFS": "كفر الشيخ", "MT": "مطروح", + }, }, "EH": { Layout: "%1\n%2\n%P %L", @@ -484,16 +533,23 @@ var formats = map[string]Format{ PostalCodePattern: "\\d{4}", }, "HK": { + Locale: Locale{Language: "zh", Script: "Hant"}, Layout: "%1\n%2\n%L\n%R", + LocalLayout: "%R\n%L\n%1\n%2", Required: []Field{FieldLine1, FieldRegion}, RegionType: RegionTypeArea, LocalityType: LocalityTypeDistrict, + // HK areas have no ISO codes assigned. Regions: map[string]string{ - // HK areas have no ISO codes assigned. "Kowloon": "Kowloon", "Hong Kong Island": "Hong Kong Island", "New Territories": "New Territories", }, + LocalRegions: map[string]string{ + "Kowloon": "九龍", + "Hong Kong Island": "香港島", + "New Territories": "新界", + }, }, "HM": { Layout: "%1\n%2\n%L %R %P", @@ -686,7 +742,9 @@ var formats = map[string]Format{ PostalCodePattern: "\\d{5}", }, "JP": { + Locale: Locale{Language: "ja"}, Layout: "%1\n%2\n%L, %R\n%P", + LocalLayout: "〒%P\n%R%L\n%1\n%2", Required: []Field{FieldLine1, FieldRegion, FieldPostalCode}, RegionType: RegionTypePrefecture, PostalCodePattern: "\\d{3}-?\\d{4}", @@ -708,6 +766,24 @@ var formats = map[string]Format{ "16": "Toyama", "30": "Wakayama", "06": "Yamagata", "35": "Yamaguchi", "19": "Yamanashi", }, + LocalRegions: map[string]string{ + "01": "北海道", "02": "青森県", "03": "岩手県", + "04": "宮城県", "05": "秋田県", "06": "山形県", + "07": "福島県", "08": "茨城県", "09": "栃木県", + "10": "群馬県", "11": "埼玉県", "12": "千葉県", + "13": "東京都", "14": "神奈川県", "15": "新潟県", + "16": "富山県", "17": "石川県", "18": "福井県", + "19": "山梨県", "20": "長野県", "21": "岐阜県", + "22": "静岡県", "23": "愛知県", "24": "三重県", + "25": "滋賀県", "26": "京都府", "27": "大阪府", + "28": "兵庫県", "29": "奈良県", "30": "和歌山県", + "31": "鳥取県", "32": "島根県", "33": "岡山県", + "34": "広島県", "35": "山口県", "36": "徳島県", + "37": "香川県", "38": "愛媛県", "39": "高知県", + "40": "福岡県", "41": "佐賀県", "42": "長崎県", + "43": "熊本県", "44": "大分県", "45": "宮崎県", + "46": "鹿児島県", "47": "沖縄県", + }, }, "KE": { Layout: "%1\n%2\n%L\n%P", @@ -735,11 +811,15 @@ var formats = map[string]Format{ RegionType: RegionTypeIsland, }, "KP": { - Layout: "%1\n%2\n%L\n%R, %P", - Required: []Field{FieldLine1, FieldLocality}, + Locale: Locale{Language: "ko"}, + Layout: "%1\n%2\n%L\n%R, %P", + LocalLayout: "%P\n%R\n%L\n%1\n%2", + Required: []Field{FieldLine1, FieldLocality}, }, "KR": { + Locale: Locale{Language: "ko"}, Layout: "%1\n%2\n%S\n%L\n%R\n%P", + LocalLayout: "%R %L%S\n%1\n%2", Required: []Field{FieldLine1, FieldLocality, FieldRegion, FieldPostalCode}, RegionType: RegionTypeDoSi, SublocalityType: SublocalityTypeDistrict, @@ -752,6 +832,14 @@ var formats = map[string]Format{ "45": "Jeollabuk-do", "46": "Jeollanam-do", "50": "Sejong", "11": "Seoul", "31": "Ulsan", }, + LocalRegions: map[string]string{ + "42": "강원", "41": "경기", "48": "경남", + "47": "경북", "29": "광주", "27": "대구", + "30": "대전", "26": "부산", "11": "서울", + "50": "세종", "31": "울산", "28": "인천", + "46": "전남", "45": "전북", "49": "제주", + "44": "충남", "43": "충북", + }, }, "KW": { Layout: "%1\n%2\n%P %L", @@ -1199,6 +1287,36 @@ var formats = map[string]Format{ "VOR": "Voronezhskaya oblast", "YAN": "Yamalo-Nenetskiy avtonomnyy okrug", "YAR": "Yaroslavskaya oblast", "YEV": "Yevreyskaya avtonomnaya oblast", "ZAB": "Zabaykalskiy kray", }, + LocalRegions: map[string]string{ + "ALT": "Алтайский край", "AMU": "Амурская область", "ARK": "Архангельская область", + "AST": "Астраханская область", "BEL": "Белгородская область", "BRY": "Брянская область", + "VLA": "Владимирская область", "VGG": "Волгоградская область", "VLG": "Вологодская область", + "VOR": "Воронежская область", "YEV": "Еврейская автономная область", "ZAB": "Забайкальский край", + "IVA": "Ивановская область", "IRK": "Иркутская область", "KB": "Кабардино-Балкарская Республика", + "KGD": "Калининградская область", "KLU": "Калужская область", "KAM": "Камчатский край", + "KC": "Карачаево-Черкесская Республика", "KEM": "Кемеровская область", "KIR": "Кировская область", + "KOS": "Костромская область", "KDA": "Краснодарский край", "KYA": "Красноярский край", + "KGN": "Курганская область", "KRS": "Курская область", "LEN": "Ленинградская область", + "LIP": "Липецкая область", "MAG": "Магаданская область", "MOW": "Москва", + "MOS": "Московская область", "MUR": "Мурманская область", "NEN": "Ненецкий автономный округ", + "NIZ": "Нижегородская область", "NGR": "Новгородская область", "NVS": "Новосибирская область", + "OMS": "Омская область", "ORE": "Оренбургская область", "ORL": "Орловская область", + "PNZ": "Пензенская область", "PER": "Пермский край", "PRI": "Приморский край", + "PSK": "Псковская область", "AD": "Республика Адыгея", "AL": "Республика Алтай", + "BA": "Республика Башкортостан", "BU": "Республика Бурятия", "DA": "Республика Дагестан", + "IN": "Республика Ингушетия", "KL": "Республика Калмыкия", "KR": "Республика Карелия", + "KO": "Республика Коми", "ME": "Республика Марий Эл", "MO": "Республика Мордовия", + "SA": "Республика Саха (Якутия)", "SE": "Республика Северная Осетия-Алания", "TA": "Республика Татарстан", + "TY": "Республика Тыва", "UD": "Республика Удмуртия", "KK": "Республика Хакасия", + "ROS": "Ростовская область", "RYA": "Рязанская область", "SAM": "Самарская область", + "SPE": "Санкт-Петербург", "SAR": "Саратовская область", "SAK": "Сахалинская область", + "SVE": "Свердловская область", "SMO": "Смоленская область", "STA": "Ставропольский край", + "TAM": "Тамбовская область", "TVE": "Тверская область", "TOM": "Томская область", + "TUL": "Тульская область", "TYU": "Тюменская область", "ULY": "Ульяновская область", + "KHA": "Хабаровский край", "KHM": "Ханты-Мансийский автономный округ", "CHE": "Челябинская область", + "CE": "Чеченская Республика", "CU": "Чувашская Республика", "CHU": "Чукотский автономный округ", + "YAN": "Ямало-Ненецкий автономный округ", "YAR": "Ярославская область", + }, }, "SA": { Layout: "%1\n%2\n%L %P", @@ -1315,7 +1433,9 @@ var formats = map[string]Format{ PostalCodePattern: "TKCA 1ZZ", }, "TH": { + Locale: Locale{Language: "th"}, Layout: "%1\n%2\n%S, %L\n%R %P", + LocalLayout: "%1\n%2\n%S %L\n%R %P", Required: []Field{FieldLine1, FieldLocality}, PostalCodePattern: "\\d{5}", Regions: map[string]string{ @@ -1346,6 +1466,34 @@ var formats = map[string]Format{ "41": "Udon Thani", "61": "Uthai Thani", "53": "Uttaradit", "95": "Yala", "35": "Yasothon", }, + LocalRegions: map[string]string{ + "81": "กระบี่", "10": "กรุงเทพมหานคร", "71": "กาญจนบุรี", + "46": "กาฬสินธุ์", "62": "กำแพงเพชร", "40": "ขอนแก่น", + "38": "จังหวัด บึงกาฬ", "22": "จันทบุรี", "24": "ฉะเชิงเทรา", + "20": "ชลบุรี", "18": "ชัยนาท", "36": "ชัยภูมิ", + "86": "ชุมพร", "57": "เชียงราย", "50": "เชียงใหม่", + "92": "ตรัง", "23": "ตราด", "63": "ตาก", + "26": "นครนายก", "73": "นครปฐม", "48": "นครพนม", + "30": "นครราชสีมา", "80": "นครศรีธรรมราช", "60": "นครสวรรค์", + "12": "นนทบุรี", "96": "นราธิวาส", "55": "น่าน", + "31": "บุรีรัมย์", "13": "ปทุมธานี", "77": "ประจวบคีรีขันธ์", + "25": "ปราจีนบุรี", "94": "ปัตตานี", "14": "พระนครศรีอยุธยา", + "56": "พะเยา", "82": "พังงา", "93": "พัทลุง", + "66": "พิจิตร", "65": "พิษณุโลก", "76": "เพชรบุรี", + "67": "เพชรบูรณ์", "54": "แพร่", "83": "ภูเก็ต", + "44": "มหาสารคาม", "49": "มุกดาหาร", "58": "แม่ฮ่องสอน", + "35": "ยโสธร", "95": "ยะลา", "45": "ร้อยเอ็ด", + "85": "ระนอง", "21": "ระยอง", "70": "ราชบุรี", + "16": "ลพบุรี", "52": "ลำปาง", "51": "ลำพูน", + "42": "เลย", "33": "ศรีสะเกษ", "47": "สกลนคร", + "90": "สงขลา", "91": "สตูล", "11": "สมุทรปราการ", + "75": "สมุทรสงคราม", "74": "สมุทรสาคร", "27": "สระแก้ว", + "19": "สระบุรี", "17": "สิงห์บุรี", "64": "สุโขทัย", + "72": "สุพรรณบุรี", "84": "สุราษฎร์ธานี", "32": "สุรินทร์", + "43": "หนองคาย", "39": "หนองบัวลำภู", "15": "อ่างทอง", + "37": "อำนาจเจริญ", "41": "อุดรธานี", "53": "อุตรดิตถ์", + "61": "อุทัยธานี", "34": "อุบลราชธานี", + }, }, "TJ": { Layout: "%1\n%2\n%P %L", @@ -1403,7 +1551,9 @@ var formats = map[string]Format{ RegionType: RegionTypeIsland, }, "TW": { + Locale: Locale{Language: "zh", Script: "Hant"}, Layout: "%1\n%2\n%L, %R %P", + LocalLayout: "%P\n%R%L\n%1\n%2", Required: []Field{FieldLine1, FieldLocality, FieldRegion, FieldPostalCode}, RegionType: RegionTypeCounty, PostalCodePattern: "\\d{3}(?:\\d{2,3})?", @@ -1417,6 +1567,16 @@ var formats = map[string]Format{ "TTT": "Taitung County", "TAO": "Taoyuan County", "ILA": "Yilan County", "YUN": "Yunlin County", }, + LocalRegions: map[string]string{ + "TXG": "台中市", "TPE": "台北市", "TTT": "台東縣", + "TNN": "台南市", "ILA": "宜蘭縣", "HUA": "花蓮縣", + "KIN": "金門縣", "NAN": "南投縣", "PIF": "屏東縣", + "MIA": "苗栗縣", "TAO": "桃園市", "KHH": "高雄市", + "KEE": "基隆市", "LIE": "連江縣", "YUN": "雲林縣", + "NWT": "新北市", "HSZ": "新竹市", "HSQ": "新竹縣", + "CYI": "嘉義市", "CYQ": "嘉義縣", "CHA": "彰化縣", + "PEN": "澎湖縣", + }, }, "TZ": { Layout: "%1\n%2\n%P %L", @@ -1424,6 +1584,7 @@ var formats = map[string]Format{ PostalCodePattern: "\\d{4,5}", }, "UA": { + Locale: Locale{Language: "uk"}, Layout: "%1\n%2\n%L\n%R\n%P", Required: []Field{FieldLine1, FieldLocality, FieldPostalCode}, RegionType: RegionTypeOblast, @@ -1439,6 +1600,17 @@ var formats = map[string]Format{ "61": "Ternopilska oblast", "05": "Vinnytska oblast", "07": "Volynska oblast", "21": "Zakarpatska oblast", "23": "Zaporizka oblast", "18": "Zhytomyrska oblast", }, + LocalRegions: map[string]string{ + "43": "Автономна Республіка Крим", "05": "Вінницька область", "07": "Волинська область", + "12": "Дніпропетровська область", "14": "Донецька область", "18": "Житомирська область", + "21": "Закарпатська область", "23": "Запорізька область", "26": "Івано-Франківська область", + "30": "Київ", "32": "Київська область", "35": "Кіровоградська область", + "09": "Луганська область", "46": "Львівська область", "48": "Миколаївська область", + "51": "Одеська область", "53": "Полтавська область", "56": "Рівненська область", + "40": "Севастополь", "59": "Сумська область", "61": "Тернопільська область", + "63": "Харківська область", "65": "Херсонська область", "68": "Хмельницька область", + "71": "Черкаська область", "77": "Чернівецька область", "74": "Чернігівська область", + }, }, "UM": { Layout: "%1\n%2\n%L %R %P", @@ -1537,6 +1709,7 @@ var formats = map[string]Format{ PostalCodePattern: "(008(?:(?:[0-4]\\d)|(?:5[01])))(?:[ \\-](\\d{4}))?", }, "VN": { + Locale: Locale{Language: "vi"}, Layout: "%1\n%2\n%L\n%R %P", Required: []Field{FieldLine1, FieldLocality}, PostalCodePattern: "\\d{5}\\d?", @@ -1563,6 +1736,29 @@ var formats = map[string]Format{ "46": "Tien Giang Province", "51": "Tra Vinh Province", "07": "Tuyen Quang Province", "49": "Vinh Long Province", "70": "Vinh Phuc Province", "06": "Yen Bai Province", }, + LocalRegions: map[string]string{ + "44": "An Giang", "43": "Bà Rịa–Vũng Tàu", "55": "Bạc Liêu", + "54": "Bắc Giang", "53": "Bắc Kạn", "56": "Bắc Ninh", + "50": "Bến Tre", "57": "Bình Dương", "31": "Bình Định", + "58": "Bình Phước", "40": "Bình Thuận", "59": "Cà Mau", + "04": "Cao Bằng", "CT": "Cần Thơ", "DN": "Đà Nẵng", + "33": "Đắk Lắk", "72": "Đăk Nông", "71": "Điện Biên", + "39": "Đồng Nai", "45": "Đồng Tháp", "30": "Gia Lai", + "03": "Hà Giang", "63": "Hà Nam", "HN": "Hà Nội", + "23": "Hà Tĩnh", "61": "Hải Dương", "HP": "Hải Phòng", + "73": "Hậu Giang", "14": "Hòa Bình", "66": "Hưng Yên", + "34": "Khánh Hòa", "47": "Kiên Giang", "28": "Kon Tum", + "01": "Lai Châu", "09": "Lạng Sơn", "02": "Lào Cai", + "35": "Lâm Đồng", "41": "Long An", "67": "Nam Định", + "22": "Nghệ An", "18": "Ninh Bình", "36": "Ninh Thuận", + "68": "Phú Thọ", "32": "Phú Yên", "24": "Quảng Bình", + "27": "Quảng Nam", "29": "Quảng Ngãi", "13": "Quảng Ninh", + "25": "Quảng Trị", "52": "Sóc Trăng", "05": "Sơn La", + "37": "Tây Ninh", "20": "Thái Bình", "69": "Thái Nguyên", + "21": "Thanh Hóa", "SG": "Thành phố Hồ Chí Minh", "26": "Thừa Thiên–Huế", + "46": "Tiền Giang", "51": "Trà Vinh", "07": "Tuyên Quang", + "49": "Vĩnh Long", "70": "Vĩnh Phúc", "06": "Yên Bái", + }, }, "WF": { Layout: "%1\n%2\n%P %L", diff --git a/locale.go b/locale.go new file mode 100644 index 0000000..0d83ee4 --- /dev/null +++ b/locale.go @@ -0,0 +1,59 @@ +// Copyright (c) 2020 Bojan Zivanovic and contributors +// SPDX-License-Identifier: MIT + +package address + +import "strings" + +// Locale represents a Unicode locale identifier. +type Locale struct { + Language string + Script string + Territory string +} + +// NewLocale creates a new Locale from its string representation. +func NewLocale(id string) Locale { + // Normalize the ID ("SR_rs_LATN" => "sr-Latn-RS"). + id = strings.ToLower(id) + id = strings.ReplaceAll(id, "_", "-") + locale := Locale{} + for i, part := range strings.Split(id, "-") { + if i == 0 { + locale.Language = part + continue + } + partLen := len(part) + if partLen == 4 { + locale.Script = strings.Title(part) + continue + } + if partLen == 2 || partLen == 3 { + locale.Territory = strings.ToUpper(part) + continue + } + } + + return locale +} + +// String returns the string representation of l. +func (l Locale) String() string { + b := strings.Builder{} + b.WriteString(l.Language) + if l.Script != "" { + b.WriteString("-") + b.WriteString(l.Script) + } + if l.Territory != "" { + b.WriteString("-") + b.WriteString(l.Territory) + } + + return b.String() +} + +// IsEmpty returns whether l is empty. +func (l Locale) IsEmpty() bool { + return l.Language == "" && l.Script == "" && l.Territory == "" +} diff --git a/locale_test.go b/locale_test.go new file mode 100644 index 0000000..857e61c --- /dev/null +++ b/locale_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2020 Bojan Zivanovic and contributors +// SPDX-License-Identifier: MIT + +package address_test + +import ( + "testing" + + "github.com/bojanz/address" +) + +func TestNewLocale(t *testing.T) { + tests := []struct { + id string + want address.Locale + }{ + {"", address.Locale{}}, + {"de", address.Locale{Language: "de"}}, + {"de-CH", address.Locale{Language: "de", Territory: "CH"}}, + {"es-419", address.Locale{Language: "es", Territory: "419"}}, + {"sr-Cyrl", address.Locale{Language: "sr", Script: "Cyrl"}}, + {"sr-Latn-RS", address.Locale{Language: "sr", Script: "Latn", Territory: "RS"}}, + {"yue-Hans", address.Locale{Language: "yue", Script: "Hans"}}, + // ID with the wrong case, ordering, delimeter. + {"SR_rs_LATN", address.Locale{Language: "sr", Script: "Latn", Territory: "RS"}}, + // ID with a variant. Variants are unsupported and ignored. + {"ca-ES-VALENCIA", address.Locale{Language: "ca", Territory: "ES"}}, + } + for _, tt := range tests { + t.Run(tt.id, func(t *testing.T) { + got := address.NewLocale(tt.id) + if got != tt.want { + t.Errorf("got %v, want %v", got, tt.want) + } + }) + } +} + +func TestLocale_String(t *testing.T) { + tests := []struct { + locale address.Locale + want string + }{ + {address.Locale{}, ""}, + {address.Locale{Language: "de"}, "de"}, + {address.Locale{Language: "de", Territory: "CH"}, "de-CH"}, + {address.Locale{Language: "sr", Script: "Cyrl"}, "sr-Cyrl"}, + {address.Locale{Language: "sr", Script: "Latn", Territory: "RS"}, "sr-Latn-RS"}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + id := tt.locale.String() + if id != tt.want { + t.Errorf("got %v, want %v", id, tt.want) + } + }) + } +} + +func TestLocale_IsEmpty(t *testing.T) { + tests := []struct { + locale address.Locale + want bool + }{ + {address.Locale{}, true}, + {address.Locale{Language: "de"}, false}, + {address.Locale{Language: "de", Territory: "CH"}, false}, + {address.Locale{Language: "sr", Script: "Cyrl"}, false}, + {address.Locale{Language: "sr", Script: "Latn", Territory: "RS"}, false}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + empty := tt.locale.IsEmpty() + if empty != tt.want { + t.Errorf("got %v, want %v", empty, tt.want) + } + }) + } +}