1 module i18n.culture;
2 
3 private {
4     string[] _countryCodes;
5     string[string] _countryNames;
6 
7     string[] _languageCodes;
8     string[string] _languageNames;
9     string[string] _languageNativeNames;
10 
11     string _getLangCode(string code) {
12         import std.uni : toLower;
13         import std.algorithm.searching : canFind;
14 
15         // Impossible case
16         if (code.length < 2) return null;
17         
18         // C = no language set
19         if (code == "C") return code;
20 
21         // Lang codes are always first, so check the 2 first code digits
22         string codecv = code[0..2].toLower();
23         if (_languageCodes.canFind(codecv)) return codecv;
24         return null;
25     }
26 
27     string _getCountryCode(string code) {
28         import std.string : indexOf;
29         import std.uni : toUpper;
30         import std.algorithm.searching : canFind;
31 
32         // Maybe it's just a country code?
33         if (code.length == 2) {
34             if (_countryCodes.canFind(code.toUpper)) return null;
35 
36             return code;
37         }
38         
39         // Check for the 2 types of seperators
40         ptrdiff_t sep = code.indexOf("_");
41         if (sep == -1) sep = code.indexOf("-");
42         if (sep == -1 && code.length == 2) return code;
43 
44         // No seperators, no single-country code
45         return null;
46     }
47 }
48 
49 /*
50     Initialize internal culture info
51 */
52 static this() {
53     _countryCodes = [
54         "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT",
55         "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI",
56         "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY",
57         "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN",
58         "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DO",
59         "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM",
60         "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM",
61         "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN",
62         "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS",
63         "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP",
64         "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT",
65         "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML",
66         "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX",
67         "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR",
68         "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN",
69         "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA",
70         "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN",
71         "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG",
72         "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW",
73         "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI",
74         "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"
75     ];
76 
77     _countryNames = [
78         "AD": "Andorra",
79         "AE": "United Arab Emirates",
80         "AF": "Afghanistan",
81         "AG": "Antigua and Barbuda",
82         "AI": "Anguilla",
83         "AL": "Albania",
84         "AM": "Armenia",
85         "AO": "Angola",
86         "AQ": "Antarctica",
87         "AR": "Argentina",
88         "AS": "American Samoa",
89         "AT": "Austria",
90         "AU": "Australia",
91         "AW": "Aruba",
92         "AX": "Aaland Islands",
93         "AZ": "Azerbaijan",
94         "BA": "Bosnia & Herzegovina",
95         "BB": "Barbados",
96         "BD": "Bangladesh",
97         "BE": "Belgium",
98         "BF": "Burkina Faso",
99         "BG": "Bulgaria",
100         "BH": "Bahrain",
101         "BI": "Burundi",
102         "BJ": "Benin",
103         "BL": "Saint Barthelemy",
104         "BM": "Bermuda",
105         "BN": "Brunei Darussalam",
106         "BO": "Bolivia",
107         "BQ": "Bonaire, Sint Eustatius & Saba",
108         "BR": "Brazil",
109         "BS": "Bahamas",
110         "BT": "Bhutan",
111         "BV": "Bouvet Island",
112         "BW": "Botswana",
113         "BY": "Belarus",
114         "BZ": "Belize",
115         "CA": "Canada",
116         "CC": "Cocos Islands",
117         "CD": "Democratic Republic of Congo",
118         "CF": "Central African Republic",
119         "CG": "Congo",
120         "CH": "Switzerland",
121         "CI": "Côte d'Ivoire",
122         "CK": "Cook Islands",
123         "CL": "Chile",
124         "CM": "Cameroon",
125         "CN": "China",
126         "CO": "Colombia",
127         "CR": "Costa Rica",
128         "CU": "Cuba",
129         "CV": "Cape Verde",
130         "CW": "Curaçao",
131         "CX": "Christmas Island",
132         "CY": "Cyprus",
133         "CZ": "Czech Republic",
134         "DE": "Germany",
135         "DJ": "Djibouti",
136         "DK": "Denmark",
137         "DM": "Dominica",
138         "DO": "Dominican Republic",
139         "DZ": "Algeria",
140         "EC": "Ecuador",
141         "EE": "Estonia",
142         "EG": "Egypt",
143         "EH": "Sahrawi Arab Democratic Republic",
144         "ER": "Eritrea",
145         "ES": "Spain",
146         "ET": "Ethiopia",
147         "FI": "Finland",
148         "FJ": "Fiji",
149         "FK": "Falkland Islands (Malvinas)",
150         "FM": "Federated States of Micronesia",
151         "FO": "Faroe Islands",
152         "FR": "France",
153         "GA": "Gabon",
154         "GB": "United Kingdom",
155         "GD": "Grenada",
156         "GE": "Georgia",
157         "GF": "French Guiana",
158         "GG": "Guernsey",
159         "GH": "Ghana",
160         "GI": "Gibraltar",
161         "GL": "Greenland",
162         "GM": "Gambia",
163         "GN": "Guinea",
164         "GP": "Guadaloupe",
165         "GQ": "Equatorial Guinea",
166         "GR": "Greece",
167         "GS": "South Georgia & South Sandwich Islands",
168         "GT": "Guatemala",
169         "GU": "Guam",
170         "GW": "Guinea-Bissau",
171         "GY": "Guyana",
172         "HK": "Hong Kong",
173         "HM": "Heard Island & McDonald Islands",
174         "HN": "Honduras",
175         "HR": "Croatia",
176         "HT": "Haiti",
177         "HU": "Hungary",
178         "ID": "Indonesia",
179         "IE": "Ireland",
180         "IL": "Israel",
181         "IM": "Isle of Man",
182         "IN": "India",
183         "IO": "British Indian Ocean Territory",
184         "IQ": "Iraq",
185         "IR": "Islamic Republic of Iran",
186         "IS": "Iceland",
187         "IT": "Italy",
188         "JE": "Jersey",
189         "JM": "Jamaica",
190         "JO": "Jordan",
191         "JP": "Japan",
192         "KE": "Kenya",
193         "KG": "Kyrgyzstan",
194         "KH": "Cambodia",
195         "KI": "Kiribati",
196         "KM": "Comoros",
197         "KN": "Saint Kitts & Nevis",
198         "KP": "Democratic People's Republic of Korea",
199         "KR": "Republic of Korea",
200         "KW": "Kuwait",
201         "KY": "Cayman Islands",
202         "KZ": "Kazakhstan",
203         "LA": "Lao People's Democratic Republic",
204         "LB": "Lebalon",
205         "LC": "Saint Lucia",
206         "LI": "Lichtenstein",
207         "LK": "Sri Lanka",
208         "LR": "Liberia",
209         "LS": "Lesotho",
210         "LT": "Lithuania",
211         "LU": "Luxembourg",
212         "LV": "Latvia",
213         "LY": "Libya",
214         "MA": "Morocco",
215         "MC": "Monaco",
216         "MD": "Republic of Moldova",
217         "ME": "Montenegro",
218         "MF": "Saint Martin (French part)",
219         "MG": "Madagascar",
220         "MH": "Marshall Islands",
221         "MK": "North Macedonia",
222         "ML": "Mali",
223         "MM": "Myanmar",
224         "MN": "Mongolia",
225         "MO": "Macao",
226         "MP": "Northen Marina Islands",
227         "MQ": "Martinique",
228         "MR": "Mauritania",
229         "MS": "Montserrat",
230         "MT": "Malta",
231         "MU": "Mauritius",
232         "MV": "Maldives",
233         "MW": "Malawi",
234         "MX": "Mexico",
235         "MY": "Malaysia",
236         "MZ": "Mozambique",
237         "NA": "Namibia",
238         "NC": "New Caledonia",
239         "NE": "Niger",
240         "NF": "Norfolk Island",
241         "NG": "Nigeria",
242         "NI": "Nicaragua",
243         "NL": "Netherlands",
244         "NO": "Norway",
245         "NP": "Nepal",
246         "NR": "Nauru",
247         "NU": "Niue",
248         "NZ": "New Zealand",
249         "OM": "Oman",
250         "PA": "Panama",
251         "PE": "Peru",
252         "PF": "French Polynesia",
253         "PG": "Papua New Guinea",
254         "PH": "Phillipines",
255         "PK": "Pakistan",
256         "PL": "Poland",
257         "PM": "Saint Pierre & Miquelon",
258         "PN": "Pitcairn",
259         "PR": "Puerto Rico",
260         "PS": "Palestine",
261         "PT": "Portugal",
262         "PW": "Palau",
263         "PY": "Paraguay",
264         "QA": "Qatar",
265         "RE": "Reunion",
266         "RO": "Romania",
267         "RS": "Serbia",
268         "RU": "Russian Federation",
269         "RW": "Rwanda",
270         "SA": "Saudi Arabia",
271         "SB": "Solomon Islands",
272         "SC": "Seychelles",
273         "SD": "Sudan",
274         "SE": "Sweden",
275         "SG": "Singapore",
276         "SH": "Saint Helena, Ascension & Tristan da Cunha",
277         "SI": "Slovenia",
278         "SJ": "Svalbard & Jan Mayen",
279         "SK": "Slovakia",
280         "SL": "Sierra Leone",
281         "SM": "San Marino",
282         "SN": "Senegal",
283         "SO": "Somalia",
284         "SR": "Suriname",
285         "SS": "South Sudan",
286         "ST": "Sao Tome & Principe",
287         "SV": "El Salvador",
288         "SX": "Sint Maarten (Dutch part)",
289         "SY": "Syrian Arab Republic",
290         "SZ": "Swaziland",
291         "TC": "Turks & Caicos Islands",
292         "TD": "Chad",
293         "TF": "French Southern Territories",
294         "TG": "Togo",
295         "TH": "Thailand",
296         "TJ": "Tajikistan",
297         "TK": "Tokelau",
298         "TL": "Timor-Leste",
299         "TM": "Turkmenistan",
300         "TN": "Tunisia",
301         "TO": "Tonga",
302         "TR": "Turkey",
303         "TT": "Trinidad & Tobago",
304         "TV": "Tuvalu",
305         "TW": "Taiwan",
306         "TZ": "United Republic of Tanzania",
307         "UA": "Ukraine",
308         "UG": "Uganda",
309         "UM": "United States Minor Outlying Islands",
310         "US": "United States",
311         "UY": "Uruguay",
312         "UZ": "Uzbekistan",
313         "VA": "Holy See (Vatican City State)",
314         "VC": "Saint Vincent & Grenadines",
315         "VE": "Bolivarian Republic of Venezuela",
316         "VG": "British Virgin Islands",
317         "VI": "US Virgin Islands",
318         "VN": "Vietnam",
319         "VU": "Vanuatu",
320         "WF": "Wallis & Futuna",
321         "WS": "Samoa",
322         "YE": "Yemen",
323         "YT": "Mayotte",
324         "ZA": "South Africa",
325         "ZM": "Zambia",
326         "ZW": "Zimbabwe"
327     ];
328 
329     _languageCodes = [
330         "aa", "ab", "ae", "af", "ak", "am", "an", "ar", "as", "av", "ay", "az",
331         "ba", "be", "bg", "bh", "bi", "bm", "bn", "bo", "br", "bs", "ca", "ce",
332         "ch", "co", "cr", "cs", "cu", "cv", "cy", "da", "de", "dv", "dz", "ee",
333         "el", "en", "eo", "es", "et", "eu", "fa", "ff", "fi", "fj", "fo", "fr",
334         "fy", "ga", "gd", "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr",
335         "ht", "hu", "hy", "hz", "ia", "id", "ie", "ig", "ii", "ik", "io", "is",
336         "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", "kk", "kl", "km", "kn",
337         "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la", "lb", "lg", "li", "ln",
338         "lo", "lt", "lu", "lv", "mg", "mh", "mi", "mk", "ml", "mn", "mr", "ms",
339         "mt", "my", "na", "nb", "nd", "ne", "ng", "nl", "nn", "no", "nr", "nv",
340         "ny", "oc", "oj", "om", "or", "os", "pa", "pi", "pl", "ps", "pt", "qu",
341         "rm", "rn", "ro", "ru", "rw", "sa", "sc", "sd", "se", "sg", "si", "sk",
342         "sl", "sm", "sn", "so", "sq", "sr", "ss", "st", "su", "sv", "sw", "ta",
343         "te", "tg", "th", "ti", "tk", "tl", "tn", "to", "tr", "ts", "tt", "tw",
344         "ty", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "wa", "wo", "xh", "yi",
345         "yo", "za", "zh", "zu"
346     ];
347 
348     _languageNames = [
349         "aa": "Afar",
350         "ab": "Abkahzian",
351         "ae": "Avestan",
352         "af": "Afrikaans",
353         "ak": "Akan",
354         "am": "Amharic",
355         "an": "Aragonese",
356         "ar": "Arabic",
357         "as": "Assamese",
358         "av": "Avaric",
359         "ay": "Aymara",
360         "az": "Azerbaijani",
361         "ba": "Bashkir",
362         "be": "Belarusian",
363         "bg": "Bulgarian",
364         "bh": "Bihari",
365         "bi": "Bislama",
366         "bm": "Bambara",
367         "bn": "Bengali",
368         "bo": "Tibetan",
369         "br": "Breton",
370         "bs": "Bosnian",
371         "ca": "Catlan",
372         "ce": "Chechen",
373         "ch": "Chamorro",
374         "co": "Corsican",
375         "cr": "Cree",
376         "cs": "Czech",
377         "cu": "Church Slavic",
378         "cv": "Chuvash",
379         "cy": "Welsh",
380         "da": "Danish",
381         "de": "German",
382         "dv": "Divehi",
383         "dz": "Dzongkha",
384         "ee": "Ewe",
385         "el": "Greek",
386         "en": "English",
387         "eo": "Esperanto",
388         "es": "Spanish",
389         "et": "Estonian",
390         "eu": "Basque",
391         "fa": "Persian",
392         "ff": "Fulah",
393         "fi": "Finnish",
394         "fj": "Fijian",
395         "fo": "Faroese",
396         "fr": "French",
397         "fy": "Western Frisian",
398         "ga": "Irish",
399         "gd": "Gaelic",
400         "gl": "Galacian",
401         "gn": "Guarani",
402         "gu": "Gujarati",
403         "gv": "Manx",
404         "ha": "Hausa",
405         "he": "Hebrew",
406         "hi": "Hindi",
407         "ho": "Hiri Motu",
408         "hr": "Croatian",
409         "ht": "Haitian",
410         "hu": "Hungarian",
411         "hy": "Armenian",
412         "hz": "Herero",
413         "ia": "Interlingua",
414         "id": "Indonesian",
415         "ie": "Interlingue",
416         "ig": "Igbo",
417         "ii": "Sichuan Yi",
418         "ik": "Inupiak",
419         "io": "Ido",
420         "is": "Icelandic",
421         "it": "Italian",
422         "iu": "Inuktitut",
423         "ja": "Japanese",
424         "jv": "Javanese",
425         "ka": "Georgian",
426         "kg": "Kongo",
427         "ki": "Kikuyu",
428         "kj": "Kuanyama",
429         "kk": "Kazakh",
430         "kl": "Greenlandic",
431         "km": "Central Khmer",
432         "kn": "Kannada",
433         "ko": "Korean",
434         "kr": "Kanuri",
435         "ks": "Kashmiri",
436         "ku": "Kurdish",
437         "kv": "Komi",
438         "kw": "Cornish",
439         "ky": "Kirghiz",
440         "la": "Latin",
441         "lb": "Luxembourgish",
442         "lg": "Ganda",
443         "li": "Limburgish",
444         "ln": "Lingala",
445         "lo": "Lao",
446         "lt": "Lithuanian",
447         "lu": "Luba-Katanga",
448         "lv": "Latvian",
449         "mg": "Malagasy",
450         "mh": "Marshallese",
451         "mi": "Maori",
452         "mk": "Macedonian",
453         "ml": "Malayalam",
454         "mn": "Mongolian",
455         "mr": "Marathi",
456         "ms": "Malay",
457         "mt": "Maltese",
458         "my": "Burmese",
459         "na": "Nauru",
460         "nb": "Norwegian Bokmål",
461         "nd": "North Ndebele",
462         "ne": "Nepali",
463         "ng": "Ndonga",
464         "nl": "Dutch",
465         "nn": "Norwegian Nynorsk",
466         "no": "Norwegian",
467         "nr": "South Ndebele",
468         "nv": "Navajo",
469         "ny": "Chichewa",
470         "oc": "Occitan",
471         "oj": "Ojibwa",
472         "om": "Omoro",
473         "or": "Oriya",
474         "os": "Ossetian",
475         "pa": "Panjabi",
476         "pi": "Pali",
477         "pl": "Polish",
478         "ps": "Pushto",
479         "pt": "Portugese",
480         "qu": "Quechua",
481         "rm": "Romansh",
482         "rn": "Rundi",
483         "ro": "Romanian",
484         "ru": "Russian",
485         "rw": "Kinyarwanda",
486         "sa": "Sanskrit",
487         "sc": "Sardinian",
488         "sd": "Sindhi",
489         "se": "Northen Sami",
490         "sg": "Sango",
491         "si": "Sinhala",
492         "sk": "Slovak",
493         "sl": "Slovenian",
494         "sm": "Samoan",
495         "sn": "Shona",
496         "so": "Somali",
497         "sq": "Albanian",
498         "sr": "Serbian",
499         "ss": "Swati",
500         "st": "Southern Sotho",
501         "su": "Sundanese",
502         "sv": "Swedish",
503         "sw": "Swahili",
504         "ta": "Tamil",
505         "te": "Tegulu",
506         "tg": "Tajik",
507         "th": "Thai",
508         "ti": "Tigrinya",
509         "tk": "Turkmen",
510         "tl": "Tagalog",
511         "tn": "Tswana",
512         "to": "Tonga",
513         "tr": "Turkish",
514         "ts": "Tsonga",
515         "tt": "Tatar",
516         "tw": "Twi",
517         "ty": "Tahitian",
518         "ug": "Ughur",
519         "uk": "Ukranian",
520         "ur": "Urdu",
521         "uz": "Uzbek",
522         "ve": "Venda",
523         "vi": "Vietnamese",
524         "vo": "Volapük",
525         "wa": "Walloon",
526         "wo": "Wolof",
527         "xh": "Xhosa",
528         "yi": "Yiddish",
529         "yo": "Yoruba",
530         "za": "Zhuang",
531         "zh": "Chinese",
532         "zu": "Zulu"
533     ];
534 
535     // _languageNativeNames = [
536     // ];
537 
538     // Set to user preferred locale
539     i18nResetLocale();
540 }
541 
542 /**
543     Validates a country code
544 
545     Returns true if the code is a valid culture code
546     Returns false if the code is not a valid culture code
547 
548     TODO: Validate whether encoding spec is correct
549 */
550 bool i18nValidateCultureCode(bool caseSensitive = true)(string code) {
551         
552     // Special case, C = no language set.
553     if (code == "C") return true;
554 
555     // Make sure we don't crash when handling lang/country get
556     // by escaping early if we have too few characters to work with
557     if (code.length < 2) return false;
558 
559     import std.uni : toUpper, toLower;
560     import std.algorithm.searching : canFind;
561     string lang = code[0..2];
562     string country = code.length >= 5 ? code[3..5] : "";
563 
564     if (code.length == 2) {
565         // Language code only
566 
567         // The main validity test
568         static if (!caseSensitive) return _languageCodes.canFind(lang.toLower);
569         else return _languageCodes.canFind(lang);
570 
571     } else if (code.length >= 5) {
572         // Country AND language code
573         
574         // Make sure we either use a MS or gettext seperator
575         if (!(code[2] == '-' || code[2] == '_')) return false;
576         
577         // The main validity test
578         static if (!caseSensitive) return _languageCodes.canFind(lang.toLower) && _countryCodes.canFind(country.toUpper);
579         else return _languageCodes.canFind(lang) && _countryCodes.canFind(country);
580 
581     }
582     return false;
583 }
584 
585 @("i18nValidateCultureCode Case Sensitive")
586 unittest {
587     assert(i18nValidateCultureCode("da")); // Should succeed
588     assert(i18nValidateCultureCode("da_DK")); // Should succeed
589     assert(i18nValidateCultureCode("de-DE")); // Should succeed
590 
591     assert(!i18nValidateCultureCode("daDK")); // Should fail
592     assert(!i18nValidateCultureCode("daaaaaDK")); // Should fail
593     assert(!i18nValidateCultureCode("da/DK")); // Should fail
594     assert(!i18nValidateCultureCode("zz_CH")); // Should fail
595     assert(!i18nValidateCultureCode("ch_ZZ")); // Should fail
596 }
597 
598 @("i18nValidateCultureCode Case Insensitive")
599 unittest {
600     assert(i18nValidateCultureCode!false("da")); // Should succeed
601     assert(i18nValidateCultureCode!false("da_dk")); // Should succeed
602     assert(i18nValidateCultureCode!false("da_DK")); // Should succeed
603     assert(i18nValidateCultureCode!false("en-US")); // Should succeed
604     assert(i18nValidateCultureCode!false("en-us")); // Should succeed
605 
606     assert(!i18nValidateCultureCode!false("daDK")); // Should fail
607     assert(!i18nValidateCultureCode!false("daaaaaDK")); // Should fail
608     assert(!i18nValidateCultureCode!false("da/DK")); // Should fail
609     assert(!i18nValidateCultureCode!false("zz_CH")); // Should fail
610     assert(!i18nValidateCultureCode!false("ch_ZZ")); // Should fail
611 }
612 
613 /**
614     Gets the (english) name of the language associated with a culture code
615 
616     Returns null if code is invalid
617 */
618 string i18nGetCultureLanguage(string code) {
619     if (!i18nValidateCultureCode(code)) return null;
620     if (code[0..2] !in _languageNames) return null;
621     return _languageNames[code[0..2]];
622 }
623 
624 @("i18nGetCultureLanguage")
625 unittest {
626     assert(i18nGetCultureLanguage("da") == "Danish");
627     assert(i18nGetCultureLanguage("de") == "German");
628     assert(i18nGetCultureLanguage("en") == "English");
629     assert(i18nGetCultureLanguage("ja") == "Japanese");
630 }
631 
632 /**
633     Gets the country associated with a culture code
634 */
635 string i18nGetCultureCountry(string code) {
636     if (!i18nValidateCultureCode(code)) return null;
637     if (code.length < 5) return null;
638     if (code[$-2..5] !in _countryNames) return null;
639     return _countryNames[code[5-2..5]];
640 }
641 
642 @("i18nGetCultureCountry")
643 unittest {
644     assert(i18nGetCultureCountry("da_DK") == "Denmark");
645     assert(i18nGetCultureCountry("en_US") == "United States");
646     assert(i18nGetCultureCountry("en_GB") == "United Kingdom");
647     assert(i18nGetCultureCountry("de_DE") == "Germany");
648     assert(i18nGetCultureCountry("ja_JP") == "Japan");
649 }
650 
651 /**
652     Gets the current locale.
653 */
654 string i18nGetLocale() {
655     import core.stdc.locale : setlocale, LC_ALL;
656     import std.string : fromStringz;
657     string locale = cast(string)setlocale(LC_ALL, null).fromStringz.idup;
658     
659     // TODO: Implement
660     return locale;
661 }
662 
663 @("i18nGetLocale")
664 unittest {
665     assert(i18nValidateCultureCode(i18nGetLocale()));
666 }
667 
668 /**
669     Sets the locale for the app.
670 */
671 void i18nSetLocale(string locale) {
672     import core.stdc.locale : setlocale, LC_ALL;
673     import core.stdc.string : memcpy;
674     import core.stdc.stdlib : malloc;
675     import std.string : toStringz, fromStringz;
676 
677     if (locale == "C") {
678         setlocale(LC_ALL, "C");
679     } else if (locale == "") {
680         setlocale(LC_ALL, "");
681     } else setlocale(LC_ALL, (locale).toStringz);
682 }
683 
684 void i18nResetLocale() {
685     import core.stdc.locale : setlocale, LC_ALL;
686     setlocale(LC_ALL, "");
687 }
688 
689 struct LocaleConv {
690     string decimalPoint;
691     string thousandSep;
692     string grouping;
693     string intCurrSymbol;
694     string currencySymbol;
695     string monDecimalPoint;
696     string monThousandsSep;
697     string monGrouping;
698     string positiveSign;
699     string negativeSign;
700     byte intFracDigits;
701     byte fracDigits;
702     byte pCSPrecedes;
703     byte pSepBySpace;
704     byte nCSPrecedes;
705     byte nSepBySpace;
706     byte pSignPosN;
707     byte nSignPosN;
708     byte intPCSPrecedes;
709     byte intP_sep_by_space;
710     byte intNCSPrecedes;
711     byte intNSepBySpace;
712     byte intPSignPosN;
713     byte intNSignPosN;
714 }
715 
716 /**
717     Returns the locale's conversion units.
718 */
719 LocaleConv i18nGetLocaleConversions() {
720     import core.stdc.locale : localeconv, lconv;
721     import std.string : fromStringz;
722     lconv* conv = localeconv();
723     return LocaleConv(
724         cast(string)conv.decimal_point.fromStringz,
725         cast(string)conv.thousands_sep.fromStringz,
726         cast(string)conv.grouping.fromStringz,
727         cast(string)conv.int_curr_symbol.fromStringz,
728         cast(string)conv.currency_symbol.fromStringz,
729         cast(string)conv.mon_decimal_point.fromStringz,
730         cast(string)conv.mon_thousands_sep.fromStringz,
731         cast(string)conv.mon_grouping.fromStringz,
732         cast(string)conv.positive_sign.fromStringz,
733         cast(string)conv.negative_sign.fromStringz,
734         conv.int_frac_digits,
735         conv.frac_digits,
736         conv.p_cs_precedes,
737         conv.p_sep_by_space,
738         conv.n_cs_precedes,
739         conv.n_sep_by_space,
740         conv.p_sign_posn,
741         conv.n_sign_posn,
742         conv.int_p_cs_precedes,
743         conv.int_p_sep_by_space,
744         conv.int_n_cs_precedes,
745         conv.int_n_sep_by_space,
746         conv.int_p_sign_posn,
747         conv.int_n_sign_posn
748     );
749 }