diff --git a/resources/dictionaries/countries.json b/resources/dictionaries/countries.json
new file mode 100644
index 0000000000..41592b0d81
--- /dev/null
+++ b/resources/dictionaries/countries.json
@@ -0,0 +1 @@
+[{"name":"Afghanistan","iso3":"AFG","iso2":"AF","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\udde6\ud83c\uddeb"},{"name":"Aland Islands","iso3":"ALA","iso2":"AX","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\udde6\ud83c\uddfd"},{"name":"Albania","iso3":"ALB","iso2":"AL","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\udde6\ud83c\uddf1"},{"name":"Algeria","iso3":"DZA","iso2":"DZ","region":"Africa","subregion":"Northern Africa","emoji":"\ud83c\udde9\ud83c\uddff"},{"name":"American Samoa","iso3":"ASM","iso2":"AS","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\udde6\ud83c\uddf8"},{"name":"Andorra","iso3":"AND","iso2":"AD","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\udde6\ud83c\udde9"},{"name":"Angola","iso3":"AGO","iso2":"AO","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\udde6\ud83c\uddf4"},{"name":"Anguilla","iso3":"AIA","iso2":"AI","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde6\ud83c\uddee"},{"name":"Antarctica","iso3":"ATA","iso2":"AQ","region":"Polar","subregion":"","emoji":"\ud83c\udde6\ud83c\uddf6"},{"name":"Antigua And Barbuda","iso3":"ATG","iso2":"AG","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde6\ud83c\uddec"},{"name":"Argentina","iso3":"ARG","iso2":"AR","region":"Americas","subregion":"South America","emoji":"\ud83c\udde6\ud83c\uddf7"},{"name":"Armenia","iso3":"ARM","iso2":"AM","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\udde6\ud83c\uddf2"},{"name":"Aruba","iso3":"ABW","iso2":"AW","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde6\ud83c\uddfc"},{"name":"Australia","iso3":"AUS","iso2":"AU","region":"Oceania","subregion":"Australia and New Zealand","emoji":"\ud83c\udde6\ud83c\uddfa"},{"name":"Austria","iso3":"AUT","iso2":"AT","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\udde6\ud83c\uddf9"},{"name":"Azerbaijan","iso3":"AZE","iso2":"AZ","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\udde6\ud83c\uddff"},{"name":"Bahamas The","iso3":"BHS","iso2":"BS","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde7\ud83c\uddf8"},{"name":"Bahrain","iso3":"BHR","iso2":"BH","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\udde7\ud83c\udded"},{"name":"Bangladesh","iso3":"BGD","iso2":"BD","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\udde7\ud83c\udde9"},{"name":"Barbados","iso3":"BRB","iso2":"BB","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde7\ud83c\udde7"},{"name":"Belarus","iso3":"BLR","iso2":"BY","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\udde7\ud83c\uddfe"},{"name":"Belgium","iso3":"BEL","iso2":"BE","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\udde7\ud83c\uddea"},{"name":"Belize","iso3":"BLZ","iso2":"BZ","region":"Americas","subregion":"Central America","emoji":"\ud83c\udde7\ud83c\uddff"},{"name":"Benin","iso3":"BEN","iso2":"BJ","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\udde7\ud83c\uddef"},{"name":"Bermuda","iso3":"BMU","iso2":"BM","region":"Americas","subregion":"Northern America","emoji":"\ud83c\udde7\ud83c\uddf2"},{"name":"Bhutan","iso3":"BTN","iso2":"BT","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\udde7\ud83c\uddf9"},{"name":"Bolivia","iso3":"BOL","iso2":"BO","region":"Americas","subregion":"South America","emoji":"\ud83c\udde7\ud83c\uddf4"},{"name":"Bonaire, Sint Eustatius and Saba","iso3":"BES","iso2":"BQ","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde7\ud83c\uddf6"},{"name":"Bosnia and Herzegovina","iso3":"BIH","iso2":"BA","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\udde7\ud83c\udde6"},{"name":"Botswana","iso3":"BWA","iso2":"BW","region":"Africa","subregion":"Southern Africa","emoji":"\ud83c\udde7\ud83c\uddfc"},{"name":"Bouvet Island","iso3":"BVT","iso2":"BV","region":"","subregion":"","emoji":"\ud83c\udde7\ud83c\uddfb"},{"name":"Brazil","iso3":"BRA","iso2":"BR","region":"Americas","subregion":"South America","emoji":"\ud83c\udde7\ud83c\uddf7"},{"name":"British Indian Ocean Territory","iso3":"IOT","iso2":"IO","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddee\ud83c\uddf4"},{"name":"Brunei","iso3":"BRN","iso2":"BN","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\udde7\ud83c\uddf3"},{"name":"Bulgaria","iso3":"BGR","iso2":"BG","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\udde7\ud83c\uddec"},{"name":"Burkina Faso","iso3":"BFA","iso2":"BF","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\udde7\ud83c\uddeb"},{"name":"Burundi","iso3":"BDI","iso2":"BI","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\udde7\ud83c\uddee"},{"name":"Cambodia","iso3":"KHM","iso2":"KH","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf0\ud83c\udded"},{"name":"Cameroon","iso3":"CMR","iso2":"CM","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\udde8\ud83c\uddf2"},{"name":"Canada","iso3":"CAN","iso2":"CA","region":"Americas","subregion":"Northern America","emoji":"\ud83c\udde8\ud83c\udde6"},{"name":"Cape Verde","iso3":"CPV","iso2":"CV","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\udde8\ud83c\uddfb"},{"name":"Cayman Islands","iso3":"CYM","iso2":"KY","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf0\ud83c\uddfe"},{"name":"Central African Republic","iso3":"CAF","iso2":"CF","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\udde8\ud83c\uddeb"},{"name":"Chad","iso3":"TCD","iso2":"TD","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\uddf9\ud83c\udde9"},{"name":"Chile","iso3":"CHL","iso2":"CL","region":"Americas","subregion":"South America","emoji":"\ud83c\udde8\ud83c\uddf1"},{"name":"China","iso3":"CHN","iso2":"CN","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\udde8\ud83c\uddf3"},{"name":"Christmas Island","iso3":"CXR","iso2":"CX","region":"Oceania","subregion":"Australia and New Zealand","emoji":"\ud83c\udde8\ud83c\uddfd"},{"name":"Cocos (Keeling) Islands","iso3":"CCK","iso2":"CC","region":"Oceania","subregion":"Australia and New Zealand","emoji":"\ud83c\udde8\ud83c\udde8"},{"name":"Colombia","iso3":"COL","iso2":"CO","region":"Americas","subregion":"South America","emoji":"\ud83c\udde8\ud83c\uddf4"},{"name":"Comoros","iso3":"COM","iso2":"KM","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf0\ud83c\uddf2"},{"name":"Congo","iso3":"COG","iso2":"CG","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\udde8\ud83c\uddec"},{"name":"Cook Islands","iso3":"COK","iso2":"CK","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\udde8\ud83c\uddf0"},{"name":"Costa Rica","iso3":"CRI","iso2":"CR","region":"Americas","subregion":"Central America","emoji":"\ud83c\udde8\ud83c\uddf7"},{"name":"Cote D'Ivoire (Ivory Coast)","iso3":"CIV","iso2":"CI","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\udde8\ud83c\uddee"},{"name":"Croatia","iso3":"HRV","iso2":"HR","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\udded\ud83c\uddf7"},{"name":"Cuba","iso3":"CUB","iso2":"CU","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde8\ud83c\uddfa"},{"name":"Cura\u00e7ao","iso3":"CUW","iso2":"CW","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde8\ud83c\uddfc"},{"name":"Cyprus","iso3":"CYP","iso2":"CY","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\udde8\ud83c\uddfe"},{"name":"Czech Republic","iso3":"CZE","iso2":"CZ","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\udde8\ud83c\uddff"},{"name":"Democratic Republic of the Congo","iso3":"COD","iso2":"CD","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\udde8\ud83c\udde9"},{"name":"Denmark","iso3":"DNK","iso2":"DK","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\udde9\ud83c\uddf0"},{"name":"Djibouti","iso3":"DJI","iso2":"DJ","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\udde9\ud83c\uddef"},{"name":"Dominica","iso3":"DMA","iso2":"DM","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde9\ud83c\uddf2"},{"name":"Dominican Republic","iso3":"DOM","iso2":"DO","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde9\ud83c\uddf4"},{"name":"East Timor","iso3":"TLS","iso2":"TL","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf9\ud83c\uddf1"},{"name":"Ecuador","iso3":"ECU","iso2":"EC","region":"Americas","subregion":"South America","emoji":"\ud83c\uddea\ud83c\udde8"},{"name":"Egypt","iso3":"EGY","iso2":"EG","region":"Africa","subregion":"Northern Africa","emoji":"\ud83c\uddea\ud83c\uddec"},{"name":"El Salvador","iso3":"SLV","iso2":"SV","region":"Americas","subregion":"Central America","emoji":"\ud83c\uddf8\ud83c\uddfb"},{"name":"Equatorial Guinea","iso3":"GNQ","iso2":"GQ","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\uddec\ud83c\uddf6"},{"name":"Eritrea","iso3":"ERI","iso2":"ER","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddea\ud83c\uddf7"},{"name":"Estonia","iso3":"EST","iso2":"EE","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddea\ud83c\uddea"},{"name":"Ethiopia","iso3":"ETH","iso2":"ET","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddea\ud83c\uddf9"},{"name":"Falkland Islands","iso3":"FLK","iso2":"FK","region":"Americas","subregion":"South America","emoji":"\ud83c\uddeb\ud83c\uddf0"},{"name":"Faroe Islands","iso3":"FRO","iso2":"FO","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddeb\ud83c\uddf4"},{"name":"Fiji Islands","iso3":"FJI","iso2":"FJ","region":"Oceania","subregion":"Melanesia","emoji":"\ud83c\uddeb\ud83c\uddef"},{"name":"Finland","iso3":"FIN","iso2":"FI","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddeb\ud83c\uddee"},{"name":"France","iso3":"FRA","iso2":"FR","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\uddeb\ud83c\uddf7"},{"name":"French Guiana","iso3":"GUF","iso2":"GF","region":"Americas","subregion":"South America","emoji":"\ud83c\uddec\ud83c\uddeb"},{"name":"French Polynesia","iso3":"PYF","iso2":"PF","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddf5\ud83c\uddeb"},{"name":"French Southern Territories","iso3":"ATF","iso2":"TF","region":"Africa","subregion":"Southern Africa","emoji":"\ud83c\uddf9\ud83c\uddeb"},{"name":"Gabon","iso3":"GAB","iso2":"GA","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\uddec\ud83c\udde6"},{"name":"Gambia The","iso3":"GMB","iso2":"GM","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddec\ud83c\uddf2"},{"name":"Georgia","iso3":"GEO","iso2":"GE","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddec\ud83c\uddea"},{"name":"Germany","iso3":"DEU","iso2":"DE","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\udde9\ud83c\uddea"},{"name":"Ghana","iso3":"GHA","iso2":"GH","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddec\ud83c\udded"},{"name":"Gibraltar","iso3":"GIB","iso2":"GI","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddec\ud83c\uddee"},{"name":"Greece","iso3":"GRC","iso2":"GR","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddec\ud83c\uddf7"},{"name":"Greenland","iso3":"GRL","iso2":"GL","region":"Americas","subregion":"Northern America","emoji":"\ud83c\uddec\ud83c\uddf1"},{"name":"Grenada","iso3":"GRD","iso2":"GD","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddec\ud83c\udde9"},{"name":"Guadeloupe","iso3":"GLP","iso2":"GP","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddec\ud83c\uddf5"},{"name":"Guam","iso3":"GUM","iso2":"GU","region":"Oceania","subregion":"Micronesia","emoji":"\ud83c\uddec\ud83c\uddfa"},{"name":"Guatemala","iso3":"GTM","iso2":"GT","region":"Americas","subregion":"Central America","emoji":"\ud83c\uddec\ud83c\uddf9"},{"name":"Guernsey and Alderney","iso3":"GGY","iso2":"GG","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddec\ud83c\uddec"},{"name":"Guinea","iso3":"GIN","iso2":"GN","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddec\ud83c\uddf3"},{"name":"Guinea-Bissau","iso3":"GNB","iso2":"GW","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddec\ud83c\uddfc"},{"name":"Guyana","iso3":"GUY","iso2":"GY","region":"Americas","subregion":"South America","emoji":"\ud83c\uddec\ud83c\uddfe"},{"name":"Haiti","iso3":"HTI","iso2":"HT","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udded\ud83c\uddf9"},{"name":"Heard Island and McDonald Islands","iso3":"HMD","iso2":"HM","region":"","subregion":"","emoji":"\ud83c\udded\ud83c\uddf2"},{"name":"Honduras","iso3":"HND","iso2":"HN","region":"Americas","subregion":"Central America","emoji":"\ud83c\udded\ud83c\uddf3"},{"name":"Hong Kong S.A.R.","iso3":"HKG","iso2":"HK","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\udded\ud83c\uddf0"},{"name":"Hungary","iso3":"HUN","iso2":"HU","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\udded\ud83c\uddfa"},{"name":"Iceland","iso3":"ISL","iso2":"IS","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddee\ud83c\uddf8"},{"name":"India","iso3":"IND","iso2":"IN","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\uddee\ud83c\uddf3"},{"name":"Indonesia","iso3":"IDN","iso2":"ID","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddee\ud83c\udde9"},{"name":"Iran","iso3":"IRN","iso2":"IR","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\uddee\ud83c\uddf7"},{"name":"Iraq","iso3":"IRQ","iso2":"IQ","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddee\ud83c\uddf6"},{"name":"Ireland","iso3":"IRL","iso2":"IE","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddee\ud83c\uddea"},{"name":"Israel","iso3":"ISR","iso2":"IL","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddee\ud83c\uddf1"},{"name":"Italy","iso3":"ITA","iso2":"IT","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddee\ud83c\uddf9"},{"name":"Jamaica","iso3":"JAM","iso2":"JM","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddef\ud83c\uddf2"},{"name":"Japan","iso3":"JPN","iso2":"JP","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\uddef\ud83c\uddf5"},{"name":"Jersey","iso3":"JEY","iso2":"JE","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddef\ud83c\uddea"},{"name":"Jordan","iso3":"JOR","iso2":"JO","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddef\ud83c\uddf4"},{"name":"Kazakhstan","iso3":"KAZ","iso2":"KZ","region":"Asia","subregion":"Central Asia","emoji":"\ud83c\uddf0\ud83c\uddff"},{"name":"Kenya","iso3":"KEN","iso2":"KE","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf0\ud83c\uddea"},{"name":"Kiribati","iso3":"KIR","iso2":"KI","region":"Oceania","subregion":"Micronesia","emoji":"\ud83c\uddf0\ud83c\uddee"},{"name":"Kosovo","iso3":"XKX","iso2":"XK","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\uddfd\ud83c\uddf0"},{"name":"Kuwait","iso3":"KWT","iso2":"KW","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf0\ud83c\uddfc"},{"name":"Kyrgyzstan","iso3":"KGZ","iso2":"KG","region":"Asia","subregion":"Central Asia","emoji":"\ud83c\uddf0\ud83c\uddec"},{"name":"Laos","iso3":"LAO","iso2":"LA","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf1\ud83c\udde6"},{"name":"Latvia","iso3":"LVA","iso2":"LV","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddf1\ud83c\uddfb"},{"name":"Lebanon","iso3":"LBN","iso2":"LB","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf1\ud83c\udde7"},{"name":"Lesotho","iso3":"LSO","iso2":"LS","region":"Africa","subregion":"Southern Africa","emoji":"\ud83c\uddf1\ud83c\uddf8"},{"name":"Liberia","iso3":"LBR","iso2":"LR","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf1\ud83c\uddf7"},{"name":"Libya","iso3":"LBY","iso2":"LY","region":"Africa","subregion":"Northern Africa","emoji":"\ud83c\uddf1\ud83c\uddfe"},{"name":"Liechtenstein","iso3":"LIE","iso2":"LI","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\uddf1\ud83c\uddee"},{"name":"Lithuania","iso3":"LTU","iso2":"LT","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddf1\ud83c\uddf9"},{"name":"Luxembourg","iso3":"LUX","iso2":"LU","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\uddf1\ud83c\uddfa"},{"name":"Macau S.A.R.","iso3":"MAC","iso2":"MO","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\uddf2\ud83c\uddf4"},{"name":"Macedonia","iso3":"MKD","iso2":"MK","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddf2\ud83c\uddf0"},{"name":"Madagascar","iso3":"MDG","iso2":"MG","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf2\ud83c\uddec"},{"name":"Malawi","iso3":"MWI","iso2":"MW","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf2\ud83c\uddfc"},{"name":"Malaysia","iso3":"MYS","iso2":"MY","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf2\ud83c\uddfe"},{"name":"Maldives","iso3":"MDV","iso2":"MV","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\uddf2\ud83c\uddfb"},{"name":"Mali","iso3":"MLI","iso2":"ML","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf2\ud83c\uddf1"},{"name":"Malta","iso3":"MLT","iso2":"MT","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddf2\ud83c\uddf9"},{"name":"Man (Isle of)","iso3":"IMN","iso2":"IM","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddee\ud83c\uddf2"},{"name":"Marshall Islands","iso3":"MHL","iso2":"MH","region":"Oceania","subregion":"Micronesia","emoji":"\ud83c\uddf2\ud83c\udded"},{"name":"Martinique","iso3":"MTQ","iso2":"MQ","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf2\ud83c\uddf6"},{"name":"Mauritania","iso3":"MRT","iso2":"MR","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf2\ud83c\uddf7"},{"name":"Mauritius","iso3":"MUS","iso2":"MU","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf2\ud83c\uddfa"},{"name":"Mayotte","iso3":"MYT","iso2":"YT","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddfe\ud83c\uddf9"},{"name":"Mexico","iso3":"MEX","iso2":"MX","region":"Americas","subregion":"Central America","emoji":"\ud83c\uddf2\ud83c\uddfd"},{"name":"Micronesia","iso3":"FSM","iso2":"FM","region":"Oceania","subregion":"Micronesia","emoji":"\ud83c\uddeb\ud83c\uddf2"},{"name":"Moldova","iso3":"MDA","iso2":"MD","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\uddf2\ud83c\udde9"},{"name":"Monaco","iso3":"MCO","iso2":"MC","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\uddf2\ud83c\udde8"},{"name":"Mongolia","iso3":"MNG","iso2":"MN","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\uddf2\ud83c\uddf3"},{"name":"Montenegro","iso3":"MNE","iso2":"ME","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddf2\ud83c\uddea"},{"name":"Montserrat","iso3":"MSR","iso2":"MS","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf2\ud83c\uddf8"},{"name":"Morocco","iso3":"MAR","iso2":"MA","region":"Africa","subregion":"Northern Africa","emoji":"\ud83c\uddf2\ud83c\udde6"},{"name":"Mozambique","iso3":"MOZ","iso2":"MZ","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf2\ud83c\uddff"},{"name":"Myanmar","iso3":"MMR","iso2":"MM","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf2\ud83c\uddf2"},{"name":"Namibia","iso3":"NAM","iso2":"NA","region":"Africa","subregion":"Southern Africa","emoji":"\ud83c\uddf3\ud83c\udde6"},{"name":"Nauru","iso3":"NRU","iso2":"NR","region":"Oceania","subregion":"Micronesia","emoji":"\ud83c\uddf3\ud83c\uddf7"},{"name":"Nepal","iso3":"NPL","iso2":"NP","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\uddf3\ud83c\uddf5"},{"name":"Netherlands","iso3":"NLD","iso2":"NL","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\uddf3\ud83c\uddf1"},{"name":"New Caledonia","iso3":"NCL","iso2":"NC","region":"Oceania","subregion":"Melanesia","emoji":"\ud83c\uddf3\ud83c\udde8"},{"name":"New Zealand","iso3":"NZL","iso2":"NZ","region":"Oceania","subregion":"Australia and New Zealand","emoji":"\ud83c\uddf3\ud83c\uddff"},{"name":"Nicaragua","iso3":"NIC","iso2":"NI","region":"Americas","subregion":"Central America","emoji":"\ud83c\uddf3\ud83c\uddee"},{"name":"Niger","iso3":"NER","iso2":"NE","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf3\ud83c\uddea"},{"name":"Nigeria","iso3":"NGA","iso2":"NG","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf3\ud83c\uddec"},{"name":"Niue","iso3":"NIU","iso2":"NU","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddf3\ud83c\uddfa"},{"name":"Norfolk Island","iso3":"NFK","iso2":"NF","region":"Oceania","subregion":"Australia and New Zealand","emoji":"\ud83c\uddf3\ud83c\uddeb"},{"name":"North Korea","iso3":"PRK","iso2":"KP","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\uddf0\ud83c\uddf5"},{"name":"Northern Mariana Islands","iso3":"MNP","iso2":"MP","region":"Oceania","subregion":"Micronesia","emoji":"\ud83c\uddf2\ud83c\uddf5"},{"name":"Norway","iso3":"NOR","iso2":"NO","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddf3\ud83c\uddf4"},{"name":"Oman","iso3":"OMN","iso2":"OM","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf4\ud83c\uddf2"},{"name":"Pakistan","iso3":"PAK","iso2":"PK","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\uddf5\ud83c\uddf0"},{"name":"Palau","iso3":"PLW","iso2":"PW","region":"Oceania","subregion":"Micronesia","emoji":"\ud83c\uddf5\ud83c\uddfc"},{"name":"Palestinian Territory Occupied","iso3":"PSE","iso2":"PS","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf5\ud83c\uddf8"},{"name":"Panama","iso3":"PAN","iso2":"PA","region":"Americas","subregion":"Central America","emoji":"\ud83c\uddf5\ud83c\udde6"},{"name":"Papua new Guinea","iso3":"PNG","iso2":"PG","region":"Oceania","subregion":"Melanesia","emoji":"\ud83c\uddf5\ud83c\uddec"},{"name":"Paraguay","iso3":"PRY","iso2":"PY","region":"Americas","subregion":"South America","emoji":"\ud83c\uddf5\ud83c\uddfe"},{"name":"Peru","iso3":"PER","iso2":"PE","region":"Americas","subregion":"South America","emoji":"\ud83c\uddf5\ud83c\uddea"},{"name":"Philippines","iso3":"PHL","iso2":"PH","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf5\ud83c\udded"},{"name":"Pitcairn Island","iso3":"PCN","iso2":"PN","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddf5\ud83c\uddf3"},{"name":"Poland","iso3":"POL","iso2":"PL","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\uddf5\ud83c\uddf1"},{"name":"Portugal","iso3":"PRT","iso2":"PT","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddf5\ud83c\uddf9"},{"name":"Puerto Rico","iso3":"PRI","iso2":"PR","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf5\ud83c\uddf7"},{"name":"Qatar","iso3":"QAT","iso2":"QA","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf6\ud83c\udde6"},{"name":"Reunion","iso3":"REU","iso2":"RE","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf7\ud83c\uddea"},{"name":"Romania","iso3":"ROU","iso2":"RO","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\uddf7\ud83c\uddf4"},{"name":"Russia","iso3":"RUS","iso2":"RU","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\uddf7\ud83c\uddfa"},{"name":"Rwanda","iso3":"RWA","iso2":"RW","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf7\ud83c\uddfc"},{"name":"Saint Helena","iso3":"SHN","iso2":"SH","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf8\ud83c\udded"},{"name":"Saint Kitts And Nevis","iso3":"KNA","iso2":"KN","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf0\ud83c\uddf3"},{"name":"Saint Lucia","iso3":"LCA","iso2":"LC","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf1\ud83c\udde8"},{"name":"Saint Pierre and Miquelon","iso3":"SPM","iso2":"PM","region":"Americas","subregion":"Northern America","emoji":"\ud83c\uddf5\ud83c\uddf2"},{"name":"Saint Vincent And The Grenadines","iso3":"VCT","iso2":"VC","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddfb\ud83c\udde8"},{"name":"Saint-Barthelemy","iso3":"BLM","iso2":"BL","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\udde7\ud83c\uddf1"},{"name":"Saint-Martin (French part)","iso3":"MAF","iso2":"MF","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf2\ud83c\uddeb"},{"name":"Samoa","iso3":"WSM","iso2":"WS","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddfc\ud83c\uddf8"},{"name":"San Marino","iso3":"SMR","iso2":"SM","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddf8\ud83c\uddf2"},{"name":"Sao Tome and Principe","iso3":"STP","iso2":"ST","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\uddf8\ud83c\uddf9"},{"name":"Saudi Arabia","iso3":"SAU","iso2":"SA","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf8\ud83c\udde6"},{"name":"Senegal","iso3":"SEN","iso2":"SN","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf8\ud83c\uddf3"},{"name":"Serbia","iso3":"SRB","iso2":"RS","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddf7\ud83c\uddf8"},{"name":"Seychelles","iso3":"SYC","iso2":"SC","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf8\ud83c\udde8"},{"name":"Sierra Leone","iso3":"SLE","iso2":"SL","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf8\ud83c\uddf1"},{"name":"Singapore","iso3":"SGP","iso2":"SG","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf8\ud83c\uddec"},{"name":"Sint Maarten (Dutch part)","iso3":"SXM","iso2":"SX","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf8\ud83c\uddfd"},{"name":"Slovakia","iso3":"SVK","iso2":"SK","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\uddf8\ud83c\uddf0"},{"name":"Slovenia","iso3":"SVN","iso2":"SI","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddf8\ud83c\uddee"},{"name":"Solomon Islands","iso3":"SLB","iso2":"SB","region":"Oceania","subregion":"Melanesia","emoji":"\ud83c\uddf8\ud83c\udde7"},{"name":"Somalia","iso3":"SOM","iso2":"SO","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf8\ud83c\uddf4"},{"name":"South Africa","iso3":"ZAF","iso2":"ZA","region":"Africa","subregion":"Southern Africa","emoji":"\ud83c\uddff\ud83c\udde6"},{"name":"South Georgia","iso3":"SGS","iso2":"GS","region":"Americas","subregion":"South America","emoji":"\ud83c\uddec\ud83c\uddf8"},{"name":"South Korea","iso3":"KOR","iso2":"KR","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\uddf0\ud83c\uddf7"},{"name":"South Sudan","iso3":"SSD","iso2":"SS","region":"Africa","subregion":"Middle Africa","emoji":"\ud83c\uddf8\ud83c\uddf8"},{"name":"Spain","iso3":"ESP","iso2":"ES","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddea\ud83c\uddf8"},{"name":"Sri Lanka","iso3":"LKA","iso2":"LK","region":"Asia","subregion":"Southern Asia","emoji":"\ud83c\uddf1\ud83c\uddf0"},{"name":"Sudan","iso3":"SDN","iso2":"SD","region":"Africa","subregion":"Northern Africa","emoji":"\ud83c\uddf8\ud83c\udde9"},{"name":"Suriname","iso3":"SUR","iso2":"SR","region":"Americas","subregion":"South America","emoji":"\ud83c\uddf8\ud83c\uddf7"},{"name":"Svalbard And Jan Mayen Islands","iso3":"SJM","iso2":"SJ","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddf8\ud83c\uddef"},{"name":"Swaziland","iso3":"SWZ","iso2":"SZ","region":"Africa","subregion":"Southern Africa","emoji":"\ud83c\uddf8\ud83c\uddff"},{"name":"Sweden","iso3":"SWE","iso2":"SE","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddf8\ud83c\uddea"},{"name":"Switzerland","iso3":"CHE","iso2":"CH","region":"Europe","subregion":"Western Europe","emoji":"\ud83c\udde8\ud83c\udded"},{"name":"Syria","iso3":"SYR","iso2":"SY","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf8\ud83c\uddfe"},{"name":"Taiwan","iso3":"TWN","iso2":"TW","region":"Asia","subregion":"Eastern Asia","emoji":"\ud83c\uddf9\ud83c\uddfc"},{"name":"Tajikistan","iso3":"TJK","iso2":"TJ","region":"Asia","subregion":"Central Asia","emoji":"\ud83c\uddf9\ud83c\uddef"},{"name":"Tanzania","iso3":"TZA","iso2":"TZ","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddf9\ud83c\uddff"},{"name":"Thailand","iso3":"THA","iso2":"TH","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddf9\ud83c\udded"},{"name":"Togo","iso3":"TGO","iso2":"TG","region":"Africa","subregion":"Western Africa","emoji":"\ud83c\uddf9\ud83c\uddec"},{"name":"Tokelau","iso3":"TKL","iso2":"TK","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddf9\ud83c\uddf0"},{"name":"Tonga","iso3":"TON","iso2":"TO","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddf9\ud83c\uddf4"},{"name":"Trinidad And Tobago","iso3":"TTO","iso2":"TT","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf9\ud83c\uddf9"},{"name":"Tunisia","iso3":"TUN","iso2":"TN","region":"Africa","subregion":"Northern Africa","emoji":"\ud83c\uddf9\ud83c\uddf3"},{"name":"Turkey","iso3":"TUR","iso2":"TR","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddf9\ud83c\uddf7"},{"name":"Turkmenistan","iso3":"TKM","iso2":"TM","region":"Asia","subregion":"Central Asia","emoji":"\ud83c\uddf9\ud83c\uddf2"},{"name":"Turks And Caicos Islands","iso3":"TCA","iso2":"TC","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddf9\ud83c\udde8"},{"name":"Tuvalu","iso3":"TUV","iso2":"TV","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddf9\ud83c\uddfb"},{"name":"Uganda","iso3":"UGA","iso2":"UG","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddfa\ud83c\uddec"},{"name":"Ukraine","iso3":"UKR","iso2":"UA","region":"Europe","subregion":"Eastern Europe","emoji":"\ud83c\uddfa\ud83c\udde6"},{"name":"United Arab Emirates","iso3":"ARE","iso2":"AE","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\udde6\ud83c\uddea"},{"name":"United Kingdom","iso3":"GBR","iso2":"GB","region":"Europe","subregion":"Northern Europe","emoji":"\ud83c\uddec\ud83c\udde7"},{"name":"United States","iso3":"USA","iso2":"US","region":"Americas","subregion":"Northern America","emoji":"\ud83c\uddfa\ud83c\uddf8"},{"name":"United States Minor Outlying Islands","iso3":"UMI","iso2":"UM","region":"Americas","subregion":"Northern America","emoji":"\ud83c\uddfa\ud83c\uddf2"},{"name":"Uruguay","iso3":"URY","iso2":"UY","region":"Americas","subregion":"South America","emoji":"\ud83c\uddfa\ud83c\uddfe"},{"name":"Uzbekistan","iso3":"UZB","iso2":"UZ","region":"Asia","subregion":"Central Asia","emoji":"\ud83c\uddfa\ud83c\uddff"},{"name":"Vanuatu","iso3":"VUT","iso2":"VU","region":"Oceania","subregion":"Melanesia","emoji":"\ud83c\uddfb\ud83c\uddfa"},{"name":"Vatican City State (Holy See)","iso3":"VAT","iso2":"VA","region":"Europe","subregion":"Southern Europe","emoji":"\ud83c\uddfb\ud83c\udde6"},{"name":"Venezuela","iso3":"VEN","iso2":"VE","region":"Americas","subregion":"South America","emoji":"\ud83c\uddfb\ud83c\uddea"},{"name":"Vietnam","iso3":"VNM","iso2":"VN","region":"Asia","subregion":"South-Eastern Asia","emoji":"\ud83c\uddfb\ud83c\uddf3"},{"name":"Virgin Islands (British)","iso3":"VGB","iso2":"VG","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddfb\ud83c\uddec"},{"name":"Virgin Islands (US)","iso3":"VIR","iso2":"VI","region":"Americas","subregion":"Caribbean","emoji":"\ud83c\uddfb\ud83c\uddee"},{"name":"Wallis And Futuna Islands","iso3":"WLF","iso2":"WF","region":"Oceania","subregion":"Polynesia","emoji":"\ud83c\uddfc\ud83c\uddeb"},{"name":"Western Sahara","iso3":"ESH","iso2":"EH","region":"Africa","subregion":"Northern Africa","emoji":"\ud83c\uddea\ud83c\udded"},{"name":"Yemen","iso3":"YEM","iso2":"YE","region":"Asia","subregion":"Western Asia","emoji":"\ud83c\uddfe\ud83c\uddea"},{"name":"Zambia","iso3":"ZMB","iso2":"ZM","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddff\ud83c\uddf2"},{"name":"Zimbabwe","iso3":"ZWE","iso2":"ZW","region":"Africa","subregion":"Eastern Africa","emoji":"\ud83c\uddff\ud83c\uddfc"}]
\ No newline at end of file
diff --git a/resources/dictionaries/currencies.json b/resources/dictionaries/currencies.json
new file mode 100644
index 0000000000..317457f03e
--- /dev/null
+++ b/resources/dictionaries/currencies.json
@@ -0,0 +1 @@
+[{"code":"AED","name":"United Arab Emirates Dirham","symbol":"\u062f.\u0625.\u200f","decimal_digits":2},{"code":"AFN","name":"Afghan Afghani","symbol":"\u060b","decimal_digits":0},{"code":"ALL","name":"Albanian Lek","symbol":"Lek","decimal_digits":0},{"code":"AMD","name":"Armenian Dram","symbol":"\u0564\u0580.","decimal_digits":0},{"code":"ARS","name":"Argentine Peso","symbol":"$","decimal_digits":2},{"code":"AUD","name":"Australian Dollar","symbol":"$","decimal_digits":2},{"code":"AZN","name":"Azerbaijani Manat","symbol":"\u043c\u0430\u043d.","decimal_digits":2},{"code":"BAM","name":"Bosnia-Herzegovina Convertible Mark","symbol":"KM","decimal_digits":2},{"code":"BDT","name":"Bangladeshi Taka","symbol":"\u09f3","decimal_digits":2},{"code":"BGN","name":"Bulgarian Lev","symbol":"\u043b\u0432.","decimal_digits":2},{"code":"BHD","name":"Bahraini Dinar","symbol":"\u062f.\u0628.\u200f","decimal_digits":3},{"code":"BIF","name":"Burundian Franc","symbol":"FBu","decimal_digits":0},{"code":"BND","name":"Brunei Dollar","symbol":"$","decimal_digits":2},{"code":"BOB","name":"Bolivian Boliviano","symbol":"Bs","decimal_digits":2},{"code":"BRL","name":"Brazilian Real","symbol":"R$","decimal_digits":2},{"code":"BWP","name":"Botswanan Pula","symbol":"P","decimal_digits":2},{"code":"BYN","name":"Belarusian Ruble","symbol":"\u0440\u0443\u0431.","decimal_digits":2},{"code":"BZD","name":"Belize Dollar","symbol":"$","decimal_digits":2},{"code":"CAD","name":"Canadian Dollar","symbol":"$","decimal_digits":2},{"code":"CDF","name":"Congolese Franc","symbol":"FrCD","decimal_digits":2},{"code":"CHF","name":"Swiss Franc","symbol":"CHF","decimal_digits":2},{"code":"CLP","name":"Chilean Peso","symbol":"$","decimal_digits":0},{"code":"CNY","name":"Chinese Yuan","symbol":"CN\u00a5","decimal_digits":2},{"code":"COP","name":"Colombian Peso","symbol":"$","decimal_digits":0},{"code":"CRC","name":"Costa Rican Col\u00f3n","symbol":"\u20a1","decimal_digits":0},{"code":"CVE","name":"Cape Verdean Escudo","symbol":"CV$","decimal_digits":2},{"code":"CZK","name":"Czech Republic Koruna","symbol":"K\u010d","decimal_digits":2},{"code":"DJF","name":"Djiboutian Franc","symbol":"Fdj","decimal_digits":0},{"code":"DKK","name":"Danish Krone","symbol":"kr","decimal_digits":2},{"code":"DOP","name":"Dominican Peso","symbol":"RD$","decimal_digits":2},{"code":"DZD","name":"Algerian Dinar","symbol":"\u062f.\u062c.\u200f","decimal_digits":2},{"code":"EEK","name":"Estonian Kroon","symbol":"kr","decimal_digits":2},{"code":"EGP","name":"Egyptian Pound","symbol":"\u062c.\u0645.\u200f","decimal_digits":2},{"code":"ERN","name":"Eritrean Nakfa","symbol":"Nfk","decimal_digits":2},{"code":"ETB","name":"Ethiopian Birr","symbol":"Br","decimal_digits":2},{"code":"EUR","name":"Euro","symbol":"\u20ac","decimal_digits":2},{"code":"GBP","name":"British Pound Sterling","symbol":"\u00a3","decimal_digits":2},{"code":"GEL","name":"Georgian Lari","symbol":"GEL","decimal_digits":2},{"code":"GHS","name":"Ghanaian Cedi","symbol":"GH\u20b5","decimal_digits":2},{"code":"GNF","name":"Guinean Franc","symbol":"FG","decimal_digits":0},{"code":"GTQ","name":"Guatemalan Quetzal","symbol":"Q","decimal_digits":2},{"code":"HKD","name":"Hong Kong Dollar","symbol":"$","decimal_digits":2},{"code":"HNL","name":"Honduran Lempira","symbol":"L","decimal_digits":2},{"code":"HRK","name":"Croatian Kuna","symbol":"kn","decimal_digits":2},{"code":"HUF","name":"Hungarian Forint","symbol":"Ft","decimal_digits":0},{"code":"IDR","name":"Indonesian Rupiah","symbol":"Rp","decimal_digits":0},{"code":"ILS","name":"Israeli New Sheqel","symbol":"\u20aa","decimal_digits":2},{"code":"INR","name":"Indian Rupee","symbol":"\u099f\u0995\u09be","decimal_digits":2},{"code":"IQD","name":"Iraqi Dinar","symbol":"\u062f.\u0639.\u200f","decimal_digits":0},{"code":"IRR","name":"Iranian Rial","symbol":"\ufdfc","decimal_digits":0},{"code":"ISK","name":"Icelandic Kr\u00f3na","symbol":"kr","decimal_digits":0},{"code":"JMD","name":"Jamaican Dollar","symbol":"$","decimal_digits":2},{"code":"JOD","name":"Jordanian Dinar","symbol":"\u062f.\u0623.\u200f","decimal_digits":3},{"code":"JPY","name":"Japanese Yen","symbol":"\uffe5","decimal_digits":0},{"code":"KES","name":"Kenyan Shilling","symbol":"Ksh","decimal_digits":2},{"code":"KHR","name":"Cambodian Riel","symbol":"\u17db","decimal_digits":2},{"code":"KMF","name":"Comorian Franc","symbol":"FC","decimal_digits":0},{"code":"KRW","name":"South Korean Won","symbol":"\u20a9","decimal_digits":0},{"code":"KWD","name":"Kuwaiti Dinar","symbol":"\u062f.\u0643.\u200f","decimal_digits":3},{"code":"KZT","name":"Kazakhstani Tenge","symbol":"\u0442\u04a3\u0433.","decimal_digits":2},{"code":"LBP","name":"Lebanese Pound","symbol":"\u0644.\u0644.\u200f","decimal_digits":0},{"code":"LKR","name":"Sri Lankan Rupee","symbol":"SL Re","decimal_digits":2},{"code":"LTL","name":"Lithuanian Litas","symbol":"Lt","decimal_digits":2},{"code":"LVL","name":"Latvian Lats","symbol":"Ls","decimal_digits":2},{"code":"LYD","name":"Libyan Dinar","symbol":"\u062f.\u0644.\u200f","decimal_digits":3},{"code":"MAD","name":"Moroccan Dirham","symbol":"\u062f.\u0645.\u200f","decimal_digits":2},{"code":"MDL","name":"Moldovan Leu","symbol":"MDL","decimal_digits":2},{"code":"MGA","name":"Malagasy Ariary","symbol":"MGA","decimal_digits":0},{"code":"MKD","name":"Macedonian Denar","symbol":"MKD","decimal_digits":2},{"code":"MMK","name":"Myanma Kyat","symbol":"K","decimal_digits":0},{"code":"MOP","name":"Macanese Pataca","symbol":"MOP$","decimal_digits":2},{"code":"MUR","name":"Mauritian Rupee","symbol":"MURs","decimal_digits":0},{"code":"MXN","name":"Mexican Peso","symbol":"$","decimal_digits":2},{"code":"MYR","name":"Malaysian Ringgit","symbol":"RM","decimal_digits":2},{"code":"MZN","name":"Mozambican Metical","symbol":"MTn","decimal_digits":2},{"code":"NAD","name":"Namibian Dollar","symbol":"N$","decimal_digits":2},{"code":"NGN","name":"Nigerian Naira","symbol":"\u20a6","decimal_digits":2},{"code":"NIO","name":"Nicaraguan C\u00f3rdoba","symbol":"C$","decimal_digits":2},{"code":"NOK","name":"Norwegian Krone","symbol":"kr","decimal_digits":2},{"code":"NPR","name":"Nepalese Rupee","symbol":"\u0928\u0947\u0930\u0942","decimal_digits":2},{"code":"NZD","name":"New Zealand Dollar","symbol":"$","decimal_digits":2},{"code":"OMR","name":"Omani Rial","symbol":"\u0631.\u0639.\u200f","decimal_digits":3},{"code":"PAB","name":"Panamanian Balboa","symbol":"B\/.","decimal_digits":2},{"code":"PEN","name":"Peruvian Nuevo Sol","symbol":"S\/.","decimal_digits":2},{"code":"PHP","name":"Philippine Peso","symbol":"\u20b1","decimal_digits":2},{"code":"PKR","name":"Pakistani Rupee","symbol":"\u20a8","decimal_digits":0},{"code":"PLN","name":"Polish Zloty","symbol":"z\u0142","decimal_digits":2},{"code":"PYG","name":"Paraguayan Guarani","symbol":"\u20b2","decimal_digits":0},{"code":"QAR","name":"Qatari Rial","symbol":"\u0631.\u0642.\u200f","decimal_digits":2},{"code":"RON","name":"Romanian Leu","symbol":"RON","decimal_digits":2},{"code":"RSD","name":"Serbian Dinar","symbol":"\u0434\u0438\u043d.","decimal_digits":0},{"code":"RUB","name":"Russian Ruble","symbol":"\u20bd.","decimal_digits":2},{"code":"RWF","name":"Rwandan Franc","symbol":"FR","decimal_digits":0},{"code":"SAR","name":"Saudi Riyal","symbol":"\u0631.\u0633.\u200f","decimal_digits":2},{"code":"SDG","name":"Sudanese Pound","symbol":"SDG","decimal_digits":2},{"code":"SEK","name":"Swedish Krona","symbol":"kr","decimal_digits":2},{"code":"SGD","name":"Singapore Dollar","symbol":"$","decimal_digits":2},{"code":"SOS","name":"Somali Shilling","symbol":"Ssh","decimal_digits":0},{"code":"SYP","name":"Syrian Pound","symbol":"\u0644.\u0633.\u200f","decimal_digits":0},{"code":"THB","name":"Thai Baht","symbol":"\u0e3f","decimal_digits":2},{"code":"TND","name":"Tunisian Dinar","symbol":"\u062f.\u062a.\u200f","decimal_digits":3},{"code":"TOP","name":"Tongan Pa\u02bbanga","symbol":"T$","decimal_digits":2},{"code":"TRY","name":"Turkish Lira","symbol":"TL","decimal_digits":2},{"code":"TTD","name":"Trinidad and Tobago Dollar","symbol":"$","decimal_digits":2},{"code":"TWD","name":"New Taiwan Dollar","symbol":"NT$","decimal_digits":2},{"code":"TZS","name":"Tanzanian Shilling","symbol":"TSh","decimal_digits":0},{"code":"UAH","name":"Ukrainian Hryvnia","symbol":"\u20b4","decimal_digits":2},{"code":"UGX","name":"Ugandan Shilling","symbol":"USh","decimal_digits":0},{"code":"USD","name":"US Dollar","symbol":"$","decimal_digits":2},{"code":"UYU","name":"Uruguayan Peso","symbol":"$","decimal_digits":2},{"code":"UZS","name":"Uzbekistan Som","symbol":"UZS","decimal_digits":0},{"code":"VEF","name":"Venezuelan Bol\u00edvar","symbol":"Bs.F.","decimal_digits":2},{"code":"VND","name":"Vietnamese Dong","symbol":"\u20ab","decimal_digits":0},{"code":"XAF","name":"CFA Franc BEAC","symbol":"FCFA","decimal_digits":0},{"code":"XOF","name":"CFA Franc BCEAO","symbol":"CFA","decimal_digits":0},{"code":"YER","name":"Yemeni Rial","symbol":"\u0631.\u064a.\u200f","decimal_digits":0},{"code":"ZAR","name":"South African Rand","symbol":"R","decimal_digits":2},{"code":"ZMK","name":"Zambian Kwacha","symbol":"ZK","decimal_digits":0},{"code":"ZWL","name":"Zimbabwean Dollar","symbol":"ZWL$","decimal_digits":0}]
\ No newline at end of file
diff --git a/resources/js/bootstrap/fieldtypes.js b/resources/js/bootstrap/fieldtypes.js
index 2608b7a861..8a4e8f773e 100644
--- a/resources/js/bootstrap/fieldtypes.js
+++ b/resources/js/bootstrap/fieldtypes.js
@@ -24,6 +24,8 @@ import Routes from '../components/collections/Routes.vue';
import TitleFormats from '../components/collections/TitleFormats.vue';
import ColorFieldtype from '../components/fieldtypes/ColorFieldtype.vue';
import DateFieldtype from '../components/fieldtypes/DateFieldtype.vue';
+import DictionaryFieldtype from "../components/fieldtypes/DictionaryFieldtype.vue";
+import DictionaryFields from "../components/fieldtypes/DictionaryFields.vue";
import FieldDisplayFieldtype from '../components/fieldtypes/FieldDisplayFieldtype.vue';
import FieldsFieldtype from '../components/fieldtypes/grid/FieldsFieldtype.vue';
import FilesFieldtype from '../components/fieldtypes/FilesFieldtype.vue';
@@ -85,6 +87,8 @@ Vue.component('collection_routes-fieldtype', Routes);
Vue.component('collection_title_formats-fieldtype', TitleFormats);
Vue.component('color-fieldtype', ColorFieldtype);
Vue.component('date-fieldtype', DateFieldtype);
+Vue.component('dictionary-fieldtype', DictionaryFieldtype);
+Vue.component('dictionary_fields-fieldtype', DictionaryFields);
Vue.component('field_display-fieldtype', FieldDisplayFieldtype);
Vue.component('fields-fieldtype', FieldsFieldtype);
Vue.component('files-fieldtype', FilesFieldtype);
diff --git a/resources/js/components/fieldtypes/DictionaryFields.vue b/resources/js/components/fieldtypes/DictionaryFields.vue
new file mode 100644
index 0000000000..bca2ad5dd7
--- /dev/null
+++ b/resources/js/components/fieldtypes/DictionaryFields.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/components/fieldtypes/DictionaryFieldtype.vue b/resources/js/components/fieldtypes/DictionaryFieldtype.vue
new file mode 100644
index 0000000000..e991b0fe34
--- /dev/null
+++ b/resources/js/components/fieldtypes/DictionaryFieldtype.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ __(option.label) }}
+
+
+
+
+
+
+
+
+ /
+
+
+
+
+
+
+
diff --git a/resources/lang/en/fieldtypes.php b/resources/lang/en/fieldtypes.php
index a2776c2c38..6d526d6b4b 100644
--- a/resources/lang/en/fieldtypes.php
+++ b/resources/lang/en/fieldtypes.php
@@ -72,6 +72,7 @@
'date.config.time_enabled' => 'Enable the timepicker.',
'date.config.time_seconds_enabled' => 'Show seconds in the timepicker.',
'date.title' => 'Date',
+ 'dictionary.config.dictionary' => 'Please select the dictionary you wish to pull options from.',
'entries.config.create' => 'Allow creation of new entries.',
'entries.config.collections' => 'Choose which collections the user can select from.',
'entries.config.query_scopes' => 'Choose which query scopes should be applied when retrieving selectable entries.',
diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php
index b4c985c413..36114bdaff 100644
--- a/resources/lang/en/messages.php
+++ b/resources/lang/en/messages.php
@@ -69,6 +69,7 @@
'collections_sort_direction_instructions' => 'The default sort direction.',
'collections_preview_target_refresh_instructions' => 'Automatically refresh the preview while editing. Disabling this will use postMessage.',
'collections_taxonomies_instructions' => 'Connect entries in this collection to taxonomies. Fields will be automatically added to publish forms.',
+ 'dictionaries_countries_region_instructions' => 'Select a region to filter the list of countries.',
'duplicate_action_warning_localization' => 'This entry is a localization. The origin entry will be duplicated.',
'duplicate_action_warning_localizations' => 'One or more selected entries are localizations. In those cases, the origin entry will be duplicated instead.',
'duplicate_action_localizations_confirmation' => 'Are you sure you want to run this action? Localizations will also be duplicated.',
diff --git a/resources/svg/icons/light/dictionary.svg b/resources/svg/icons/light/dictionary.svg
new file mode 100644
index 0000000000..4147382376
--- /dev/null
+++ b/resources/svg/icons/light/dictionary.svg
@@ -0,0 +1 @@
+
diff --git a/resources/views/extend/forms/fields/dictionary.antlers.html b/resources/views/extend/forms/fields/dictionary.antlers.html
new file mode 100644
index 0000000000..b093f6a026
--- /dev/null
+++ b/resources/views/extend/forms/fields/dictionary.antlers.html
@@ -0,0 +1,21 @@
+
diff --git a/resources/views/forms/automagic-email.antlers.html b/resources/views/forms/automagic-email.antlers.html
index f8fd90528b..cab5234e64 100644
--- a/resources/views/forms/automagic-email.antlers.html
+++ b/resources/views/forms/automagic-email.antlers.html
@@ -12,7 +12,7 @@
{{ elseif fieldtype == "radio" }}
{{ value:label ?? value }}
- {{ elseif fieldtype == "select" || fieldtype == "checkboxes" }}
+ {{ elseif fieldtype == "select" || fieldtype == "checkboxes" || fieldtype == "dictionary" }}
{{ value }}{{ label ?? value }}{{ if !last }}, {{ /if }}{{ /value }}
{{ elseif value|is_iterable }}
diff --git a/routes/cp.php b/routes/cp.php
index 959e0f63c1..75af4e9206 100644
--- a/routes/cp.php
+++ b/routes/cp.php
@@ -45,6 +45,7 @@
use Statamic\Http\Controllers\CP\Fields\FieldsetController;
use Statamic\Http\Controllers\CP\Fields\FieldtypesController;
use Statamic\Http\Controllers\CP\Fields\MetaController;
+use Statamic\Http\Controllers\CP\Fieldtypes\DictionaryFieldtypeController;
use Statamic\Http\Controllers\CP\Fieldtypes\FilesFieldtypeController;
use Statamic\Http\Controllers\CP\Fieldtypes\MarkdownFieldtypeController;
use Statamic\Http\Controllers\CP\Fieldtypes\RelationshipFieldtypeController;
@@ -309,6 +310,7 @@
Route::get('relationship/filters', [RelationshipFieldtypeController::class, 'filters'])->name('relationship.filters');
Route::post('markdown', [MarkdownFieldtypeController::class, 'preview'])->name('markdown.preview');
Route::post('files/upload', [FilesFieldtypeController::class, 'upload'])->name('files.upload');
+ Route::get('dictionaries/{dictionary}', DictionaryFieldtypeController::class)->name('dictionary-fieldtype');
});
Route::group(['prefix' => 'api', 'as' => 'api.'], function () {
diff --git a/src/Console/Commands/MakeDictionary.php b/src/Console/Commands/MakeDictionary.php
new file mode 100644
index 0000000000..f1a8618496
--- /dev/null
+++ b/src/Console/Commands/MakeDictionary.php
@@ -0,0 +1,74 @@
+argument('addon')) {
+ $this->updateServiceProvider();
+ }
+ }
+
+ /**
+ * Update the Service Provider to register dictionary components.
+ */
+ protected function updateServiceProvider()
+ {
+ $factory = new BuilderFactory();
+
+ $dictionaryClassValue = $factory->classConstFetch('Dictionaries\\'.$this->getNameInput(), 'class');
+
+ try {
+ PHPFile::load("addons/{$this->package}/src/ServiceProvider.php")
+ ->add()->protected()->property('dictionaries', $dictionaryClassValue)
+ ->save();
+ } catch (\Exception $e) {
+ $this->comment("Don't forget to register the Dictionary class in your addon's service provider.");
+ }
+ }
+}
diff --git a/src/Console/Commands/stubs/dictionary.php.stub b/src/Console/Commands/stubs/dictionary.php.stub
new file mode 100644
index 0000000000..1857928fac
--- /dev/null
+++ b/src/Console/Commands/stubs/dictionary.php.stub
@@ -0,0 +1,46 @@
+getItems()
+ ->when($search ?? false, function ($collection) use ($search) {
+ return $collection->filter(fn ($item) => str_contains($item['name'], $search));
+ })
+ ->mapWithKeys(fn ($item) => [$item['slug'] => $item['name']])
+ ->all();
+ }
+
+ /**
+ * Returns a single option.
+ *
+ * @param string $key
+ * @return string|array
+ */
+ public function get(string $key): string|array
+ {
+ return $this->getItems()->firstWhere('slug', $key);
+ }
+
+ private function getItems(): Collection
+ {
+ return collect([
+ ['name' => 'January', 'slug' => 'january'],
+ ['name' => 'February', 'slug' => 'february'],
+ ['name' => 'March', 'slug' => 'march'],
+ // ...
+ ]);
+ }
+}
diff --git a/src/Dictionaries/Countries.php b/src/Dictionaries/Countries.php
new file mode 100644
index 0000000000..5a20c1bf9d
--- /dev/null
+++ b/src/Dictionaries/Countries.php
@@ -0,0 +1,48 @@
+getCountries()
+ ->when($this->context['region'] ?? false, function ($collection) {
+ return $collection->where('region', $this->context['region']);
+ })
+ ->when($search ?? false, function ($collection) use ($search) {
+ return $collection->filter(function (array $country) use ($search) {
+ return str_contains(strtolower($country['name']), strtolower($search));
+ });
+ })
+ ->mapWithKeys(function (array $country) {
+ return [$country['iso3'] => "{$country['emoji']} {$country['name']}"];
+ })
+ ->all();
+ }
+
+ public function get(string $key): string|array
+ {
+ return $this->getCountries()->firstWhere('iso3', $key);
+ }
+
+ protected function fieldItems()
+ {
+ return [
+ 'region' => [
+ 'display' => __('Region'),
+ 'instructions' => __('statamic::messages.dictionaries_countries_region_instructions'),
+ 'type' => 'select',
+ 'options' => $this->getCountries()->unique('region')->pluck('region', 'region')->filter()->all(),
+ ],
+ ];
+ }
+
+ private function getCountries(): Collection
+ {
+ return collect(json_decode(File::get(__DIR__.'/../../resources/dictionaries/countries.json'), true));
+ }
+}
diff --git a/src/Dictionaries/Currencies.php b/src/Dictionaries/Currencies.php
new file mode 100644
index 0000000000..c31ff4557b
--- /dev/null
+++ b/src/Dictionaries/Currencies.php
@@ -0,0 +1,34 @@
+getCurrencies()
+ ->when($search ?? false, function ($collection) use ($search) {
+ return $collection->filter(function (array $currency) use ($search) {
+ return str_contains(strtolower($currency['name']), strtolower($search))
+ || str_contains(strtolower($currency['code']), strtolower($search));
+ });
+ })
+ ->mapWithKeys(function (array $currency) {
+ return [$currency['code'] => "{$currency['name']} ({$currency['code']})"];
+ })
+ ->all();
+ }
+
+ public function get(string $key): string|array
+ {
+ return $this->getCurrencies()->firstWhere('code', $key);
+ }
+
+ private function getCurrencies(): Collection
+ {
+ return collect(json_decode(File::get(__DIR__.'/../../resources/dictionaries/currencies.json'), true));
+ }
+}
diff --git a/src/Dictionaries/Dictionary.php b/src/Dictionaries/Dictionary.php
new file mode 100644
index 0000000000..f4688d7c23
--- /dev/null
+++ b/src/Dictionaries/Dictionary.php
@@ -0,0 +1,40 @@
+context = $context;
+
+ return $this;
+ }
+
+ protected function fieldItems()
+ {
+ return $this->fields;
+ }
+}
diff --git a/src/Dictionaries/DictionaryRepository.php b/src/Dictionaries/DictionaryRepository.php
new file mode 100644
index 0000000000..c57a774033
--- /dev/null
+++ b/src/Dictionaries/DictionaryRepository.php
@@ -0,0 +1,35 @@
+map(fn ($class) => app($class))
+ ->filter()
+ ->values();
+ }
+
+ public function find(string $handle, array $context = []): ?Dictionary
+ {
+ if (! $dictionary = app('statamic.dictionaries')->get($handle)) {
+ return null;
+ }
+
+ $dictionary = app($dictionary);
+
+ if (! $dictionary) {
+ return null;
+ }
+
+ if (! method_exists($dictionary, 'context')) {
+ return $dictionary;
+ }
+
+ return $dictionary->context($context);
+ }
+}
diff --git a/src/Dictionaries/Timezones.php b/src/Dictionaries/Timezones.php
new file mode 100644
index 0000000000..e99c67450f
--- /dev/null
+++ b/src/Dictionaries/Timezones.php
@@ -0,0 +1,19 @@
+filter(fn ($timezone) => $search ? str_contains(strtolower($timezone), strtolower($search)) : true)
+ ->mapWithKeys(fn ($timezone) => [$timezone => $timezone])
+ ->all();
+ }
+
+ public function get(string $key): string|array
+ {
+ return $key;
+ }
+}
diff --git a/src/Exceptions/DictionaryNotFoundException.php b/src/Exceptions/DictionaryNotFoundException.php
new file mode 100644
index 0000000000..dd672f1985
--- /dev/null
+++ b/src/Exceptions/DictionaryNotFoundException.php
@@ -0,0 +1,11 @@
+dictionaryHandle}] not found");
+ }
+}
diff --git a/src/Exceptions/UndefinedDictionaryException.php b/src/Exceptions/UndefinedDictionaryException.php
new file mode 100644
index 0000000000..a9c1f0f56d
--- /dev/null
+++ b/src/Exceptions/UndefinedDictionaryException.php
@@ -0,0 +1,13 @@
+ __('Options'),
+ 'fields' => [
+ 'dictionary' => [
+ 'type' => 'dictionary_fields',
+ 'hide_display' => true,
+ 'full_width_setting' => true,
+ ],
+ ],
+ ],
+ [
+ 'display' => __('Selection'),
+ 'fields' => [
+ 'placeholder' => [
+ 'display' => __('Placeholder'),
+ 'instructions' => __('statamic::fieldtypes.select.config.placeholder'),
+ 'type' => 'text',
+ 'default' => '',
+ ],
+ 'multiple' => [
+ 'display' => __('Multiple'),
+ 'instructions' => __('statamic::fieldtypes.select.config.multiple'),
+ 'type' => 'toggle',
+ 'default' => false,
+ ],
+ 'max_items' => [
+ 'display' => __('Max Items'),
+ 'instructions' => __('statamic::messages.max_items_instructions'),
+ 'min' => 1,
+ 'type' => 'integer',
+ ],
+ 'clearable' => [
+ 'display' => __('Clearable'),
+ 'instructions' => __('statamic::fieldtypes.select.config.clearable'),
+ 'type' => 'toggle',
+ 'default' => false,
+ ],
+ 'searchable' => [
+ 'display' => __('Searchable'),
+ 'instructions' => __('statamic::fieldtypes.select.config.searchable'),
+ 'type' => 'toggle',
+ 'default' => true,
+ ],
+ ],
+ ],
+ [
+ 'display' => __('Data'),
+ 'fields' => [
+ 'default' => [
+ 'display' => __('Default Value'),
+ 'instructions' => __('statamic::messages.fields_default_instructions'),
+ 'type' => 'text',
+ ],
+ ],
+ ],
+ ];
+ }
+
+ public function preload(): array
+ {
+ return [
+ 'url' => cp_route('dictionary-fieldtype', $this->dictionary()->handle()),
+ 'selectedOptions' => collect($this->dictionary()->options())
+ ->only($this->field->value())
+ ->map(fn ($label, $value) => ['value' => $value, 'label' => $label])
+ ->values()
+ ->all(),
+ ];
+ }
+
+ public function augment($value)
+ {
+ if (is_null($value)) {
+ return [];
+ }
+
+ $dictionary = $this->dictionary();
+
+ if ($this->multiple()) {
+ return collect($value)->map(function ($value) use ($dictionary) {
+ return $dictionary->get($value);
+ })->all();
+ }
+
+ return $dictionary->get($value);
+ }
+
+ public function extraRenderableFieldData(): array
+ {
+ return [
+ 'options' => $this->dictionary()->options(),
+ ];
+ }
+
+ protected function multiple(): bool
+ {
+ return $this->config('multiple');
+ }
+
+ public function dictionary(): \Statamic\Dictionaries\Dictionary
+ {
+ if (! $this->config('dictionary')) {
+ throw new UndefinedDictionaryException();
+ }
+
+ $config = $this->config('dictionary');
+
+ $handle = is_array($config) ? Arr::get($config, 'type') : $config;
+ $context = is_array($config) ? Arr::except($config, 'type') : [];
+
+ $dictionary = \Statamic\Facades\Dictionary::find($handle, $context);
+
+ if (! $dictionary) {
+ throw new DictionaryNotFoundException($handle);
+ }
+
+ return $dictionary;
+ }
+}
diff --git a/src/Fieldtypes/DictionaryFields.php b/src/Fieldtypes/DictionaryFields.php
new file mode 100644
index 0000000000..35cac097bc
--- /dev/null
+++ b/src/Fieldtypes/DictionaryFields.php
@@ -0,0 +1,100 @@
+ 'type',
+ 'field' => [
+ 'display' => __('Dictionary'),
+ 'instructions' => __('statamic::fieldtypes.dictionary.config.dictionary'),
+ 'type' => 'select',
+ 'options' => Dictionary::all()
+ ->mapWithKeys(fn ($dictionary) => [$dictionary->handle() => $dictionary->title()])
+ ->all(),
+ 'max_items' => 1,
+ 'validate' => 'required',
+ ],
+ ]]);
+
+ return [
+ 'type' => [
+ 'fields' => $typeField->toPublishArray(),
+ 'meta' => $typeField->meta(),
+ ],
+ 'dictionaries' => Dictionary::all()->mapWithKeys(function (\Statamic\Dictionaries\Dictionary $dictionary) {
+ return [$dictionary->handle() => [
+ 'fields' => $dictionary->fields()->toPublishArray(),
+ 'meta' => $dictionary->fields()->meta(),
+ 'defaults' => $dictionary->fields()->all()->map(function ($field) {
+ return $field->fieldtype()->preProcess($field->defaultValue());
+ }),
+ ]];
+ }),
+ ];
+ }
+
+ public function preProcess($data): array
+ {
+ if (is_null($data)) {
+ return ['type' => null];
+ }
+
+ if (is_string($data)) {
+ return ['type' => $data];
+ }
+
+ $dictionary = Dictionary::find($data['type']);
+
+ return array_merge(
+ ['type' => $data['type']],
+ $dictionary->fields()->addValues($data)->preProcess()->values()->all()
+ );
+ }
+
+ public function process($data): string|array
+ {
+ $dictionary = Dictionary::find($data['type']);
+ $values = $dictionary->fields()->addValues($data)->process()->values();
+
+ if ($values->filter()->isEmpty()) {
+ return $dictionary->handle();
+ }
+
+ return array_merge(['type' => $dictionary->handle()], $values->all());
+ }
+
+ public function extraRules(): array
+ {
+ if (! $dictionary = Arr::get($this->field->value(), 'type')) {
+ return [
+ $this->field->handle().'.type' => ['required'],
+ ];
+ }
+
+ $dictionary = Dictionary::find($dictionary);
+
+ $rules = $dictionary
+ ->fields()
+ ->addValues((array) $this->field->value())
+ ->validator()
+ ->withContext([
+ 'prefix' => $this->field->handle().'.',
+ ])
+ ->rules();
+
+ return collect($rules)->mapWithKeys(function ($rules, $handle) {
+ return [$this->field->handle().'.'.$handle => $rules];
+ })->all();
+ }
+}
diff --git a/src/Http/Controllers/CP/Fieldtypes/DictionaryFieldtypeController.php b/src/Http/Controllers/CP/Fieldtypes/DictionaryFieldtypeController.php
new file mode 100644
index 0000000000..5d442c916f
--- /dev/null
+++ b/src/Http/Controllers/CP/Fieldtypes/DictionaryFieldtypeController.php
@@ -0,0 +1,46 @@
+fieldtype($request);
+
+ return [
+ 'data' => $fieldtype->dictionary()->options($request->search),
+ ];
+ }
+
+ protected function fieldtype($request)
+ {
+ $config = $this->getConfig($request);
+
+ return Fieldtype::find($config['type'])->setField(
+ new Field('relationship', $config)
+ );
+ }
+
+ private function getConfig($request)
+ {
+ // The fieldtype base64-encodes the config.
+ $json = base64_decode($request->config);
+
+ // The json may include unicode characters, so we'll try to convert it to UTF-8.
+ // See https://github.com/statamic/cms/issues/566
+ $utf8 = mb_convert_encoding($json, 'UTF-8', mb_list_encodings());
+
+ // In PHP 8.1 there's a bug where encoding will return null. It's fixed in 8.1.2.
+ // In this case, we'll fall back to the original JSON, but without the encoding.
+ // Issue #566 may still occur, but it's better than failing completely.
+ $json = empty($utf8) ? $json : $utf8;
+
+ return json_decode($json, true);
+ }
+}
diff --git a/src/Providers/AddonServiceProvider.php b/src/Providers/AddonServiceProvider.php
index 307bd9e76c..44d463ed76 100644
--- a/src/Providers/AddonServiceProvider.php
+++ b/src/Providers/AddonServiceProvider.php
@@ -10,6 +10,7 @@
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Statamic\Actions\Action;
+use Statamic\Dictionaries\Dictionary;
use Statamic\Exceptions\NotBootedException;
use Statamic\Extend\Manifest;
use Statamic\Facades\Addon;
@@ -55,6 +56,11 @@ abstract class AddonServiceProvider extends ServiceProvider
*/
protected $actions = [];
+ /**
+ * @var list>
+ */
+ protected $dictionaries = [];
+
/**
* @var list>
*/
@@ -187,6 +193,7 @@ public function boot()
->bootTags()
->bootScopes()
->bootActions()
+ ->bootDictionaries()
->bootFieldtypes()
->bootModifiers()
->bootWidgets()
@@ -258,6 +265,15 @@ protected function bootActions()
return $this;
}
+ protected function bootDictionaries()
+ {
+ foreach ($this->dictionaries as $class) {
+ $class::register();
+ }
+
+ return $this;
+ }
+
protected function bootFieldtypes()
{
foreach ($this->fieldtypes as $class) {
diff --git a/src/Providers/ConsoleServiceProvider.php b/src/Providers/ConsoleServiceProvider.php
index 79f5bb4517..618a82358b 100644
--- a/src/Providers/ConsoleServiceProvider.php
+++ b/src/Providers/ConsoleServiceProvider.php
@@ -22,6 +22,7 @@ class ConsoleServiceProvider extends ServiceProvider
Commands\LicenseSet::class,
Commands\MakeAction::class,
Commands\MakeAddon::class,
+ Commands\MakeDictionary::class,
Commands\MakeFieldtype::class,
Commands\MakeModifier::class,
Commands\MakeScope::class,
diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php
index 10a0da94a3..30de3f4d76 100644
--- a/src/Providers/ExtensionServiceProvider.php
+++ b/src/Providers/ExtensionServiceProvider.php
@@ -6,6 +6,8 @@
use Illuminate\Support\ServiceProvider;
use Statamic\Actions;
use Statamic\Actions\Action;
+use Statamic\Dictionaries;
+use Statamic\Dictionaries\Dictionary;
use Statamic\Extend\Manifest;
use Statamic\Fields\Fieldtype;
use Statamic\Fieldtypes;
@@ -48,6 +50,12 @@ class ExtensionServiceProvider extends ServiceProvider
Actions\Impersonate::class,
];
+ protected $dictionaries = [
+ Dictionaries\Countries::class,
+ Dictionaries\Currencies::class,
+ Dictionaries\Timezones::class,
+ ];
+
protected $fieldtypes = [
Fieldtypes\Arr::class,
Fieldtypes\AssetContainer::class,
@@ -63,6 +71,8 @@ class ExtensionServiceProvider extends ServiceProvider
Fieldtypes\Collections::class,
Fieldtypes\Color::class,
Fieldtypes\Date::class,
+ Fieldtypes\Dictionary::class,
+ Fieldtypes\DictionaryFields::class,
Fieldtypes\Entries::class,
Fieldtypes\FieldDisplay::class,
Fieldtypes\Files::class,
@@ -263,6 +273,11 @@ protected function registerExtensions()
'directory' => 'Actions',
'extensions' => $this->actions,
],
+ 'dictionaries' => [
+ 'class' => Dictionary::class,
+ 'directory' => 'Dictionaries',
+ 'extensions' => $this->dictionaries,
+ ],
'fieldtypes' => [
'class' => Fieldtype::class,
'directory' => 'Fieldtypes',
diff --git a/tests/Console/Commands/Concerns/CleansUpGeneratedPaths.php b/tests/Console/Commands/Concerns/CleansUpGeneratedPaths.php
index c72fb1dcc3..45d2a63ed4 100644
--- a/tests/Console/Commands/Concerns/CleansUpGeneratedPaths.php
+++ b/tests/Console/Commands/Concerns/CleansUpGeneratedPaths.php
@@ -11,6 +11,7 @@ protected function cleanupPaths()
$dirs = [
base_path('addons'),
base_path('app/Actions'),
+ base_path('app/Dictionaries'),
base_path('app/Fieldtypes'),
base_path('app/Modifiers'),
base_path('app/Scopes'),
diff --git a/tests/Console/Commands/MakeDictionaryTest.php b/tests/Console/Commands/MakeDictionaryTest.php
new file mode 100644
index 0000000000..48c9a61832
--- /dev/null
+++ b/tests/Console/Commands/MakeDictionaryTest.php
@@ -0,0 +1,92 @@
+files = app(Filesystem::class);
+ $this->fakeSuccessfulComposerRequire();
+ }
+
+ public function tearDown(): void
+ {
+ $this->cleanupPaths();
+
+ parent::tearDown();
+ }
+
+ #[Test]
+ public function it_can_make_a_dictionary()
+ {
+ $path = base_path('app/Dictionaries/Provinces.php');
+
+ $this->assertFileDoesNotExist($path);
+
+ $this->artisan('statamic:make:dictionary', ['name' => 'Provinces']);
+
+ $this->assertFileExists($path);
+ $this->assertStringContainsString('namespace App\Dictionaries;', $this->files->get($path));
+ }
+
+ #[Test]
+ public function it_will_not_overwrite_an_existing_dictionary()
+ {
+ $path = base_path('app/Dictionaries/Provinces.php');
+
+ $this->artisan('statamic:make:dictionary', ['name' => 'Provinces']);
+ $this->files->put($path, 'overwritten action');
+
+ $this->assertStringContainsString('overwritten action', $this->files->get($path));
+
+ $this->artisan('statamic:make:dictionary', ['name' => 'Provinces']);
+
+ $this->assertStringContainsString('overwritten action', $this->files->get($path));
+ }
+
+ #[Test]
+ public function using_force_option_will_overwrite_original_dictionary()
+ {
+ $path = base_path('app/Dictionaries/Provinces.php');
+
+ $this->artisan('statamic:make:dictionary', ['name' => 'Provinces']);
+ $this->files->put($path, 'overwritten action');
+
+ $this->assertStringContainsString('overwritten action', $this->files->get($path));
+
+ $this->artisan('statamic:make:dictionary', ['name' => 'Provinces', '--force' => true]);
+
+ $this->assertStringNotContainsString('overwritten action', $this->files->get($path));
+ }
+
+ #[Test]
+ public function it_can_make_a_dictionary_into_an_addon()
+ {
+ $path = base_path('addons/yoda/bag-odah');
+
+ $this->artisan('statamic:make:addon', ['addon' => 'yoda/bag-odah']);
+
+ Composer::shouldReceive('installedPath')->andReturn($path);
+
+ $this->assertFileDoesNotExist($action = "$path/src/Dictionaries/Provinces.php");
+
+ $this->artisan('statamic:make:dictionary', ['name' => 'Provinces', 'addon' => 'yoda/bag-odah']);
+
+ $this->assertFileExists($action);
+ $this->assertStringContainsString('namespace Yoda\BagOdah\Dictionaries;', $this->files->get($action));
+ }
+}
diff --git a/tests/Dictionaries/DictionariesTest.php b/tests/Dictionaries/DictionariesTest.php
new file mode 100644
index 0000000000..1f5f4add92
--- /dev/null
+++ b/tests/Dictionaries/DictionariesTest.php
@@ -0,0 +1,129 @@
+assertCount(4, $all); // The built-in dictionaries + our fake one
+ $this->assertEveryItem($all, fn ($item) => $item instanceof Dictionary);
+ }
+
+ #[Test]
+ public function can_get_a_dictionary()
+ {
+ $find = DictionaryFacade::find('fake_dictionary');
+
+ $this->assertInstanceOf(Dictionary::class, $find);
+ $this->assertSame('fake_dictionary', $find->handle());
+ }
+
+ #[Test]
+ public function can_get_options()
+ {
+ $dictionary = DictionaryFacade::find('fake_dictionary');
+
+ $this->assertEquals([
+ 'foo' => 'Foo',
+ 'bar' => 'Bar',
+ 'baz' => 'Baz',
+ 'qux' => 'Qux',
+ ], $dictionary->options());
+ }
+
+ #[Test]
+ public function can_get_options_with_search_query()
+ {
+ $dictionary = DictionaryFacade::find('fake_dictionary');
+
+ $this->assertEquals([
+ 'bar' => 'Bar',
+ 'baz' => 'Baz',
+ ], $dictionary->options('ba'));
+ }
+
+ #[Test]
+ public function can_get_option()
+ {
+ $dictionary = DictionaryFacade::find('fake_dictionary');
+
+ $this->assertEquals([
+ 'name' => 'Foo',
+ 'id' => 'foo',
+ ], $dictionary->get('foo'));
+ }
+
+ #[Test]
+ public function ensure_context_is_passed_to_dictionary()
+ {
+ $dictionary = DictionaryFacade::find('fake_dictionary', [
+ 'sort_in_alphabetical_order' => true,
+ ]);
+
+ // When the sort_in_alphabetical_order context is passed,
+ // the options should be returned in alphabetical order.
+ $this->assertEquals([
+ 'bar' => 'Bar',
+ 'baz' => 'Baz',
+ 'foo' => 'Foo',
+ 'qux' => 'Qux',
+ ], $dictionary->options());
+ }
+}
+
+class FakeDictionary extends Dictionary
+{
+ public function options(?string $search = null): array
+ {
+ return $this->data()
+ ->when($search ?? false, function ($collection) use ($search) {
+ return $collection->filter(fn ($item) => str_contains($item['id'], $search));
+ })
+ ->mapWithKeys(fn ($item) => [$item['id'] => $item['name']])
+ ->when($this->context['sort_in_alphabetical_order'] ?? false, function ($collection) {
+ return $collection->sortBy('id');
+ })
+ ->all();
+ }
+
+ public function get(string $key): string|array
+ {
+ return $this->data()->firstWhere('id', $key);
+ }
+
+ protected function data()
+ {
+ return collect([
+ ['name' => 'Foo', 'id' => 'foo'],
+ ['name' => 'Bar', 'id' => 'bar'],
+ ['name' => 'Baz', 'id' => 'baz'],
+ ['name' => 'Qux', 'id' => 'qux'],
+ ]);
+ }
+
+ protected function fieldItems()
+ {
+ return [
+ 'sort_in_alphabetical_order' => [
+ 'display' => 'Sort in alphabetical order?',
+ 'type' => 'toggle',
+ ],
+ ];
+ }
+}
diff --git a/tests/Fieldtypes/DictionariesTest.php b/tests/Fieldtypes/DictionariesTest.php
new file mode 100644
index 0000000000..d818ebdbbe
--- /dev/null
+++ b/tests/Fieldtypes/DictionariesTest.php
@@ -0,0 +1,106 @@
+ 'dictionary', 'dictionary' => 'countries']));
+ $field->setValue(['USA', 'AUS', 'CAN', 'DEU', 'GBR']);
+
+ $fieldtype = FieldtypeRepository::find('dictionary');
+ $fieldtype->setField($field);
+
+ $preload = $fieldtype->preload();
+
+ $this->assertArraySubset([
+ 'url' => 'http://localhost/cp/fieldtypes/dictionaries/countries',
+ 'selectedOptions' => [
+ ['value' => 'AUS', 'label' => 'π¦πΊ Australia'],
+ ['value' => 'CAN', 'label' => 'π¨π¦ Canada'],
+ ['value' => 'DEU', 'label' => 'π©πͺ Germany'],
+ ['value' => 'GBR', 'label' => 'π¬π§ United Kingdom'],
+ ['value' => 'USA', 'label' => 'πΊπΈ United States'],
+ ],
+ ], $preload);
+ }
+
+ #[Test]
+ public function it_augments_a_single_option()
+ {
+ $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries']));
+
+ $fieldtype = FieldtypeRepository::find('dictionary');
+ $fieldtype->setField($field);
+
+ $augment = $fieldtype->augment('USA');
+
+ $this->assertEquals([
+ 'name' => 'United States',
+ 'iso3' => 'USA',
+ 'iso2' => 'US',
+ 'region' => 'Americas',
+ 'subregion' => 'Northern America',
+ 'emoji' => 'πΊπΈ',
+ ], $augment);
+ }
+
+ #[Test]
+ public function it_augments_multiple_options()
+ {
+ $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries', 'multiple' => true]));
+
+ $fieldtype = FieldtypeRepository::find('dictionary');
+ $fieldtype->setField($field);
+
+ $augment = $fieldtype->augment(['USA', 'GBR']);
+
+ $this->assertEquals([
+ [
+ 'name' => 'United States',
+ 'iso3' => 'USA',
+ 'iso2' => 'US',
+ 'region' => 'Americas',
+ 'subregion' => 'Northern America',
+ 'emoji' => 'πΊπΈ',
+ ],
+ [
+ 'name' => 'United Kingdom',
+ 'iso3' => 'GBR',
+ 'iso2' => 'GB',
+ 'region' => 'Europe',
+ 'subregion' => 'Northern Europe',
+ 'emoji' => 'π¬π§',
+ ],
+ ], $augment);
+ }
+
+ #[Test]
+ public function it_returns_extra_renderable_field_data()
+ {
+ $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries']));
+ $field->setValue(['USA', 'AUS', 'CAN', 'DEU', 'GBR']);
+
+ $fieldtype = FieldtypeRepository::find('dictionary');
+ $fieldtype->setField($field);
+
+ $extraRenderableFieldData = $fieldtype->extraRenderableFieldData();
+
+ $this->assertArraySubset([
+ 'options' => [
+ 'AUS' => 'π¦πΊ Australia',
+ 'CAN' => 'π¨π¦ Canada',
+ 'DEU' => 'π©πͺ Germany',
+ 'GBR' => 'π¬π§ United Kingdom',
+ 'USA' => 'πΊπΈ United States',
+ ],
+ ], $extraRenderableFieldData);
+ }
+}
diff --git a/tests/Fieldtypes/DictionaryFieldsTest.php b/tests/Fieldtypes/DictionaryFieldsTest.php
new file mode 100644
index 0000000000..862c3cbf4a
--- /dev/null
+++ b/tests/Fieldtypes/DictionaryFieldsTest.php
@@ -0,0 +1,155 @@
+preload();
+
+ $this->assertArraySubset([
+ 'type' => [
+ 'fields' => [
+ ['handle' => 'type', 'type' => 'select'],
+ ],
+ 'meta' => collect(['type' => null]),
+ ],
+ ], $preload);
+
+ $this->assertArraySubset([
+ 'fake_dictionary' => [
+ 'fields' => [
+ ['handle' => 'category', 'type' => 'select'],
+ ],
+ 'meta' => collect(['category' => null]),
+ 'defaults' => collect(['category' => null]),
+ ],
+ ], $preload['dictionaries']->all());
+ }
+
+ #[Test]
+ public function it_pre_processes_dictionary_fields()
+ {
+ $fieldtype = FieldtypeRepository::find('dictionary_fields');
+
+ $preProcess = $fieldtype->preProcess([
+ 'type' => 'fake_dictionary',
+ 'category' => 'foo',
+ ]);
+
+ $this->assertEquals([
+ 'type' => 'fake_dictionary',
+ 'category' => 'foo',
+ ], $preProcess);
+ }
+
+ #[Test]
+ public function it_pre_processes_dictionary_fields_saved_as_a_string()
+ {
+ $fieldtype = FieldtypeRepository::find('dictionary_fields');
+
+ $preProcess = $fieldtype->preProcess('fake_dictionary');
+
+ $this->assertEquals([
+ 'type' => 'fake_dictionary',
+ ], $preProcess);
+ }
+
+ #[Test]
+ public function it_processes_dictionary_fields()
+ {
+ $fieldtype = FieldtypeRepository::find('dictionary_fields');
+
+ $process = $fieldtype->process([
+ 'type' => 'fake_dictionary',
+ 'category' => 'foo',
+ ]);
+
+ $this->assertEquals([
+ 'type' => 'fake_dictionary',
+ 'category' => 'foo',
+ ], $process);
+ }
+
+ #[Test]
+ public function it_processes_dictionary_fields_into_a_string_when_dictionary_has_no_config_values()
+ {
+ $fieldtype = FieldtypeRepository::find('dictionary_fields');
+
+ $process = $fieldtype->process([
+ 'type' => 'fake_dictionary',
+ ]);
+
+ $this->assertEquals('fake_dictionary', $process);
+ }
+
+ #[Test]
+ public function it_returns_validation_rules()
+ {
+ $field = (new Field('test', ['type' => 'dictionary_fields']))->setValue(['type' => 'fake_dictionary']);
+
+ $fieldtype = FieldtypeRepository::find('dictionary_fields');
+ $fieldtype->setField($field);
+
+ $extraRules = $fieldtype->extraRules();
+
+ $this->assertEquals([
+ 'test.category' => ['required'],
+ ], $extraRules);
+ }
+
+ #[Test]
+ public function it_returns_validation_rules_when_no_dictionary_is_selected()
+ {
+ $field = (new Field('test', ['type' => 'dictionary_fields']));
+
+ $fieldtype = FieldtypeRepository::find('dictionary_fields');
+ $fieldtype->setField($field);
+
+ $extraRules = $fieldtype->extraRules();
+
+ $this->assertEquals([
+ 'test.type' => ['required'],
+ ], $extraRules);
+ }
+}
+
+class FakeDictionary extends Dictionary
+{
+ public function options(?string $search = null): array
+ {
+ return [];
+ }
+
+ public function get(string $key): string|array
+ {
+ return [];
+ }
+
+ protected function fieldItems()
+ {
+ return [
+ 'category' => [
+ 'type' => 'select',
+ 'validate' => 'required',
+ ],
+ ];
+ }
+}