diff --git a/resources/css/components/fieldtypes/dictionary-fields.css b/resources/css/components/fieldtypes/dictionary-fields.css new file mode 100644 index 0000000000..f910a979a1 --- /dev/null +++ b/resources/css/components/fieldtypes/dictionary-fields.css @@ -0,0 +1,20 @@ +.dictionary_fields-fieldtype { + @apply p-0; + + .publish-fields { + @apply w-full; + } + + .config-field { + @apply md:flex flex-wrap border-b border-gray-400 dark:border-dark-900 w-full; + @apply p-3 @sm:p-4 m-0; + + .field-inner { + @apply w-full md:w-1/2 rtl:md:pl-8 ltr:md:pr-8; + } + + .field-inner + div { + @apply w-full md:w-1/2; + } + } +} diff --git a/resources/css/cp.css b/resources/css/cp.css index 791941b281..0c9768b493 100644 --- a/resources/css/cp.css +++ b/resources/css/cp.css @@ -58,6 +58,7 @@ @import "components/fieldtypes/checkboxes"; @import "components/fieldtypes/code"; @import "components/fieldtypes/datetime"; +@import "components/fieldtypes/dictionary-fields"; @import "components/fieldtypes/environment"; @import "components/fieldtypes/grid"; @import "components/fieldtypes/hidden"; diff --git a/resources/css/vendors/vue-select.css b/resources/css/vendors/vue-select.css index f4cfbbc050..4f2222c637 100644 --- a/resources/css/vendors/vue-select.css +++ b/resources/css/vendors/vue-select.css @@ -173,6 +173,12 @@ &.sortable-item { @apply !cursor-grab; } + &.invalid { + @apply border-red-300 dark:border-dark-red bg-red-100 dark:bg-red-400 text-red-500 dark:text-red-950; + background-image: none; + + .vs__deselect { @apply text-red-500 dark:text-red-950 } + } } .vs__deselect { diff --git a/resources/js/bootstrap/fieldtypes.js b/resources/js/bootstrap/fieldtypes.js index 245cb53d69..fb18e8689c 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'; @@ -86,6 +88,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..6c92a372d0 --- /dev/null +++ b/resources/js/components/fieldtypes/DictionaryFields.vue @@ -0,0 +1,82 @@ + + + diff --git a/resources/js/components/fieldtypes/DictionaryFieldtype.vue b/resources/js/components/fieldtypes/DictionaryFieldtype.vue new file mode 100644 index 0000000000..7cf2d83611 --- /dev/null +++ b/resources/js/components/fieldtypes/DictionaryFieldtype.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/resources/lang/en/fieldtypes.php b/resources/lang/en/fieldtypes.php index 050eb22567..58f8b983b6 100644 --- a/resources/lang/en/fieldtypes.php +++ b/resources/lang/en/fieldtypes.php @@ -72,6 +72,10 @@ 'date.config.time_enabled' => 'Enable the timepicker.', 'date.config.time_seconds_enabled' => 'Show seconds in the timepicker.', 'date.title' => 'Date', + 'dictionary.config.dictionary' => 'The dictionary you wish to pull options from.', + 'dictionary.file.config.filename' => 'The filename containing your options, relative to the `resources/dictionaries` directory.', + 'dictionary.file.config.label' => "The key containing the options' labels. By default it's `label`. Alternatively, you may use Antlers.", + 'dictionary.file.config.value' => "The key containing the options' values. By default it's `value`.", '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 e218aa22c7..1ac3670ab5 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' => 'Optionally filter the countries by region.', '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..05b2bc54ef --- /dev/null +++ b/src/Console/Commands/stubs/dictionary.php.stub @@ -0,0 +1,21 @@ + 'Alabama', 'abbr' => 'AL', 'capital' => 'Montgomery'], + ['name' => 'Alaska', 'abbr' => 'AK', 'capital' => 'Juneau'], + ['name' => 'Arizona', 'abbr' => 'AZ', 'capital' => 'Phoenix'], + // ... + ]; + } +} diff --git a/src/Dictionaries/BasicDictionary.php b/src/Dictionaries/BasicDictionary.php new file mode 100644 index 0000000000..45d6e5f322 --- /dev/null +++ b/src/Dictionaries/BasicDictionary.php @@ -0,0 +1,69 @@ +collectItems()->get($key); + } + + public function options(?string $search = null): array + { + return $this + ->getFilteredItems() + ->when($search, fn ($collection) => $collection->filter(fn ($item) => $this->matchesSearchQuery($search, $item))) + ->mapWithKeys(fn (Item $item) => [$item->value() => $item->label()]) + ->all(); + } + + protected function getFilteredItems(): Collection + { + return $this->collectItems(); + } + + protected function collectItems(): Collection + { + return collect($this->getItems())->mapWithKeys(function ($arr) { + $item = new Item($key = $this->getItemValue($arr), $this->getItemLabel($arr), $arr); + + return [$key => $item]; + }); + } + + protected function getItemValue(array $item): string + { + return $item[$this->valueKey]; + } + + protected function getItemLabel(array $item): string + { + return $item[$this->labelKey]; + } + + protected function matchesSearchQuery(string $query, Item $item): bool + { + $query = strtolower($query); + + foreach ($item->data() as $key => $value) { + if (! empty($this->searchable) && ! in_array($key, $this->searchable)) { + continue; + } + + if (str_contains(strtolower($value), $query)) { + return true; + } + } + + return false; + } + + abstract protected function getItems(): array; +} diff --git a/src/Dictionaries/Countries.php b/src/Dictionaries/Countries.php new file mode 100644 index 0000000000..669c20ec15 --- /dev/null +++ b/src/Dictionaries/Countries.php @@ -0,0 +1,299 @@ + 'Africa', + 'americas' => 'Americas', + 'asia' => 'Asia', + 'europe' => 'Europe', + 'oceania' => 'Oceania', + 'polar' => 'Polar', + ]; + + protected function getItemLabel(array $item): string + { + return "{$item['emoji']} {$item['name']}"; + } + + protected function fieldItems() + { + return [ + 'region' => [ + 'display' => __('Region'), + 'instructions' => __('statamic::messages.dictionaries_countries_region_instructions'), + 'type' => 'select', + 'options' => $this->regions, + ], + ]; + } + + protected function getFilteredItems(): Collection + { + return $this + ->collectItems() + ->when($this->config['region'] ?? false, fn ($collection, $region) => $collection->where('region', $this->regions[$region])); + } + + protected function getItems(): array + { + return [ + ['name' => 'Afghanistan', 'iso3' => 'AFG', 'iso2' => 'AF', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ซ'], + ['name' => 'Aland Islands', 'iso3' => 'ALA', 'iso2' => 'AX', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ฝ'], + ['name' => 'Albania', 'iso3' => 'ALB', 'iso2' => 'AL', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ฑ'], + ['name' => 'Algeria', 'iso3' => 'DZA', 'iso2' => 'DZ', 'region' => 'Africa', 'subregion' => 'Northern Africa', 'emoji' => '๐Ÿ‡ฉ๐Ÿ‡ฟ'], + ['name' => 'American Samoa', 'iso3' => 'ASM', 'iso2' => 'AS', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ธ'], + ['name' => 'Andorra', 'iso3' => 'AND', 'iso2' => 'AD', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ฉ'], + ['name' => 'Angola', 'iso3' => 'AGO', 'iso2' => 'AO', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ด'], + ['name' => 'Anguilla', 'iso3' => 'AIA', 'iso2' => 'AI', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ฎ'], + ['name' => 'Antarctica', 'iso3' => 'ATA', 'iso2' => 'AQ', 'region' => 'Polar', 'subregion' => '', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ถ'], + ['name' => 'Antigua And Barbuda', 'iso3' => 'ATG', 'iso2' => 'AG', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ฌ'], + ['name' => 'Argentina', 'iso3' => 'ARG', 'iso2' => 'AR', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ท'], + ['name' => 'Armenia', 'iso3' => 'ARM', 'iso2' => 'AM', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ฒ'], + ['name' => 'Aruba', 'iso3' => 'ABW', 'iso2' => 'AW', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ผ'], + ['name' => 'Australia', 'iso3' => 'AUS', 'iso2' => 'AU', 'region' => 'Oceania', 'subregion' => 'Australia and New Zealand', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡บ'], + ['name' => 'Austria', 'iso3' => 'AUT', 'iso2' => 'AT', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡น'], + ['name' => 'Azerbaijan', 'iso3' => 'AZE', 'iso2' => 'AZ', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ฟ'], + ['name' => 'Bahamas The', 'iso3' => 'BHS', 'iso2' => 'BS', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ธ'], + ['name' => 'Bahrain', 'iso3' => 'BHR', 'iso2' => 'BH', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ญ'], + ['name' => 'Bangladesh', 'iso3' => 'BGD', 'iso2' => 'BD', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฉ'], + ['name' => 'Barbados', 'iso3' => 'BRB', 'iso2' => 'BB', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ง'], + ['name' => 'Belarus', 'iso3' => 'BLR', 'iso2' => 'BY', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ง๐Ÿ‡พ'], + ['name' => 'Belgium', 'iso3' => 'BEL', 'iso2' => 'BE', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ช'], + ['name' => 'Belize', 'iso3' => 'BLZ', 'iso2' => 'BZ', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฟ'], + ['name' => 'Benin', 'iso3' => 'BEN', 'iso2' => 'BJ', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฏ'], + ['name' => 'Bermuda', 'iso3' => 'BMU', 'iso2' => 'BM', 'region' => 'Americas', 'subregion' => 'Northern America', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฒ'], + ['name' => 'Bhutan', 'iso3' => 'BTN', 'iso2' => 'BT', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ง๐Ÿ‡น'], + ['name' => 'Bolivia', 'iso3' => 'BOL', 'iso2' => 'BO', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ด'], + ['name' => 'Bonaire, Sint Eustatius and Saba', 'iso3' => 'BES', 'iso2' => 'BQ', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ถ'], + ['name' => 'Bosnia and Herzegovina', 'iso3' => 'BIH', 'iso2' => 'BA', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฆ'], + ['name' => 'Botswana', 'iso3' => 'BWA', 'iso2' => 'BW', 'region' => 'Africa', 'subregion' => 'Southern Africa', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ผ'], + ['name' => 'Bouvet Island', 'iso3' => 'BVT', 'iso2' => 'BV', 'region' => '', 'subregion' => '', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ป'], + ['name' => 'Brazil', 'iso3' => 'BRA', 'iso2' => 'BR', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ท'], + ['name' => 'British Indian Ocean Territory', 'iso3' => 'IOT', 'iso2' => 'IO', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ด'], + ['name' => 'Brunei', 'iso3' => 'BRN', 'iso2' => 'BN', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ณ'], + ['name' => 'Bulgaria', 'iso3' => 'BGR', 'iso2' => 'BG', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฌ'], + ['name' => 'Burkina Faso', 'iso3' => 'BFA', 'iso2' => 'BF', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ซ'], + ['name' => 'Burundi', 'iso3' => 'BDI', 'iso2' => 'BI', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฎ'], + ['name' => 'Cambodia', 'iso3' => 'KHM', 'iso2' => 'KH', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ญ'], + ['name' => 'Cameroon', 'iso3' => 'CMR', 'iso2' => 'CM', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฒ'], + ['name' => 'Canada', 'iso3' => 'CAN', 'iso2' => 'CA', 'region' => 'Americas', 'subregion' => 'Northern America', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฆ'], + ['name' => 'Cape Verde', 'iso3' => 'CPV', 'iso2' => 'CV', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ป'], + ['name' => 'Cayman Islands', 'iso3' => 'CYM', 'iso2' => 'KY', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡พ'], + ['name' => 'Central African Republic', 'iso3' => 'CAF', 'iso2' => 'CF', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ซ'], + ['name' => 'Chad', 'iso3' => 'TCD', 'iso2' => 'TD', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡น๐Ÿ‡ฉ'], + ['name' => 'Chile', 'iso3' => 'CHL', 'iso2' => 'CL', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฑ'], + ['name' => 'China', 'iso3' => 'CHN', 'iso2' => 'CN', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ณ'], + ['name' => 'Christmas Island', 'iso3' => 'CXR', 'iso2' => 'CX', 'region' => 'Oceania', 'subregion' => 'Australia and New Zealand', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฝ'], + ['name' => 'Cocos (Keeling) Islands', 'iso3' => 'CCK', 'iso2' => 'CC', 'region' => 'Oceania', 'subregion' => 'Australia and New Zealand', 'emoji' => '๐Ÿ‡จ๐Ÿ‡จ'], + ['name' => 'Colombia', 'iso3' => 'COL', 'iso2' => 'CO', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ด'], + ['name' => 'Comoros', 'iso3' => 'COM', 'iso2' => 'KM', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ฒ'], + ['name' => 'Congo', 'iso3' => 'COG', 'iso2' => 'CG', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฌ'], + ['name' => 'Cook Islands', 'iso3' => 'COK', 'iso2' => 'CK', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฐ'], + ['name' => 'Costa Rica', 'iso3' => 'CRI', 'iso2' => 'CR', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ท'], + ['name' => 'Cote D\'Ivoire (Ivory Coast)', 'iso3' => 'CIV', 'iso2' => 'CI', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฎ'], + ['name' => 'Croatia', 'iso3' => 'HRV', 'iso2' => 'HR', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ญ๐Ÿ‡ท'], + ['name' => 'Cuba', 'iso3' => 'CUB', 'iso2' => 'CU', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡จ๐Ÿ‡บ'], + ['name' => 'Cura\\u00e7ao', 'iso3' => 'CUW', 'iso2' => 'CW', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ผ'], + ['name' => 'Cyprus', 'iso3' => 'CYP', 'iso2' => 'CY', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡จ๐Ÿ‡พ'], + ['name' => 'Czech Republic', 'iso3' => 'CZE', 'iso2' => 'CZ', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฟ'], + ['name' => 'Democratic Republic of the Congo', 'iso3' => 'COD', 'iso2' => 'CD', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ฉ'], + ['name' => 'Denmark', 'iso3' => 'DNK', 'iso2' => 'DK', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฉ๐Ÿ‡ฐ'], + ['name' => 'Djibouti', 'iso3' => 'DJI', 'iso2' => 'DJ', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฉ๐Ÿ‡ฏ'], + ['name' => 'Dominica', 'iso3' => 'DMA', 'iso2' => 'DM', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฉ๐Ÿ‡ฒ'], + ['name' => 'Dominican Republic', 'iso3' => 'DOM', 'iso2' => 'DO', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฉ๐Ÿ‡ด'], + ['name' => 'East Timor', 'iso3' => 'TLS', 'iso2' => 'TL', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ฑ'], + ['name' => 'Ecuador', 'iso3' => 'ECU', 'iso2' => 'EC', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ช๐Ÿ‡จ'], + ['name' => 'Egypt', 'iso3' => 'EGY', 'iso2' => 'EG', 'region' => 'Africa', 'subregion' => 'Northern Africa', 'emoji' => '๐Ÿ‡ช๐Ÿ‡ฌ'], + ['name' => 'El Salvador', 'iso3' => 'SLV', 'iso2' => 'SV', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ป'], + ['name' => 'Equatorial Guinea', 'iso3' => 'GNQ', 'iso2' => 'GQ', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ถ'], + ['name' => 'Eritrea', 'iso3' => 'ERI', 'iso2' => 'ER', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ช๐Ÿ‡ท'], + ['name' => 'Estonia', 'iso3' => 'EST', 'iso2' => 'EE', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ช๐Ÿ‡ช'], + ['name' => 'Ethiopia', 'iso3' => 'ETH', 'iso2' => 'ET', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ช๐Ÿ‡น'], + ['name' => 'Falkland Islands', 'iso3' => 'FLK', 'iso2' => 'FK', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ซ๐Ÿ‡ฐ'], + ['name' => 'Faroe Islands', 'iso3' => 'FRO', 'iso2' => 'FO', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ซ๐Ÿ‡ด'], + ['name' => 'Fiji Islands', 'iso3' => 'FJI', 'iso2' => 'FJ', 'region' => 'Oceania', 'subregion' => 'Melanesia', 'emoji' => '๐Ÿ‡ซ๐Ÿ‡ฏ'], + ['name' => 'Finland', 'iso3' => 'FIN', 'iso2' => 'FI', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ซ๐Ÿ‡ฎ'], + ['name' => 'France', 'iso3' => 'FRA', 'iso2' => 'FR', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ซ๐Ÿ‡ท'], + ['name' => 'French Guiana', 'iso3' => 'GUF', 'iso2' => 'GF', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ซ'], + ['name' => 'French Polynesia', 'iso3' => 'PYF', 'iso2' => 'PF', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ซ'], + ['name' => 'French Southern Territories', 'iso3' => 'ATF', 'iso2' => 'TF', 'region' => 'Africa', 'subregion' => 'Southern Africa', 'emoji' => '๐Ÿ‡น๐Ÿ‡ซ'], + ['name' => 'Gabon', 'iso3' => 'GAB', 'iso2' => 'GA', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ฆ'], + ['name' => 'Gambia The', 'iso3' => 'GMB', 'iso2' => 'GM', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ฒ'], + ['name' => 'Georgia', 'iso3' => 'GEO', 'iso2' => 'GE', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ช'], + ['name' => 'Germany', 'iso3' => 'DEU', 'iso2' => 'DE', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ฉ๐Ÿ‡ช'], + ['name' => 'Ghana', 'iso3' => 'GHA', 'iso2' => 'GH', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ญ'], + ['name' => 'Gibraltar', 'iso3' => 'GIB', 'iso2' => 'GI', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ฎ'], + ['name' => 'Greece', 'iso3' => 'GRC', 'iso2' => 'GR', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ท'], + ['name' => 'Greenland', 'iso3' => 'GRL', 'iso2' => 'GL', 'region' => 'Americas', 'subregion' => 'Northern America', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ฑ'], + ['name' => 'Grenada', 'iso3' => 'GRD', 'iso2' => 'GD', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ฉ'], + ['name' => 'Guadeloupe', 'iso3' => 'GLP', 'iso2' => 'GP', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ต'], + ['name' => 'Guam', 'iso3' => 'GUM', 'iso2' => 'GU', 'region' => 'Oceania', 'subregion' => 'Micronesia', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡บ'], + ['name' => 'Guatemala', 'iso3' => 'GTM', 'iso2' => 'GT', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡น'], + ['name' => 'Guernsey and Alderney', 'iso3' => 'GGY', 'iso2' => 'GG', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ฌ'], + ['name' => 'Guinea', 'iso3' => 'GIN', 'iso2' => 'GN', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ณ'], + ['name' => 'Guinea-Bissau', 'iso3' => 'GNB', 'iso2' => 'GW', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ผ'], + ['name' => 'Guyana', 'iso3' => 'GUY', 'iso2' => 'GY', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡พ'], + ['name' => 'Haiti', 'iso3' => 'HTI', 'iso2' => 'HT', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ญ๐Ÿ‡น'], + ['name' => 'Heard Island and McDonald Islands', 'iso3' => 'HMD', 'iso2' => 'HM', 'region' => '', 'subregion' => '', 'emoji' => '๐Ÿ‡ญ๐Ÿ‡ฒ'], + ['name' => 'Honduras', 'iso3' => 'HND', 'iso2' => 'HN', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡ญ๐Ÿ‡ณ'], + ['name' => 'Hong Kong S.A.R.', 'iso3' => 'HKG', 'iso2' => 'HK', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡ญ๐Ÿ‡ฐ'], + ['name' => 'Hungary', 'iso3' => 'HUN', 'iso2' => 'HU', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ญ๐Ÿ‡บ'], + ['name' => 'Iceland', 'iso3' => 'ISL', 'iso2' => 'IS', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ธ'], + ['name' => 'India', 'iso3' => 'IND', 'iso2' => 'IN', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ณ'], + ['name' => 'Indonesia', 'iso3' => 'IDN', 'iso2' => 'ID', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ฉ'], + ['name' => 'Iran', 'iso3' => 'IRN', 'iso2' => 'IR', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ท'], + ['name' => 'Iraq', 'iso3' => 'IRQ', 'iso2' => 'IQ', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ถ'], + ['name' => 'Ireland', 'iso3' => 'IRL', 'iso2' => 'IE', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ช'], + ['name' => 'Israel', 'iso3' => 'ISR', 'iso2' => 'IL', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ฑ'], + ['name' => 'Italy', 'iso3' => 'ITA', 'iso2' => 'IT', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡น'], + ['name' => 'Jamaica', 'iso3' => 'JAM', 'iso2' => 'JM', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฏ๐Ÿ‡ฒ'], + ['name' => 'Japan', 'iso3' => 'JPN', 'iso2' => 'JP', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡ฏ๐Ÿ‡ต'], + ['name' => 'Jersey', 'iso3' => 'JEY', 'iso2' => 'JE', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฏ๐Ÿ‡ช'], + ['name' => 'Jordan', 'iso3' => 'JOR', 'iso2' => 'JO', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฏ๐Ÿ‡ด'], + ['name' => 'Kazakhstan', 'iso3' => 'KAZ', 'iso2' => 'KZ', 'region' => 'Asia', 'subregion' => 'Central Asia', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ฟ'], + ['name' => 'Kenya', 'iso3' => 'KEN', 'iso2' => 'KE', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ช'], + ['name' => 'Kiribati', 'iso3' => 'KIR', 'iso2' => 'KI', 'region' => 'Oceania', 'subregion' => 'Micronesia', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ฎ'], + ['name' => 'Kosovo', 'iso3' => 'XKX', 'iso2' => 'XK', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ฝ๐Ÿ‡ฐ'], + ['name' => 'Kuwait', 'iso3' => 'KWT', 'iso2' => 'KW', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ผ'], + ['name' => 'Kyrgyzstan', 'iso3' => 'KGZ', 'iso2' => 'KG', 'region' => 'Asia', 'subregion' => 'Central Asia', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ฌ'], + ['name' => 'Laos', 'iso3' => 'LAO', 'iso2' => 'LA', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡ฆ'], + ['name' => 'Latvia', 'iso3' => 'LVA', 'iso2' => 'LV', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡ป'], + ['name' => 'Lebanon', 'iso3' => 'LBN', 'iso2' => 'LB', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡ง'], + ['name' => 'Lesotho', 'iso3' => 'LSO', 'iso2' => 'LS', 'region' => 'Africa', 'subregion' => 'Southern Africa', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡ธ'], + ['name' => 'Liberia', 'iso3' => 'LBR', 'iso2' => 'LR', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡ท'], + ['name' => 'Libya', 'iso3' => 'LBY', 'iso2' => 'LY', 'region' => 'Africa', 'subregion' => 'Northern Africa', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡พ'], + ['name' => 'Liechtenstein', 'iso3' => 'LIE', 'iso2' => 'LI', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡ฎ'], + ['name' => 'Lithuania', 'iso3' => 'LTU', 'iso2' => 'LT', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡น'], + ['name' => 'Luxembourg', 'iso3' => 'LUX', 'iso2' => 'LU', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡บ'], + ['name' => 'Macau S.A.R.', 'iso3' => 'MAC', 'iso2' => 'MO', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ด'], + ['name' => 'Macedonia', 'iso3' => 'MKD', 'iso2' => 'MK', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฐ'], + ['name' => 'Madagascar', 'iso3' => 'MDG', 'iso2' => 'MG', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฌ'], + ['name' => 'Malawi', 'iso3' => 'MWI', 'iso2' => 'MW', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ผ'], + ['name' => 'Malaysia', 'iso3' => 'MYS', 'iso2' => 'MY', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡พ'], + ['name' => 'Maldives', 'iso3' => 'MDV', 'iso2' => 'MV', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ป'], + ['name' => 'Mali', 'iso3' => 'MLI', 'iso2' => 'ML', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฑ'], + ['name' => 'Malta', 'iso3' => 'MLT', 'iso2' => 'MT', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡น'], + ['name' => 'Man (Isle of)', 'iso3' => 'IMN', 'iso2' => 'IM', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฎ๐Ÿ‡ฒ'], + ['name' => 'Marshall Islands', 'iso3' => 'MHL', 'iso2' => 'MH', 'region' => 'Oceania', 'subregion' => 'Micronesia', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ญ'], + ['name' => 'Martinique', 'iso3' => 'MTQ', 'iso2' => 'MQ', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ถ'], + ['name' => 'Mauritania', 'iso3' => 'MRT', 'iso2' => 'MR', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ท'], + ['name' => 'Mauritius', 'iso3' => 'MUS', 'iso2' => 'MU', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡บ'], + ['name' => 'Mayotte', 'iso3' => 'MYT', 'iso2' => 'YT', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡พ๐Ÿ‡น'], + ['name' => 'Mexico', 'iso3' => 'MEX', 'iso2' => 'MX', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฝ'], + ['name' => 'Micronesia', 'iso3' => 'FSM', 'iso2' => 'FM', 'region' => 'Oceania', 'subregion' => 'Micronesia', 'emoji' => '๐Ÿ‡ซ๐Ÿ‡ฒ'], + ['name' => 'Moldova', 'iso3' => 'MDA', 'iso2' => 'MD', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฉ'], + ['name' => 'Monaco', 'iso3' => 'MCO', 'iso2' => 'MC', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡จ'], + ['name' => 'Mongolia', 'iso3' => 'MNG', 'iso2' => 'MN', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ณ'], + ['name' => 'Montenegro', 'iso3' => 'MNE', 'iso2' => 'ME', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ช'], + ['name' => 'Montserrat', 'iso3' => 'MSR', 'iso2' => 'MS', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ธ'], + ['name' => 'Morocco', 'iso3' => 'MAR', 'iso2' => 'MA', 'region' => 'Africa', 'subregion' => 'Northern Africa', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฆ'], + ['name' => 'Mozambique', 'iso3' => 'MOZ', 'iso2' => 'MZ', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฟ'], + ['name' => 'Myanmar', 'iso3' => 'MMR', 'iso2' => 'MM', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ฒ'], + ['name' => 'Namibia', 'iso3' => 'NAM', 'iso2' => 'NA', 'region' => 'Africa', 'subregion' => 'Southern Africa', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ฆ'], + ['name' => 'Nauru', 'iso3' => 'NRU', 'iso2' => 'NR', 'region' => 'Oceania', 'subregion' => 'Micronesia', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ท'], + ['name' => 'Nepal', 'iso3' => 'NPL', 'iso2' => 'NP', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ต'], + ['name' => 'Netherlands', 'iso3' => 'NLD', 'iso2' => 'NL', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ฑ'], + ['name' => 'New Caledonia', 'iso3' => 'NCL', 'iso2' => 'NC', 'region' => 'Oceania', 'subregion' => 'Melanesia', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡จ'], + ['name' => 'New Zealand', 'iso3' => 'NZL', 'iso2' => 'NZ', 'region' => 'Oceania', 'subregion' => 'Australia and New Zealand', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ฟ'], + ['name' => 'Nicaragua', 'iso3' => 'NIC', 'iso2' => 'NI', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ฎ'], + ['name' => 'Niger', 'iso3' => 'NER', 'iso2' => 'NE', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ช'], + ['name' => 'Nigeria', 'iso3' => 'NGA', 'iso2' => 'NG', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ฌ'], + ['name' => 'Niue', 'iso3' => 'NIU', 'iso2' => 'NU', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡บ'], + ['name' => 'Norfolk Island', 'iso3' => 'NFK', 'iso2' => 'NF', 'region' => 'Oceania', 'subregion' => 'Australia and New Zealand', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ซ'], + ['name' => 'North Korea', 'iso3' => 'PRK', 'iso2' => 'KP', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ต'], + ['name' => 'Northern Mariana Islands', 'iso3' => 'MNP', 'iso2' => 'MP', 'region' => 'Oceania', 'subregion' => 'Micronesia', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ต'], + ['name' => 'Norway', 'iso3' => 'NOR', 'iso2' => 'NO', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ณ๐Ÿ‡ด'], + ['name' => 'Oman', 'iso3' => 'OMN', 'iso2' => 'OM', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ด๐Ÿ‡ฒ'], + ['name' => 'Pakistan', 'iso3' => 'PAK', 'iso2' => 'PK', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ฐ'], + ['name' => 'Palau', 'iso3' => 'PLW', 'iso2' => 'PW', 'region' => 'Oceania', 'subregion' => 'Micronesia', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ผ'], + ['name' => 'Palestinian Territory Occupied', 'iso3' => 'PSE', 'iso2' => 'PS', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ธ'], + ['name' => 'Panama', 'iso3' => 'PAN', 'iso2' => 'PA', 'region' => 'Americas', 'subregion' => 'Central America', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ฆ'], + ['name' => 'Papua new Guinea', 'iso3' => 'PNG', 'iso2' => 'PG', 'region' => 'Oceania', 'subregion' => 'Melanesia', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ฌ'], + ['name' => 'Paraguay', 'iso3' => 'PRY', 'iso2' => 'PY', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ต๐Ÿ‡พ'], + ['name' => 'Peru', 'iso3' => 'PER', 'iso2' => 'PE', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ช'], + ['name' => 'Philippines', 'iso3' => 'PHL', 'iso2' => 'PH', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ญ'], + ['name' => 'Pitcairn Island', 'iso3' => 'PCN', 'iso2' => 'PN', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ณ'], + ['name' => 'Poland', 'iso3' => 'POL', 'iso2' => 'PL', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ฑ'], + ['name' => 'Portugal', 'iso3' => 'PRT', 'iso2' => 'PT', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ต๐Ÿ‡น'], + ['name' => 'Puerto Rico', 'iso3' => 'PRI', 'iso2' => 'PR', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ท'], + ['name' => 'Qatar', 'iso3' => 'QAT', 'iso2' => 'QA', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ถ๐Ÿ‡ฆ'], + ['name' => 'Reunion', 'iso3' => 'REU', 'iso2' => 'RE', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ท๐Ÿ‡ช'], + ['name' => 'Romania', 'iso3' => 'ROU', 'iso2' => 'RO', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ท๐Ÿ‡ด'], + ['name' => 'Russia', 'iso3' => 'RUS', 'iso2' => 'RU', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ท๐Ÿ‡บ'], + ['name' => 'Rwanda', 'iso3' => 'RWA', 'iso2' => 'RW', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ท๐Ÿ‡ผ'], + ['name' => 'Saint Helena', 'iso3' => 'SHN', 'iso2' => 'SH', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ญ'], + ['name' => 'Saint Kitts And Nevis', 'iso3' => 'KNA', 'iso2' => 'KN', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ณ'], + ['name' => 'Saint Lucia', 'iso3' => 'LCA', 'iso2' => 'LC', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡จ'], + ['name' => 'Saint Pierre and Miquelon', 'iso3' => 'SPM', 'iso2' => 'PM', 'region' => 'Americas', 'subregion' => 'Northern America', 'emoji' => '๐Ÿ‡ต๐Ÿ‡ฒ'], + ['name' => 'Saint Vincent And The Grenadines', 'iso3' => 'VCT', 'iso2' => 'VC', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ป๐Ÿ‡จ'], + ['name' => 'Saint-Barthelemy', 'iso3' => 'BLM', 'iso2' => 'BL', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ง๐Ÿ‡ฑ'], + ['name' => 'Saint-Martin (French part)', 'iso3' => 'MAF', 'iso2' => 'MF', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ฒ๐Ÿ‡ซ'], + ['name' => 'Samoa', 'iso3' => 'WSM', 'iso2' => 'WS', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡ผ๐Ÿ‡ธ'], + ['name' => 'San Marino', 'iso3' => 'SMR', 'iso2' => 'SM', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฒ'], + ['name' => 'Sao Tome and Principe', 'iso3' => 'STP', 'iso2' => 'ST', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡น'], + ['name' => 'Saudi Arabia', 'iso3' => 'SAU', 'iso2' => 'SA', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฆ'], + ['name' => 'Senegal', 'iso3' => 'SEN', 'iso2' => 'SN', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ณ'], + ['name' => 'Serbia', 'iso3' => 'SRB', 'iso2' => 'RS', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ท๐Ÿ‡ธ'], + ['name' => 'Seychelles', 'iso3' => 'SYC', 'iso2' => 'SC', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡จ'], + ['name' => 'Sierra Leone', 'iso3' => 'SLE', 'iso2' => 'SL', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฑ'], + ['name' => 'Singapore', 'iso3' => 'SGP', 'iso2' => 'SG', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฌ'], + ['name' => 'Sint Maarten (Dutch part)', 'iso3' => 'SXM', 'iso2' => 'SX', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฝ'], + ['name' => 'Slovakia', 'iso3' => 'SVK', 'iso2' => 'SK', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฐ'], + ['name' => 'Slovenia', 'iso3' => 'SVN', 'iso2' => 'SI', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฎ'], + ['name' => 'Solomon Islands', 'iso3' => 'SLB', 'iso2' => 'SB', 'region' => 'Oceania', 'subregion' => 'Melanesia', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ง'], + ['name' => 'Somalia', 'iso3' => 'SOM', 'iso2' => 'SO', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ด'], + ['name' => 'South Africa', 'iso3' => 'ZAF', 'iso2' => 'ZA', 'region' => 'Africa', 'subregion' => 'Southern Africa', 'emoji' => '๐Ÿ‡ฟ๐Ÿ‡ฆ'], + ['name' => 'South Georgia', 'iso3' => 'SGS', 'iso2' => 'GS', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ธ'], + ['name' => 'South Korea', 'iso3' => 'KOR', 'iso2' => 'KR', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡ฐ๐Ÿ‡ท'], + ['name' => 'South Sudan', 'iso3' => 'SSD', 'iso2' => 'SS', 'region' => 'Africa', 'subregion' => 'Middle Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ธ'], + ['name' => 'Spain', 'iso3' => 'ESP', 'iso2' => 'ES', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ช๐Ÿ‡ธ'], + ['name' => 'Sri Lanka', 'iso3' => 'LKA', 'iso2' => 'LK', 'region' => 'Asia', 'subregion' => 'Southern Asia', 'emoji' => '๐Ÿ‡ฑ๐Ÿ‡ฐ'], + ['name' => 'Sudan', 'iso3' => 'SDN', 'iso2' => 'SD', 'region' => 'Africa', 'subregion' => 'Northern Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฉ'], + ['name' => 'Suriname', 'iso3' => 'SUR', 'iso2' => 'SR', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ท'], + ['name' => 'Svalbard And Jan Mayen Islands', 'iso3' => 'SJM', 'iso2' => 'SJ', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฏ'], + ['name' => 'Swaziland', 'iso3' => 'SWZ', 'iso2' => 'SZ', 'region' => 'Africa', 'subregion' => 'Southern Africa', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ฟ'], + ['name' => 'Sweden', 'iso3' => 'SWE', 'iso2' => 'SE', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡ช'], + ['name' => 'Switzerland', 'iso3' => 'CHE', 'iso2' => 'CH', 'region' => 'Europe', 'subregion' => 'Western Europe', 'emoji' => '๐Ÿ‡จ๐Ÿ‡ญ'], + ['name' => 'Syria', 'iso3' => 'SYR', 'iso2' => 'SY', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ธ๐Ÿ‡พ'], + ['name' => 'Taiwan', 'iso3' => 'TWN', 'iso2' => 'TW', 'region' => 'Asia', 'subregion' => 'Eastern Asia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ผ'], + ['name' => 'Tajikistan', 'iso3' => 'TJK', 'iso2' => 'TJ', 'region' => 'Asia', 'subregion' => 'Central Asia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ฏ'], + ['name' => 'Tanzania', 'iso3' => 'TZA', 'iso2' => 'TZ', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡น๐Ÿ‡ฟ'], + ['name' => 'Thailand', 'iso3' => 'THA', 'iso2' => 'TH', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ญ'], + ['name' => 'Togo', 'iso3' => 'TGO', 'iso2' => 'TG', 'region' => 'Africa', 'subregion' => 'Western Africa', 'emoji' => '๐Ÿ‡น๐Ÿ‡ฌ'], + ['name' => 'Tokelau', 'iso3' => 'TKL', 'iso2' => 'TK', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ฐ'], + ['name' => 'Tonga', 'iso3' => 'TON', 'iso2' => 'TO', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ด'], + ['name' => 'Trinidad And Tobago', 'iso3' => 'TTO', 'iso2' => 'TT', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡น๐Ÿ‡น'], + ['name' => 'Tunisia', 'iso3' => 'TUN', 'iso2' => 'TN', 'region' => 'Africa', 'subregion' => 'Northern Africa', 'emoji' => '๐Ÿ‡น๐Ÿ‡ณ'], + ['name' => 'Turkey', 'iso3' => 'TUR', 'iso2' => 'TR', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ท'], + ['name' => 'Turkmenistan', 'iso3' => 'TKM', 'iso2' => 'TM', 'region' => 'Asia', 'subregion' => 'Central Asia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ฒ'], + ['name' => 'Turks And Caicos Islands', 'iso3' => 'TCA', 'iso2' => 'TC', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡น๐Ÿ‡จ'], + ['name' => 'Tuvalu', 'iso3' => 'TUV', 'iso2' => 'TV', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡น๐Ÿ‡ป'], + ['name' => 'Uganda', 'iso3' => 'UGA', 'iso2' => 'UG', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡บ๐Ÿ‡ฌ'], + ['name' => 'Ukraine', 'iso3' => 'UKR', 'iso2' => 'UA', 'region' => 'Europe', 'subregion' => 'Eastern Europe', 'emoji' => '๐Ÿ‡บ๐Ÿ‡ฆ'], + ['name' => 'United Arab Emirates', 'iso3' => 'ARE', 'iso2' => 'AE', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡ช'], + ['name' => 'United Kingdom', 'iso3' => 'GBR', 'iso2' => 'GB', 'region' => 'Europe', 'subregion' => 'Northern Europe', 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ง'], + ['name' => 'United States', 'iso3' => 'USA', 'iso2' => 'US', 'region' => 'Americas', 'subregion' => 'Northern America', 'emoji' => '๐Ÿ‡บ๐Ÿ‡ธ'], + ['name' => 'United States Minor Outlying Islands', 'iso3' => 'UMI', 'iso2' => 'UM', 'region' => 'Americas', 'subregion' => 'Northern America', 'emoji' => '๐Ÿ‡บ๐Ÿ‡ฒ'], + ['name' => 'Uruguay', 'iso3' => 'URY', 'iso2' => 'UY', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡บ๐Ÿ‡พ'], + ['name' => 'Uzbekistan', 'iso3' => 'UZB', 'iso2' => 'UZ', 'region' => 'Asia', 'subregion' => 'Central Asia', 'emoji' => '๐Ÿ‡บ๐Ÿ‡ฟ'], + ['name' => 'Vanuatu', 'iso3' => 'VUT', 'iso2' => 'VU', 'region' => 'Oceania', 'subregion' => 'Melanesia', 'emoji' => '๐Ÿ‡ป๐Ÿ‡บ'], + ['name' => 'Vatican City State (Holy See)', 'iso3' => 'VAT', 'iso2' => 'VA', 'region' => 'Europe', 'subregion' => 'Southern Europe', 'emoji' => '๐Ÿ‡ป๐Ÿ‡ฆ'], + ['name' => 'Venezuela', 'iso3' => 'VEN', 'iso2' => 'VE', 'region' => 'Americas', 'subregion' => 'South America', 'emoji' => '๐Ÿ‡ป๐Ÿ‡ช'], + ['name' => 'Vietnam', 'iso3' => 'VNM', 'iso2' => 'VN', 'region' => 'Asia', 'subregion' => 'South-Eastern Asia', 'emoji' => '๐Ÿ‡ป๐Ÿ‡ณ'], + ['name' => 'Virgin Islands (British)', 'iso3' => 'VGB', 'iso2' => 'VG', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ป๐Ÿ‡ฌ'], + ['name' => 'Virgin Islands (US)', 'iso3' => 'VIR', 'iso2' => 'VI', 'region' => 'Americas', 'subregion' => 'Caribbean', 'emoji' => '๐Ÿ‡ป๐Ÿ‡ฎ'], + ['name' => 'Wallis And Futuna Islands', 'iso3' => 'WLF', 'iso2' => 'WF', 'region' => 'Oceania', 'subregion' => 'Polynesia', 'emoji' => '๐Ÿ‡ผ๐Ÿ‡ซ'], + ['name' => 'Western Sahara', 'iso3' => 'ESH', 'iso2' => 'EH', 'region' => 'Africa', 'subregion' => 'Northern Africa', 'emoji' => '๐Ÿ‡ช๐Ÿ‡ญ'], + ['name' => 'Yemen', 'iso3' => 'YEM', 'iso2' => 'YE', 'region' => 'Asia', 'subregion' => 'Western Asia', 'emoji' => '๐Ÿ‡พ๐Ÿ‡ช'], + ['name' => 'Zambia', 'iso3' => 'ZMB', 'iso2' => 'ZM', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฟ๐Ÿ‡ฒ'], + ['name' => 'Zimbabwe', 'iso3' => 'ZWE', 'iso2' => 'ZW', 'region' => 'Africa', 'subregion' => 'Eastern Africa', 'emoji' => '๐Ÿ‡ฟ๐Ÿ‡ผ'], + ]; + } +} diff --git a/src/Dictionaries/Currencies.php b/src/Dictionaries/Currencies.php new file mode 100644 index 0000000000..dce2b0d7bb --- /dev/null +++ b/src/Dictionaries/Currencies.php @@ -0,0 +1,138 @@ + 'AED', 'name' => 'United Arab Emirates Dirham', 'symbol' => 'ุฏ.ุฅ.โ€', 'decimals' => 2], + ['code' => 'AFN', 'name' => 'Afghan Afghani', 'symbol' => 'ุ‹', 'decimals' => 0], + ['code' => 'ALL', 'name' => 'Albanian Lek', 'symbol' => 'Lek', 'decimals' => 0], + ['code' => 'AMD', 'name' => 'Armenian Dram', 'symbol' => 'ีคึ€.', 'decimals' => 0], + ['code' => 'ARS', 'name' => 'Argentine Peso', 'symbol' => '$', 'decimals' => 2], + ['code' => 'AUD', 'name' => 'Australian Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'AZN', 'name' => 'Azerbaijani Manat', 'symbol' => 'ะผะฐะฝ.', 'decimals' => 2], + ['code' => 'BAM', 'name' => 'Bosnia-Herzegovina Convertible Mark', 'symbol' => 'KM', 'decimals' => 2], + ['code' => 'BDT', 'name' => 'Bangladeshi Taka', 'symbol' => 'เงณ', 'decimals' => 2], + ['code' => 'BGN', 'name' => 'Bulgarian Lev', 'symbol' => 'ะปะฒ.', 'decimals' => 2], + ['code' => 'BHD', 'name' => 'Bahraini Dinar', 'symbol' => 'ุฏ.ุจ.โ€', 'decimals' => 3], + ['code' => 'BIF', 'name' => 'Burundian Franc', 'symbol' => 'FBu', 'decimals' => 0], + ['code' => 'BND', 'name' => 'Brunei Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'BOB', 'name' => 'Bolivian Boliviano', 'symbol' => 'Bs', 'decimals' => 2], + ['code' => 'BRL', 'name' => 'Brazilian Real', 'symbol' => 'R$', 'decimals' => 2], + ['code' => 'BWP', 'name' => 'Botswanan Pula', 'symbol' => 'P', 'decimals' => 2], + ['code' => 'BYN', 'name' => 'Belarusian Ruble', 'symbol' => 'ั€ัƒะฑ.', 'decimals' => 2], + ['code' => 'BZD', 'name' => 'Belize Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'CAD', 'name' => 'Canadian Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'CDF', 'name' => 'Congolese Franc', 'symbol' => 'FrCD', 'decimals' => 2], + ['code' => 'CHF', 'name' => 'Swiss Franc', 'symbol' => 'CHF', 'decimals' => 2], + ['code' => 'CLP', 'name' => 'Chilean Peso', 'symbol' => '$', 'decimals' => 0], + ['code' => 'CNY', 'name' => 'Chinese Yuan', 'symbol' => 'CNยฅ', 'decimals' => 2], + ['code' => 'COP', 'name' => 'Colombian Peso', 'symbol' => '$', 'decimals' => 0], + ['code' => 'CRC', 'name' => "Costa Rican Col\u00f3n", 'symbol' => 'โ‚ก', 'decimals' => 0], + ['code' => 'CVE', 'name' => 'Cape Verdean Escudo', 'symbol' => 'CV$', 'decimals' => 2], + ['code' => 'CZK', 'name' => 'Czech Republic Koruna', 'symbol' => 'Kฤ', 'decimals' => 2], + ['code' => 'DJF', 'name' => 'Djiboutian Franc', 'symbol' => 'Fdj', 'decimals' => 0], + ['code' => 'DKK', 'name' => 'Danish Krone', 'symbol' => 'kr', 'decimals' => 2], + ['code' => 'DOP', 'name' => 'Dominican Peso', 'symbol' => 'RD$', 'decimals' => 2], + ['code' => 'DZD', 'name' => 'Algerian Dinar', 'symbol' => 'ุฏ.ุฌ.โ€', 'decimals' => 2], + ['code' => 'EEK', 'name' => 'Estonian Kroon', 'symbol' => 'kr', 'decimals' => 2], + ['code' => 'EGP', 'name' => 'Egyptian Pound', 'symbol' => 'ุฌ.ู….โ€', 'decimals' => 2], + ['code' => 'ERN', 'name' => 'Eritrean Nakfa', 'symbol' => 'Nfk', 'decimals' => 2], + ['code' => 'ETB', 'name' => 'Ethiopian Birr', 'symbol' => 'Br', 'decimals' => 2], + ['code' => 'EUR', 'name' => 'Euro', 'symbol' => 'โ‚ฌ', 'decimals' => 2], + ['code' => 'GBP', 'name' => 'British Pound Sterling', 'symbol' => 'ยฃ', 'decimals' => 2], + ['code' => 'GEL', 'name' => 'Georgian Lari', 'symbol' => 'GEL', 'decimals' => 2], + ['code' => 'GHS', 'name' => 'Ghanaian Cedi', 'symbol' => 'GHโ‚ต', 'decimals' => 2], + ['code' => 'GNF', 'name' => 'Guinean Franc', 'symbol' => 'FG', 'decimals' => 0], + ['code' => 'GTQ', 'name' => 'Guatemalan Quetzal', 'symbol' => 'Q', 'decimals' => 2], + ['code' => 'HKD', 'name' => 'Hong Kong Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'HNL', 'name' => 'Honduran Lempira', 'symbol' => 'L', 'decimals' => 2], + ['code' => 'HRK', 'name' => 'Croatian Kuna', 'symbol' => 'kn', 'decimals' => 2], + ['code' => 'HUF', 'name' => 'Hungarian Forint', 'symbol' => 'Ft', 'decimals' => 0], + ['code' => 'IDR', 'name' => 'Indonesian Rupiah', 'symbol' => 'Rp', 'decimals' => 0], + ['code' => 'ILS', 'name' => 'Israeli New Sheqel', 'symbol' => 'โ‚ช', 'decimals' => 2], + ['code' => 'INR', 'name' => 'Indian Rupee', 'symbol' => 'เฆŸเฆ•เฆพ', 'decimals' => 2], + ['code' => 'IQD', 'name' => 'Iraqi Dinar', 'symbol' => 'ุฏ.ุน.โ€', 'decimals' => 0], + ['code' => 'IRR', 'name' => 'Iranian Rial', 'symbol' => '๏ทผ', 'decimals' => 0], + ['code' => 'ISK', 'name' => "Icelandic Kr\u00f3na", 'symbol' => 'kr', 'decimals' => 0], + ['code' => 'JMD', 'name' => 'Jamaican Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'JOD', 'name' => 'Jordanian Dinar', 'symbol' => 'ุฏ.ุฃ.โ€', 'decimals' => 3], + ['code' => 'JPY', 'name' => 'Japanese Yen', 'symbol' => '๏ฟฅ', 'decimals' => 0], + ['code' => 'KES', 'name' => 'Kenyan Shilling', 'symbol' => 'Ksh', 'decimals' => 2], + ['code' => 'KHR', 'name' => 'Cambodian Riel', 'symbol' => 'แŸ›', 'decimals' => 2], + ['code' => 'KMF', 'name' => 'Comorian Franc', 'symbol' => 'FC', 'decimals' => 0], + ['code' => 'KRW', 'name' => 'South Korean Won', 'symbol' => 'โ‚ฉ', 'decimals' => 0], + ['code' => 'KWD', 'name' => 'Kuwaiti Dinar', 'symbol' => 'ุฏ.ูƒ.โ€', 'decimals' => 3], + ['code' => 'KZT', 'name' => 'Kazakhstani Tenge', 'symbol' => 'ั‚าฃะณ.', 'decimals' => 2], + ['code' => 'LBP', 'name' => 'Lebanese Pound', 'symbol' => 'ู„.ู„.โ€', 'decimals' => 0], + ['code' => 'LKR', 'name' => 'Sri Lankan Rupee', 'symbol' => 'SL Re', 'decimals' => 2], + ['code' => 'LTL', 'name' => 'Lithuanian Litas', 'symbol' => 'Lt', 'decimals' => 2], + ['code' => 'LVL', 'name' => 'Latvian Lats', 'symbol' => 'Ls', 'decimals' => 2], + ['code' => 'LYD', 'name' => 'Libyan Dinar', 'symbol' => 'ุฏ.ู„.โ€', 'decimals' => 3], + ['code' => 'MAD', 'name' => 'Moroccan Dirham', 'symbol' => 'ุฏ.ู….โ€', 'decimals' => 2], + ['code' => 'MDL', 'name' => 'Moldovan Leu', 'symbol' => 'MDL', 'decimals' => 2], + ['code' => 'MGA', 'name' => 'Malagasy Ariary', 'symbol' => 'MGA', 'decimals' => 0], + ['code' => 'MKD', 'name' => 'Macedonian Denar', 'symbol' => 'MKD', 'decimals' => 2], + ['code' => 'MMK', 'name' => 'Myanma Kyat', 'symbol' => 'K', 'decimals' => 0], + ['code' => 'MOP', 'name' => 'Macanese Pataca', 'symbol' => 'MOP$', 'decimals' => 2], + ['code' => 'MUR', 'name' => 'Mauritian Rupee', 'symbol' => 'MURs', 'decimals' => 0], + ['code' => 'MXN', 'name' => 'Mexican Peso', 'symbol' => '$', 'decimals' => 2], + ['code' => 'MYR', 'name' => 'Malaysian Ringgit', 'symbol' => 'RM', 'decimals' => 2], + ['code' => 'MZN', 'name' => 'Mozambican Metical', 'symbol' => 'MTn', 'decimals' => 2], + ['code' => 'NAD', 'name' => 'Namibian Dollar', 'symbol' => 'N$', 'decimals' => 2], + ['code' => 'NGN', 'name' => 'Nigerian Naira', 'symbol' => 'โ‚ฆ', 'decimals' => 2], + ['code' => 'NIO', 'name' => "Nicaraguan C\u00f3rdoba", 'symbol' => 'C$', 'decimals' => 2], + ['code' => 'NOK', 'name' => 'Norwegian Krone', 'symbol' => 'kr', 'decimals' => 2], + ['code' => 'NPR', 'name' => 'Nepalese Rupee', 'symbol' => 'เคจเฅ‡เคฐเฅ‚', 'decimals' => 2], + ['code' => 'NZD', 'name' => 'New Zealand Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'OMR', 'name' => 'Omani Rial', 'symbol' => 'ุฑ.ุน.โ€', 'decimals' => 3], + ['code' => 'PAB', 'name' => 'Panamanian Balboa', 'symbol' => 'B/.', 'decimals' => 2], + ['code' => 'PEN', 'name' => 'Peruvian Nuevo Sol', 'symbol' => 'S/.', 'decimals' => 2], + ['code' => 'PHP', 'name' => 'Philippine Peso', 'symbol' => 'โ‚ฑ', 'decimals' => 2], + ['code' => 'PKR', 'name' => 'Pakistani Rupee', 'symbol' => 'โ‚จ', 'decimals' => 0], + ['code' => 'PLN', 'name' => 'Polish Zloty', 'symbol' => 'zล‚', 'decimals' => 2], + ['code' => 'PYG', 'name' => 'Paraguayan Guarani', 'symbol' => 'โ‚ฒ', 'decimals' => 0], + ['code' => 'QAR', 'name' => 'Qatari Rial', 'symbol' => 'ุฑ.ู‚.โ€', 'decimals' => 2], + ['code' => 'RON', 'name' => 'Romanian Leu', 'symbol' => 'RON', 'decimals' => 2], + ['code' => 'RSD', 'name' => 'Serbian Dinar', 'symbol' => 'ะดะธะฝ.', 'decimals' => 0], + ['code' => 'RUB', 'name' => 'Russian Ruble', 'symbol' => 'โ‚ฝ.', 'decimals' => 2], + ['code' => 'RWF', 'name' => 'Rwandan Franc', 'symbol' => 'FR', 'decimals' => 0], + ['code' => 'SAR', 'name' => 'Saudi Riyal', 'symbol' => 'ุฑ.ุณ.โ€', 'decimals' => 2], + ['code' => 'SDG', 'name' => 'Sudanese Pound', 'symbol' => 'SDG', 'decimals' => 2], + ['code' => 'SEK', 'name' => 'Swedish Krona', 'symbol' => 'kr', 'decimals' => 2], + ['code' => 'SGD', 'name' => 'Singapore Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'SOS', 'name' => 'Somali Shilling', 'symbol' => 'Ssh', 'decimals' => 0], + ['code' => 'SYP', 'name' => 'Syrian Pound', 'symbol' => 'ู„.ุณ.โ€', 'decimals' => 0], + ['code' => 'THB', 'name' => 'Thai Baht', 'symbol' => 'เธฟ', 'decimals' => 2], + ['code' => 'TND', 'name' => 'Tunisian Dinar', 'symbol' => 'ุฏ.ุช.โ€', 'decimals' => 3], + ['code' => 'TOP', 'name' => "Tongan Pa\u02bbanga", 'symbol' => 'T$', 'decimals' => 2], + ['code' => 'TRY', 'name' => 'Turkish Lira', 'symbol' => 'TL', 'decimals' => 2], + ['code' => 'TTD', 'name' => 'Trinidad and Tobago Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'TWD', 'name' => 'New Taiwan Dollar', 'symbol' => 'NT$', 'decimals' => 2], + ['code' => 'TZS', 'name' => 'Tanzanian Shilling', 'symbol' => 'TSh', 'decimals' => 0], + ['code' => 'UAH', 'name' => 'Ukrainian Hryvnia', 'symbol' => 'โ‚ด', 'decimals' => 2], + ['code' => 'UGX', 'name' => 'Ugandan Shilling', 'symbol' => 'USh', 'decimals' => 0], + ['code' => 'USD', 'name' => 'US Dollar', 'symbol' => '$', 'decimals' => 2], + ['code' => 'UYU', 'name' => 'Uruguayan Peso', 'symbol' => '$', 'decimals' => 2], + ['code' => 'UZS', 'name' => 'Uzbekistan Som', 'symbol' => 'UZS', 'decimals' => 0], + ['code' => 'VEF', 'name' => "Venezuelan Bol\u00edvar", 'symbol' => 'Bs.F.', 'decimals' => 2], + ['code' => 'VND', 'name' => 'Vietnamese Dong', 'symbol' => 'โ‚ซ', 'decimals' => 0], + ['code' => 'XAF', 'name' => 'CFA Franc BEAC', 'symbol' => 'FCFA', 'decimals' => 0], + ['code' => 'XOF', 'name' => 'CFA Franc BCEAO', 'symbol' => 'CFA', 'decimals' => 0], + ['code' => 'YER', 'name' => 'Yemeni Rial', 'symbol' => 'ุฑ.ูŠ.โ€', 'decimals' => 0], + ['code' => 'ZAR', 'name' => 'South African Rand', 'symbol' => 'R', 'decimals' => 2], + ['code' => 'ZMK', 'name' => 'Zambian Kwacha', 'symbol' => 'ZK', 'decimals' => 0], + ['code' => 'ZWL', 'name' => 'Zimbabwean Dollar', 'symbol' => 'ZWL$', 'decimals' => 0], + ]; + } +} diff --git a/src/Dictionaries/Dictionary.php b/src/Dictionaries/Dictionary.php new file mode 100644 index 0000000000..c94771a254 --- /dev/null +++ b/src/Dictionaries/Dictionary.php @@ -0,0 +1,71 @@ +config = $config; + + return $this; + } + + public function config(): array + { + return $this->config; + } + + protected function fieldItems() + { + return $this->fields; + } + + public function getGqlType() + { + $name = str(class_basename($this))->singular()->value(); + + return new DictionaryType($name, $this->getGqlFields()); + } + + protected function getGqlFields(): array + { + // By default, we will make non-nullable strings out of all the keys + // of the first option. This is an easy way for it to "just work", + // and of course it may be easily overridden per dictionary. + $firstOption = collect($this->options())->keys()->first(); + + return collect($this->get($firstOption)) + ->map(fn ($value) => ['type' => GraphQL::nonNull($this->getInferredGqlType($value))]) + ->all(); + } + + private function getInferredGqlType($value) + { + if (is_int($value)) { + return GraphQL::int(); + } + + if (is_bool($value)) { + return GraphQL::boolean(); + } + + return GraphQL::string(); + } +} diff --git a/src/Dictionaries/DictionaryRepository.php b/src/Dictionaries/DictionaryRepository.php new file mode 100644 index 0000000000..55e43df95b --- /dev/null +++ b/src/Dictionaries/DictionaryRepository.php @@ -0,0 +1,30 @@ +map(fn ($class) => app($class)) + ->filter() + ->values(); + } + + public function find(string $handle, array $context = []): ?Dictionary + { + if (! $dictionary = app('statamic.dictionaries')->get($handle)) { + return null; + } + + /** @var $dictionary Dictionary */ + if (! $dictionary = app($dictionary)) { + return null; + } + + return $dictionary->setConfig($context); + } +} diff --git a/src/Dictionaries/File.php b/src/Dictionaries/File.php new file mode 100644 index 0000000000..57bdc173b6 --- /dev/null +++ b/src/Dictionaries/File.php @@ -0,0 +1,85 @@ + [ + 'type' => 'slug', + 'display' => __('Filename'), + 'instructions' => __('statamic::fieldtypes.dictionary.file.config.filename'), + 'validate' => ['required'], + ], + 'label' => [ + 'type' => 'text', + 'display' => __('Label'), + 'instructions' => __('statamic::fieldtypes.dictionary.file.config.label'), + ], + 'value' => [ + 'type' => 'text', + 'display' => __('Value'), + 'instructions' => __('statamic::fieldtypes.dictionary.file.config.value'), + ], + ]; + } + + public function setConfig(array $config): Dictionary + { + if ($value = $config['value'] ?? null) { + $this->valueKey = $value; + } + + if ($label = $config['label'] ?? null) { + $this->labelKey = $label; + } + + return parent::setConfig($config); + } + + protected function getItemLabel(array $item): string + { + if (str_contains($this->labelKey, '{{')) { + return (string) Antlers::parse($this->labelKey, $item); + } + + return parent::getItemLabel($item); + } + + protected function getItems(): array + { + $path = resource_path('dictionaries').'/'.$this->config['filename']; + + if (! file_exists($path)) { + throw new \Exception('Dictionary file ['.$path.'] does not exist.'); + } + + $extension = pathinfo($path, PATHINFO_EXTENSION); + + return match ($extension) { + 'json' => json_decode(file_get_contents($path), true), + 'yaml' => YAML::file($path)->parse(), + 'csv' => $this->fromCsv($path), + }; + } + + private function fromCsv(string $path): array + { + $rows = []; + + if (($handle = fopen($path, 'r')) !== false) { + $headers = fgetcsv($handle, 1000, ','); + while (($data = fgetcsv($handle, 1000, ',')) !== false) { + $rows[] = array_combine($headers, $data); + } + fclose($handle); + } + + return $rows; + } +} diff --git a/src/Dictionaries/Item.php b/src/Dictionaries/Item.php new file mode 100644 index 0000000000..5ee458f75a --- /dev/null +++ b/src/Dictionaries/Item.php @@ -0,0 +1,43 @@ +extra = array_merge( + $extra, + ['label' => $label] + ); + } + + public function data(): array + { + return Arr::except($this->extra, ['label']); + } + + public function offsetExists(mixed $offset): bool + { + return true; + } + + public function offsetGet(mixed $offset): mixed + { + return $this->data()[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->extra[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->extra[$offset]); + } +} diff --git a/src/Dictionaries/Timezones.php b/src/Dictionaries/Timezones.php new file mode 100644 index 0000000000..8a4e9420d0 --- /dev/null +++ b/src/Dictionaries/Timezones.php @@ -0,0 +1,33 @@ +map(fn ($tz) => ['name' => $tz, 'offset' => $this->getOffset($tz)]) + ->all(); + } + + private function getOffset(string $tz): string + { + $tz = new DateTimeZone($tz); + $utcTime = Carbon::now('UTC'); + $offsetInSecs = $tz->getOffset($utcTime); + $hoursAndSec = gmdate('H:i', abs($offsetInSecs)); + + return stripos($offsetInSecs, '-') === false ? "+{$hoursAndSec}" : "-{$hoursAndSec}"; + } +} 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' => '', + ], + 'max_items' => [ + 'display' => __('Max Items'), + 'instructions' => __('statamic::messages.max_items_instructions'), + 'min' => 1, + 'type' => 'integer', + ], + ], + ], + [ + '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' => $this->getItemData($this->field->value()), + ]; + } + + private function getItemData($values) + { + return collect($values)->map(function ($key) { + $item = $this->dictionary()->get($key); + + return [ + 'value' => $item?->value() ?? $key, + 'label' => $item?->label() ?? $key, + 'invalid' => ! $item, + ]; + })->values()->all(); + } + + public function augment($value) + { + if ($this->multiple() && is_null($value)) { + return []; + } + + $dictionary = $this->dictionary(); + + if ($this->multiple()) { + return collect($value)->map(function ($value) use ($dictionary) { + return $dictionary->get($value); + })->filter()->all(); + } + + $item = $value ? $dictionary->get($value) : null; + + return $item ?? new Item(null, null, []); + } + + public function extraRenderableFieldData(): array + { + return [ + 'multiple' => $this->multiple(), + 'options' => $this->dictionary()->options(), + ]; + } + + protected function multiple(): bool + { + return $this->config('max_items') !== 1; + } + + public function dictionary(): DictionaryInstance + { + $config = is_array($config = $this->config('dictionary')) ? $config : ['type' => $config]; + + if (! $handle = Arr::pull($config, 'type')) { + throw new UndefinedDictionaryException; + } + + if ($dictionary = Dictionaries::find($handle, $config)) { + return $dictionary; + } + + throw new DictionaryNotFoundException($handle); + } + + public function toGqlType() + { + $type = GraphQL::type($this->dictionary()->getGqlType()->name); + + return $this->multiple() + ? $this->multiSelectGqlType($type) + : $this->singleSelectGqlType($type); + } + + private function singleSelectGqlType($type) + { + return [ + 'type' => $type, + 'resolve' => function ($item, $args, $context, $info) { + $resolved = $item->resolveGqlValue($info->fieldName); + + return is_null($resolved->value()) ? null : $resolved; + }, + ]; + } + + private function multiSelectGqlType($type) + { + return [ + 'type' => GraphQL::listOf($type), + 'resolve' => function ($item, $args, $context, $info) { + $resolved = $item->resolveGqlValue($info->fieldName); + + return empty($resolved) ? null : $resolved; + }, + ]; + } + + public function addGqlTypes() + { + GraphQL::addType($this->dictionary()->getGqlType()); + } +} 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/GraphQL/Types/DictionaryType.php b/src/GraphQL/Types/DictionaryType.php new file mode 100644 index 0000000000..97524e08ef --- /dev/null +++ b/src/GraphQL/Types/DictionaryType.php @@ -0,0 +1,19 @@ +attributes['name'] = 'Dictionary_'.$name; + $this->fields = $fields; + } + + public function fields(): array + { + return $this->fields; + } +} 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..e499359dc5 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,13 @@ class ExtensionServiceProvider extends ServiceProvider Actions\Impersonate::class, ]; + protected $dictionaries = [ + Dictionaries\Countries::class, + Dictionaries\Currencies::class, + Dictionaries\File::class, + Dictionaries\Timezones::class, + ]; + protected $fieldtypes = [ Fieldtypes\Arr::class, Fieldtypes\AssetContainer::class, @@ -63,6 +72,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 +274,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/CountriesTest.php b/tests/Dictionaries/CountriesTest.php new file mode 100644 index 0000000000..ffcb6fdadc --- /dev/null +++ b/tests/Dictionaries/CountriesTest.php @@ -0,0 +1,99 @@ +options(); + + $this->assertCount(250, $options); + $this->assertEquals([ + 'AFG' => '๐Ÿ‡ฆ๐Ÿ‡ซ Afghanistan', + 'ALA' => '๐Ÿ‡ฆ๐Ÿ‡ฝ Aland Islands', + 'ALB' => '๐Ÿ‡ฆ๐Ÿ‡ฑ Albania', + 'DZA' => '๐Ÿ‡ฉ๐Ÿ‡ฟ Algeria', + 'ASM' => '๐Ÿ‡ฆ๐Ÿ‡ธ American Samoa', + ], array_slice($options, 0, 5)); + } + + #[Test] + public function it_filters_options_by_region() + { + $options = (new Countries)->setConfig(['region' => 'oceania'])->options(); + + $this->assertCount(27, $options); + $this->assertEquals([ + 'ASM' => '๐Ÿ‡ฆ๐Ÿ‡ธ American Samoa', + 'AUS' => '๐Ÿ‡ฆ๐Ÿ‡บ Australia', + 'CXR' => '๐Ÿ‡จ๐Ÿ‡ฝ Christmas Island', + 'CCK' => '๐Ÿ‡จ๐Ÿ‡จ Cocos (Keeling) Islands', + 'COK' => '๐Ÿ‡จ๐Ÿ‡ฐ Cook Islands', + ], array_slice($options, 0, 5)); + } + + #[Test] + #[DataProvider('searchProvider')] + public function it_searches_options($query, $expected) + { + $this->assertEquals($expected, (new Countries)->options($query)); + } + + public static function searchProvider() + { + return [ + 'au' => [ + 'au', + [ + 'AUS' => '๐Ÿ‡ฆ๐Ÿ‡บ Australia', + 'AUT' => '๐Ÿ‡ฆ๐Ÿ‡น Austria', + 'GNB' => '๐Ÿ‡ฌ๐Ÿ‡ผ Guinea-Bissau', + 'MAC' => '๐Ÿ‡ฒ๐Ÿ‡ด Macau S.A.R.', + 'MRT' => '๐Ÿ‡ฒ๐Ÿ‡ท Mauritania', + 'MUS' => '๐Ÿ‡ฒ๐Ÿ‡บ Mauritius', + 'NRU' => '๐Ÿ‡ณ๐Ÿ‡ท Nauru', + 'PLW' => '๐Ÿ‡ต๐Ÿ‡ผ Palau', + 'SAU' => '๐Ÿ‡ธ๐Ÿ‡ฆ Saudi Arabia', + 'TKL' => '๐Ÿ‡น๐Ÿ‡ฐ Tokelau', + ], + ], + 'us' => [ + 'us', + [ + 'AUS' => '๐Ÿ‡ฆ๐Ÿ‡บ Australia', + 'AUT' => '๐Ÿ‡ฆ๐Ÿ‡น Austria', + 'BLR' => '๐Ÿ‡ง๐Ÿ‡พ Belarus', + 'BES' => '๐Ÿ‡ง๐Ÿ‡ถ Bonaire, Sint Eustatius and Saba', + 'CYP' => '๐Ÿ‡จ๐Ÿ‡พ Cyprus', + 'MUS' => '๐Ÿ‡ฒ๐Ÿ‡บ Mauritius', + 'RUS' => '๐Ÿ‡ท๐Ÿ‡บ Russia', + 'USA' => '๐Ÿ‡บ๐Ÿ‡ธ United States', + 'VIR' => '๐Ÿ‡ป๐Ÿ‡ฎ Virgin Islands (US)', + ], + ], + ]; + } + + #[Test] + public function it_gets_array_from_value() + { + $item = (new Countries)->get('AUS'); + $this->assertInstanceOf(Item::class, $item); + $this->assertEquals([ + 'name' => 'Australia', + 'iso3' => 'AUS', + 'iso2' => 'AU', + 'region' => 'Oceania', + 'subregion' => 'Australia and New Zealand', + 'emoji' => '๐Ÿ‡ฆ๐Ÿ‡บ', + ], $item->data()); + } +} diff --git a/tests/Dictionaries/CurrenciesTest.php b/tests/Dictionaries/CurrenciesTest.php new file mode 100644 index 0000000000..0328dce849 --- /dev/null +++ b/tests/Dictionaries/CurrenciesTest.php @@ -0,0 +1,107 @@ +options(); + + $this->assertCount(119, $options); + $option = $options['USD']; + $this->assertEquals('US Dollar (USD)', $option); + } + + #[Test] + #[DataProvider('searchProvider')] + public function it_searches_options($query, $expected) + { + $this->assertEquals($expected, (new Currencies)->options($query)); + } + + public static function searchProvider() + { + return [ + 'euro' => [ + 'euro', + [ + 'EUR' => 'Euro (EUR)', + ], + ], + 'dollar' => [ + 'dollar', + [ + 'AUD' => 'Australian Dollar (AUD)', + 'BZD' => 'Belize Dollar (BZD)', + 'CAD' => 'Canadian Dollar (CAD)', + 'HKD' => 'Hong Kong Dollar (HKD)', + 'JMD' => 'Jamaican Dollar (JMD)', + 'NAD' => 'Namibian Dollar (NAD)', + 'NZD' => 'New Zealand Dollar (NZD)', + 'SGD' => 'Singapore Dollar (SGD)', + 'TTD' => 'Trinidad and Tobago Dollar (TTD)', + 'USD' => 'US Dollar (USD)', + 'BND' => 'Brunei Dollar (BND)', + 'TWD' => 'New Taiwan Dollar (TWD)', + 'ZWL' => 'Zimbabwean Dollar (ZWL)', + ], + ], + 'dollar symbol' => [ + '$', + [ + 'ARS' => 'Argentine Peso (ARS)', + 'AUD' => 'Australian Dollar (AUD)', + 'BND' => 'Brunei Dollar (BND)', + 'BRL' => 'Brazilian Real (BRL)', + 'BZD' => 'Belize Dollar (BZD)', + 'CAD' => 'Canadian Dollar (CAD)', + 'CLP' => 'Chilean Peso (CLP)', + 'COP' => 'Colombian Peso (COP)', + 'CVE' => 'Cape Verdean Escudo (CVE)', + 'DOP' => 'Dominican Peso (DOP)', + 'HKD' => 'Hong Kong Dollar (HKD)', + 'JMD' => 'Jamaican Dollar (JMD)', + 'MOP' => 'Macanese Pataca (MOP)', + 'MXN' => 'Mexican Peso (MXN)', + 'NAD' => 'Namibian Dollar (NAD)', + 'NIO' => "Nicaraguan C\u00f3rdoba (NIO)", + 'NZD' => 'New Zealand Dollar (NZD)', + 'SGD' => 'Singapore Dollar (SGD)', + 'TOP' => "Tongan Pa\u02bbanga (TOP)", + 'TTD' => 'Trinidad and Tobago Dollar (TTD)', + 'TWD' => 'New Taiwan Dollar (TWD)', + 'USD' => 'US Dollar (USD)', + 'UYU' => 'Uruguayan Peso (UYU)', + 'ZWL' => 'Zimbabwean Dollar (ZWL)', + ], + ], + 'pound symbol' => [ + 'ยฃ', + [ + 'GBP' => 'British Pound Sterling (GBP)', + ], + ], + ]; + } + + #[Test] + public function it_gets_array_from_value() + { + $item = (new Currencies)->get('USD'); + $this->assertInstanceOf(Item::class, $item); + $this->assertEquals([ + 'name' => 'US Dollar', + 'code' => 'USD', + 'symbol' => '$', + 'decimals' => 2, + ], $item->data()); + } +} diff --git a/tests/Dictionaries/DictionaryRepositoryTest.php b/tests/Dictionaries/DictionaryRepositoryTest.php new file mode 100644 index 0000000000..62df5a27ab --- /dev/null +++ b/tests/Dictionaries/DictionaryRepositoryTest.php @@ -0,0 +1,97 @@ +repo = new DictionaryRepository; + + FakeDictionary::register(); + } + + #[Test] + public function can_get_all_dictionaries() + { + $all = $this->repo->all(); + + $this->assertCount(5, $all); // The built-in dictionaries + our fake one + $this->assertEveryItem($all, fn ($item) => $item instanceof Dictionary); + } + + #[Test] + public function can_get_a_dictionary() + { + $find = $this->repo->find('fake_dictionary'); + + $this->assertInstanceOf(Dictionary::class, $find); + $this->assertSame('fake_dictionary', $find->handle()); + } + + #[Test] + public function ensure_context_is_passed_to_dictionary() + { + $dictionary = $this->repo->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->config['sort_in_alphabetical_order'] ?? false, function ($collection) { + return $collection->sortBy('id'); + }) + ->all(); + } + + public function get(string $key): ?Item + { + 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/Dictionaries/FileTest.php b/tests/Dictionaries/FileTest.php new file mode 100644 index 0000000000..cf5f45b8a4 --- /dev/null +++ b/tests/Dictionaries/FileTest.php @@ -0,0 +1,183 @@ + 'apple', 'label' => 'Apple', 'emoji' => '๐ŸŽ'], + ['value' => 'banana', 'label' => 'Banana', 'emoji' => '๐ŸŒ'], + ['value' => 'cherry', 'label' => 'Cherry', 'emoji' => '๐Ÿ’'], + ]; + + Filesystem::put( + resource_path('dictionaries').'/items.'.$extension, + $fileDumpCallback($arr, 'value', 'label') + ); + + $options = (new File) + ->setConfig(['filename' => 'items.'.$extension]) + ->options(); + + $this->assertCount(3, $options); + $this->assertEquals([ + 'apple' => 'Apple', + 'banana' => 'Banana', + 'cherry' => 'Cherry', + ], $options); + } + + #[Test] + #[DataProvider('optionProvider')] + public function it_gets_options_with_custom_value_and_label_keys( + $extension, + $fileDumpCallback + ) { + $arr = [ + ['id' => 'apple', 'name' => 'Apple', 'emoji' => '๐ŸŽ'], + ['id' => 'banana', 'name' => 'Banana', 'emoji' => '๐ŸŒ'], + ['id' => 'cherry', 'name' => 'Cherry', 'emoji' => '๐Ÿ’'], + ]; + + Filesystem::put( + resource_path('dictionaries').'/items.'.$extension, + $fileDumpCallback($arr, 'id', 'name') + ); + + $options = (new File) + ->setConfig([ + 'filename' => 'items.'.$extension, + 'value' => 'id', + 'label' => 'name', + ]) + ->options(); + + $this->assertCount(3, $options); + $this->assertEquals([ + 'apple' => 'Apple', + 'banana' => 'Banana', + 'cherry' => 'Cherry', + ], $options); + } + + #[Test] + #[DataProvider('optionProvider')] + public function it_gets_options_with_antlers_label( + $extension, + $fileDumpCallback + ) { + $arr = [ + ['value' => 'apple', 'name' => 'Apple', 'emoji' => '๐ŸŽ'], + ['value' => 'banana', 'name' => 'Banana', 'emoji' => '๐ŸŒ'], + ['value' => 'cherry', 'name' => 'Cherry', 'emoji' => '๐Ÿ’'], + ]; + + Filesystem::put( + resource_path('dictionaries').'/items.'.$extension, + $fileDumpCallback($arr, 'value', 'name') + ); + + $options = (new File) + ->setConfig([ + 'filename' => 'items.'.$extension, + 'label' => '{{ emoji }} {{ name }}!', + ]) + ->options(); + + $this->assertCount(3, $options); + $this->assertEquals([ + 'apple' => '๐ŸŽ Apple!', + 'banana' => '๐ŸŒ Banana!', + 'cherry' => '๐Ÿ’ Cherry!', + ], $options); + } + + public static function optionProvider() + { + return [ + 'yaml' => ['yaml', fn ($arr, $value, $label) => YAML::dump($arr)], + 'json' => ['json', fn ($arr, $value, $label) => json_encode($arr)], + 'csv' => ['csv', fn ($arr, $value, $label) => "{$value},{$label},emoji".PHP_EOL.implode(PHP_EOL, array_map(fn ($item) => implode(',', $item), $arr))], + ]; + } + + #[Test] + #[DataProvider('searchProvider')] + public function it_searches_options($query, $expected) + { + $arr = [ + ['value' => 'apple', 'label' => 'Apple', 'emoji' => '๐ŸŽ'], + ['value' => 'banana', 'label' => 'Banana', 'emoji' => '๐ŸŒ'], + ['value' => 'cherry', 'label' => 'Cherry', 'emoji' => '๐Ÿ’'], + ]; + + Filesystem::put( + resource_path('dictionaries').'/items.yaml', + YAML::dump($arr) + ); + + $dictionary = (new File)->setConfig(['filename' => 'items.yaml']); + + $this->assertEquals($expected, $dictionary->options($query)); + } + + public static function searchProvider() + { + return [ + 'e' => [ + 'e', + [ + 'apple' => 'Apple', + 'cherry' => 'Cherry', + ], + ], + 'n' => [ + 'n', + [ + 'banana' => 'Banana', + ], + ], + ]; + } + + #[Test] + public function it_gets_array_from_value() + { + $arr = [ + ['value' => 'apple', 'label' => 'Apple', 'emoji' => '๐ŸŽ'], + ['value' => 'banana', 'label' => 'Banana', 'emoji' => '๐ŸŒ'], + ['value' => 'cherry', 'label' => 'Cherry', 'emoji' => '๐Ÿ’'], + ]; + + Filesystem::put( + resource_path('dictionaries').'/items.yaml', + YAML::dump($arr) + ); + + $item = (new File) + ->setConfig(['filename' => 'items.yaml']) + ->get('banana'); + + $this->assertInstanceOf(Item::class, $item); + $this->assertEquals('Banana', $item->label()); + $this->assertEquals('banana', $item->value()); + $this->assertEquals([ + 'value' => 'banana', + 'emoji' => '๐ŸŒ', + ], $item->data()); + } +} diff --git a/tests/Dictionaries/ItemTest.php b/tests/Dictionaries/ItemTest.php new file mode 100644 index 0000000000..6dc623060b --- /dev/null +++ b/tests/Dictionaries/ItemTest.php @@ -0,0 +1,31 @@ + 'Apple', // Ensures the label argument takes precedence. + 'color' => 'red', + 'emoji' => '๐ŸŽ', + ]); + + $this->assertEquals('apple', $item->value()); + $this->assertEquals('๐ŸŽ Apple', $item->label()); + $this->assertEquals(['color' => 'red', 'emoji' => '๐ŸŽ'], $item->data()); + $this->assertEquals([ + 'key' => 'apple', + 'value' => 'apple', + 'color' => 'red', + 'emoji' => '๐ŸŽ', + 'label' => '๐ŸŽ Apple', + ], $item->toArray()); + } +} diff --git a/tests/Dictionaries/TimezonesTest.php b/tests/Dictionaries/TimezonesTest.php new file mode 100644 index 0000000000..b426a4ae51 --- /dev/null +++ b/tests/Dictionaries/TimezonesTest.php @@ -0,0 +1,115 @@ +options(); + + $this->assertCount(419, $options); + $this->assertEquals([ + 'Africa/Abidjan' => 'Africa/Abidjan (+00:00)', + 'Africa/Accra' => 'Africa/Accra (+00:00)', + 'Africa/Addis_Ababa' => 'Africa/Addis_Ababa (+03:00)', + 'Pacific/Wake' => 'Pacific/Wake (+12:00)', + 'Pacific/Wallis' => 'Pacific/Wallis (+12:00)', + 'UTC' => 'UTC (+00:00)', + ], [...array_slice($options, 0, 3), ...array_slice($options, -3, 3)]); + } + + #[Test] + #[DataProvider('searchProvider')] + public function it_searches_options($query, $expected) + { + // UTC offsets can change during daylight saving time, so we need to freeze time. + Carbon::setTestNow('2024-07-23'); + + $this->assertEquals($expected, (new Timezones)->options($query)); + } + + public static function searchProvider() + { + return [ + 'new' => [ + 'new', + [ + 'America/New_York' => 'America/New_York (-04:00)', + 'America/North_Dakota/New_Salem' => 'America/North_Dakota/New_Salem (-05:00)', + ], + ], + 'ten' => [ + '10', + [ + 'Antarctica/DumontDUrville' => 'Antarctica/DumontDUrville (+10:00)', + 'Antarctica/Macquarie' => 'Antarctica/Macquarie (+10:00)', + 'Asia/Ust-Nera' => 'Asia/Ust-Nera (+10:00)', + 'Asia/Vladivostok' => 'Asia/Vladivostok (+10:00)', + 'Australia/Brisbane' => 'Australia/Brisbane (+10:00)', + 'Australia/Hobart' => 'Australia/Hobart (+10:00)', + 'Australia/Lindeman' => 'Australia/Lindeman (+10:00)', + 'Australia/Lord_Howe' => 'Australia/Lord_Howe (+10:30)', + 'Australia/Melbourne' => 'Australia/Melbourne (+10:00)', + 'Australia/Sydney' => 'Australia/Sydney (+10:00)', + 'Pacific/Chuuk' => 'Pacific/Chuuk (+10:00)', + 'Pacific/Guam' => 'Pacific/Guam (+10:00)', + 'Pacific/Honolulu' => 'Pacific/Honolulu (-10:00)', + 'Pacific/Port_Moresby' => 'Pacific/Port_Moresby (+10:00)', + 'Pacific/Rarotonga' => 'Pacific/Rarotonga (-10:00)', + 'Pacific/Saipan' => 'Pacific/Saipan (+10:00)', + 'Pacific/Tahiti' => 'Pacific/Tahiti (-10:00)', + ], + ], + 'plus ten' => [ + '+10', + [ + 'Antarctica/DumontDUrville' => 'Antarctica/DumontDUrville (+10:00)', + 'Antarctica/Macquarie' => 'Antarctica/Macquarie (+10:00)', + 'Asia/Ust-Nera' => 'Asia/Ust-Nera (+10:00)', + 'Asia/Vladivostok' => 'Asia/Vladivostok (+10:00)', + 'Australia/Brisbane' => 'Australia/Brisbane (+10:00)', + 'Australia/Hobart' => 'Australia/Hobart (+10:00)', + 'Australia/Lindeman' => 'Australia/Lindeman (+10:00)', + 'Australia/Lord_Howe' => 'Australia/Lord_Howe (+10:30)', + 'Australia/Melbourne' => 'Australia/Melbourne (+10:00)', + 'Australia/Sydney' => 'Australia/Sydney (+10:00)', + 'Pacific/Chuuk' => 'Pacific/Chuuk (+10:00)', + 'Pacific/Guam' => 'Pacific/Guam (+10:00)', + 'Pacific/Port_Moresby' => 'Pacific/Port_Moresby (+10:00)', + 'Pacific/Saipan' => 'Pacific/Saipan (+10:00)', + ], + ], + 'minus ten' => [ + '-10', + [ + 'Pacific/Honolulu' => 'Pacific/Honolulu (-10:00)', + 'Pacific/Rarotonga' => 'Pacific/Rarotonga (-10:00)', + 'Pacific/Tahiti' => 'Pacific/Tahiti (-10:00)', + ], + ], + ]; + } + + #[Test] + public function it_gets_array_from_value() + { + // UTC offsets can change during daylight saving time, so we need to freeze time. + Carbon::setTestNow('2024-07-23'); + + $item = (new Timezones)->get('America/New_York'); + $this->assertInstanceOf(Item::class, $item); + $this->assertEquals([ + 'name' => 'America/New_York', + 'offset' => '-04:00', + ], $item->data()); + } +} diff --git a/tests/Feature/GraphQL/Fieldtypes/DictionaryFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/DictionaryFieldtypeTest.php new file mode 100644 index 0000000000..6ec56a0fa7 --- /dev/null +++ b/tests/Feature/GraphQL/Fieldtypes/DictionaryFieldtypeTest.php @@ -0,0 +1,91 @@ +createEntryWithFields([ + 'undefined' => [ + 'value' => null, + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'countries']], + ], + 'country' => [ + 'value' => 'USA', + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'countries'], 'max_items' => 1], + ], + 'countries' => [ + 'value' => ['AUS', 'USA'], + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'countries']], + ], + 'timezone' => [ + 'value' => 'America/New_York', + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'timezones'], 'max_items' => 1], + ], + 'timezones' => [ + 'value' => ['Australia/Sydney', 'America/New_York'], + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'timezones']], + ], + 'currency' => [ + 'value' => 'USD', + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'currencies'], 'max_items' => 1], + ], + 'currencies' => [ + 'value' => ['GBP', 'USD'], + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'currencies']], + ], + ]); + + $this->assertGqlEntryHas(' + undefined { name, iso2 } + country { name, iso2 } + countries { name, iso2 } + timezone { name, offset } + timezones { name, offset } + currency { name, code, symbol } + currencies { name, code, symbol } + ', [ + 'undefined' => null, + 'country' => ['name' => 'United States', 'iso2' => 'US'], + 'countries' => [['name' => 'Australia', 'iso2' => 'AU'], ['name' => 'United States', 'iso2' => 'US']], + 'timezone' => ['name' => 'America/New_York', 'offset' => '-04:00'], + 'timezones' => [['name' => 'Australia/Sydney', 'offset' => '+10:00'], ['name' => 'America/New_York', 'offset' => '-04:00']], + 'currency' => ['name' => 'US Dollar', 'code' => 'USD', 'symbol' => '$'], + 'currencies' => [['name' => 'British Pound Sterling', 'code' => 'GBP', 'symbol' => 'ยฃ'], ['name' => 'US Dollar', 'code' => 'USD', 'symbol' => '$']], + ]); + } + + #[Test] + public function it_filters_out_invalid_values() + { + $this->createEntryWithFields([ + 'timezone' => [ + 'value' => 'Somewhere/Nowhere', + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'timezones'], 'max_items' => 1], + ], + 'timezones' => [ + 'value' => ['Somewhere/Nowhere', 'America/New_York'], + 'field' => ['type' => 'dictionary', 'dictionary' => ['type' => 'timezones']], + ], + ]); + + $this->assertGqlEntryHas(' + timezone { name } + timezones { name } + ', [ + 'timezone' => null, + 'timezones' => [['name' => 'America/New_York']], + ]); + } +} diff --git a/tests/Fieldtypes/DictionaryFieldsTest.php b/tests/Fieldtypes/DictionaryFieldsTest.php new file mode 100644 index 0000000000..ade9f3e1c5 --- /dev/null +++ b/tests/Fieldtypes/DictionaryFieldsTest.php @@ -0,0 +1,156 @@ +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): ?Item + { + return []; + } + + protected function fieldItems() + { + return [ + 'category' => [ + 'type' => 'select', + 'validate' => 'required', + ], + ]; + } +} diff --git a/tests/Fieldtypes/DictionaryTest.php b/tests/Fieldtypes/DictionaryTest.php new file mode 100644 index 0000000000..eac8639263 --- /dev/null +++ b/tests/Fieldtypes/DictionaryTest.php @@ -0,0 +1,245 @@ + 'dictionary', 'dictionary' => $dictionaryConfig])); + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + + $dictionary = $fieldtype->dictionary(); + $this->assertInstanceOf(Countries::class, $dictionary); + $this->assertEquals($expectedConfig, $dictionary->config()); + } + + public static function dictionaryConfigProvider() + { + return [ + 'string' => [ + 'countries', + [], + ], + 'array' => [ + ['type' => 'countries', 'foo' => 'bar', 'baz' => 'qux'], + ['foo' => 'bar', 'baz' => 'qux'], + ], + ]; + } + + #[Test] + #[DataProvider('undefinedDictionaryConfigProvider')] + public function it_throw_exception_when_dictionary_is_undefined($dictionaryConfig) + { + $this->expectException(UndefinedDictionaryException::class); + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => $dictionaryConfig])); + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + $fieldtype->dictionary(); + } + + public static function undefinedDictionaryConfigProvider() + { + return [ + 'string' => [null], + 'array' => [['foo' => 'bar']], + ]; + } + + #[Test] + #[DataProvider('invalidDictionaryConfigProvider')] + public function it_throws_exception_when_invalid_dictionary_is_defined($dictionaryConfig) + { + $this->expectException(DictionaryNotFoundException::class); + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => $dictionaryConfig])); + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + $fieldtype->dictionary(); + } + + public static function invalidDictionaryConfigProvider() + { + return [ + 'string' => ['invalid'], + 'array' => [['type' => 'invalid']], + ]; + } + + #[Test] + public function it_returns_preload_data() + { + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries'])); + $field->setValue(['USA', 'AUS', 'CAN', 'BLA', 'DEU', 'GBR']); + + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + + $preload = $fieldtype->preload(); + + $this->assertArraySubset([ + 'url' => 'http://localhost/cp/fieldtypes/dictionaries/countries', + 'selectedOptions' => [ + ['value' => 'USA', 'label' => '๐Ÿ‡บ๐Ÿ‡ธ United States', 'invalid' => false], + ['value' => 'AUS', 'label' => '๐Ÿ‡ฆ๐Ÿ‡บ Australia', 'invalid' => false], + ['value' => 'CAN', 'label' => '๐Ÿ‡จ๐Ÿ‡ฆ Canada', 'invalid' => false], + ['value' => 'BLA', 'label' => 'BLA', 'invalid' => true], + ['value' => 'DEU', 'label' => '๐Ÿ‡ฉ๐Ÿ‡ช Germany', 'invalid' => false], + ['value' => 'GBR', 'label' => '๐Ÿ‡ฌ๐Ÿ‡ง United Kingdom', 'invalid' => false], + ], + ], $preload); + } + + #[Test] + public function it_augments_a_single_option() + { + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries', 'max_items' => 1])); + + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + + $augmented = $fieldtype->augment('USA'); + $this->assertInstanceOf(Item::class, $augmented); + $this->assertEquals('USA', $augmented->value()); + $this->assertEquals('๐Ÿ‡บ๐Ÿ‡ธ United States', $augmented->label()); + $this->assertEquals([ + 'key' => 'USA', + 'name' => 'United States', + 'iso3' => 'USA', + 'iso2' => 'US', + 'region' => 'Americas', + 'subregion' => 'Northern America', + 'emoji' => '๐Ÿ‡บ๐Ÿ‡ธ', + 'value' => 'USA', + 'label' => '๐Ÿ‡บ๐Ÿ‡ธ United States', + ], $augmented->toArray()); + + $augmented = $fieldtype->augment(null); + $this->assertInstanceOf(Item::class, $augmented); + $this->assertNull($augmented->value()); + $this->assertNull($augmented->label()); + } + + #[Test] + public function it_augments_multiple_options() + { + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries'])); + + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + + $augment = $fieldtype->augment(['USA', 'GBR']); + + $this->assertEveryItemIsInstanceOf(Item::class, $augment); + + $this->assertEquals([ + [ + 'name' => 'United States', + 'key' => 'USA', + 'iso3' => 'USA', + 'iso2' => 'US', + 'region' => 'Americas', + 'subregion' => 'Northern America', + 'emoji' => '๐Ÿ‡บ๐Ÿ‡ธ', + 'value' => 'USA', + 'label' => '๐Ÿ‡บ๐Ÿ‡ธ United States', + ], + [ + 'name' => 'United Kingdom', + 'key' => 'GBR', + 'iso3' => 'GBR', + 'iso2' => 'GB', + 'region' => 'Europe', + 'subregion' => 'Northern Europe', + 'emoji' => '๐Ÿ‡ฌ๐Ÿ‡ง', + 'value' => 'GBR', + 'label' => '๐Ÿ‡ฌ๐Ÿ‡ง United Kingdom', + ], + ], collect($augment)->toArray()); + } + + #[Test] + public function it_augments_to_empty_array_when_null_and_configured_for_multiple() + { + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries'])); + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + + $this->assertEquals([], $fieldtype->augment(null)); + } + + #[Test] + public function invalid_value_augments_to_null() + { + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries', 'max_items' => 1])); + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + + $augmented = $fieldtype->augment('invalid'); + $this->assertInstanceOf(Item::class, $augmented); + $this->assertNull($augmented->value()); + $this->assertNull($augmented->label()); + } + + #[Test] + public function it_filters_out_invalid_values_when_augmenting_multiple() + { + $field = (new Field('test', ['type' => 'dictionary', 'dictionary' => 'countries'])); + + $fieldtype = FieldtypeRepository::find('dictionary'); + $fieldtype->setField($field); + + $augment = $fieldtype->augment(['USA', 'Invalid']); + + $this->assertCount(1, $augment); + $this->assertEveryItemIsInstanceOf(Item::class, $augment); + $this->assertEquals([ + [ + 'name' => 'United States', + 'key' => 'USA', + 'iso3' => 'USA', + 'iso2' => 'US', + 'region' => 'Americas', + 'subregion' => 'Northern America', + 'emoji' => '๐Ÿ‡บ๐Ÿ‡ธ', + 'value' => 'USA', + 'label' => '๐Ÿ‡บ๐Ÿ‡ธ United States', + ], + ], collect($augment)->toArray()); + } + + #[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); + } +}