diff --git a/core/core.field_type_categories.yml b/core/core.field_type_categories.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c71b43d4b54ac25cb59ce12101e4b74077b92332
--- /dev/null
+++ b/core/core.field_type_categories.yml
@@ -0,0 +1,18 @@
+plain_text:
+  label: 'Plain text'
+  description: 'Text field that does not support markup.'
+  weight: -50
+number:
+  label: 'Number'
+  description: 'Field to store number. I.e. id, price, or quantity.'
+  weight: -40
+reference:
+  label: 'Reference'
+  description: 'Field to reference other content.'
+  weight: -30
+date_time:
+  label: 'Date and time'
+  description: 'Field to store date and time values.'
+  weight: -10
+general:
+  class: \Drupal\Core\Field\FallbackFieldTypeCategory
diff --git a/core/core.services.yml b/core/core.services.yml
index cce73e7794e1deea84e11d73c276b47a2ea91466..e84dfbb29eacd1ee7c4d2faa0153cbbf918bcb77 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -744,8 +744,12 @@ services:
   Drupal\Core\Block\BlockManagerInterface: '@plugin.manager.block'
   plugin.manager.field.field_type:
     class: Drupal\Core\Field\FieldTypePluginManager
-    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@typed_data_manager']
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@typed_data_manager', '@plugin.manager.field.field_type_category']
   Drupal\Core\Field\FieldTypePluginManagerInterface: '@plugin.manager.field.field_type'
+  plugin.manager.field.field_type_category:
+    class: \Drupal\Core\Field\FieldTypeCategoryManager
+    arguments: [ '%app.root%', '@module_handler', '@cache.discovery' ]
+  Drupal\Core\Field\FieldTypeCategoryManagerInterface: '@plugin.manager.field.field_type_category'
   plugin.manager.field.widget:
     class: Drupal\Core\Field\WidgetPluginManager
     arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@plugin.manager.field.field_type']
