diff --git a/core/modules/system/src/Plugin/views/field/BulkForm.php b/core/modules/system/src/Plugin/views/field/BulkForm.php
index f9f581b..f89930a 100644
--- a/core/modules/system/src/Plugin/views/field/BulkForm.php
+++ b/core/modules/system/src/Plugin/views/field/BulkForm.php
@@ -24,18 +24,7 @@
  *
  * @ViewsField("bulk_form")
  */
-class BulkForm extends FieldPluginBase implements CacheableDependencyInterface {
-
-  use RedirectDestinationTrait;
-  use UncacheableFieldHandlerTrait;
-  use EntityTranslationRenderTrait;
-
-  /**
-   * The entity manager.
-   *
-   * @var \Drupal\Core\Entity\EntityManagerInterface
-   */
-  protected $entityManager;
+class BulkForm extends SelectFormBase {
 
   /**
    * The action storage.
@@ -52,13 +41,6 @@ class BulkForm extends FieldPluginBase implements CacheableDependencyInterface {
   protected $actions = array();
 
   /**
-   * The language manager.
-   *
-   * @var \Drupal\Core\Language\LanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
    * Constructs a new BulkForm object.
    *
    * @param array $configuration
@@ -73,11 +55,8 @@ class BulkForm extends FieldPluginBase implements CacheableDependencyInterface {
    *   The language manager.
    */
   public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-
-    $this->entityManager = $entity_manager;
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_manager, $language_manager);
     $this->actionStorage = $entity_manager->getStorage('action');
-    $this->languageManager = $language_manager;
   }
 
   /**
diff --git a/core/modules/system/src/Plugin/views/field/SelectFormBase.php b/core/modules/system/src/Plugin/views/field/SelectFormBase.php
new file mode 100644
index 0000000..ea909c4
--- /dev/null
+++ b/core/modules/system/src/Plugin/views/field/SelectFormBase.php
@@ -0,0 +1,309 @@
+<?php
+
+namespace Drupal\system\Plugin\views\field;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Routing\RedirectDestinationTrait;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\views\Entity\Render\EntityTranslationRenderTrait;
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\Plugin\views\field\FieldPluginBase;
+use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait;
+use Drupal\views\Plugin\views\style\Table;
+use Drupal\views\ResultRow;
+use Drupal\views\ViewExecutable;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a base class select forms.
+ */
+abstract class SelectFormBase extends FieldPluginBase implements CacheableDependencyInterface {
+
+  use RedirectDestinationTrait;
+  use UncacheableFieldHandlerTrait;
+  use EntityTranslationRenderTrait;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new BulkForm object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->entityManager = $entity_manager;
+    $this->languageManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity.manager'),
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    // @todo Consider making the bulk operation form cacheable. See
+    //   https://www.drupal.org/node/2503009.
+    return 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->languageManager->isMultilingual() ? $this->getEntityTranslationRenderer()->getCacheContexts() : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId() {
+    return $this->getEntityType();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityManager() {
+    return $this->entityManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getLanguageManager() {
+    return $this->languageManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getView() {
+    return $this->view;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preRender(&$values) {
+    parent::preRender($values);
+
+    // If the view is using a table style, provide a placeholder for a
+    // "select all" checkbox.
+    if (!empty($this->view->style_plugin) && $this->view->style_plugin instanceof Table) {
+      // Add the tableselect css classes.
+      $this->options['element_label_class'] .= 'select-all';
+      // Hide the actual label of the field on the table header.
+      $this->options['label'] = '';
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue(ResultRow $row, $field = NULL) {
+    return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
+  }
+
+  /**
+   * Form constructor for the bulk form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function viewsForm(&$form, FormStateInterface $form_state) {
+    // Make sure we do not accidentally cache this form.
+    // @todo Evaluate this again in https://www.drupal.org/node/2503009.
+    $form['#cache']['max-age'] = 0;
+
+    // Add the tableselect javascript.
+    $form['#attached']['library'][] = 'core/drupal.tableselect';
+    $use_revision = array_key_exists('revision', $this->view->getQuery()->getEntityTableInfo());
+
+    // Only add the bulk form options and buttons if there are results.
+    if (!empty($this->view->result)) {
+      // Render checkboxes for all rows.
+      $form[$this->options['id']]['#tree'] = TRUE;
+      foreach ($this->view->result as $row_index => $row) {
+        $entity = $this->getEntityTranslation($this->getEntity($row), $row);
+
+        $form[$this->options['id']][$row_index] = array(
+          '#type' => 'checkbox',
+          // We are not able to determine a main "title" for each row, so we can
+          // only output a generic label.
+          '#title' => $this->t('Select this item'),
+          '#title_display' => 'invisible',
+          '#default_value' => !empty($form_state->getValue($this->options['id'])[$row_index]) ? 1 : NULL,
+          '#return_value' => $this->calculateEntityBulkFormKey($entity, $use_revision),
+        );
+      }
+
+      // Replace the form submit button label.
+      $form['actions']['submit']['#value'] = $this->t('Selected items');
+    }
+    else {
+      // Remove the default actions build array.
+      unset($form['actions']);
+    }
+  }
+
+  /**
+   * Returns the message to be displayed when there are no selected items.
+   *
+   * @return string
+   *   Message displayed when no items are selected.
+   */
+  protected function emptySelectedMessage() {
+    return $this->t('No items selected.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewsFormValidate(&$form, FormStateInterface $form_state) {
+    $selected = array_filter($form_state->getValue($this->options['id']));
+    if (empty($selected)) {
+      $form_state->setErrorByName('', $this->emptySelectedMessage());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    if ($this->languageManager->isMultilingual()) {
+      $this->getEntityTranslationRenderer()->query($this->query, $this->relationship);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clickSortable() {
+    return FALSE;
+  }
+
+  /**
+   * Wraps drupal_set_message().
+   */
+  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
+    drupal_set_message($message, $type, $repeat);
+  }
+
+  /**
+   * Calculates a bulk form key.
+   *
+   * This generates a key that is used as the checkbox return value when
+   * submitting a bulk form. This key allows the entity for the row to be loaded
+   * totally independently of the executed view row.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to calculate a bulk form key for.
+   * @param bool $use_revision
+   *   Whether the revision id should be added to the bulk form key. This should
+   *   be set to TRUE only if the view is listing entity revisions.
+   *
+   * @return string
+   *   The bulk form key representing the entity's id, language and revision (if
+   *   applicable) as one string.
+   *
+   * @see self::loadEntityFromBulkFormKey()
+   */
+  protected function calculateEntityBulkFormKey(EntityInterface $entity, $use_revision) {
+    $key_parts = [$entity->language()->getId(), $entity->id()];
+
+    if ($entity instanceof RevisionableInterface && $use_revision) {
+      $key_parts[] = $entity->getRevisionId();
+    }
+
+    // An entity ID could be an arbitrary string (although they are typically
+    // numeric). JSON then Base64 encoding ensures the bulk_form_key is
+    // safe to use in HTML, and that the key parts can be retrieved.
+    $key = json_encode($key_parts);
+    return base64_encode($key);
+  }
+
+  /**
+   * Loads an entity based on a bulk form key.
+   *
+   * @param string $bulk_form_key
+   *   The bulk form key representing the entity's id, language and revision (if
+   *   applicable) as one string.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The entity loaded in the state (language, optionally revision) specified
+   *   as part of the bulk form key.
+   */
+  protected function loadEntityFromBulkFormKey($bulk_form_key) {
+    $key = base64_decode($bulk_form_key);
+    $key_parts = json_decode($key);
+    $revision_id = NULL;
+
+    // If there are 3 items, vid will be last.
+    if (count($key_parts) === 3) {
+      $revision_id = array_pop($key_parts);
+    }
+
+    // The first two items will always be langcode and ID.
+    $id = array_pop($key_parts);
+    $langcode = array_pop($key_parts);
+
+    // Load the entity or a specific revision depending on the given key.
+    $storage = $this->entityManager->getStorage($this->getEntityType());
+    $entity = $revision_id ? $storage->loadRevision($revision_id) : $storage->load($id);
+
+    if ($entity instanceof TranslatableInterface) {
+      $entity = $entity->getTranslation($langcode);
+    }
+
+    return $entity;
+  }
+
+}
