diff --git a/composer.json b/composer.json
index fc3b373..624d5ca 100644
--- a/composer.json
+++ b/composer.json
@@ -40,7 +40,7 @@
     "source": "http://cgit.drupalcode.org/node_revision_delete"
   },
   "require": {
-    "drupal/core": "^10.3 || ^11"
+    "drupal/core": "^10.3 || ^11 || ^12"
   },
   "extra": {
     "drush": {
diff --git a/node_revision_delete.info.yml b/node_revision_delete.info.yml
index 204c352..0dde08a 100644
--- a/node_revision_delete.info.yml
+++ b/node_revision_delete.info.yml
@@ -3,7 +3,7 @@ description: Track and prune node revisions.
 package: Content
 
 type: module
-core_version_requirement: ^10.3 || ^11
+core_version_requirement: ^10.3 || ^11 || ^12
 
 dependencies:
   - drupal:node
diff --git a/node_revision_delete.module b/node_revision_delete.module
index a743763..457c501 100644
--- a/node_revision_delete.module
+++ b/node_revision_delete.module
@@ -5,168 +5,29 @@
  * Contains node_revision_delete.module.
  */
 
-use Drupal\Component\Utility\Xss;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\node_revision_delete\Hook\NodeRevisionDeleteHooks;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Link;
 use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
 use Drupal\node\NodeInterface;
 use Drupal\node\NodeTypeInterface;
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function node_revision_delete_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-
-    case 'help.page.node_revision_delete':
-
-      $output = '';
-      $output .= '<h3>' . t('Introduction') . '</h3>';
-      $output .= '<p>' . t('The Node Revision Delete module lets you track and prune old revisions of content types.') . '</p>';
-      $output .= '<h3>' . t('Uses') . '</h3>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Configuring the module') . '</dt>';
-      $output .= '<dd>' . t('You can manage the module settings via the <a href=":config-page">configuration page</a>. You can configure how many revisions you want to keep per content type and configure how long revision should be kept. When saving the configuration you can optionally start a batch job to queue all content to delete revisions that are allowed to be deleted. For this you need the <em>Administer Node Revision Delete</em> permission.', [':config-page' => Url::fromRoute('node_revision_delete.admin_settings')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring content types') . '</dt>';
-      $output .= '<dd>' . t('You can override the default settings of the module for each content type. Under the Publishing options tab of the content type, configure how many revisions you want to keep for the content type and how long revision should be kept.') . '</dd>';
-      $output .= '</dl>';
-
-      return $output;
-
-  }
+  return \Drupal::service(NodeRevisionDeleteHooks::class)->help($route_name, $route_match);
 }
 
 /**
  * Implements hook_form_BASE_FORM_ID_alter().
  */
+#[LegacyHook]
 function node_revision_delete_form_node_type_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  /** @var \Drupal\node_revision_delete\Plugin\NodeRevisionDeletePluginManager $plugin_manager */
-  $plugin_manager = \Drupal::service('plugin.manager.node_revision_delete');
-  $plugin_definitions = $plugin_manager->getDefinitions();
-
-  // Getting the content type entity.
-  /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
-  $form_object = $form_state->getFormObject();
-  $content_type = $form_object->getEntity();
-  assert($content_type instanceof NodeTypeInterface);
-
-  // Detect if this build is an AJAX rebuild caused by toggling the override
-  // checkbox. When that happens we always show fresh defaults so the visible
-  // form matches what would actually be saved.
-  $triggering_element = $form_state->getTriggeringElement();
-  $is_override_ajax = $form_state->isRebuilding()
-    && $triggering_element !== NULL
-    && ($triggering_element['#name'] ?? '') === 'node_revision_delete[override]';
-
-  if ($is_override_ajax) {
-    // Drop any user input the admin made for the plugin sub-forms so the
-    // rebuilt form picks up the fresh #default_value below.
-    $input = $form_state->getUserInput();
-    foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
-      unset($input[$plugin_id]);
-      $form_state->unsetValue($plugin_id);
-    }
-    $form_state->setUserInput($input);
-  }
-
-  // Decide which settings to display. On the initial build the form mirrors
-  // what is saved on the node type. On an AJAX rebuild (or for new node
-  // types) we always show the defaults.
-  $has_saved_override = !$content_type->isNew() && !empty($content_type->getThirdPartySettings('node_revision_delete'));
-  if ($is_override_ajax || !$has_saved_override) {
-    $current_settings = [];
-    foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
-      $current_settings[$plugin_id] = $plugin_manager->getDefaultPluginSettings($plugin_id);
-    }
-  }
-  else {
-    $current_settings = $plugin_manager->getSettingsNodeType($content_type->id())['plugin'];
-  }
-
-  // The override state comes from the form value during a rebuild and from
-  // the saved third-party settings on the initial build.
-  $override = $is_override_ajax
-    ? (bool) $form_state->getValue(['node_revision_delete', 'override'])
-    : $has_saved_override;
-
-  $form['node_revision_delete'] = [
-    '#type' => 'details',
-    '#title' => t('Node Revision Delete settings'),
-    '#collapsed' => FALSE,
-    '#collapsible' => TRUE,
-    '#tree' => FALSE,
-    '#weight' => 10,
-    '#group' => 'additional_settings',
-    '#attributes' => ['class' => ['node-revision-delete-settings-form']],
-  ];
-
-  // Wrap the plugin sub-forms in a container so the AJAX callback can replace
-  // the whole group when override is toggled.
-  $form['node_revision_delete']['plugins_wrapper'] = [
-    '#type' => 'container',
-    '#attributes' => ['id' => 'node-revision-delete-plugins-wrapper'],
-  ];
-
-  $form['node_revision_delete']['plugins_wrapper']['override'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Override default settings'),
-    '#default_value' => $override,
-    '#attributes' => [
-      'class' => ['node-revision-delete-override'],
-    ],
-    '#parents' => ['node_revision_delete', 'override'],
-    '#ajax' => [
-      'callback' => 'node_revision_delete_override_ajax_callback',
-      'wrapper' => 'node-revision-delete-plugins-wrapper',
-    ],
-  ];
-
-  foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
-    /** @var \Drupal\node_revision_delete\Plugin\NodeRevisionDeleteInterface $plugin */
-    $plugin = $plugin_manager->getPlugin($plugin_id, $current_settings[$plugin_id]['settings'] ?? []);
-    $form['node_revision_delete']['plugins_wrapper'][$plugin_id] = [
-      '#type' => 'details',
-      '#title' => $plugin_definition['label'],
-      '#open' => TRUE,
-      // We need the #tree key for subforms to work.
-      // @see https://www.drupal.org/project/drupal/issues/3053890
-      '#tree' => TRUE,
-      '#attributes' => [
-        'class' => ['node-revision-delete-plugin-settings'],
-      ],
-    ];
-    $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['status'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Enabled'),
-      '#default_value' => $current_settings[$plugin_id]['status'] ?? 0,
-    ];
-
-    $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] = [
-      '#type' => 'container',
-      '#states' => [
-        'visible' => [
-          ':input[name="' . $plugin_id . '[status]"]' => ['checked' => TRUE],
-        ],
-      ],
-    ];
-    $plugin_form_state = SubformState::createForSubform($form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'], $form, $form_state);
-    $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] = $plugin->buildConfigurationForm($form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] ?? [], $plugin_form_state);
-
-    // When override is off, disable the plugin sub-form server-side. This
-    // ensures Drupal substitutes the #default_value for any user input before
-    // validation, so required fields (whose values come from the global
-    // defaults) do not block saving the node type.
-    if (!$override) {
-      $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['status']['#disabled'] = TRUE;
-      $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings']['#disabled'] = TRUE;
-    }
-  }
-  $form['#attached']['library'][] = 'node_revision_delete/summaries';
-
-  $form['#entity_builders'][] = 'node_revision_delete_form_node_type_form_builder';
+  \Drupal::service(NodeRevisionDeleteHooks::class)->formNodeTypeFormAlter($form, $form_state, $form_id);
 }
 
 /**
@@ -209,133 +70,17 @@ function node_revision_delete_form_node_type_form_builder(string $entity_type, N
 /**
  * Implements hook_node_update().
  */