diff --git a/core/lib/Drupal/Core/Field/FallbackFieldTypeCategory.php b/core/lib/Drupal/Core/Field/FallbackFieldTypeCategory.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a83aa0737403464b7340071d38e1468d9bbd32c
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FallbackFieldTypeCategory.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+/**
+ * Fallback plugin class for FieldTypeCategoryManager.
+ */
+class FallbackFieldTypeCategory extends FieldTypeCategory {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration) {
+    $plugin_definition = [
+      'label' => $configuration['label'] ?? '',
+      'description' => $configuration['description'] ?? '',
+      'weight' => $configuration['weight'] ?? 0,
+    ];
+    parent::__construct($configuration, $configuration['unique_identifier'], $plugin_definition);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Field/FieldTypeCategory.php b/core/lib/Drupal/Core/Field/FieldTypeCategory.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac04b5124947cc269ac3b1f481913d44a7dbce6f
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldTypeCategory.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * Default object used for field_type_categories plugins.
+ *
+ * @see \Drupal\Core\Field\FieldTypeCategoryManager
+ */
+class FieldTypeCategory extends PluginBase implements FieldTypeCategoryInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel(): TranslatableMarkup {
+    return $this->pluginDefinition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription(): TranslatableMarkup {
+    return $this->pluginDefinition['description'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWeight(): int {
+    return $this->pluginDefinition['weight'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Field/FieldTypeCategoryInterface.php b/core/lib/Drupal/Core/Field/FieldTypeCategoryInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..e9a1a0d21a851cc61560d983159245cab975ecf6
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldTypeCategoryInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * Provides an object that returns the category info about the field type.
+ */
+interface FieldTypeCategoryInterface {
+
+  /**
+   * Returns the field group label.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The category label.
+   */
+  public function getLabel(): TranslatableMarkup;
+
+  /**
+   * Returns the field group description.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The category description.
+   */
+  public function getDescription(): TranslatableMarkup;
+
+  /**
+   * Returns the field group weight.
+   *
+   * @return int
+   *   The weight.
+   */
+  public function getWeight(): int;
+
+}
diff --git a/core/lib/Drupal/Core/Field/FieldTypeCategoryManager.php b/core/lib/Drupal/Core/Field/FieldTypeCategoryManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..81a246751ebc2ba6ae14f894aad4280e0e98f8f2
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldTypeCategoryManager.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+use Drupal\Component\Plugin\FallbackPluginManagerInterface;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\Discovery\YamlDiscovery;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Defines a field type category info plugin manager.
+ *
+ * A module can define field type categories in a
+ * MODULE_NAME.field_type_categories.yml file contained in the module's
+ * base directory. Each plugin has the following structure:
+ * @code
+ *   CATEGORY_NAME:
+ *     label: STRING
+ *     description: STRING
+ *     weight: INTEGER
+ * @endcode
+ * For example:
+ * @code
+ * text:
+ *   label: Text
+ *   description: Text fields.
+ *   weight: 2
+ * @endcode
+ *
+ * @see \Drupal\Core\Field\FieldTypeCategoryInterface
+ * @see \Drupal\Core\Field\FieldTypeCategory
+ * @see \hook_field_type_category_info_alter
+ */
+class FieldTypeCategoryManager extends DefaultPluginManager implements FieldTypeCategoryManagerInterface, FallbackPluginManagerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaults = [
+    'label' => '',
+    'description' => '',
+    'weight' => 0,
+    'class' => FieldTypeCategory::class,
+  ];
+
+  /**
+   * Constructs a new FieldTypeCategoryManager.
+   *
+   * @param string $root
+   *   The app root.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend.
+   */
+  public function __construct(protected readonly string $root, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) {
+    $this->moduleHandler = $module_handler;
+    $this->alterInfo('field_type_category_info');
+    $this->setCacheBackend($cache_backend, 'field_type_category_info_plugins');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDiscovery(): YamlDiscovery {
+    if (!isset($this->discovery)) {
+      $directories = ['core' => $this->root . '/core'];
+      $directories += $this->moduleHandler->getModuleDirectories();
+      $this->discovery = new YamlDiscovery('field_type_categories', $directories);
+      $this->discovery
+        ->addTranslatableProperty('label')
+        ->addTranslatableProperty('description');
+    }
+    return $this->discovery;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFallbackPluginId($plugin_id, array $configuration = []): string {
+    return 'general';
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Field/FieldTypeCategoryManagerInterface.php b/core/lib/Drupal/Core/Field/FieldTypeCategoryManagerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc8dae960d3384455975bda098d4e7ad98120ba3
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldTypeCategoryManagerInterface.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+
+/**
+ * Defines an interface for field type category managers.
+ */
+interface FieldTypeCategoryManagerInterface extends PluginManagerInterface {
+}
diff --git a/core/lib/Drupal/Core/Field/FieldTypePluginManager.php b/core/lib/Drupal/Core/Field/FieldTypePluginManager.php
index ee68da04e4303e9d572d8a6d520e8f7845909b6d..81160c701cb210936278a8048fc4f5b1f59ad168 100644
--- a/core/lib/Drupal/Core/Field/FieldTypePluginManager.php
+++ b/core/lib/Drupal/Core/Field/FieldTypePluginManager.php
@@ -46,12 +46,18 @@ class FieldTypePluginManager extends DefaultPluginManager implements FieldTypePl
    *   The module handler.
    * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
    *   The typed data manager.
+   * @param \Drupal\Core\Field\FieldTypeCategoryManagerInterface|null $fieldTypeCategoryManager
+   *   The field type category plugin manager.
    */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, TypedDataManagerInterface $typed_data_manager) {
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, TypedDataManagerInterface $typed_data_manager, protected ?FieldTypeCategoryManagerInterface $fieldTypeCategoryManager = NULL) {
     parent::__construct('Plugin/Field/FieldType', $namespaces, $module_handler, 'Drupal\Core\Field\FieldItemInterface', 'Drupal\Core\Field\Annotation\FieldType');
     $this->alterInfo('field_info');
     $this->setCacheBackend($cache_backend, 'field_types_plugins');
     $this->typedDataManager = $typed_data_manager;
+    if ($this->fieldTypeCategoryManager === NULL) {
+      @trigger_error('Calling FieldTypePluginManager::__construct() without the $fieldTypeCategoryManager argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3375737', E_USER_DEPRECATED);
+      $this->fieldTypeCategoryManager = \Drupal::service('plugin.manager.field.field_type_category');
+    }
   }
 
   /**
@@ -162,10 +168,10 @@ public function getFieldSettingsSummary(FieldDefinitionInterface $field_definiti
    */
   public function getGroupedDefinitions(array $definitions = NULL, $label_key = 'label') {
     $grouped_categories = $this->getGroupedDefinitionsTrait($definitions, $label_key);
-    $category_info = \Drupal::moduleHandler()->invokeAll('field_type_category_info');
+    $category_info = $this->fieldTypeCategoryManager->getDefinitions();
     foreach ($grouped_categories as $group => $definitions) {
       if (!isset($category_info[$group]) && $group !== static::DEFAULT_CATEGORY) {
-        assert(FALSE, "\"$group\" must be defined in hook_field_type_category_info().");
+        assert(FALSE, "\"$group\" must be defined in MODULE_NAME.field_type_categories.yml");
         $grouped_categories[static::DEFAULT_CATEGORY] += $definitions;
         unset($grouped_categories[$group]);
       }
diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php
index e8b07637b3047b322cd94c6632b5d5fcf773bf7c..ed9b86a8afff9732aa5c1c66cc0bdf118985453e 100644
--- a/core/modules/field/field.api.php
+++ b/core/modules/field/field.api.php
@@ -410,6 +410,31 @@ function hook_field_purge_field(\Drupal\field\Entity\FieldConfig $field) {
     ->execute();
 }
 
+/**
+ * Allows modules to alter the field type category information.
+ *
+ * This hook provides a way for modules to modify or add to the existing
+ * category information. Modules can use this hook to modify the properties of
+ * existing categories. It can also be used to define custom field type
+ * categories although the use of YAML-based plugins should be preferred over
+ * the hook.
+ *
+ * @param array &$categories
+ *   An associative array of field type categories, keyed by category machine
+ *    name.
+ *
+ * @see \Drupal\Core\Field\FieldTypeCategoryManager
+ */
+function hook_field_type_category_info_alter(array &$categories) {
+  // Modify or add field type categories.
+  $categories['my_custom_category'] = [
+    'label' => 'My Custom Category',
+    'description' => 'This is a custom category for my field types.',
+  ];
+  // Modify the properties of an existing category.
+  $categories['text']['description'] = 'Modified Text';
+}
+
 /**
  * @} End of "addtogroup field_purge".
  */
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index 3b637910a88f7ea59ae3498802cb83b20a7810ca..d5a8182048a68b6b489a5dbdeb7b2f0db61ddf29 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -426,31 +426,3 @@ function field_field_config_presave(FieldConfigInterface $field) {
     ]);
   }
 }
-
-/**
- * Returns field category properties.
- */
-function field_field_type_category_info() {
-  return [
-    'plain_text' => [
-      'label' => t('Plain text'),
-      'description' => t('Text field that does not support markup.'),
-      'weight' => -50,
-    ],
-    'number' => [
-      'label' => t('Number'),
-      'description' => t('Field to store number. I.e. id, price, or quantity.'),
-      'weight' => -40,
-    ],
-    'reference' => [
-      'label' => t('Reference'),
-      'description' => t('Field to reference other content.'),
-      'weight' => -30,
-    ],
-    'date_time' => [
-      'label' => t('Date and time'),
-      'description' => t('Field to store date and time values.'),
-      'weight' => -10,
-    ],
-  ];
-}
diff --git a/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.field_type_categories.yml b/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.field_type_categories.yml
new file mode 100644
index 0000000000000000000000000000000000000000..35b725c686308e4fbf6355d97ff301c308ddbcfd
--- /dev/null
+++ b/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.field_type_categories.yml
@@ -0,0 +1,4 @@
+test_category:
+  label: 'Test category'
+  description: 'This is a test field type category.'
+  weight: -10
diff --git a/core/modules/field/tests/src/Kernel/FieldTypeCategoryDiscoveryTest.php b/core/modules/field/tests/src/Kernel/FieldTypeCategoryDiscoveryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0dbf670fdcbeb26142def09cec5b48f4207ac0cb
--- /dev/null
+++ b/core/modules/field/tests/src/Kernel/FieldTypeCategoryDiscoveryTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\Tests\field\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests discovery of field type categories provided by modules.
+ *
+ * @group field
+ */
+class FieldTypeCategoryDiscoveryTest extends KernelTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  protected static $modules = [
+    'field_plugins_test',
+  ];
+
+  /**
+   * Tests custom field type categories created by modules.
+   */
+  public function testFieldTypeCategories() {
+    $category = \Drupal::service('plugin.manager.field.field_type_category')->createInstance('test_category');
+    $expected = [
+      'Test category',
+      'This is a test field type category.',
+      -10,
+    ];
+
+    $this->assertSame($expected, [
+      (string) $category->getLabel(),
+      (string) $category->getDescription(),
+      $category->getWeight(),
+    ]);
+  }
+
+}
diff --git a/core/modules/field_ui/field_ui.api.php b/core/modules/field_ui/field_ui.api.php
index 9437339a85f6217bfe895ac6d7d6641e6bb69b40..87f55ae6e1a5daba83f587268a2ad789cbb14e1a 100644
--- a/core/modules/field_ui/field_ui.api.php
+++ b/core/modules/field_ui/field_ui.api.php
@@ -122,32 +122,6 @@ function hook_field_widget_settings_summary_alter(array &$summary, array $contex
   }
 }
 
-/**
- * Provides information about field categories used for the UI.
- *
- * @return array
- *   Returns an array with the field category information.
- */
-function hook_field_type_category_info() {
-  return [
-    'selection_list' => [
-      'label' => t('Selection list'),
-      'description' => t('Field to select from predefined options.'),
-      'weight' => -30,
-    ],
-    'number' => [
-      'label' => t('Number'),
-      'description' => t('Field to store number. I.e. id, price, or quantity.'),
-      'weight' => -20,
-    ],
-    'date_time' => [
-      'label' => t('Date and time'),
-      'description' => t('Field to store date and time values.'),
-      'weight' => -12,
-    ],
-  ];
-}
-
 /**
  * @} End of "addtogroup field_types".
  */
diff --git a/core/modules/field_ui/src/Form/FieldStorageAddForm.php b/core/modules/field_ui/src/Form/FieldStorageAddForm.php
index 52d633a01d5a2295c15bd1c368c711f4e015cc0b..2134c00c35da7f52bc179358c0941ab7bea5f766 100644
--- a/core/modules/field_ui/src/Form/FieldStorageAddForm.php
+++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php
@@ -8,7 +8,8 @@
 use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Field\FieldTypePluginManager;
+use Drupal\Core\Field\FallbackFieldTypeCategory;
+use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -83,17 +84,23 @@ class FieldStorageAddForm extends FormBase {
    *   The field type plugin manager.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The configuration factory.
-   * @param \Drupal\Core\Entity\EntityFieldManagerInterface|null $entity_field_manager
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
    *   (optional) The entity field manager.
    * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
    *   (optional) The entity display repository.
+   * @param \Drupal\Core\Field\FieldTypeCategoryManagerInterface|null $fieldTypeCategoryManager
+   *   The field type category plugin manager.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, ConfigFactoryInterface $config_factory, EntityFieldManagerInterface $entity_field_manager = NULL, EntityDisplayRepositoryInterface $entity_display_repository = NULL) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, ConfigFactoryInterface $config_factory, EntityFieldManagerInterface $entity_field_manager, EntityDisplayRepositoryInterface $entity_display_repository, protected ?FieldTypeCategoryManagerInterface $fieldTypeCategoryManager = NULL) {
     $this->entityTypeManager = $entity_type_manager;
     $this->fieldTypePluginManager = $field_type_plugin_manager;
     $this->configFactory = $config_factory;
     $this->entityFieldManager = $entity_field_manager;
     $this->entityDisplayRepository = $entity_display_repository;
+    if ($this->fieldTypeCategoryManager === NULL) {
+      @trigger_error('Calling FieldStorageAddForm::__construct() without the $fieldTypeCategoryManager argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3375740', E_USER_DEPRECATED);
+      $this->fieldTypeCategoryManager = \Drupal::service('plugin.manager.field.field_type_category');
+    }
   }
 
   /**
@@ -112,7 +119,8 @@ public static function create(ContainerInterface $container) {
       $container->get('plugin.manager.field.field_type'),
       $container->get('config.factory'),
       $container->get('entity_field.manager'),
-      $container->get('entity_display.repository')
+      $container->get('entity_display.repository'),
+      $container->get('plugin.manager.field.field_type_category'),
     );
   }
 
@@ -150,16 +158,12 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
     $field_type_options = $unique_definitions = [];
     $grouped_definitions = $this->fieldTypePluginManager->getGroupedDefinitions($this->fieldTypePluginManager->getUiDefinitions());
     // Invoke a hook to get category properties.
-    $category_info = \Drupal::moduleHandler()->invokeAll('field_type_category_info');
     foreach ($grouped_definitions as $category => $field_types) {
       foreach ($field_types as $name => $field_type) {
         $unique_definitions[$category][$name] = ['unique_identifier' => $name] + $field_type;
-        if (isset($category_info[$category])) {
-          // Get the category label from the hook if it is defined in the hook.
-          $field_type_options[$category_info[$category]['label']->render()] = ['unique_identifier' => $name] + $field_type;
-        }
-        else {
-          $field_type_options[$field_type['label']->render()] = ['unique_identifier' => $name] + $field_type;
+        if ($this->fieldTypeCategoryManager->hasDefinition($category)) {
+          $category_plugin = $this->fieldTypeCategoryManager->createInstance($category, $unique_definitions[$category][$name]);
+          $field_type_options[$category_plugin->getPluginId()] = ['unique_identifier' => $name] + $field_type;
         }
       }
     }
@@ -177,12 +181,9 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
     ];
     $field_type_options_radios = [];
     foreach ($field_type_options as $field_option => $val) {
-      // Boolean flag for whether a field option is to be displayed as a group.
-      // When an option should be displayed as a group, its category value
-      // is a string id instead of TranslatableMarkup. The string id maps
-      // to the values returned by the field_type_category_info hook.
-      $display_as_group = is_string($val['category']) && $val['category'] !== FieldTypePluginManager::DEFAULT_CATEGORY;
-      $option_info = $display_as_group ? $category_info[$val['category']] : $val;
+      /** @var  \Drupal\Core\Field\FieldTypeCategoryInterface $category_info */
+      $category_info = $this->fieldTypeCategoryManager->createInstance($val['category'], $val);
+      $display_as_group = !($category_info instanceof FallbackFieldTypeCategory);
       $cleaned_class_name = Html::getClass($val['unique_identifier']);
       $field_type_options_radios[$field_option] = [
         '#type' => 'container',
@@ -190,7 +191,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
           'class' => ['field-option', 'js-click-to-select'],
           'checked' => $this->getRequest()->request->get('new_storage_type') !== NULL && $this->getRequest()->request->get('new_storage_type') == ($display_as_group ? $val['category'] : $val['unique_identifier']),
         ],
-        '#weight' => $option_info['weight'] ?? 1,
+        '#weight' => $category_info->getWeight(),
         'thumb' => [
           '#type' => 'container',
           '#attributes' => [
@@ -211,7 +212,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
         ],
         'radio' => [
           '#type' => 'radio',
-          '#title' => $field_option,
+          '#title' => $category_info->getLabel(),
           '#parents' => ['new_storage_type'],
           '#title_display' => 'before',
           '#description_display' => 'before',
@@ -234,7 +235,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
             '#attributes' => [
               'class' => ['field-option__description'],
             ],
-            '#markup' => $option_info['description'] ?? NULL,
+            '#markup' => $category_info->getDescription(),
           ],
           '#variant' => 'field-option',
         ],
@@ -261,14 +262,13 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
     $selected_field_type = NULL;
     foreach ($field_type_options_radios as $field_type_options_radio) {
       if ($field_type_options_radio['#attributes']['checked']) {
-        $selected_field_label = $field_type_options_radio['radio']['#title'];
         $selected_field_type = $field_type_options_radio['radio']['#return_value'];
         $form_state->setValue('selected_field_type', $selected_field_type);
         break;
       }
     }
-    if (isset($selected_field_label)) {
-      $group_display = $field_type_options_radios[$selected_field_label]['#data']['#group_display'];
+    if (isset($selected_field_type)) {
+      $group_display = $field_type_options_radios[$selected_field_type]['#data']['#group_display'];
       if ($group_display) {
         $form['group_field_options_wrapper']['label'] = [
           '#type' => 'label',
@@ -286,7 +286,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
           $radio_element = [
             '#type' => 'radio',
             '#theme_wrappers' => ['form_element__new_storage_type'],
-            '#title' => $option['label']->render(),
+            '#title' => $option['label'],
             '#description' => [
               '#theme' => 'item_list',
               '#items' => $unique_definitions[$selected_field_type][$option_key]['description'],
diff --git a/core/modules/file/file.field_type_categories.yml b/core/modules/file/file.field_type_categories.yml
new file mode 100644
index 0000000000000000000000000000000000000000..efb40e7eeb03dce1c233cabd6ae56a51137aecd3
--- /dev/null
+++ b/core/modules/file/file.field_type_categories.yml
@@ -0,0 +1,4 @@
+file_upload:
+  label: 'File upload'
+  description: 'Field to upload any type of files.'
+  weight: -15
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 1fa00345e651e1305a3dc41a20cc6a585e0f3f55..fa17d9c8de81bbf5ad9bd652a043637ff44357ad 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -1553,19 +1553,6 @@ function file_field_find_file_reference_column(FieldDefinitionInterface $field)
   return FALSE;
 }
 
-/**
- * Implements hook_field_type_category_info().
- */
-function file_field_type_category_info() {
-  return [
-    'file_upload' => [
-      'label' => t('File upload'),
-      'description' => t('Field to upload any type of files.'),
-      'weight' => -15,
-    ],
-  ];
-}
-
 /**
  * Implements hook_preprocess_form_element__new_storage_type().
  */
diff --git a/core/modules/options/options.field_type_categories.yml b/core/modules/options/options.field_type_categories.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9c23ff067057556ecb1d3fb497dd696a470e0955
--- /dev/null
+++ b/core/modules/options/options.field_type_categories.yml
@@ -0,0 +1,4 @@
+selection_list:
+  label: 'Selection list'
+  description: 'Field to select from predefined options.'
+  weight: -15
diff --git a/core/modules/options/options.module b/core/modules/options/options.module
index 244a766acd61d5e024c72ff720c833d8bb39bb44..2feed0daf8060235a5291991cde9f088ad92b4cf 100644
--- a/core/modules/options/options.module
+++ b/core/modules/options/options.module
@@ -139,19 +139,6 @@ function _options_values_in_use($entity_type, $field_name, $values) {
   return FALSE;
 }
 
-/**
- * Implements hook_field_type_category_info().
- */
-function options_field_type_category_info() {
-  return [
-    'selection_list' => [
-      'label' => t('Selection list'),
-      'description' => t('Field to select from predefined options.'),
-      'weight' => -15,
-    ],
-  ];
-}
-
 /**
  * Implements hook_form_FORM_ID_alter().
  *
diff --git a/core/modules/text/text.field_type_categories.yml b/core/modules/text/text.field_type_categories.yml
new file mode 100644
index 0000000000000000000000000000000000000000..787cfc9795727e2c21f9e09b920d249b0a86f89e
--- /dev/null
+++ b/core/modules/text/text.field_type_categories.yml
@@ -0,0 +1,4 @@
+formatted_text:
+  label: 'Formatted text'
+  description: 'Text field with markup support and optional editor.'
+  weight: -45
diff --git a/core/modules/text/text.module b/core/modules/text/text.module
index eb74636478acf3c94e14da017a3d295d401130b0..54f513281d4fe7eda8aac5a51014754996e8668e 100644
--- a/core/modules/text/text.module
+++ b/core/modules/text/text.module
@@ -165,19 +165,6 @@ function text_summary($text, $format = NULL, $size = NULL) {
   return $summary;
 }
 
-/**
- * Implements hook_field_type_category_info().
- */
-function text_field_type_category_info() {
-  return [
-    'formatted_text' => [
-      'label' => t('Formatted text'),
-      'description' => t('Text field with markup support and optional editor.'),
-      'weight' => -45,
-    ],
-  ];
-}
-
 /**
  * Implements hook_preprocess_form_element__new_storage_type().
  */
diff --git a/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php b/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php
index c2f45ae311805558e007dafa07cf1d1afbbc0a14..3398baf9f866e64e96cdadb86bd5a7cd62be7a1f 100644
--- a/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
 use Drupal\Core\Field\FieldTypePluginManager;
 use Drupal\Core\TypedData\TypedDataManagerInterface;
 use Drupal\Tests\UnitTestCase;
@@ -38,11 +39,13 @@ protected function setUp(): void {
       ->with($module_name)
       ->willReturn(TRUE);
     $typed_data_manager = $this->createMock(TypedDataManagerInterface::class);
+    $field_type_category_manager = $this->createMock(FieldTypeCategoryManagerInterface::class);
     $plugin_manager = new FieldTypePluginManager(
       $namespaces,
       $this->createMock('Drupal\Core\Cache\CacheBackendInterface'),
       $module_handler,
-      $typed_data_manager
+      $typed_data_manager,
+      $field_type_category_manager,
     );
 
     $container = new ContainerBuilder();