diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
index 62fb13e..6f0ac8f 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -135,6 +135,13 @@ public function loadRevision($revision_id) {
   /**
    * {@inheritdoc}
    */
+  public function loadMultipleRevisions(array $revision_ids) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function deleteRevision($revision_id) {
     return NULL;
   }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
index 3dc00c9..94efc11 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
@@ -41,6 +41,13 @@ public function loadRevision($revision_id) {
   /**
    * {@inheritdoc}
    */
+  public function loadMultipleRevisions(array $revision_ids) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function deleteRevision($revision_id) {
   }
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index e92930a..efd13d6 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -244,27 +244,38 @@ public function finalizePurge(FieldStorageDefinitionInterface $storage_definitio
    * {@inheritdoc}
    */
   public function loadRevision($revision_id) {
-    $revision = $this->doLoadRevisionFieldItems($revision_id);
+    $revisions = $this->loadMultipleRevisions([$revision_id]);
 
-    if ($revision) {
+    return isset($revisions[$revision_id]) ? $revisions[$revision_id] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultipleRevisions(array $revision_ids) {
+    $revisions = $this->doLoadRevisionFieldItems($revision_ids);
+
+    // The hooks are invoked keyed by entity revision ID so we have to do one by
+    // one.
+    foreach ($revisions as $revision) {
       $entities = [$revision->id() => $revision];
       $this->invokeStorageLoadHook($entities);
       $this->postLoad($entities);
     }
 
-    return $revision;
+    return $revisions;
   }
 
   /**
    * Actually loads revision field item values from the storage.
    *
-   * @param int|string $revision_id
-   *   The revision identifier.
+   * @param array $revision_ids
+   *   An array of revision identifiers.
    *
-   * @return \Drupal\Core\Entity\EntityInterface|null
-   *   The specified entity revision or NULL if not found.
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   The specified entity revisions or an empty array if none are found.
    */
-  abstract protected function doLoadRevisionFieldItems($revision_id);
+  abstract protected function doLoadRevisionFieldItems($revision_ids);
 
   /**
    * {@inheritdoc}
@@ -597,22 +608,24 @@ protected function populateAffectedRevisionTranslations(ContentEntityInterface $
   }
 
   /**
-   * Ensures integer entity IDs are valid.
+   * Ensures integer entity key values are valid.
    *
    * The identifier sanitization provided by this method has been introduced
    * as Drupal used to rely on the database to facilitate this, which worked
    * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
    *
    * @param array $ids
-   *   The entity IDs to verify.
+   *   The entity key values to verify.
+   * @param string $key
+   *   (optional) The entity key to sanitise values for. Defaults to 'id'.
    *
    * @return array
-   *   The sanitized list of entity IDs.
+   *   The sanitized list of entity key values.
    */
-  protected function cleanIds(array $ids) {
+  protected function cleanIds(array $ids, $key = 'id') {
     $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
-    $id_definition = $definitions[$this->entityType->getKey('id')];
-    if ($id_definition->getType() == 'integer') {
+    $field_name = $this->entityType->getKey($key);
+    if ($field_name && $definitions[$field_name]->getType() == 'integer') {
       $ids = array_filter($ids, function ($id) {
         return is_numeric($id) && $id == (int) $id;
       });
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
index 9f50674..77df2a2 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
@@ -83,6 +83,17 @@ public function loadUnchanged($id);
   public function loadRevision($revision_id);
 
   /**
+   * Loads multiple revisions.
+   *
+   * @param array $revision_ids
+   *   An array of revision IDs to load.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   The specified entity revisions or empty array if none found.
+   */
+  public function loadMultipleRevisions(array $revision_ids);
+
+  /**
    * Delete a specific entity revision.
    *
    * A revision can only be deleted if it's not the currently active one.
diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
index cd2f26e..fd542d9 100644
--- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
@@ -133,6 +133,13 @@ public function loadRevision($revision_id) {
   /**
    * {@inheritdoc}
    */
+  public function loadMultipleRevisions(array $revision_ids) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function deleteRevision($revision_id) {
     return NULL;
   }
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 22238d4..08d9688 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -469,9 +469,11 @@ protected function getFromStorage(array $ids = NULL) {
    * Maps from storage records to entity objects, and attaches fields.
    *
    * @param array $records
-   *   Associative array of query results, keyed on the entity ID.
+   *   Associative array of query results, keyed on the entity ID or revision
+   *   ID.
    * @param bool $load_from_revision
-   *   Flag to indicate whether revisions should be loaded or not.
+   *   (optional) Flag to indicate whether revisions should be loaded or not.
+   *   Defaults to FALSE.
    *
    * @return array
    *   An array of entity objects implementing the EntityInterface.
@@ -506,7 +508,7 @@ protected function mapFromStorageRecords(array $records, $load_from_revision = F
     $translations = array_fill_keys(array_keys($values), []);
 
     // Load values from shared and dedicated tables.
-    $this->loadFromSharedTables($values, $translations);
+    $this->loadFromSharedTables($values, $translations, $load_from_revision);
     $this->loadFromDedicatedTables($values, $load_from_revision);
 
     $entities = [];
@@ -523,11 +525,15 @@ protected function mapFromStorageRecords(array $records, $load_from_revision = F
    * Loads values for fields stored in the shared data tables.
    *
    * @param array &$values
-   *   Associative array of entities values, keyed on the entity ID.
+   *   Associative array of entities values, keyed on the entity ID or the
+   *   revision ID.
    * @param array &$translations
    *   List of translations, keyed on the entity ID.
+   * @param bool $load_from_revision
+   *   Flag to indicate whether revisions should be loaded or not.
    */
-  protected function loadFromSharedTables(array &$values, array &$translations) {
+  protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
+    $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
     if ($this->dataTable) {
       // If a revision table is available, we need all the properties of the
       // latest revision. Otherwise we fall back to the data table.
@@ -535,8 +541,8 @@ protected function loadFromSharedTables(array &$values, array &$translations) {
       $alias = $this->revisionDataTable ? 'revision' : 'data';
       $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
         ->fields($alias)
-        ->condition($alias . '.' . $this->idKey, array_keys($values), 'IN')
-        ->orderBy($alias . '.' . $this->idKey);
+        ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
+        ->orderBy($alias . '.' . $record_key);
 
       $table_mapping = $this->getTableMapping();
       if ($this->revisionDataTable) {
@@ -581,7 +587,7 @@ protected function loadFromSharedTables(array &$values, array &$translations) {
 
       $result = $query->execute();
       foreach ($result as $row) {
-        $id = $row[$this->idKey];
+        $id = $row[$record_key];
 
         // Field values in default language are stored with
         // LanguageInterface::LANGCODE_DEFAULT as key.
@@ -608,20 +614,29 @@ protected function loadFromSharedTables(array &$values, array &$translations) {
   /**
    * {@inheritdoc}
    */
-  protected function doLoadRevisionFieldItems($revision_id) {
-    $revision = NULL;
+  protected function doLoadRevisionFieldItems($revision_ids) {
+    // Support the case when a single revision ID is passed in.
+    $load_single = !is_array($revision_ids);
+    $revision_ids = (array) $revision_ids;
 
-    // Build and execute the query.
-    $query_result = $this->buildQuery([], $revision_id)->execute();
-    $records = $query_result->fetchAllAssoc($this->idKey);
+    $revisions = [];
+
+    // Sanitize IDs. Before feeding ID array into buildQuery, check whether
+    // it is empty as this would load all entity revisions.
+    $revision_ids = $this->cleanIds($revision_ids, 'revision');
+
+    if (!empty($revision_ids)) {
+      // Build and execute the query.
+      $query_result = $this->buildQuery(NULL, $revision_ids)->execute();
+      $records = $query_result->fetchAllAssoc($this->revisionKey);
 
-    if (!empty($records)) {
-      // Convert the raw records to entity objects.
-      $entities = $this->mapFromStorageRecords($records, TRUE);
-      $revision = reset($entities) ?: NULL;
+      // Map the loaded records into entity objects and according fields.
+      if ($records) {
+        $revisions = $this->mapFromStorageRecords($records, TRUE);
+      }
     }
 
-    return $revision;
+    return $load_single ? (isset($revisions[$revision_ids]) ? $revisions[$revision_ids] : NULL) : $revisions;
   }
 
   /**
@@ -677,20 +692,20 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
    *
    * @param array|null $ids
    *   An array of entity IDs, or NULL to load all entities.
-   * @param $revision_id
-   *   The ID of the revision to load, or FALSE if this query is asking for the
+   * @param array|bool $revision_ids
+   *   The IDs of the revision to load, or FALSE if this query is asking for the
    *   most current revision(s).
    *
    * @return \Drupal\Core\Database\Query\Select
    *   A SelectQuery object for loading the entity.
    */
-  protected function buildQuery($ids, $revision_id = FALSE) {
+  protected function buildQuery($ids, $revision_ids = FALSE) {
     $query = $this->database->select($this->entityType->getBaseTable(), 'base');
 
     $query->addTag($this->entityTypeId . '_load_multiple');
 
-    if ($revision_id) {
-      $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", [':revisionId' => $revision_id]);
+    if ($revision_ids) {
+      $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey}");
     }
     elseif ($this->revisionTable) {
       $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
@@ -727,6 +742,9 @@ protected function buildQuery($ids, $revision_id = FALSE) {
     if ($ids) {
       $query->condition("base.{$this->idKey}", $ids, 'IN');
     }
+    if ($revision_ids) {
+      $query->condition("revision.{$this->revisionKey}", $revision_ids, 'IN');
+    }
 
     return $query;
   }
@@ -1114,8 +1132,7 @@ protected function getQueryServiceName() {
    * @param array &$values
    *   An array of values keyed by entity ID.
    * @param bool $load_from_revision
-   *   (optional) Flag to indicate whether revisions should be loaded or not,
-   *   defaults to FALSE.
+   *   Flag to indicate whether revisions should be loaded or not.
    */
   protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
     if (empty($values)) {
@@ -1167,21 +1184,22 @@ protected function loadFromDedicatedTables(array &$values, $load_from_revision)
       foreach ($results as $row) {
         $bundle = $row->bundle;
 
+        $value_key = !$load_from_revision ? $row->entity_id : $row->revision_id;
         // Field values in default language are stored with
         // LanguageInterface::LANGCODE_DEFAULT as key.
         $langcode = LanguageInterface::LANGCODE_DEFAULT;
-        if ($this->langcodeKey && isset($default_langcodes[$row->entity_id]) && $row->langcode != $default_langcodes[$row->entity_id]) {
+        if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row->langcode != $default_langcodes[$value_key]) {
           $langcode = $row->langcode;
         }
 
-        if (!isset($values[$row->entity_id][$field_name][$langcode])) {
-          $values[$row->entity_id][$field_name][$langcode] = [];
+        if (!isset($values[$value_key][$field_name][$langcode])) {
+          $values[$value_key][$field_name][$langcode] = [];
         }
 
         // Ensure that records for non-translatable fields having invalid
         // languages are skipped.
         if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
-          if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$row->entity_id][$field_name][$langcode]) < $storage_definition->getCardinality()) {
+          if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$value_key][$field_name][$langcode]) < $storage_definition->getCardinality()) {
             $item = [];
             // For each column declared by the field, populate the item from the
             // prefixed database column.
@@ -1192,7 +1210,7 @@ protected function loadFromDedicatedTables(array &$values, $load_from_revision)
             }
 
             // Add the item to the field values for the entity.
-            $values[$row->entity_id][$field_name][$langcode][] = $item;
+            $values[$value_key][$field_name][$langcode][] = $item;
           }
         }
       }
diff --git a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
index caf5fa2..8b544c4 100644
--- a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
+++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
@@ -3,6 +3,8 @@
 namespace Drupal\Tests\system\Functional\Entity;
 
 use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
 
@@ -60,20 +62,42 @@ public function testRevisions() {
    *   The entity type to run the tests with.
    */
   protected function runRevisionsTests($entity_type) {
+    // Create a translatable test field.
+    $field_storage = FieldStorageConfig::create([
+      'entity_type' => $entity_type,
+      'field_name' => 'translatable_test_field',
+      'type' => 'text',
+      'cardinality' => 2,
+    ]);
+    $field_storage->save();
+
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'label' => $this->randomMachineName(),
+      'bundle' => $entity_type,
+      'translatable' => TRUE,
+    ]);
+    $field->save();
+
+    entity_get_form_display($entity_type, $entity_type, 'default')
+      ->setComponent('translatable_test_field')
+      ->save();
+
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = \Drupal::entityTypeManager()->getStorage($entity_type);
 
     // Create initial entity.
-    $entity = $this->container->get('entity_type.manager')
-      ->getStorage($entity_type)
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $storage
       ->create([
         'name' => 'foo',
         'user_id' => $this->webUser->id(),
       ]);
-    $entity->field_test_text->value = 'bar';
+    $entity->translatable_test_field->value = 'bar';
+    $entity->addTranslation('de', ['name' => 'foo - de']);
     $entity->save();
 
-    $names = [];
-    $texts = [];
-    $created = [];
+    $values = [];
     $revision_ids = [];
 
     // Create three revisions.
@@ -81,45 +105,74 @@ protected function runRevisionsTests($entity_type) {
     for ($i = 0; $i < $revision_count; $i++) {
       $legacy_revision_id = $entity->revision_id->value;
       $legacy_name = $entity->name->value;
-      $legacy_text = $entity->field_test_text->value;
+      $legacy_text = $entity->translatable_test_field->value;
 
-      $entity = $this->container->get('entity_type.manager')
-        ->getStorage($entity_type)->load($entity->id->value);
+      $entity = $storage->load($entity->id->value);
       $entity->setNewRevision(TRUE);
-      $names[] = $entity->name->value = $this->randomMachineName(32);
-      $texts[] = $entity->field_test_text->value = $this->randomMachineName(32);
-      $created[] = $entity->created->value = time() + $i + 1;
+      $values['en'][$i] = [
+        'name' => $this->randomMachineName(32),
+        'translatable_test_field' => [
+          $this->randomMachineName(32),
+          $this->randomMachineName(32),
+        ],
+        'created' => time() + $i + 1,
+      ];
+      $entity->set('name', $values['en'][$i]['name']);
+      $entity->set('translatable_test_field', $values['en'][$i]['translatable_test_field']);
+      $entity->set('created', $values['en'][$i]['created']);
       $entity->save();
       $revision_ids[] = $entity->revision_id->value;
 
+      // Add some values for a translation of this revision.
+      if ($entity->getEntityType()->isTranslatable()) {
+        $values['de'][$i] = [
+          'name' => $this->randomMachineName(32),
+          'translatable_test_field' => [
+            $this->randomMachineName(32),
+            $this->randomMachineName(32),
+          ],
+        ];
+        $translation = $entity->getTranslation('de');
+        $translation->set('name', $values['de'][$i]['name']);
+        $translation->set('translatable_test_field', $values['de'][$i]['translatable_test_field']);
+        $translation->save();
+      }
+
       // Check that the fields and properties contain new content.
       $this->assertTrue($entity->revision_id->value > $legacy_revision_id, format_string('%entity_type: Revision ID changed.', ['%entity_type' => $entity_type]));
       $this->assertNotEqual($entity->name->value, $legacy_name, format_string('%entity_type: Name changed.', ['%entity_type' => $entity_type]));
-      $this->assertNotEqual($entity->field_test_text->value, $legacy_text, format_string('%entity_type: Text changed.', ['%entity_type' => $entity_type]));
+      $this->assertNotEqual($entity->translatable_test_field->value, $legacy_text, format_string('%entity_type: Text changed.', ['%entity_type' => $entity_type]));
     }
 
-    $storage = $this->container->get('entity_type.manager')->getStorage($entity_type);
+    $revisions = $storage->loadMultipleRevisions($revision_ids);
     for ($i = 0; $i < $revision_count; $i++) {
       // Load specific revision.
-      $entity_revision = $storage->loadRevision($revision_ids[$i]);
+      $entity_revision = $revisions[$revision_ids[$i]];
 
       // Check if properties and fields contain the revision specific content.
       $this->assertEqual($entity_revision->revision_id->value, $revision_ids[$i], format_string('%entity_type: Revision ID matches.', ['%entity_type' => $entity_type]));
-      $this->assertEqual($entity_revision->name->value, $names[$i], format_string('%entity_type: Name matches.', ['%entity_type' => $entity_type]));
-      $this->assertEqual($entity_revision->field_test_text->value, $texts[$i], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
+      $this->assertEqual($entity_revision->name->value, $values['en'][$i]['name'], format_string('%entity_type: Name matches.', ['%entity_type' => $entity_type]));
+      $this->assertEqual($entity_revision->translatable_test_field[0]->value, $values['en'][$i]['translatable_test_field'][0], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
+      $this->assertEqual($entity_revision->translatable_test_field[1]->value, $values['en'][$i]['translatable_test_field'][1], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
+
+      // Check the translated values.
+      if ($entity->getEntityType()->isTranslatable()) {
+        $revision_translation = $entity_revision->getTranslation('de');
+        $this->assertEqual($revision_translation->name->value, $values['de'][$i]['name'], format_string('%entity_type: Name matches.', ['%entity_type' => $entity_type]));
+        $this->assertEqual($revision_translation->translatable_test_field[0]->value, $values['de'][$i]['translatable_test_field'][0], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
+        $this->assertEqual($revision_translation->translatable_test_field[1]->value, $values['de'][$i]['translatable_test_field'][1], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
+      }
 
       // Check non-revisioned values are loaded.
       $this->assertTrue(isset($entity_revision->created->value), format_string('%entity_type: Non-revisioned field is loaded.', ['%entity_type' => $entity_type]));
-      $this->assertEqual($entity_revision->created->value, $created[2], format_string('%entity_type: Non-revisioned field value is the same between revisions.', ['%entity_type' => $entity_type]));
+      $this->assertEqual($entity_revision->created->value, $values['en'][2]['created'], format_string('%entity_type: Non-revisioned field value is the same between revisions.', ['%entity_type' => $entity_type]));
     }
 
     // Confirm the correct revision text appears in the edit form.
-    $entity = $this->container->get('entity_type.manager')
-      ->getStorage($entity_type)
-      ->load($entity->id->value);
+    $entity = $storage->load($entity->id->value);
     $this->drupalGet($entity_type . '/manage/' . $entity->id->value . '/edit');
     $this->assertFieldById('edit-name-0-value', $entity->name->value, format_string('%entity_type: Name matches in UI.', ['%entity_type' => $entity_type]));
-    $this->assertFieldById('edit-field-test-text-0-value', $entity->field_test_text->value, format_string('%entity_type: Text matches in UI.', ['%entity_type' => $entity_type]));
+    $this->assertFieldById('edit-translatable-test-field-0-value', $entity->translatable_test_field->value, format_string('%entity_type: Text matches in UI.', ['%entity_type' => $entity_type]));
   }
 
   /**