+#[LegacyHook]
 function node_revision_delete_node_update(NodeInterface $node) {
-  $disable_automatic_queueing = \Drupal::config('node_revision_delete.settings')->get('disable_automatic_queueing');
-  $has_enabled_plugins = \Drupal::service('node_revision_delete')->contentTypeHasEnabledPlugins($node->getType());
-  if ($disable_automatic_queueing || !$has_enabled_plugins) {
-    return;
-  }
-
-  // Add updated nodes to the queue to determine if any revisions are ready to
-  // be deleted.
-  $nid = (int) $node->id();
-  if (\Drupal::service('node_revision_delete')->createQueueItem($nid)->queued()) {
-    if (\Drupal::config('node_revision_delete.settings')->get('verbose_log')) {
-      \Drupal::logger('node_revision_delete')->info('Node @id was added to the Node Revision Delete queue.', ['@id' => $node->id()]);
-    }
-  }
+  \Drupal::service(NodeRevisionDeleteHooks::class)->nodeUpdate($node);
 }
 
 /**
  * Implements hook_form_BASE_FORM_ID_alter().
  */
+#[LegacyHook]
 function node_revision_delete_form_node_revision_delete_confirm_alter(&$form, FormStateInterface &$form_state, $form_id) {
-  // Get the node id from URL.
-  $nid = \Drupal::routeMatch()->getRawParameter('node');
-  // Get the revision id from URL.
-  $revision_id = \Drupal::routeMatch()->getRawParameter('node_revision');
-
-  // Get list of revision IDs older than current revision for the content
-  // language. Use TYPE_CONTENT to ensure the correct language is used even when
-  // the "Account administration pages" language negotiation is active, which
-  // would cause getCurrentLanguage() (TYPE_INTERFACE) to return the admin
-  // language.
-  $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
-  /** @var \Drupal\node_revision_delete\NodeRevisionDeleteInterface $service */
-  $service = \Drupal::service('node_revision_delete');
-  $vids_before = $service->getPreviousRevisionIds($nid, $revision_id, $langcode);
-  // Count sum of old revisions.
-  $revision_number = count($vids_before);
-
-  // If there are old revisions.
-  if ($revision_number > 0) {
-    $form['revision_list'] = [
-      '#type' => 'details',
-      '#title' => t('Delete prior revisions'),
-      '#open' => FALSE,
-    ];
-
-    $form['revision_list']['delete_prior_revisions'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Also delete %revs_no revisions prior to this one.', ['%revs_no' => $revision_number]),
-    ];
-
-    $headers = [
-      t('Revision ID'),
-      [
-        'data' => t('Revision'),
-        // Hide the description on narrow width devices.
-        'class' => [RESPONSIVE_PRIORITY_MEDIUM],
-      ],
-    ];
-    // Cache service for later uses.
-    $renderer = \Drupal::service('renderer');
-    $dateFormatter = \Drupal::service('date.formatter');
-
-    // Only load and display the 20 most recent prior revisions to avoid
-    // unbounded entity loads on nodes with many revisions.
-    $display_vids = array_slice($vids_before, 0, 20);
-    $remaining = $revision_number - count($display_vids);
-
-    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
-    $rows = [];
-    /** @var \Drupal\node\NodeInterface[] $revisions */
-    $revisions = $node_storage->loadMultipleRevisions($display_vids);
-    krsort($revisions);
-    foreach ($revisions as $revision) {
-      if ($revision->hasTranslation($langcode)) {
-        $revision = $revision->getTranslation($langcode);
-      }
-      $username = [
-        '#theme' => 'username',
-        '#account' => $revision->getRevisionUser(),
-      ];
-
-      // Build link to view revision.
-      $date = $dateFormatter->format($revision->getRevisionCreationTime(), 'short');
-      $revision_url = new Url('entity.node.revision', [
-        'node' => $revision->id(),
-        'node_revision' => $revision->getRevisionId(),
-      ]);
-      $revision_link = Link::fromTextAndUrl($date, $revision_url)->toRenderable();
-
-      $revision_info = [
-        '#type' => 'inline_template',
-        '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
-        '#context' => [
-          'date' => $renderer->renderInIsolation($revision_link),
-          'username' => $renderer->renderInIsolation($username),
-          'message' => [
-            '#markup' => $revision->getRevisionLogMessage(),
-            '#allowed_tags' => Xss::getHtmlTagList(),
-          ],
-        ],
-      ];
-
-      $rows[] = [
-        $revision->getRevisionId(),
-        ['data' => $revision_info],
-      ];
-    }
-
-    if ($remaining > 0) {
-      $rows[] = [
-        [
-          'data' => \Drupal::translation()->formatPlural($remaining, 'And @count more revision that will also be deleted.', 'And @count more revisions that will also be deleted.'),
-          'colspan' => 2,
-        ],
-      ];
-    }
-
-    $form['revision_list']['table_markup'] = [
-      '#type' => 'table',
-      '#header' => $headers,
-      '#rows' => $rows,
-    ];
-
-    // Custom submit handler to handle bulk revisions delete.
-    $form['#submit'][] = '_node_revision_bulk_delete_submit';
-  }
+  \Drupal::service(NodeRevisionDeleteHooks::class)->formNodeRevisionDeleteConfirmAlter($form, $form_state, $form_id);
 }
 
 /**
diff --git a/node_revision_delete.services.yml b/node_revision_delete.services.yml
index 28850ff..4ba9440 100644
--- a/node_revision_delete.services.yml
+++ b/node_revision_delete.services.yml
@@ -25,3 +25,7 @@ services:
   Drupal\node_revision_delete\NodeRevisionDeleteInterface: '@node_revision_delete'
 
   Drupal\node_revision_delete\EventSubscriber\ConfigSubscriber: ~
+
+  Drupal\node_revision_delete\Hook\NodeRevisionDeleteHooks:
+    class: Drupal\node_revision_delete\Hook\NodeRevisionDeleteHooks
+    autowire: true
diff --git a/src/Hook/NodeRevisionDeleteHooks.php b/src/Hook/NodeRevisionDeleteHooks.php
new file mode 100644
index 0000000..b4fe174
--- /dev/null
+++ b/src/Hook/NodeRevisionDeleteHooks.php
@@ -0,0 +1,308 @@
+<?php
+
+namespace Drupal\node_revision_delete\Hook;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Link;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\node\NodeTypeInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for node_revision_delete.
+ */
+class NodeRevisionDeleteHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.node_revision_delete':
+        $output = '';
+        $output .= '<h3>' . $this->t('Introduction') . '</h3>';
+        $output .= '<p>' . $this->t('The Node Revision Delete module lets you track and prune old revisions of content types.') . '</p>';
+        $output .= '<h3>' . $this->t('Uses') . '</h3>';
+        $output .= '<dl>';
+        $output .= '<dt>' . $this->t('Configuring the module') . '</dt>';
+        $output .= '<dd>' . $this->t('You can manage the module settings via the <a href=":config-page">configuration page</a>. You can configure how many revisions you want to keep per content type and configure how long revision should be kept. When saving the configuration you can optionally start a batch job to queue all content to delete revisions that are allowed to be deleted. For this you need the <em>Administer Node Revision Delete</em> permission.', [
+          ':config-page' => Url::fromRoute('node_revision_delete.admin_settings')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . $this->t('Configuring content types') . '</dt>';
+        $output .= '<dd>' . $this->t('You can override the default settings of the module for each content type. Under the Publishing options tab of the content type, configure how many revisions you want to keep for the content type and how long revision should be kept.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter().
+   */
+  #[Hook('form_node_type_form_alter')]
+  public function formNodeTypeFormAlter(&$form, FormStateInterface $form_state, $form_id) {
+    /** @var \Drupal\node_revision_delete\Plugin\NodeRevisionDeletePluginManager $plugin_manager */
+    $plugin_manager = \Drupal::service('plugin.manager.node_revision_delete');
+    $plugin_definitions = $plugin_manager->getDefinitions();
+    // Getting the content type entity.
+    /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
+    $form_object = $form_state->getFormObject();
+    $content_type = $form_object->getEntity();
+    assert($content_type instanceof NodeTypeInterface);
+    // Detect if this build is an AJAX rebuild caused by toggling the override
+    // checkbox. When that happens we always show fresh defaults so the visible
+    // form matches what would actually be saved.
+    $triggering_element = $form_state->getTriggeringElement();
+    $is_override_ajax = $form_state->isRebuilding() && $triggering_element !== NULL && ($triggering_element['#name'] ?? '') === 'node_revision_delete[override]';
+    if ($is_override_ajax) {
+      // Drop any user input the admin made for the plugin sub-forms so the
+      // rebuilt form picks up the fresh #default_value below.
+      $input = $form_state->getUserInput();
+      foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
+        unset($input[$plugin_id]);
+        $form_state->unsetValue($plugin_id);
+      }
+      $form_state->setUserInput($input);
+    }
+    // Decide which settings to display. On the initial build the form mirrors
+    // what is saved on the node type. On an AJAX rebuild (or for new node
+    // types) we always show the defaults.
+    $has_saved_override = !$content_type->isNew() && !empty($content_type->getThirdPartySettings('node_revision_delete'));
+    if ($is_override_ajax || !$has_saved_override) {
+      $current_settings = [];
+      foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
+        $current_settings[$plugin_id] = $plugin_manager->getDefaultPluginSettings($plugin_id);
+      }
+    }
+    else {
+      $current_settings = $plugin_manager->getSettingsNodeType($content_type->id())['plugin'];
+    }
+    // The override state comes from the form value during a rebuild and from
+    // the saved third-party settings on the initial build.
+    $override = $is_override_ajax ? (bool) $form_state->getValue([
+      'node_revision_delete',
+      'override',
+    ]) : $has_saved_override;
+    $form['node_revision_delete'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Node Revision Delete settings'),
+      '#collapsed' => FALSE,
+      '#collapsible' => TRUE,
+      '#tree' => FALSE,
+      '#weight' => 10,
+      '#group' => 'additional_settings',
+      '#attributes' => [
+        'class' => [
+          'node-revision-delete-settings-form',
+        ],
+      ],
+    ];
+    // Wrap the plugin sub-forms in a container so the AJAX callback can replace
+    // the whole group when override is toggled.
+    $form['node_revision_delete']['plugins_wrapper'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'id' => 'node-revision-delete-plugins-wrapper',
+      ],
+    ];
+    $form['node_revision_delete']['plugins_wrapper']['override'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Override default settings'),
+      '#default_value' => $override,
+      '#attributes' => [
+        'class' => [
+          'node-revision-delete-override',
+        ],
+      ],
+      '#parents' => [
+        'node_revision_delete',
+        'override',
+      ],
+      '#ajax' => [
+        'callback' => 'node_revision_delete_override_ajax_callback',
+        'wrapper' => 'node-revision-delete-plugins-wrapper',
+      ],
+    ];
+    foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
+      /** @var \Drupal\node_revision_delete\Plugin\NodeRevisionDeleteInterface $plugin */
+      $plugin = $plugin_manager->getPlugin($plugin_id, $current_settings[$plugin_id]['settings'] ?? []);
+      $form['node_revision_delete']['plugins_wrapper'][$plugin_id] = [
+        '#type' => 'details',
+        '#title' => $plugin_definition['label'],
+        '#open' => TRUE,
+            // We need the #tree key for subforms to work.
+            // @see https://www.drupal.org/project/drupal/issues/3053890
+        '#tree' => TRUE,
+        '#attributes' => [
+          'class' => [
+            'node-revision-delete-plugin-settings',
+          ],
+        ],
+      ];
+      $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['status'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Enabled'),
+        '#default_value' => $current_settings[$plugin_id]['status'] ?? 0,
+      ];
+      $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] = [
+        '#type' => 'container',
+        '#states' => [
+          'visible' => [
+            ':input[name="' . $plugin_id . '[status]"]' => [
+              'checked' => TRUE,
+            ],
+          ],
+        ],
+      ];
+      $plugin_form_state = SubformState::createForSubform($form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'], $form, $form_state);
+      $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] = $plugin->buildConfigurationForm($form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] ?? [], $plugin_form_state);
+      // When override is off, disable the plugin sub-form server-side. This
+      // ensures Drupal substitutes the #default_value for any user input before
+      // validation, so required fields (whose values come from the global
+      // defaults) do not block saving the node type.
+      if (!$override) {
+        $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['status']['#disabled'] = TRUE;
+        $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings']['#disabled'] = TRUE;
+      }
+    }
+    $form['#attached']['library'][] = 'node_revision_delete/summaries';
+    $form['#entity_builders'][] = 'node_revision_delete_form_node_type_form_builder';
+  }
+
+  /**
+   * Implements hook_node_update().
+   */
+  #[Hook('node_update')]
+  public static function nodeUpdate(NodeInterface $node) {
+    $disable_automatic_queueing = \Drupal::config('node_revision_delete.settings')->get('disable_automatic_queueing');
+    $has_enabled_plugins = \Drupal::service('node_revision_delete')->contentTypeHasEnabledPlugins($node->getType());
+    if ($disable_automatic_queueing || !$has_enabled_plugins) {
+      return;
+    }
+    // Add updated nodes to the queue to determine if any revisions are ready to
+    // be deleted.
+    $nid = (int) $node->id();
+    if (\Drupal::service('node_revision_delete')->createQueueItem($nid)->queued()) {
+      if (\Drupal::config('node_revision_delete.settings')->get('verbose_log')) {
+        \Drupal::logger('node_revision_delete')->info('Node @id was added to the Node Revision Delete queue.', [
+          '@id' => $node->id(),
+        ]);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter().
+   */
+  #[Hook('form_node_revision_delete_confirm_alter')]
+  public function formNodeRevisionDeleteConfirmAlter(&$form, FormStateInterface &$form_state, $form_id) {
+    // Get the node id from URL.
+    $nid = \Drupal::routeMatch()->getRawParameter('node');
+    // Get the revision id from URL.
+    $revision_id = \Drupal::routeMatch()->getRawParameter('node_revision');
+    // Get list of revision IDs older than current revision for the content
+    // language. Use TYPE_CONTENT to ensure the correct language is used even when
+    // the "Account administration pages" language negotiation is active, which
+    // would cause getCurrentLanguage() (TYPE_INTERFACE) to return the admin
+    // language.
+    $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
+    /** @var \Drupal\node_revision_delete\NodeRevisionDeleteInterface $service */
+    $service = \Drupal::service('node_revision_delete');
+    $vids_before = $service->getPreviousRevisionIds($nid, $revision_id, $langcode);
+    // Count sum of old revisions.
+    $revision_number = count($vids_before);
+    // If there are old revisions.
+    if ($revision_number > 0) {
+      $form['revision_list'] = [
+        '#type' => 'details',
+        '#title' => $this->t('Delete prior revisions'),
+        '#open' => FALSE,
+      ];
+      $form['revision_list']['delete_prior_revisions'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Also delete %revs_no revisions prior to this one.', [
+          '%revs_no' => $revision_number,
+        ]),
+      ];
+      $headers = [
+        $this->t('Revision ID'),
+            [
+              'data' => $this->t('Revision'),
+                // Hide the description on narrow width devices.
+              'class' => [
+                RESPONSIVE_PRIORITY_MEDIUM,
+              ],
+            ],
+      ];
+      // Cache service for later uses.
+      $renderer = \Drupal::service('renderer');
+      $dateFormatter = \Drupal::service('date.formatter');
+      // Only load and display the 20 most recent prior revisions to avoid
+      // unbounded entity loads on nodes with many revisions.
+      $display_vids = array_slice($vids_before, 0, 20);
+      $remaining = $revision_number - count($display_vids);
+      $node_storage = \Drupal::entityTypeManager()->getStorage('node');
+      $rows = [];
+      /** @var \Drupal\node\NodeInterface[] $revisions */
+      $revisions = $node_storage->loadMultipleRevisions($display_vids);
+      krsort($revisions);
+      foreach ($revisions as $revision) {
+        if ($revision->hasTranslation($langcode)) {
+          $revision = $revision->getTranslation($langcode);
+        }
+        $username = [
+          '#theme' => 'username',
+          '#account' => $revision->getRevisionUser(),
+        ];
+        // Build link to view revision.
+        $date = $dateFormatter->format($revision->getRevisionCreationTime(), 'short');
+        $revision_url = new Url('entity.node.revision', [
+          'node' => $revision->id(),
+          'node_revision' => $revision->getRevisionId(),
+        ]);
+        $revision_link = Link::fromTextAndUrl($date, $revision_url)->toRenderable();
+        $revision_info = [
+          '#type' => 'inline_template',
+          '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
+          '#context' => [
+            'date' => $renderer->renderInIsolation($revision_link),
+            'username' => $renderer->renderInIsolation($username),
+            'message' => [
+              '#markup' => $revision->getRevisionLogMessage(),
+              '#allowed_tags' => Xss::getHtmlTagList(),
+            ],
+          ],
+        ];
+        $rows[] = [
+          $revision->getRevisionId(),
+              [
+                'data' => $revision_info,
+              ],
+        ];
+      }
+      if ($remaining > 0) {
+        $rows[] = [
+              [
+                'data' => \Drupal::translation()->formatPlural($remaining, 'And @count more revision that will also be deleted.', 'And @count more revisions that will also be deleted.'),
+                'colspan' => 2,
+              ],
+        ];
+      }
+      $form['revision_list']['table_markup'] = [
+        '#type' => 'table',
+        '#header' => $headers,
+        '#rows' => $rows,
+      ];
+      // Custom submit handler to handle bulk revisions delete.
+      $form['#submit'][] = '_node_revision_bulk_delete_submit';
+    }
+  }
+
+}
diff --git a/tests/modules/node_revision_delete_legacy_test/node_revision_delete_legacy_test.info.yml b/tests/modules/node_revision_delete_legacy_test/node_revision_delete_legacy_test.info.yml
index 474a241..35f01a1 100644
--- a/tests/modules/node_revision_delete_legacy_test/node_revision_delete_legacy_test.info.yml
+++ b/tests/modules/node_revision_delete_legacy_test/node_revision_delete_legacy_test.info.yml
@@ -2,7 +2,7 @@ name: Node Revision Delete Legacy Test
 description: Provides a legacy node revision delete plugin for testing backward compatibility.
 package: Testing
 type: module
-core_version_requirement: ^10.3 || ^11
+core_version_requirement: ^10.3 || ^11 || ^12
 
 dependencies:
   - node_revision_delete:node_revision_delete
diff --git a/tests/src/Kernel/NodeRevisionDeleteLegacyTest.php b/tests/src/Kernel/NodeRevisionDeleteLegacyTest.php
index f6ee3ee..e7c2fdb 100644
--- a/tests/src/Kernel/NodeRevisionDeleteLegacyTest.php
+++ b/tests/src/Kernel/NodeRevisionDeleteLegacyTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\node_revision_delete\Kernel;
 
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\node\NodeInterface;
@@ -359,12 +361,11 @@ class NodeRevisionDeleteLegacyTest extends KernelTestBase {
 
   /**
    * Test getPreviousRevisions().
-   *
-   * @group legacy
    */
+  #[IgnoreDeprecations]
   public function testGetPreviousRevisions(): void {
     if (floatval(\Drupal::VERSION) < 11) {
-      $this->expectDeprecation('Drupal\node_revision_delete\NodeRevisionDelete::getPreviousRevisions() is deprecated in node_revision_delete:2.1.0 and is removed from node_revision_delete:3.0.0. Use getPreviousRevisionIds() instead. See https://www.drupal.org/node/3584874');
+      DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage('Drupal\node_revision_delete\NodeRevisionDelete::getPreviousRevisions() is deprecated in node_revision_delete:2.1.0 and is removed from node_revision_delete:3.0.0. Use getPreviousRevisionIds() instead. See https://www.drupal.org/node/3584874'), fn() => $this->expectDeprecation('Drupal\node_revision_delete\NodeRevisionDelete::getPreviousRevisions() is deprecated in node_revision_delete:2.1.0 and is removed from node_revision_delete:3.0.0. Use getPreviousRevisionIds() instead. See https://www.drupal.org/node/3584874'));
     }
     else {
       $this->expectUserDeprecationMessage('Drupal\node_revision_delete\NodeRevisionDelete::getPreviousRevisions() is deprecated in node_revision_delete:2.1.0 and is removed from node_revision_delete:3.0.0. Use getPreviousRevisionIds() instead. See https://www.drupal.org/node/3584874');
