diff --git a/core/core.services.yml b/core/core.services.yml
index 1c02e0b85d..6b75c66b6f 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -891,6 +891,9 @@ services:
   entity.query.keyvalue:
     class: Drupal\Core\Entity\KeyValueStore\Query\QueryFactory
     arguments: ['@keyvalue']
+  entity.common_queries:
+    class: Drupal\Core\Entity\Query\Queries
+    arguments: ['@entity_type.manager']
   router.dumper:
     class: Drupal\Core\Routing\MatcherDumper
     arguments: ['@database', '@state']
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 2c62589fa2..5be5ac9c2e 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -351,6 +351,74 @@ public function getRevisionId() {
     return $this->getEntityKey('revision');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function createNewRevision($default_revision = TRUE, $copy_untranslatable_fields = NULL) {
+    $new_revision = NULL;
+    $storage = $this->entityTypeManager()->getStorage($this->entityTypeId);
+    $translatable = $this->isTranslatable();
+
+    if ($translatable) {
+      $active_langcode = $this->language()->getId();
+
+      // Start from the default revision and merge the values of the latest
+      // translation affected revision for the active language. This allows to
+      // create pending revisions that can always be saved as the new default
+      // revision without reverting changes in other languages.
+      /** @var \Drupal\Core\Entity\ContentEntityBase $new_revision */
+      $new_revision = clone $storage->load($this->id());
+      $new_revision = $new_revision->hasTranslation($active_langcode) ?
+        $new_revision->getTranslation($active_langcode) : $new_revision->addTranslation($active_langcode);
+
+      // We skip untranslatable fields unless they are configured to affect only
+      // the default translation, so that we can ensure we always have only one
+      // affected translation in pending revisions. This constraint is enforced
+      // by EntityUntranslatableFieldsConstraintValidator.
+      if (!isset($copy_untranslatable_fields)) {
+        $copy_untranslatable_fields = $this->isDefaultTranslation() && $this->isDefaultTranslationAffectedOnly() ;
+      }
+      foreach ($new_revision->getFieldDefinitions() as $field_name => $definition) {
+        if ($copy_untranslatable_fields || $definition->isTranslatable()) {
+          $new_revision->set($field_name, $this->get($field_name)->getValue());
+        }
+      }
+
+      // Preserve a reference to the current revision, as the new revision would
+      // appear as being originated from the default revision otherwise.
+      $new_revision->set($this->getEntityType()->getKey('revision'), $this->getLoadedRevisionId());
+      $new_revision->updateLoadedRevisionId();
+
+      // Populate the "original" property with the current values, given that
+      // the new revision is not stored anywhere.
+      $new_revision->original = clone $new_revision;
+    }
+    else {
+      $new_revision = clone $this;
+    }
+
+    // Eventually mark the new revision as such.
+    $new_revision->setNewRevision();
+    $new_revision->isDefaultRevision($default_revision);
+
+    // Actually make sure the current translation is marked as affected, even if
+    // there are no explicit changes, to be sure this revision can be attributed
+    // to the correct translation.
+    if ($translatable) {
+      $new_revision->setRevisionTranslationAffected(TRUE);
+    }
+
+    return $new_revision;
+  }
+
+  /**
+   * TODO
+   */
+  public function isDefaultRevisionType() {
+    $revision_type = $this->get('revision_type')->value;
+    return isset($revision_type) && intval($revision_type) === 1;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -713,6 +781,12 @@ public function onChange($name) {
         elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
           unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
         }
+        // If the revision identifier field is being populated with the original
+        // value, we need to make sure the "new revision" flag is reset
+        // accordingly.
+        if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId()) {
+          $this->newRevision = FALSE;
+        }
       }
     }
 
@@ -1337,10 +1411,15 @@ public function hasTranslationChanges() {
     // The list of fields to skip from the comparision.
     $skip_fields = $this->getFieldsToSkipFromTranslationChangesCheck();
 
+    // We check also untranslatable fields, so that a change to those will mark
+    // all translations as affected, unless they are configured to only affect
+    // the default translation.
+    $skip_untranslatable_fields = !$this->isDefaultTranslation() && $this->isDefaultTranslationAffectedOnly();
+
     foreach ($this->getFieldDefinitions() as $field_name => $definition) {
       // @todo Avoid special-casing the following fields. See
       //    https://www.drupal.org/node/2329253.
-      if (in_array($field_name, $skip_fields, TRUE)) {
+      if (in_array($field_name, $skip_fields, TRUE) || ($skip_untranslatable_fields && !$definition->isTranslatable())) {
         continue;
       }
       $field = $this->get($field_name);
@@ -1361,4 +1440,14 @@ public function hasTranslationChanges() {
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isDefaultTranslationAffectedOnly() {
+    $bundle_name = $this->bundle();
+    $bundle_info = \Drupal::service('entity_type.bundle.info')
+      ->getBundleInfo($this->getEntityTypeId());
+    return !empty($bundle_info[$bundle_name]['untranslatable_fields.default_translation_affected']);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 92bbbae42f..a5c4dad24a 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Query\Queries;
 use Drupal\Core\Form\FormStateInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -127,6 +128,16 @@ public function form(array $form, FormStateInterface $form_state) {
       $this->addRevisionableFormFields($form);
     }
 
+    // Store the latest revision id at the time the form is built, so it can be
+    // used later to perform checks on concurrent edits.
+    $entity_type = $this->entity->getEntityType();
+    if ($entity_type->isRevisionable()) {
+      $form['original_latest_revision_id'] = [
+        '#type' => 'hidden',
+        '#default_value' => Queries::get()->getLatestRevisionId($entity_type->id(), $this->entity->id()),
+      ];
+    }
+
     $form['footer'] = [
       '#type' => 'container',
       '#weight' => 99,
@@ -155,6 +166,13 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     $entity = parent::buildEntity($form, $form_state);
 
+    // Ensure the original latest revision identifier is available for
+    // validators.
+    $original_latest_revision_id = $form_state->getValue('original_latest_revision_id');
+    if ($original_latest_revision_id) {
+      $entity->original_latest_revision_id = $original_latest_revision_id;
+    }
+
     // Mark the entity as requiring validation.
     $entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
index d045152f6a..012c4a7b44 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -71,4 +71,26 @@ public function getLoadedRevisionId();
    */
   public function updateLoadedRevisionId();
 
+  /**
+   * TODO
+   * @param bool $default_revision
+   * @param null $copy_untranslatable_fields
+   * @return static
+   */
+  public function createNewRevision($default_revision = TRUE, $copy_untranslatable_fields = NULL);
+
+  /**
+   * TODO
+   */
+  public function isDefaultRevisionType();
+
+  /**
+   * Checks if untranslatable fields should affect only the default translation.
+   *
+   * @return bool
+   *   TRUE if untranslatable fields should affect only the default translation,
+   *   FALSE otherwise.
+   */
+  public function isDefaultTranslationAffectedOnly();
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 874a3416bc..0c2ac573dd 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -288,6 +288,9 @@ protected function doSave($id, EntityInterface $entity) {
     }
 
     $this->populateAffectedRevisionTranslations($entity);
+    if ($this->entityType->isRevisionable()) {
+      $entity->set('revision_type', $entity->isDefaultRevision());
+    }
     $this->doSaveFieldItems($entity);
 
     return $return;
@@ -323,6 +326,14 @@ protected function doPreSave(EntityInterface $entity) {
       $entity->updateLoadedRevisionId();
     }
 
+    // Make sure that the original entity is the revision the current entity was
+    // originated from, otherwise we would always compare the current values
+    // with the default values, which is not correct for pending revisions.
+    // TODO add test coverage
+    if (!isset($entity->original) && $this->entityType->isRevisionable()) {
+      $entity->original = $this->loadRevision($entity->getLoadedRevisionId());
+    }
+
     $id = parent::doPreSave($entity);
 
     if (!$entity->isNew()) {
diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManager.php b/core/lib/Drupal/Core/Entity/EntityFieldManager.php
index d6e70eef75..0db2b3acc6 100644
--- a/core/lib/Drupal/Core/Entity/EntityFieldManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php
@@ -220,6 +220,16 @@ protected function buildBaseFieldDefinitions($entity_type_id) {
       }
     }
 
+    // TODO
+    if ($entity_type->isRevisionable()) {
+      $base_field_definitions['revision_type'] = BaseFieldDefinition::create('boolean')
+        ->setLabel($this->t('Revision type'))
+        ->setDescription($this->t('A flag indicating whether this is a pending revision or a default revision.'))
+        ->setTranslatable(FALSE)
+        ->setRevisionable(TRUE)
+        ->setDefaultValue(TRUE);
+    }
+
     // Make sure that revisionable entity types are correctly defined.
     if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
       // The 'revision_translation_affected' field should always be defined.
diff --git a/core/lib/Drupal/Core/Entity/EntityResolverManager.php b/core/lib/Drupal/Core/Entity/EntityResolverManager.php
index e9a87d6a6e..10ad93a994 100644
--- a/core/lib/Drupal/Core/Entity/EntityResolverManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityResolverManager.php
@@ -196,6 +196,30 @@ protected function setParametersFromEntityInformation(Route $route) {
     }
   }
 
+  /**
+   * Ensure revisionable entities load the latest revision on entity forms.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route object.
+   */
+  protected function setLatestRevisionFlag(Route $route) {
+    if (!$entity_form = $route->getDefault('_entity_form')) {
+      return;
+    }
+    // Only set the flag on entity types which are revisionable.
+    list($entity_type) = explode('.', $entity_form, 2);
+    if (!isset($this->getEntityTypes()[$entity_type]) || !$this->getEntityTypes()[$entity_type]->isRevisionable()) {
+      return;
+    }
+    $parameters = $route->getOption('parameters') ?: [];
+    foreach ($parameters as &$parameter) {
+      if ($parameter['type'] === 'entity:' . $entity_type && !isset($parameter['load_latest_revision'])) {
+        $parameter['load_latest_revision'] = TRUE;
+      }
+    }
+    $route->setOption('parameters', $parameters);
+  }
+
   /**
    * Set the upcasting route objects.
    *
@@ -212,6 +236,7 @@ public function setRouteOptions(Route $route) {
 
     // Try to use _entity_* information on the route.
     $this->setParametersFromEntityInformation($route);
+    $this->setLatestRevisionFlag($route);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index 9584ed3e26..de16f951f6 100644
--- a/core/lib/Drupal/Core/Entity/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -311,11 +311,16 @@ public function __construct($definition) {
       $this->checkStorageClass($this->handlers['storage']);
     }
 
-    // Automatically add the EntityChanged constraint if the entity type tracks
-    // the changed time.
+    // Automatically add the "EntityChanged" constraint if the entity type
+    // tracks the changed time.
     if ($this->entityClassImplements(EntityChangedInterface::class)) {
       $this->addConstraint('EntityChanged');
     }
+    // Automatically add the "EntityUntranslatableFields" constraint if we have
+    // an entity type supporting translatable fields and pending revisions.
+    if ($this->entityClassImplements(ContentEntityInterface::class)) {
+      $this->addConstraint('EntityUntranslatableFields');
+    }
 
     // Ensure a default list cache tag is set.
     if (empty($this->list_cache_tags)) {
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
index b1fee2a294..423086d0f4 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
@@ -2,6 +2,10 @@
 
 namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
 
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Query\Queries;
+use Drupal\Core\Entity\RevisionableInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 
@@ -14,17 +18,55 @@ class EntityChangedConstraintValidator extends ConstraintValidator {
    * {@inheritdoc}
    */
   public function validate($entity, Constraint $constraint) {
-    if (isset($entity)) {
-      /** @var \Drupal\Core\Entity\EntityInterface $entity */
-      if (!$entity->isNew()) {
-        $saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
+    if (isset($entity) && !$entity->isNew()) {
+      $valid = TRUE;
+      $check_changed_time = TRUE;
+
+      if ($entity instanceof RevisionableInterface && isset($entity->original_latest_revision_id)) {
+        // If the entity is revisionable, we need to make sure that no new
+        // revision was created while the entity was being changed.
+        $latest_revision_id = $this->getLatestRevisionId($entity);
+        $valid = $entity->original_latest_revision_id == $latest_revision_id;
+        // If the current revision was created starting from a past revision
+        // there is no point in checking the changed time, as in this case it
+        // may very well be less than the default revision's. This should happen
+        // only when changing different translations and is legal as long as
+        // translation changes are merged into default revisions.
+        $loaded_revision_id = $entity->getLoadedRevisionId();
+        $check_changed_time = $entity->original_latest_revision_id == $loaded_revision_id;
+      }
+
+      if ($valid && $entity instanceof EntityChangedInterface && $check_changed_time) {
+        /** @var \Drupal\Core\Entity\EntityChangedInterface|\Drupal\Core\Entity\RevisionableInterface $stored_entity */
+        $stored_entity = \Drupal::entityTypeManager()
+          ->getStorage($entity->getEntityTypeId())
+          ->loadUnchanged($entity->id());
         // A change to any other translation must add a violation to the current
         // translation because there might be untranslatable shared fields.
-        if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) {
-          $this->context->addViolation($constraint->message);
-        }
+        $valid = $stored_entity->getChangedTimeAcrossTranslations() <= $entity->getChangedTimeAcrossTranslations();
+      }
+
+      if (!$valid) {
+        $this->context->addViolation($constraint->message);
       }
     }
   }
 
+  /**
+   * Returns the latest revision identifier for the specified entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   An entity object.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  protected function getLatestRevisionId(EntityInterface $entity) {
+    $entity_type = $entity->getEntityType();
+    if (!$entity instanceof RevisionableInterface || $entity->isNew() || !$entity_type->isRevisionable()) {
+      return NULL;
+    }
+    return Queries::get()->getLatestRevisionId($entity->getEntityTypeId(), $entity->id());
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php
new file mode 100644
index 0000000000..1b8a33d3c8
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Validation constraint for the entity changed timestamp.
+ *
+ * @Constraint(
+ *   id = "EntityUntranslatableFields",
+ *   label = @Translation("Entity untranslatable fields", context = "Validation"),
+ *   type = {"entity"}
+ * )
+ */
+class EntityUntranslatableFieldsConstraint extends Constraint {
+
+  public $message = 'Fields shared among translations can be changed only in default revisions.';
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php
new file mode 100644
index 0000000000..367e1295bc
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Field\ChangedFieldItemList;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the EntityChanged constraint.
+ */
+class EntityUntranslatableFieldsConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($entity, Constraint $constraint) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+
+    // Untranslatable fields restrictions apply only to pending revisions of
+    // multilingual entities.
+    if ($entity->isNew() || $entity->isDefaultRevision() || !$entity->isTranslatable() || !$entity->getEntityType()->isRevisionable()) {
+      return;
+    }
+
+    // To avoid unintentional reverts and data losses, we forbid changes to
+    // untranslatable fields in pending revisions for multilingual entities,
+    // until we support conflict management. The only case where pending
+    // revisions are acceptable is when untranslatable fields affect only the
+    // default translation, in which case a pending revision contains only one
+    // affected translation. Even in this case, multiple translations would be
+    // affected in a single revision, if we allowed changes to untranslatable
+    // fields while editing non-default translations, so that is forbidden too.
+    if ($this->hasUntranslatableFieldsChanges($entity)) {
+      if ($entity->isDefaultTranslationAffectedOnly()) {
+        foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
+          if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
+            $this->context->addViolation($constraint->message);
+            break;
+          }
+        }
+      }
+      else {
+        $this->context->addViolation($constraint->message);
+      }
+    }
+  }
+
+  /**
+   * Checks whether an entity has untranslatable fields changes.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *  A content entity object.
+   *
+   * @return bool
+   *   TRUE if untranslatable fields have changes, FALSE otherwise.
+   */
+  protected function hasUntranslatableFieldsChanges(ContentEntityInterface $entity) {
+    $skip_fields = $this->getFieldsToSkipFromTranslationChangesCheck($entity);
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $original */
+    $original = isset($entity->original) ?
+      $entity->original :
+      \Drupal::entityTypeManager()
+        ->getStorage($entity->getEntityTypeId())
+        ->loadRevision($entity->getLoadedRevisionId());
+
+    foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
+      if (in_array($field_name, $skip_fields, TRUE) || $definition->isTranslatable() || $definition->isComputed()) {
+        continue;
+      }
+
+      // When saving entities in the user interface, the changed timestamp is
+      // automatically incremented by ContentEntityForm::submitForm() even if
+      // nothing was actually changed. Thus, the changed time needs to be
+      // ignored when determining whether there are any actual changes in the
+      // entity.
+      $field = $entity->get($field_name);
+      if ($field instanceof ChangedFieldItemList) {
+        continue;
+      }
+
+      $items = $field->filterEmptyItems();
+      $original_items = $original->get($field_name)->filterEmptyItems();
+      if (!$items->equals($original_items)) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Returns an array of field names to skip when checking for changes.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   A content entity object.
+   *
+   * @return array
+   *   An array of field names.
+   */
+  protected function getFieldsToSkipFromTranslationChangesCheck(ContentEntityInterface $entity) {
+    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+    $entity_type = $entity->getEntityType();
+
+    // A list of known revision metadata fields which should be skipped from
+    // the comparision.
+    $fields = [
+      $entity_type->getKey('revision'),
+      $entity_type->getKey('revision_translation_affected'),
+      'revision_type',
+    ];
+    $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys()));
+
+    return $fields;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Query/Queries.php b/core/lib/Drupal/Core/Entity/Query/Queries.php
new file mode 100644
index 0000000000..6517ec9d7e
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Query/Queries.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\Core\Entity\Query;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Implements a collection of common entity queries.
+ *
+ * @internal
+ */
+class Queries {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Returns an instance of the Queries class.
+   *
+   * @return static
+   */
+  public static function get() {
+    return \Drupal::service('entity.common_queries');
+  }
+
+  /**
+   * Queries constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Returns the latest revision identifier for an entity.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param int $entity_id
+   *   The entity identifier.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  public function getLatestRevisionId($entity_type_id, $entity_id) {
+    $id_key = $this->entityTypeManager
+      ->getDefinition($entity_type_id)
+      ->getKey('id');
+
+    $result = $this->entityTypeManager
+      ->getStorage($entity_type_id)
+      ->getQuery()
+      ->latestRevision()
+      ->condition($id_key, $entity_id)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    return key($result);
+  }
+
+  /**
+   * Returns the latest affected revision identifier for an entity translation.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param int $entity_id
+   *   The entity identifier.
+   * @param string $langcode
+   *   The language of the translation.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  public function getLatestAffectedRevisionId($entity_type_id, $entity_id, $langcode) {
+    $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+    $id_key = $entity_type->getKey('id');
+    $revision_key = $entity_type->getKey('revision');
+    $rta_key = $entity_type->getKey('revision_translation_affected');
+
+    $result = $this->entityTypeManager
+      ->getStorage($entity_type_id)
+      ->getQuery()
+      ->allRevisions()
+      ->condition($id_key, $entity_id)
+      ->condition($rta_key, 1, '=', $langcode)
+      ->range(0, 1)
+      ->sort($revision_key, 'DESC')
+      ->accessCheck(FALSE)
+      ->execute();
+
+    return key($result);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php
index 60712eeb21..fe1cc6f1c3 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php
@@ -293,6 +293,10 @@ protected function copyData(array &$sandbox) {
         // returned was always TRUE.
         $entity->set($revision_translation_affected_key, TRUE);
 
+        // We cannot know what type the revision was so we assume the core
+        // default behavior of always creating a default revision.
+        $entity->set('revision_type', TRUE);
+
         // Treat the entity as new in order to make the storage do an INSERT
         // rather than an UPDATE.
         $entity->enforceIsNew(TRUE);
@@ -380,6 +384,19 @@ protected function updateFieldStorageDefinitionsToRevisionable(ContentEntityType
     }
     $updated_storage_definitions[$entity_type->getKey('revision')] = $revision_field;
 
+    $revision_type_field = BaseFieldDefinition::create('boolean')
+      ->setName('revision_type')
+      ->setLabel(new TranslatableMarkup('Revision type'))
+      ->setDescription(new TranslatableMarkup('A flag indicating whether this is a pending revision or a default revision.'))
+      ->setTranslatable(FALSE)
+      ->setRevisionable(TRUE)
+      ->setDefaultValue(TRUE);
+
+    if ($update_cached_definitions) {
+      $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_type_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_type_field);
+    }
+    $updated_storage_definitions[$revision_type_field->getName()] = $revision_type_field;
+
     // Add the 'revision_translation_affected' field if needed.
     if ($entity_type->isTranslatable()) {
       $revision_translation_affected_field = BaseFieldDefinition::create('boolean')
diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
index 67f6a89c8e..cffae8bc3d 100644
--- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
+++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
@@ -4,6 +4,10 @@
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Query\Queries;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\TypedData\TranslatableInterface;
 use Symfony\Component\Routing\Route;
 
@@ -60,15 +64,79 @@ public function __construct(EntityManagerInterface $entity_manager) {
    */
   public function convert($value, $definition, $name, array $defaults) {
     $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
-    if ($storage = $this->entityManager->getStorage($entity_type_id)) {
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    $entity_definition = $this->entityManager->getDefinition($entity_type_id);
+
+    $entity = NULL;
+
+    // If the entity type is revisionable and the parameter has the
+    // "load_latest_revision" flag, load the latest revision.
+    if (!empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) {
+      $entity = $this->loadLatestRevision($storage, $entity_definition, $value);
+    }
+    else {
       $entity = $storage->load($value);
-      // If the entity type is translatable, ensure we return the proper
-      // translation object for the current context.
-      if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
-        $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
+    }
+
+    // If the entity type is translatable, ensure we return the proper
+    // translation object for the current context.
+    if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
+      $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
+    }
+
+    return $entity;
+  }
+
+  /**
+   * Load the latest revision of an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity definition.
+   * @param mixed $value
+   *   The raw value.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|null
+   *   The loaded entity if it exists, NULL otherwise.
+   */
+  protected function loadLatestRevision(EntityStorageInterface $storage, EntityTypeInterface $entity_type, $value) {
+    $latest_revision_id = NULL;
+    $entity_type_id = $entity_type->id();
+    $language_manager = \Drupal::languageManager(); // FIXME
+
+    // @todo, replace this query with a standardized way of getting the
+    //   latest revision in https://www.drupal.org/node/2784201.
+    if ($entity_type->isTranslatable() && $language_manager->isMultilingual()) {
+      $langcode = $language_manager
+        ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
+        ->getId();
+      $latest_revision_id = Queries::get()
+        ->getLatestAffectedRevisionId($entity_type_id, $value, $langcode);
+
+      // If the latest affected revision was a default revision, it is fine to
+      // load the latest revision instead. This will allow to handle all cases
+      // that do not work with pending revisions and always expect the latest
+      // revision being loaded (and being the default revision).
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
+      $revision = $storage->loadRevision($latest_revision_id);
+      if ($revision && (!$revision->isDefaultRevisionType() || $revision->isDefaultRevision())) {
+        return $revision;
       }
-      return $entity;
+      else {
+        // Fall back to the latest revision.
+        $latest_revision_id = NULL;
+      }
+    }
+
+    // Fall back to the latest revisions if no affected revision for the current
+    // language could be found. This is acceptable as it means the entity is not
+    // translated. This is the correct logic also on monolingual sites.
+    if (!isset($latest_revision_id)) {
+      $latest_revision_id = Queries::get()->getLatestRevisionId($entity_type_id, $value);
     }
+
+    return $storage->loadRevision($latest_revision_id);
   }
 
   /**
diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module
index 5a7af0e2c9..9c75ec5e14 100644
--- a/core/modules/content_moderation/content_moderation.module
+++ b/core/modules/content_moderation/content_moderation.module
@@ -118,6 +118,15 @@ function content_moderation_entity_translation_delete(EntityInterface $translati
     ->entityTranslationDelete($translation);
 }
 
+/**
+ * Implements hook_entity_prepare_form().
+ */
+function content_moderation_entity_prepare_form(EntityInterface $entity, $operation, FormStateInterface $form_state) {
+  \Drupal::service('class_resolver')
+    ->getInstanceFromDefinition(EntityTypeInfo::class)
+    ->entityPrepareForm($entity, $operation, $form_state);
+}
+
 /**
  * Implements hook_form_alter().
  */
diff --git a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
index f2c6917818..c44ab09841 100644
--- a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
+++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
@@ -35,11 +35,6 @@ public function onPresave(ContentEntityInterface $entity, $default_revision, $pu
     // This is probably not necessary if configuration is setup correctly.
     $entity->setNewRevision(TRUE);
     $entity->isDefaultRevision($default_revision);
-    if ($entity->hasField('revision_translation_affected')) {
-      // @todo remove this when revision and translation issues have been
-      // resolved. https://www.drupal.org/node/2860097
-      $entity->set('revision_translation_affected', TRUE);
-    }
 
     // Update publishing status if it can be updated and if it needs updating.
     if (($entity instanceof EntityPublishedInterface) && $entity->isPublished() !== $published_state) {
diff --git a/core/modules/content_moderation/src/Entity/Routing/EntityModerationRouteProvider.php b/core/modules/content_moderation/src/Entity/Routing/EntityModerationRouteProvider.php
index f72a47ede7..b7003eb1f8 100644
--- a/core/modules/content_moderation/src/Entity/Routing/EntityModerationRouteProvider.php
+++ b/core/modules/content_moderation/src/Entity/Routing/EntityModerationRouteProvider.php
@@ -88,7 +88,7 @@ protected function getLatestVersionRoute(EntityTypeInterface $entity_type) {
         ->setOption('parameters', [
           $entity_type_id => [
             'type' => 'entity:' . $entity_type_id,
-            'load_pending_revision' => 1,
+            'load_latest_revision' => 1,
           ],
         ]);
 
diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php
index a7fcf7e8d9..d9a59a16ac 100644
--- a/core/modules/content_moderation/src/EntityOperations.php
+++ b/core/modules/content_moderation/src/EntityOperations.php
@@ -101,7 +101,6 @@ public function entityPresave(EntityInterface $entity) {
       // This entity is default if it is new, a new translation, the default
       // revision, or the default revision is not published.
       $update_default_revision = $entity->isNew()
-        || $entity->isNewTranslation()
         || $current_state->isDefaultRevisionState()
         || !$this->moderationInfo->isDefaultRevisionPublished($entity);
 
@@ -248,20 +247,13 @@ public function entityView(array &$build, EntityInterface $entity, EntityViewDis
     if (!$this->moderationInfo->isModeratedEntity($entity)) {
       return;
     }
-    if (!$this->moderationInfo->isLatestRevision($entity)) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    if ($entity->isDefaultRevisionType() || !$this->moderationInfo->isLatestAffectedRevision($entity)) {
       return;
     }
     if ($this->moderationInfo->isLiveRevision($entity)) {
       return;
     }
-    // Don't display the moderation form when when:
-    // - The revision is not translation affected.
-    // - There are more than one translation languages.
-    // - The entity has pending revisions.
-    if (!$this->moderationInfo->isPendingRevisionAllowed($entity)) {
-      return;
-    }
-
     $component = $display->getComponent('content_moderation_control');
     if ($component) {
       $build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php
index 4ef25d9b12..19260ea0fd 100644
--- a/core/modules/content_moderation/src/EntityTypeInfo.php
+++ b/core/modules/content_moderation/src/EntityTypeInfo.php
@@ -7,10 +7,12 @@
 use Drupal\Core\Entity\ContentEntityFormInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Form\FormInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -268,6 +270,31 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
     return $fields;
   }
 
+  /**
+   * TODO
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   * @param $operation
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *
+   * @see hook_entity_prepare_form()
+   */
+  public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) {
+    /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
+    $form_object = $form_state->getFormObject();
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    if ($this->isModeratedEntityForm($form_object, $operation) && !$entity->isNew()) {
+      // TODO
+      $new_revision = $entity->createNewRevision(FALSE);
+
+      // TODO
+      $revision_key = $new_revision->getEntityType()->getKey('revision');
+      $new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
+
+      $form_object->setEntity($new_revision);
+    }
+  }
+
   /**
    * Alters bundle forms to enforce revision handling.
    *
@@ -291,57 +318,15 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id
         $this->entityTypeManager->getHandler($config_entity_type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
       }
     }
-    elseif ($form_object instanceof ContentEntityFormInterface && in_array($form_object->getOperation(), ['edit', 'default'])) {
+    elseif ($this->isModeratedEntityForm($form_object)) {
+      /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
       $entity = $form_object->getEntity();
       if ($this->moderationInfo->isModeratedEntity($entity)) {
         $this->entityTypeManager
           ->getHandler($entity->getEntityTypeId(), 'moderation')
           ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
 
-        if (!$this->moderationInfo->isPendingRevisionAllowed($entity)) {
-          $latest_revision = $this->moderationInfo->getLatestRevision($entity->getEntityTypeId(), $entity->id());
-          if ($entity->bundle()) {
-            $bundle_type_id = $entity->getEntityType()->getBundleEntityType();
-            $bundle = $this->entityTypeManager->getStorage($bundle_type_id)->load($entity->bundle());
-            $type_label = $bundle->label();
-          }
-          else {
-            $type_label = $entity->getEntityType()->getLabel();
-          }
-
-          $translation = $this->moderationInfo->getAffectedRevisionTranslation($latest_revision);
-          $args = [
-            '@type_label' => $type_label,
-            '@latest_revision_edit_url' => $translation->toUrl('edit-form', ['language' => $translation->language()])->toString(),
-            '@latest_revision_delete_url' => $translation->toUrl('delete-form', ['language' => $translation->language()])->toString(),
-          ];
-          $label = $this->t('Unable to save this @type_label.', $args);
-          $message = $this->t('<a href="@latest_revision_edit_url">Publish</a> or <a href="@latest_revision_delete_url">delete</a> the latest revision to allow all workflow transitions.', $args);
-          $full_message = $this->t('Unable to save this @type_label. <a href="@latest_revision_edit_url">Publish</a> or <a href="@latest_revision_delete_url">delete</a> the latest revision to allow all workflow transitions.', $args);
-          drupal_set_message($full_message, 'error');
-
-          $form['moderation_state']['#access'] = FALSE;
-          $form['actions']['#access'] = FALSE;
-          $form['invalid_transitions'] = [
-            'label' => [
-              '#type' => 'item',
-              '#prefix' => '<strong class="label">',
-              '#markup' => $label,
-              '#suffix' => '</strong>',
-            ],
-            'message' => [
-              '#type' => 'item',
-              '#markup' => $message,
-            ],
-            '#weight' => 999,
-            '#no_valid_transitions' => TRUE,
-          ];
-
-          if ($form['footer']) {
-            $form['invalid_transitions']['#group'] = 'footer';
-          }
-        }
-
         // Submit handler to redirect to the latest version, if available.
         $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
 
@@ -360,6 +345,19 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id
     }
   }
 
+  /**
+   * TODO
+   *
+   * @param \Drupal\Core\Form\FormInterface $form_object
+   * @param $operation
+   * @return bool
+   */
+  protected function isModeratedEntityForm(FormInterface $form_object, $operation = NULL) {
+    return $form_object instanceof ContentEntityFormInterface &&
+      in_array($operation ?: $form_object->getOperation(), ['edit', 'default']) &&
+      $this->moderationInfo->isModeratedEntity($form_object->getEntity());
+  }
+
   /**
    * Redirect content entity edit forms on save, if there is a pending revision.
    *
diff --git a/core/modules/content_moderation/src/Form/EntityModerationForm.php b/core/modules/content_moderation/src/Form/EntityModerationForm.php
index d0d3d0ef78..9f61a0cd0b 100644
--- a/core/modules/content_moderation/src/Form/EntityModerationForm.php
+++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php
@@ -126,6 +126,7 @@ public function buildForm(array $form, FormStateInterface $form_state, ContentEn
   public function submitForm(array &$form, FormStateInterface $form_state) {
     /** @var ContentEntityInterface $entity */
     $entity = $form_state->get('entity');
+    $entity = $entity->createNewRevision($entity->isDefaultRevision());
 
     $new_state = $form_state->getValue('new_state');
 
diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php
index 58eaa9fc7a..426eb01ed9 100644
--- a/core/modules/content_moderation/src/ModerationInformation.php
+++ b/core/modules/content_moderation/src/ModerationInformation.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Query\Queries;
 use Drupal\Core\TypedData\TranslatableInterface;
 
 /**
@@ -127,23 +128,35 @@ public function getAffectedRevisionTranslation(ContentEntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  public function isPendingRevisionAllowed(ContentEntityInterface $entity) {
-    return !(!$entity->isRevisionTranslationAffected() && count($entity->getTranslationLanguages()) > 1 && $this->hasPendingRevision($entity));
+  public function isLatestRevision(ContentEntityInterface $entity) {
+    return $entity->getRevisionId() == $this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id());
   }
 
   /**
    * {@inheritdoc}
    */
-  public function isLatestRevision(ContentEntityInterface $entity) {
-    return $entity->getRevisionId() == $this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id());
+  public function isLatestAffectedRevision(ContentEntityInterface $entity) {
+    $latest_revision_id = Queries::get()->getLatestAffectedRevisionId($entity->getEntityTypeId(), $entity->id(), $entity->language()->getId());
+    return $entity->getRevisionId() == $latest_revision_id;
   }
 
   /**
    * {@inheritdoc}
    */
   public function hasPendingRevision(ContentEntityInterface $entity) {
-    return $this->isModeratedEntity($entity)
-      && !($this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id()) == $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id()));
+    $result = FALSE;
+    if ($this->isModeratedEntity($entity)) {
+      $latest_revision_id = Queries::get()->getLatestAffectedRevisionId($entity->getEntityTypeId(), $entity->id(), $entity->language()->getId());
+      $default_revision_id = $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id());
+      if ($latest_revision_id != $default_revision_id) {
+        /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */
+        $latest_revision = $this->entityTypeManager
+          ->getStorage($entity->getEntityTypeId())
+          ->loadRevision($latest_revision_id);
+        $result = !$latest_revision->isDefaultRevisionType();
+      }
+    }
+    return $result;
   }
 
   /**
@@ -151,8 +164,7 @@ public function hasPendingRevision(ContentEntityInterface $entity) {
    */
   public function isLiveRevision(ContentEntityInterface $entity) {
     $workflow = $this->getWorkflowForEntity($entity);
-    return $this->isLatestRevision($entity)
-      && $entity->isDefaultRevision()
+    return $entity->isDefaultRevision()
       && $entity->moderation_state->value
       && $workflow->getTypePlugin()->getState($entity->moderation_state->value)->isPublishedState();
   }
@@ -162,7 +174,12 @@ public function isLiveRevision(ContentEntityInterface $entity) {
    */
   public function isDefaultRevisionPublished(ContentEntityInterface $entity) {
     $workflow = $this->getWorkflowForEntity($entity);
-    $default_revision = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
+
+    // Load the entity from the storage to make sure the statically cached
+    // object was not polluted by local changes.
+    $default_revision = \Drupal::entityTypeManager()
+      ->getStorage($entity->getEntityTypeId())
+      ->loadUnchanged($entity->id());
 
     // Ensure we are checking all translations of the default revision.
     if ($default_revision instanceof TranslatableInterface && $default_revision->isTranslatable()) {
diff --git a/core/modules/content_moderation/src/ModerationInformationInterface.php b/core/modules/content_moderation/src/ModerationInformationInterface.php
index 1dafb3f71c..2d198110ca 100644
--- a/core/modules/content_moderation/src/ModerationInformationInterface.php
+++ b/core/modules/content_moderation/src/ModerationInformationInterface.php
@@ -100,19 +100,6 @@ public function getDefaultRevisionId($entity_type_id, $entity_id);
    */
   public function getAffectedRevisionTranslation(ContentEntityInterface $entity);
 
-  /**
-   * Determines if pending revisions are allowed.
-   *
-   * @internal
-   *
-   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
-   *   The content entity.
-   *
-   * @return bool
-   *   If pending revisions are allowed.
-   */
-  public function isPendingRevisionAllowed(ContentEntityInterface $entity);
-
   /**
    * Determines if an entity is a latest revision.
    *
@@ -125,6 +112,11 @@ public function isPendingRevisionAllowed(ContentEntityInterface $entity);
    */
   public function isLatestRevision(ContentEntityInterface $entity);
 
+  /**
+   * TODO
+   */
+  public function isLatestAffectedRevision(ContentEntityInterface $entity);
+
   /**
    * Determines if a pending revision exists for the specified entity.
    *
diff --git a/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
index 8e7e3617a3..8886e2cf65 100644
--- a/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
+++ b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
@@ -85,19 +85,19 @@ protected function isEditFormPage(Route $route) {
   public function convert($value, $definition, $name, array $defaults) {
     $entity = parent::convert($value, $definition, $name, $defaults);
 
-    if ($entity && $this->moderationInformation->isModeratedEntity($entity) && !$this->moderationInformation->isLatestRevision($entity)) {
-      $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
-      $latest_revision = $this->moderationInformation->getLatestRevision($entity_type_id, $value);
-
-      if ($latest_revision instanceof EntityInterface) {
-        // If the entity type is translatable, ensure we return the proper
-        // translation object for the current context.
-        if ($entity instanceof TranslatableInterface) {
-          $latest_revision = $this->entityManager->getTranslationFromContext($latest_revision, NULL, ['operation' => 'entity_upcast']);
-        }
-        $entity = $latest_revision;
-      }
-    }
+//    if ($entity && $this->moderationInformation->isModeratedEntity($entity) && !$this->moderationInformation->isLatestRevision($entity)) {
+//      $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
+//      $latest_revision = $this->moderationInformation->getLatestRevision($entity_type_id, $value);
+//
+//      if ($latest_revision instanceof EntityInterface) {
+//        // If the entity type is translatable, ensure we return the proper
+//        // translation object for the current context.
+//        if ($entity instanceof TranslatableInterface) {
+//          $latest_revision = $this->entityManager->getTranslationFromContext($latest_revision, NULL, ['operation' => 'entity_upcast']);
+//        }
+//        $entity = $latest_revision;
+//      }
+//    }
 
     return $entity;
   }
diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php
index 4270a1fb53..a8566041db 100644
--- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php
+++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php
@@ -160,7 +160,6 @@ protected function updateModeratedEntity($moderation_state_id) {
       // This entity is default if it is new, a new translation, the default
       // revision state, or the default revision is not published.
       $update_default_revision = $entity->isNew()
-        || $entity->isNewTranslation()
         || $current_state->isDefaultRevisionState()
         || !$content_moderation_info->isDefaultRevisionPublished($entity);
 
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test/content_moderation_test.info.yml b/core/modules/content_moderation/tests/modules/content_moderation_test/content_moderation_test.info.yml
new file mode 100644
index 0000000000..1416f147f9
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test/content_moderation_test.info.yml
@@ -0,0 +1,9 @@
+name: 'Content moderation test'
+type: module
+description: 'Provides a testing module for Content Moderation.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - content_moderation
+  - node
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test/content_moderation_test.module b/core/modules/content_moderation/tests/modules/content_moderation_test/content_moderation_test.module
new file mode 100644
index 0000000000..514ff54de7
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test/content_moderation_test.module
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file Test module for Content Moderation.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * {@inheritdoc}
+ */
+function content_moderation_test_form_node_moderated_content_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  if (\Drupal::state()->get('content_moderation_test.concurrent_edit')) {
+    /** @var \Drupal\Core\Entity\ContentEntityForm $form_object */
+    $form_object = $form_state->getFormObject();
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $form_object->getEntity();
+    // We simulate a concurrent edit by setting the revision ID at the time the
+    // form is instantiated to the previous revision ID.
+    $form['vid']['#default_value'] = $entity->getRevisionId() - 1;
+  }
+}
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
index 326eb7cc97..1c4d07e850 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\content_moderation\Functional;
 
+use Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraint;
 use Drupal\workflows\Entity\Workflow;
 
 /**
@@ -21,6 +22,7 @@ class ModerationFormTest extends ModerationStateTestBase {
     'content_moderation',
     'locale',
     'content_translation',
+    'content_moderation_test',
   ];
 
   /**
@@ -525,4 +527,41 @@ public function testWorkflowInUse() {
     }
   }
 
+  /**
+   * Tests that concurrent edits of the same draft are not allowed.
+   */
+  public function testConcurrentDraftEdit() {
+    // Create new moderated content in published status.
+    $title = 'Some moderated content';
+    $edit = [
+      'title[0][value]' => $title,
+      'body[0][value]' => 'First version of the content.',
+      'moderation_state[0][state]' => 'published',
+    ];
+    $this->drupalPostForm('node/add/moderated_content', $edit, t('Save'));
+
+    $node = $this->drupalGetNodeByTitle($title);
+    $edit_path = sprintf('node/%d/edit', $node->id());
+
+    // Create a draft.
+    $edit = [
+      'body[0][value]' => 'Second version of the content.',
+      'moderation_state[0][state]' => 'draft',
+    ];
+    $this->drupalPostForm($edit_path, $edit, t('Save'));
+    $revision_id = $this->getLatestRevisionId($node);
+
+    // Simulate a concurrent draft post: we expect to see a validation error in
+    // this case.
+    \Drupal::state()->set('content_moderation_test.concurrent_edit', 1);
+    $edit = [
+      'body[0][value]' => 'Third version of the content.',
+      'moderation_state[0][state]' => 'draft',
+    ];
+    $this->drupalPostForm($edit_path, $edit, t('Save'));
+    $this->assertEquals($revision_id, $this->getLatestRevisionId($node));
+    $this->assertSession()
+      ->responseContains((new EntityChangedConstraint())->message);
+  }
+
 }
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php
index 92f51a8286..9e7f60b10d 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\content_moderation\Functional;
 
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\user\Entity\Role;
@@ -151,4 +152,19 @@ protected function grantUserPermissionToCreateContentOfType(AccountInterface $ac
     $role->save();
   }
 
+  /**
+   * Returns the latest revision identifier for the specified node.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   A content entity object.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  protected function getLatestRevisionId(ContentEntityInterface $entity) {
+    /** @var \Drupal\content_moderation\ModerationInformation $content_moderation_info */
+    $content_moderation_info = \Drupal::service('content_moderation.moderation_information');
+    return $content_moderation_info->getLatestRevisionId($entity->getEntityTypeId(), $entity->id());
+  }
+
 }
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
index 2fefc0148c..9b898c6d3f 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
@@ -199,14 +199,16 @@ public function testInvalidStateMultilingual() {
     $this->assertCount(1, $violations);
     $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
 
-    // From the default french published revision, there should be none.
-    $node_fr = Node::load($node->id())->getTranslation('fr');
-    $this->assertEquals('published', $node_fr->moderation_state->value);
-    $node_fr->moderation_state = 'archived';
-    $violations = $node_fr->validate();
-    $this->assertCount(0, $violations);
-
-    // From the latest french revision, there should also be no violation.
+    // @todo Creating a new revision from a past revision is currently not
+    //   supported. Evaluate this again in https://www.drupal.org/node/2860097.
+    // // From the default French published revision, there should be none.
+    // $node_fr = Node::load($node->id())->getTranslation('fr');
+    // $this->assertEquals('published', $node_fr->moderation_state->value);
+    // $node_fr->moderation_state = 'archived';
+    // $violations = $node_fr->validate();
+    // $this->assertCount(0, $violations);
+
+    // From the latest French revision, there should be no violation.
     $node_fr = $node->getTranslation('fr');
     $this->assertEquals('published', $node_fr->moderation_state->value);
     $node_fr->moderation_state = 'archived';
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 8dbd1702af..e0a0b43df3 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -413,10 +413,6 @@ function content_translation_entity_presave(EntityInterface $entity) {
     // If we are creating a new translation we need to use the source language
     // as original language, since source values are the only ones available to
     // compare against.
-    if (!isset($entity->original)) {
-      $entity->original = \Drupal::entityTypeManager()
-        ->getStorage($entity->entityType())->loadUnchanged($entity->id());
-    }
     $langcode = $entity->language()->getId();
     /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
     $manager = \Drupal::service('content_translation.manager');
diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php
index 190778d42c..095efa0260 100644
--- a/core/modules/content_translation/src/Controller/ContentTranslationController.php
+++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\Query\Queries;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
@@ -87,6 +88,7 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
     $handler = $this->entityManager()->getHandler($entity_type_id, 'translation');
     $manager = $this->manager;
     $entity_type = $entity->getEntityType();
+    $revisionable = $entity_type->isRevisionable();
 
     // Start collecting the cacheability metadata, starting with the entity and
     // later merge in the access result cacheability metadata.
@@ -121,6 +123,20 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
         $language_name = $language->getName();
         $langcode = $language->getId();
 
+        // If the entity type is revisionable, we may have pending revisions
+        // with translations not available yet in the default revision. Thus we
+        // need to load the latest affected revision for the each language to be
+        // sure we are listing all available translations.
+        if ($revisionable) {
+          $latest_revision_id = Queries::get()->getLatestAffectedRevisionId($entity_type_id, $entity->id(), $langcode);
+          if ($latest_revision_id) {
+            $entity = $this->entityTypeManager()
+              ->getStorage($entity_type_id)
+              ->loadRevision($latest_revision_id);
+            $translations = $entity->getTranslationLanguages();
+          }
+        }
+
         $add_url = new Url(
           "entity.$entity_type_id.content_translation_add",
           [
diff --git a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
index 6aae426abe..d1107eb85f 100644
--- a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
+++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
@@ -70,6 +70,7 @@ protected function alterRoutes(RouteCollection $collection) {
           'parameters' => [
             $entity_type_id => [
               'type' => 'entity:' . $entity_type_id,
+              'load_latest_revision' => TRUE,
             ],
           ],
           '_admin_route' => $is_admin,
@@ -102,6 +103,7 @@ protected function alterRoutes(RouteCollection $collection) {
             ],
             $entity_type_id => [
               'type' => 'entity:' . $entity_type_id,
+              'load_latest_revision' => TRUE,
             ],
           ],
           '_admin_route' => $is_admin,
@@ -127,6 +129,7 @@ protected function alterRoutes(RouteCollection $collection) {
             ],
             $entity_type_id => [
               'type' => 'entity:' . $entity_type_id,
+              'load_latest_revision' => TRUE,
             ],
           ],
           '_admin_route' => $is_admin,
@@ -152,6 +155,7 @@ protected function alterRoutes(RouteCollection $collection) {
             ],
             $entity_type_id => [
               'type' => 'entity:' . $entity_type_id,
+              'load_latest_revision' => TRUE,
             ],
           ],
           '_admin_route' => $is_admin,
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index 48401475fc..00658a1e05 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\Query\Queries;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Url;
 use Drupal\node\NodeStorageInterface;
@@ -173,6 +174,7 @@ public function revisionOverview(NodeInterface $node) {
 
     $rows = [];
     $default_revision = $node->getRevisionId();
+    $current_revision_displayed = FALSE;
 
     foreach ($this->getRevisionIds($node, $node_storage) as $vid) {
       /** @var \Drupal\node\NodeInterface $revision */
@@ -187,11 +189,15 @@ public function revisionOverview(NodeInterface $node) {
 
         // Use revision link to link to revisions that are not active.
         $date = $this->dateFormatter->format($revision->revision_timestamp->value, 'short');
-        if ($vid != $node->getRevisionId()) {
+
+        // TODO
+        $current_revision = $vid == $default_revision || (!$current_revision_displayed && $revision->isDefaultRevisionType());
+        if (!$current_revision) {
           $link = $this->l($date, new Url('entity.node.revision', ['node' => $node->id(), 'node_revision' => $vid]));
         }
         else {
           $link = $node->link($date);
+          $current_revision_displayed = TRUE;
         }
 
         $row = [];
@@ -210,7 +216,7 @@ public function revisionOverview(NodeInterface $node) {
         $this->renderer->addCacheableDependency($column['data'], $username);
         $row[] = $column;
 
-        if ($vid == $default_revision) {
+        if ($current_revision) {
           $row[] = [
             'data' => [
               '#prefix' => '<em>',
diff --git a/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
index 0c68d8b254..be44e46b82 100644
--- a/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
+++ b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
@@ -88,10 +88,15 @@ public function buildForm(array $form, FormStateInterface $form_state, $node_rev
     $this->langcode = $langcode;
     $form = parent::buildForm($form, $form_state, $node_revision);
 
+    // Unless untranslatable fields are configured to affect only the default
+    // translation, we need to ask the user whether they should be included in
+    // the revert process.
+    $default_translation_affected = $this->revision->isDefaultTranslationAffectedOnly();
     $form['revert_untranslated_fields'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Revert content shared among translations'),
-      '#default_value' => FALSE,
+      '#default_value' => $default_translation_affected && $this->revision->isDefaultTranslation(),
+      '#access' => !$default_translation_affected,
     ];
 
     return $form;
@@ -101,24 +106,10 @@ public function buildForm(array $form, FormStateInterface $form_state, $node_rev
    * {@inheritdoc}
    */
   protected function prepareRevertedRevision(NodeInterface $revision, FormStateInterface $form_state) {
-    $revert_untranslated_fields = $form_state->getValue('revert_untranslated_fields');
-
-    /** @var \Drupal\node\NodeInterface $default_revision */
-    $latest_revision = $this->nodeStorage->load($revision->id());
-    $latest_revision_translation = $latest_revision->getTranslation($this->langcode);
-
-    $revision_translation = $revision->getTranslation($this->langcode);
-
-    foreach ($latest_revision_translation->getFieldDefinitions() as $field_name => $definition) {
-      if ($definition->isTranslatable() || $revert_untranslated_fields) {
-        $latest_revision_translation->set($field_name, $revision_translation->get($field_name)->getValue());
-      }
-    }
-
-    $latest_revision_translation->setNewRevision();
-    $latest_revision_translation->isDefaultRevision(TRUE);
-
-    return $latest_revision_translation;
+    $revert_untranslated_fields = (bool) $form_state->getValue('revert_untranslated_fields');
+    return $revision
+      ->getTranslation($this->langcode)
+      ->createNewRevision(TRUE, $revert_untranslated_fields);
   }
 
 }
diff --git a/core/modules/node/src/NodeViewBuilder.php b/core/modules/node/src/NodeViewBuilder.php
index f0971fb3bb..cab5653d5f 100644
--- a/core/modules/node/src/NodeViewBuilder.php
+++ b/core/modules/node/src/NodeViewBuilder.php
@@ -90,7 +90,9 @@ public static function renderLinks($node_entity_id, $view_mode, $langcode, $is_i
     ];
 
     if (!$is_in_preview) {
-      $entity = Node::load($node_entity_id)->getTranslation($langcode);
+      /** @var \Drupal\node\NodeInterface $entity */
+      $entity = \Drupal::entityManager()
+        ->getTranslationFromContext(Node::load($node_entity_id), $langcode);
       $links['node'] = static::buildLinks($entity, $view_mode);
 
       // Allow other modules to alter the node links.
diff --git a/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
index a437e4aba8..3646ab3e01 100644
--- a/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
+++ b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
@@ -160,10 +160,17 @@ public function testNodeRevisionsTabWithDefaultRevision() {
 
     $this->drupalGet('node/' . $node_id . '/revisions');
 
+    // Verify that the latest affected revision having been a default revision
+    // is displayed as the current one.
+    $this->assertNoLinkByHref('/node/' . $node_id . '/revisions/1/revert');
+    $elements = $this->xpath('//tr[contains(@class, "revision-current")]/td/a[1]');
+    // The site may be installed in a subdirectory, so check if the URL is
+    // contained in the retrieved one.
+    $this->assertContains('/node/1', current($elements)->getAttribute('href'));
+
     // Verify that the default revision can be an older revision than the latest
     // one.
-    // Assert that the revisions with translations changes are shown: 1 and 4.
-    $this->assertLinkByHref('/node/' . $node_id . '/revisions/1/revert');
+    // Assert that the revisions with translations changes are shown.
     $this->assertLinkByHref('/node/' . $node_id . '/revisions/4/revert');
 
     // Assert that the revisions without translations changes are filtered out:
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
index 8b67cc62d2..1ba2f1ae49 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
@@ -121,6 +121,11 @@ protected function getExpectedNormalizedEntity() {
           'value' => TRUE,
         ],
       ],
+      'revision_type' => [
+        [
+          'value' => TRUE,
+        ],
+      ],
       'default_langcode' => [
         [
           'value' => TRUE,
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
index 475a1ca6cb..3ce2eb3580 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
@@ -210,6 +210,11 @@ protected function getExpectedNormalizedEntity() {
           'value' => TRUE,
         ],
       ],
+      'revision_type' => [
+        [
+          'value' => TRUE,
+        ],
+      ],
     ];
   }
 
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
index 492ff642e4..b16cfb76a9 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
@@ -152,6 +152,11 @@ protected function getExpectedNormalizedEntity() {
           'value' => TRUE,
         ],
       ],
+      'revision_type' => [
+        [
+          'value' => TRUE,
+        ],
+      ],
       'default_langcode' => [
         [
           'value' => TRUE,
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index 5b71b93b7a..6de8ff7c4d 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -129,6 +129,9 @@ public function testNormalize() {
       'revision_translation_affected' => [
         ['value' => TRUE],
       ],
+      'revision_type' => [
+        ['value' => TRUE],
+      ],
       'non_rev_field' => [],
       'field_test_text' => [
         [
@@ -201,6 +204,7 @@ public function testSerialize() {
       'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
       'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
       'revision_translation_affected' => '<revision_translation_affected><value>1</value></revision_translation_affected>',
+      'revision_type' => '<revision_type><value>1</value></revision_type>',
       'non_rev_field' => '<non_rev_field/>',
       'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format></field_test_text>',
     ];
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index bb69bd33f5..088c038608 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -2042,3 +2042,37 @@ function system_update_8403() {
     }
   }
 }
+
+/**
+ * Add the 'revision_type' field to all entity types.
+ */
+function system_update_8501() {
+  $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+
+  // Get a list of revisionable and translatable entity types.
+  /** @var \Drupal\Core\Entity\ContentEntityTypeInterface[] $definitions */
+  $definitions = array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) use ($definition_update_manager) {
+    if ($entity_type = $definition_update_manager->getEntityType($entity_type->id())) {
+      return $entity_type->isRevisionable();
+    }
+    return FALSE;
+  });
+
+  foreach ($definitions as $entity_type_id => $entity_type) {
+    $field_name = 'revision_type';
+
+    // Install the 'revision_type' field if needed.
+    if (!$definition_update_manager->getFieldStorageDefinition($field_name, $entity_type_id)) {
+      $storage_definition = BaseFieldDefinition::create('boolean')
+        ->setLabel(t('Revision type'))
+        ->setDescription(t('A flag indicating whether this is a pending revision or a default revision.'))
+        ->setTranslatable(FALSE)
+        ->setRevisionable(TRUE)
+        ->setDefaultValue(TRUE)
+        ->setInitialValue(TRUE);
+
+      $definition_update_manager
+        ->installFieldStorageDefinition($field_name, $entity_type_id, $entity_type_id, $storage_definition);
+    }
+  }
+}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 025cb5fc2f..a934ad65ac 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -206,6 +206,26 @@ function entity_test_entity_bundle_info() {
   return $bundles;
 }
 
+/**
+ * Implements hook_entity_bundle_info_alter().
+ */
+function entity_test_entity_bundle_info_alter(&$bundles) {
+  $entity_info = \Drupal::entityTypeManager()->getDefinitions();
+  $state = \Drupal::state();
+  foreach ($bundles as $entity_type_id => &$all_bundle_info) {
+    if ($entity_info[$entity_type_id]->getProvider() == 'entity_test') {
+      if ($state->get('entity_test.translation')) {
+        foreach ($all_bundle_info as $bundle_name => &$bundle_info) {
+          $bundle_info['translatable'] = TRUE;
+          if ($state->get('entity_test.untranslatable_fields.default_translation_affected')) {
+            $bundle_info['untranslatable_fields.default_translation_affected'] = TRUE;
+          }
+        }
+      }
+    }
+  }
+}
+
 /**
  * Implements hook_entity_view_mode_info_alter().
  */
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php
index 391307576e..13a169df49 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php
@@ -71,6 +71,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     $fields['name']->setRevisionable(TRUE);
     $fields['user_id']->setRevisionable(TRUE);
     $fields['changed']->setRevisionable(TRUE);
+    $fields['not_translatable']->setRevisionable(TRUE);
 
     return $fields;
   }
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
index 4aca50032b..a36d04dc15 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
@@ -374,7 +374,6 @@ protected function runUpdates() {
 
       // Ensure that the update hooks updated all entity schema.
       $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates();
-      $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.');
       if ($needs_updates) {
         foreach (\Drupal::entityDefinitionUpdateManager()
           ->getChangeSummary() as $entity_type_id => $summary) {
@@ -383,6 +382,7 @@ protected function runUpdates() {
           }
         }
       }
+      $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.');
     }
   }
 
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
new file mode 100644
index 0000000000..bba37473a4
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\entity_test\Entity\EntityTestMulRevChanged;
+
+/**
+ * Tests concurrent entity saves and revisions.
+ *
+ * @group Entity
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraintValidator
+ */
+class ContentEntityConcurrentRevisionSaveTest extends EntityKernelTestBase {
+
+  const SAVE_NO_REVISION = 0x0;
+  const SAVE_NEW_REVISION = 0x1;
+  const SAVE_PENDING_REVISION = 0x3;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['user', 'system', 'field', 'text', 'filter', 'entity_test'];
+
+  /**
+   * The EntityTestMulRevChanged entity type storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Counter used to generate meaningful entity labels.
+   *
+   * @var int
+   */
+  protected $saveCounter;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('entity_test_mulrev_changed');
+    $this->storage = $this->entityManager->getStorage('entity_test_mulrev_changed');
+    $this->saveCounter = 1;
+  }
+
+  /**
+   * Data provider for testConcurrentSave().
+   */
+  public function dataTestConcurrentSave() {
+    return [
+      'pending revision - pending revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_PENDING_REVISION]],
+      'no revision - pending revision' => [[static::SAVE_NO_REVISION, static::SAVE_PENDING_REVISION]],
+      'new revision - pending revision' => [[static::SAVE_NEW_REVISION, static::SAVE_PENDING_REVISION]],
+      'pending revision - no revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_NO_REVISION]],
+      'no revision - no revision' => [[static::SAVE_NO_REVISION, static::SAVE_NO_REVISION]],
+      'new revision - no revision' => [[static::SAVE_NEW_REVISION, static::SAVE_NO_REVISION]],
+      'pending revision - new revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_NEW_REVISION]],
+      'no revision - new revision' => [[static::SAVE_NO_REVISION, static::SAVE_NEW_REVISION]],
+      'new revision - new revision' => [[static::SAVE_NEW_REVISION, static::SAVE_NEW_REVISION]],
+    ];
+  }
+
+  /**
+   * Tests concurrent entity saves with various save types.
+   *
+   * @param int[] $type
+   *   An array of save types as defined by the related ::SAVE_* constants. One
+   *   for each of the two concurrent saves.
+   *
+   * @covers ::validate
+   *
+   * @dataProvider dataTestConcurrentSave
+   */
+  public function testConcurrentSave(array $type) {
+    $user = $this->createUser();
+
+    // Create a test entity.
+    $entity = EntityTestMulRevChanged::create([
+      'not_translatable' => $this->randomString(),
+      'user_id' => $user->id(),
+      'language' => 'en',
+    ]);
+    $this->doEntitySave($entity, static::SAVE_NO_REVISION, FALSE);
+
+    // Perform two "concurrent" saves and verify the second one triggers a
+    // validation error.
+    $entity->original_latest_revision_id = $entity->getRevisionId();
+    $entity_other_instance = clone $entity;
+    $this->doEntitySave($entity, $type[0], FALSE);
+    $this->doEntitySave($entity_other_instance, $type[1], TRUE);
+  }
+
+  /**
+   * Performs an entity save of the specified "type".
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity to be saved.
+   * @param int $type
+   *   The save type, as defined by the ::SAVE_* constants.
+   * @param bool $error_expected
+   *   Flag indicating whether a validation error is expected.
+   */
+  protected function doEntitySave(ContentEntityInterface $entity, $type, $error_expected) {
+    // Set a meaningful entity label, useful for debugging.
+    $entity->set('name', 'Title ' . ($this->saveCounter++));
+    if ($type & static::SAVE_NEW_REVISION) {
+      $entity->setNewRevision();
+    }
+    if ($type === static::SAVE_PENDING_REVISION) {
+      $entity->isDefaultRevision(FALSE);
+    }
+    $violations = $entity->validate();
+    $this->assertEquals($error_expected, (bool) $violations->count());
+    $entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php
new file mode 100644
index 0000000000..2d07d7e6df
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php
@@ -0,0 +1,465 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\Query\Queries;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\entity_test\Entity\EntityTestMulRevChanged;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\user\Entity\User;
+
+/**
+ * Test decoupled translation revisions.
+ *
+ * @group entity
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase
+ */
+class EntityDecoupledTranslationRevisionsTest extends EntityKernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'system',
+    'entity_test',
+    'language',
+  ];
+
+  /**
+   * The entity bundle info service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleInfo;
+
+  /**
+   * The entity storage.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * The translations of the test entity.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityInterface[] $translations
+   */
+  protected $translations;
+
+  /**
+   * The previous revision identifiers for the various revision translations.
+   *
+   * @var int[]
+   */
+  protected $previousRevisionId = [];
+
+  /**
+   * The previous unstranslatable field value.
+   *
+   * @var string[]
+   */
+  protected $previousUntranslatableFieldValue;
+
+  /**
+   * The current edit sequence step index.
+   *
+   * @var int
+   */
+  protected $stepIndex;
+
+  /**
+   * The current edit sequence step info.
+   *
+   * @var array
+   */
+  protected $stepInfo;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity_type_id = 'entity_test_mulrev_changed';
+    $this->installEntitySchema($entity_type_id);
+    $this->storage = $this->container->get('entity_type.manager')
+      ->getStorage($entity_type_id);
+
+    $this->installConfig(['language']);
+    $langcodes = ['it', 'fr'];
+    foreach ($langcodes as $langcode) {
+      ConfigurableLanguage::createFromLangcode($langcode)->save();
+    }
+
+    $values = [
+      'name' => $this->randomString(),
+      'status' => 1,
+    ];
+    User::create($values)->save();
+
+    // Make sure entity bundles are translatable.
+    $this->state->set('entity_test.translation', TRUE);
+    $this->bundleInfo = \Drupal::service('entity_type.bundle.info');
+    $this->bundleInfo->clearCachedBundles();
+  }
+
+  /**
+   * Data provider for ::testDecoupledDefaultRevisions.
+   */
+  public function dataTestDecoupledPendingRevisions() {
+    $sets = [];
+
+    $sets['Intermixed languages - No initial default translation'] = [[
+      ['en', TRUE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', TRUE],
+      ['it', TRUE],
+    ]];
+
+    $sets['Intermixed languages - With initial default translation'] = [[
+      ['en', TRUE],
+      ['it', TRUE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', TRUE],
+      ['it', TRUE],
+    ]];
+
+    $sets['Alternate languages - No initial default translation'] = [[
+      ['en', TRUE],
+      ['en', FALSE],
+      ['en', FALSE],
+      ['en', TRUE],
+      ['it', FALSE],
+      ['en', TRUE],
+      ['it', FALSE],
+      ['it', FALSE],
+      ['it', TRUE],
+    ]];
+
+    $sets['Alternate languages - With initial default translation'] = [[
+      ['en', TRUE],
+      ['it', TRUE],
+      ['en', TRUE],
+      ['en', FALSE],
+      ['en', FALSE],
+      ['en', TRUE],
+      ['it', TRUE],
+      ['it', FALSE],
+      ['it', FALSE],
+      ['it', TRUE],
+    ]];
+
+    $sets['Multiple languages - No initial default translation'] = [[
+      ['en', TRUE],
+      ['it', FALSE],
+      ['fr', FALSE],
+      ['en', FALSE],
+      ['en', TRUE],
+      ['it', TRUE],
+      ['fr', FALSE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', TRUE],
+      ['fr', TRUE],
+      ['it', TRUE],
+      ['fr', TRUE],
+    ]];
+
+    $sets['Multiple languages - With initial default translation'] = [[
+      ['en', TRUE],
+      ['it', TRUE],
+      ['fr', TRUE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', TRUE],
+      ['it', TRUE],
+      ['fr', FALSE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', TRUE],
+      ['fr', TRUE],
+      ['it', TRUE],
+      ['fr', TRUE],
+    ]];
+
+    return $sets;
+  }
+
+  /**
+   * Test decoupled default revisions.
+   *
+   * @param array[] $sequence
+   *   An array with arrays of arguments for the ::doSaveNewRevision() method as
+   *   values. Every child array corresponds to a method invocation.
+   *
+   * @covers ::createNewRevision
+   *
+   * @dataProvider dataTestDecoupledPendingRevisions
+   */
+  public function testDecoupledPendingRevisions($sequence) {
+    $revision_id = $this->doTestEditSequence($sequence);
+    $this->assertEquals(count($sequence), $revision_id);
+  }
+
+  /**
+   * Data provider for ::testUntranslatableFields.
+   */
+  public function dataTestUntranslatableFields() {
+    $sets = [];
+
+    $sets['Default behavior - Only default revisions supported'] = [
+      [
+        ['en', TRUE, TRUE],
+        ['it', FALSE, TRUE, FALSE],
+        ['en', FALSE, TRUE, FALSE],
+        ['en', TRUE, TRUE],
+        ['it', TRUE, TRUE],
+        ['en', FALSE],
+        ['it', FALSE],
+      ],
+      FALSE,
+    ];
+
+    $sets['Alternative behavior - Untranslatable fields affect only default translation'] = [
+      [
+        ['en', TRUE, TRUE],
+        ['it', FALSE, TRUE, FALSE],
+        ['en', FALSE, TRUE],
+        ['it', FALSE],
+        ['it', TRUE],
+        ['en', TRUE, TRUE],
+        ['it', FALSE],
+        ['en', FALSE],
+        ['it', TRUE],
+        ['en', TRUE, TRUE],
+      ],
+      TRUE,
+    ];
+
+    return $sets;
+  }
+
+  /**
+   * Tests that untranslatable fields are handled correctly.
+   *
+   * @param array[] $sequence
+   *   An array with arrays of arguments for the ::doSaveNewRevision() method as
+   *   values. Every child array corresponds to a method invocation.
+   *
+   * @param bool $default_translation_affected
+   *   Whether untranslatable field changes affect all revisions or only the
+   *   default revision.
+   *
+   * @covers ::createNewRevision
+   * @covers \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator::validate
+   *
+   * @dataProvider dataTestUntranslatableFields
+   */
+  public function testUntranslatableFields($sequence, $default_translation_affected) {
+    // Configure the untranslatable fields edit mode.
+    $this->state->set('entity_test.untranslatable_fields.default_translation_affected', $default_translation_affected);
+    $this->bundleInfo->clearCachedBundles();
+
+    // Test that a new entity is always valid.
+    $entity = EntityTestMulRevChanged::create();
+    $entity->set('not_translatable', 0);
+    $violations = $entity->validate();
+    $this->assertEmpty($violations);
+
+    // Test the specified sequence.
+    $this->doTestEditSequence($sequence);
+  }
+
+  /**
+   * Actually tests an edit step sequence.
+   *
+   * @param array[] $sequence
+   *   An array of sequence steps.
+   *
+   * @return int
+   *   The latest saved revision id.
+   */
+  protected function doTestEditSequence($sequence) {
+    $revision_id = NULL;
+    foreach ($sequence as $index => $step) {
+      $this->stepIndex = $index;
+      $revision_id = call_user_func_array([$this, 'doEditStep'], $step);
+    }
+    return $revision_id;
+  }
+
+  /**
+   * Saves a new revision of the test entity.
+   *
+   * @param string $active_langcode
+   *   The language of the translation for which a new revision will be saved.
+   * @param bool $default_revision
+   *   Whether the revision should be flagged as the default revision.
+   * @param bool $untranslatable_update
+   *   (optional) Whether an untranslatable field update should be performed.
+   *   Defaults to FALSE.
+   * @param bool $valid
+   *   (optional) Whether entity validation is expected to succeed. Defaults to
+   *   TRUE.
+   *
+   * @return int
+   *   The new revision identifier.
+   */
+  protected function doEditStep($active_langcode, $default_revision, $untranslatable_update = FALSE, $valid = TRUE) {
+    $this->stepInfo = [$active_langcode, $default_revision, $untranslatable_update, $valid];
+
+    // If changes to untranslatable fields affect only the default translation,
+    // we can different values for untranslatable fields in the various
+    // revision translations, so we need to track their previous value per
+    // language.
+    $all_translations_affected = !$this->state->get('entity_test.untranslatable_fields.default_translation_affected');
+    $previous_untranslatable_field_langcode = $all_translations_affected ? LanguageInterface::LANGCODE_DEFAULT : $active_langcode;
+
+    // Initialize previous data tracking.
+    if (!isset($this->translations)) {
+      $this->translations[$active_langcode] = EntityTestMulRevChanged::create();
+      $this->previousRevisionId[$active_langcode] = 0;
+      $this->previousUntranslatableFieldValue[$previous_untranslatable_field_langcode] = NULL;
+    }
+    if (!isset($this->translations[$active_langcode])) {
+      $this->translations[$active_langcode] = reset($this->translations)->addTranslation($active_langcode);
+      $this->previousRevisionId[$active_langcode] = 0;
+      $this->previousUntranslatableFieldValue[$active_langcode] = NULL;
+    }
+
+    // We want to update previous data only if we expect a valid result,
+    // otherwise we would be just polluting it with invalid values.
+    if ($valid) {
+      $entity = &$this->translations[$active_langcode];
+      $previous_revision_id = &$this->previousRevisionId[$active_langcode];
+      $previous_untranslatable_field_value = &$this->previousUntranslatableFieldValue[$previous_untranslatable_field_langcode];
+    }
+    else {
+      $entity = clone $this->translations[$active_langcode];
+      $previous_revision_id = $this->previousRevisionId[$active_langcode];
+      $previous_untranslatable_field_value = $this->previousUntranslatableFieldValue[$previous_untranslatable_field_langcode];
+    }
+
+    // Check that after instantiating a new revision for the specified
+    // translation, we are resuming work from where we left the last time. If
+    // that is the case, the label generated for the previous revision should
+    // match the stored one.
+    if (!$entity->isNew()) {
+      $previous_label = NULL;
+      if (!$entity->isNewTranslation()) {
+        $previous_label = $this->generateNewEntityLabel($entity, $previous_revision_id);
+      }
+      $previous_revision_id = (int) $entity->getLoadedRevisionId();
+      $latest_affected_revision_id = Queries::get()->getLatestAffectedRevisionId($entity->getEntityTypeId(), $entity->id(), $entity->language()->getId());
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_affected_revision */
+      $latest_affected_revision = $this->storage->loadRevision($latest_affected_revision_id);
+      $translation = $latest_affected_revision->hasTranslation($active_langcode) ?
+        $latest_affected_revision->getTranslation($active_langcode) : $latest_affected_revision->addTranslation($active_langcode);
+      $entity = $translation->createNewRevision($default_revision);
+      $this->assertEquals($previous_label, $entity->label(), $this->formatMessage('Loaded translatable field value does not match the previous one.'));
+    }
+
+    $value = $entity->get('not_translatable')->value;
+    if (isset($previous_untranslatable_field_value)) {
+      $this->assertEquals($previous_untranslatable_field_value, $value, $this->formatMessage('Loaded untranslatable field value does not match the previous one.'));
+    }
+
+    // Perform a change and store it.
+    $label = $this->generateNewEntityLabel($entity, $previous_revision_id, TRUE);
+    $entity->set('name', $label);
+    if ($untranslatable_update) {
+      // Store the revision id of the previous untranslatable fields update in
+      // the new value, besides the upcoming revision id. Useful for analyzing
+      // test failures.
+      $prev = 0;
+      if (isset($value)) {
+        preg_match('/^\d+ -> (\d+)$/', $value, $matches);
+        $prev = $matches[1];
+      }
+      $value = $prev . ' -> ' . ($entity->getLoadedRevisionId() + 1);
+      $entity->set('not_translatable', $value);
+      $previous_untranslatable_field_value = $value;
+    }
+
+    $violations = $entity->validate();
+    $this->assertEquals($valid, !$violations->count(), $this->formatMessage('Validation does not match the expected result.'));
+
+    if ($valid) {
+      $entity->save();
+
+      // Reload the current revision translation and the default revision to
+      // make sure data was stored correctly.
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+      $entity = $this->storage->loadRevision($entity->getRevisionId());
+      $entity = $entity->getTranslation($active_langcode);
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $default_entity */
+      $default_entity = $this->storage->loadUnchanged($entity->id());
+
+      // Verify that the values for the current revision translation match the
+      // expected ones, while for the other translations they match the default
+      // revision. We also need to verify that only the current revision
+      // translation was marked as affected.
+      foreach ($entity->getTranslationLanguages() as $langcode => $language) {
+        $translation = $entity->getTranslation($langcode);
+        $rta_expected = $langcode == $active_langcode || ($untranslatable_update && $all_translations_affected);
+        $this->assertEquals($rta_expected, $translation->isRevisionTranslationAffected(), $this->formatMessage("'$langcode' translation incorrectly affected"));
+        $label_expected = $label;
+        if ($langcode !== $active_langcode) {
+          $default_translation = $default_entity->hasTranslation($langcode) ? $default_entity->getTranslation($langcode) : $default_entity;
+          $label_expected = $default_translation->label();
+        }
+        $this->assertEquals($label_expected, $translation->label(), $this->formatMessage("Incorrect '$langcode' translation label"));
+      }
+    }
+
+    return $entity->getRevisionId();
+  }
+
+  /**
+   * Generates a new label for the specified revision.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $revision
+   *   An entity object.
+   * @param int $previous_revision_id
+   *   The previous revision identifier for this revision translation.
+   * @param bool $next
+   *   (optional) Whether the label describes the current revision or the one
+   *   to be created. Defaults to FALSE.
+   *
+   * @return string
+   *   A revision label.
+   */
+  protected function generateNewEntityLabel(ContentEntityInterface $revision, $previous_revision_id, $next = FALSE) {
+    $language_label = $revision->language()->getName();
+    $revision_type = $revision->isDefaultRevision() ? 'Default' : 'Pending';
+    $revision_id = $next ?
+      Queries::get()->getLatestRevisionId($revision->getEntityTypeId(), $revision->id()) + 1 :
+      $revision->getLoadedRevisionId();
+    return sprintf('%s (%s %d -> %d)', $language_label, $revision_type, $previous_revision_id, $revision_id);
+  }
+
+  /**
+   * Formats an assertion message.
+   *
+   * @param string $message
+   *   The human-readable message.
+   *
+   * @return string
+   *   The formatted message.
+   */
+  protected function formatMessage($message) {
+    $params = $this->stepInfo;
+    array_unshift($params, $this->stepIndex + 1);
+    array_unshift($params, '[Step %d] ' . $message . ' (langcode: %s, default_revision: %d, untranslatable_update: %d, valid: %d)');
+    return call_user_func_array('sprintf', $params);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
index e2e4ae273e..542b5bfa05 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
@@ -113,6 +113,7 @@ public function testEntityTypeUpdateWithoutData() {
         // The revision key is now defined, so the revision field needs to be
         // created.
         t('The %field_name field needs to be installed.', ['%field_name' => 'Revision ID']),
+        t('The %field_name field needs to be installed.', ['%field_name' => 'Revision type']),
       ],
     ];
     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
index a3b99fe0c9..5a1d5fa4fc 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
@@ -167,4 +167,39 @@ public function testSaveInHookEntityInsert() {
 
   }
 
+  /**
+   * Tests manual revert of the revision ID value.
+   *
+   * @covers \Drupal\Core\Entity\ContentEntityBase::getRevisionId
+   * @covers \Drupal\Core\Entity\ContentEntityBase::getLoadedRevisionId
+   * @covers \Drupal\Core\Entity\ContentEntityBase::setNewRevision
+   * @covers \Drupal\Core\Entity\ContentEntityBase::isNewRevision
+   */
+  public function testNewRevisionRevert() {
+    $entity = EntityTestMulRev::create(['name' => 'EntityLoadedRevisionTest']);
+    $entity->save();
+
+    // Check that revision ID field is reset while the loaded revision ID is
+    // preserved when flagging a new revision.
+    $revision_id = $entity->getRevisionId();
+    $entity->setNewRevision();
+    $this->assertNull($entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+    $this->assertTrue($entity->isNewRevision());
+
+    // Check that manually reverting the new revision state by assigning a value
+    // to the revision ID field works as expected.
+    $key = $entity->getEntityType()->getKey('revision');
+    $entity->set($key, $revision_id);
+    $this->assertEquals($revision_id, $entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+    $this->assertFalse($entity->isNewRevision());
+
+    // Check that flagging a new revision again works correctly.
+    $entity->setNewRevision();
+    $this->assertNull($entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+    $this->assertTrue($entity->isNewRevision());
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypeConstraintsTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypeConstraintsTest.php
index a58b47c1ce..cdf4b96fb5 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypeConstraintsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypeConstraintsTest.php
@@ -24,7 +24,11 @@ public function testConstraintDefinition() {
     // Test reading the annotation. There should be two constraints, the defined
     // constraint and the automatically added EntityChanged constraint.
     $entity_type = $this->entityManager->getDefinition('entity_test_constraints');
-    $default_constraints = ['NotNull' => [], 'EntityChanged' => NULL];
+    $default_constraints = [
+      'NotNull' => [],
+      'EntityChanged' => NULL,
+      'EntityUntranslatableFields' => NULL,
+    ];
     $this->assertEqual($default_constraints, $entity_type->getConstraints());
 
     // Enable our test module and test extending constraints.
diff --git a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php
new file mode 100644
index 0000000000..55fbba9ed1
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\KernelTests\Core\ParamConverter;
+
+use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * Tests the entity converter when the "load_latest_revision" flag is set.
+ *
+ * @group ParamConverter
+ * @coversDefaultClass \Drupal\Core\ParamConverter\EntityConverter
+ */
+class EntityConverterLatestRevisionTest extends KernelTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'entity_test',
+    'user',
+    'language',
+    'system',
+  ];
+
+  /**
+   * The entity converter service.
+   *
+   * @var \Drupal\Core\ParamConverter\EntityConverter
+   */
+  protected $converter;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('entity_test_mulrev');
+    $this->installConfig(['system', 'language']);
+
+    $this->converter = $this->container->get('paramconverter.entity');
+
+    ConfigurableLanguage::createFromLangcode('de')->save();
+  }
+
+  /**
+   * Tests with no matching entity.
+   */
+  public function testNoEntity() {
+    $converted = $this->converter->convert(1, [
+      'load_latest_revision' => TRUE,
+      'type' => 'entity:entity_test_mulrev',
+    ], 'foo', []);
+    $this->assertEquals(NULL, $converted);
+  }
+
+  /**
+   * Tests with no pending revision.
+   */
+  public function testEntityNoPendingRevision() {
+    $entity = EntityTestMulRev::create();
+    $entity->save();
+
+    $converted = $this->converter->convert(1, [
+      'load_latest_revision' => TRUE,
+      'type' => 'entity:entity_test_mulrev',
+    ], 'foo', []);
+    $this->assertEquals($entity->getLoadedRevisionId(), $converted->getLoadedRevisionId());
+  }
+
+  /**
+   * Tests with a pending revision.
+   */
+  public function testEntityWithPendingRevision() {
+    $entity = EntityTestMulRev::create();
+    $entity->save();
+
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $entity->createNewRevision(FALSE);
+    $entity->save();
+
+    $converted = $this->converter->convert(1, [
+      'load_latest_revision' => TRUE,
+      'type' => 'entity:entity_test_mulrev',
+    ], 'foo', []);
+
+    $this->assertEquals($entity->getLoadedRevisionId(), $converted->getLoadedRevisionId());
+  }
+
+  /**
+   * Tests with a translated pending revision.
+   */
+  public function testWithTranslatedPendingRevision() {
+    $entity = EntityTestMulRev::create();
+    $entity->save();
+
+    // Create a translated pending revision.
+    $translated_entity = $entity->addTranslation('de');
+    $translated_entity->isDefaultRevision(FALSE);
+    $translated_entity->setNewRevision(TRUE);
+    $translated_entity->save();
+
+    // Change the site language so the converters will attempt to load entities
+    // with 'de'.
+    $this->config('system.site')->set('default_langcode', 'de')->save();
+
+    // The default loaded language is still 'en'.
+    EntityTestMulRev::load($entity->id());
+    $this->assertEquals('en', $entity->language()->getId());
+
+    // The converter will load the latest revision in the correct language.
+    $converted = $this->converter->convert(1, [
+      'load_latest_revision' => TRUE,
+      'type' => 'entity:entity_test_mulrev',
+    ], 'foo', []);
+    $this->assertEquals('de', $converted->language()->getId());
+    $this->assertEquals($translated_entity->getLoadedRevisionId(), $converted->getLoadedRevisionId());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php
index ee02caef0b..d9a53b4f33 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Entity\Entity;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityResolverManager;
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormInterface;
 use Drupal\Core\Form\FormStateInterface;
@@ -445,23 +446,153 @@ protected function setupEntityTypes() {
     $definition->expects($this->any())
       ->method('getClass')
       ->will($this->returnValue('Drupal\Tests\Core\Entity\SimpleTestEntity'));
+    $definition->expects($this->any())
+      ->method('isRevisionable')
+      ->willReturn(FALSE);
+    $revisionable_definition = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+    $revisionable_definition->expects($this->any())
+      ->method('getClass')
+      ->will($this->returnValue('Drupal\Tests\Core\Entity\SimpleTestEntity'));
+    $revisionable_definition->expects($this->any())
+      ->method('isRevisionable')
+      ->willReturn(TRUE);
     $this->entityManager->expects($this->any())
       ->method('getDefinitions')
       ->will($this->returnValue([
         'entity_test' => $definition,
+        'entity_test_rev' => $revisionable_definition,
       ]));
     $this->entityManager->expects($this->any())
       ->method('getDefinition')
-      ->will($this->returnCallback(function ($entity_type) use ($definition) {
+      ->will($this->returnCallback(function ($entity_type) use ($definition, $revisionable_definition) {
         if ($entity_type == 'entity_test') {
           return $definition;
         }
+        elseif ($entity_type === 'entity_test_rev') {
+          return $revisionable_definition;
+        }
         else {
           return NULL;
         }
       }));
   }
 
+  /**
+   * @covers ::setLatestRevisionFlag
+   *
+   * @dataProvider setLatestRevisionFlagTestCases
+   */
+  public function testSetLatestRevisionFlag($defaults, $parameters, $expected_parameters = FALSE) {
+    $route = new Route('/foo/{entity_test}', $defaults, [], [
+      'parameters' => $parameters,
+    ]);
+    $this->setupEntityTypes();
+    $this->entityResolverManager->setRouteOptions($route);
+    // If expected parameters have not been provided, assert they are unchanged.
+    $this->assertEquals($expected_parameters ?: $parameters, $route->getOption('parameters'));
+  }
+
+  /**
+   * Data provider for ::testSetLatestRevisionFlag.
+   */
+  public function setLatestRevisionFlagTestCases() {
+    return [
+      'Entity parameter not on an entity form' => [
+        [],
+        [
+          'entity_test' => [
+            'type' => 'entity:entity_test_rev',
+          ],
+        ],
+      ],
+      'Entity parameter on an entity form' => [
+        [
+          '_entity_form' => 'entity_test_rev.edit'
+        ],
+        [
+          'entity_test_rev' => [
+            'type' => 'entity:entity_test_rev',
+          ],
+        ],
+        [
+          'entity_test_rev' => [
+            'type' => 'entity:entity_test_rev',
+            'load_latest_revision' => TRUE,
+          ],
+        ],
+      ],
+      'Multiple entity parameters on an entity form' => [
+        [
+          '_entity_form' => 'entity_test_rev.edit'
+        ],
+        [
+          'entity_test_rev' => [
+            'type' => 'entity:entity_test_rev',
+          ],
+          'node' => [
+            'type' => 'entity:node',
+          ],
+        ],
+        [
+          'entity_test_rev' => [
+            'type' => 'entity:entity_test_rev',
+            'load_latest_revision' => TRUE,
+          ],
+          'node' => [
+            'type' => 'entity:node',
+          ],
+        ],
+      ],
+      'Overriden load_latest_revision flag does not change' => [
+        [
+          '_entity_form' => 'entity_test_rev.edit'
+        ],
+        [
+          'entity_test_rev' => [
+            'type' => 'entity:entity_test_rev',
+            'load_latest_revision' => FALSE,
+          ],
+        ],
+      ],
+      'Non-revisionable entity type will not change' => [
+        [
+          '_entity_form' => 'entity_test.edit'
+        ],
+        [
+          'entity_test' => [
+            'type' => 'entity:entity_test',
+          ],
+        ],
+        FALSE,
+        FALSE,
+      ],
+      'Overriden load_latest_revision flag does not change with multiple parameters' => [
+        [
+          '_entity_form' => 'entity_test_rev.edit'
+        ],
+        [
+          'entity_test_rev' => [
+            'type' => 'entity:entity_test_rev',
+          ],
+          'node' => [
+            'type' => 'entity:node',
+            'load_latest_revision' => FALSE,
+          ],
+        ],
+        [
+          'entity_test_rev' => [
+            'type' => 'entity:entity_test_rev',
+            'load_latest_revision' => TRUE,
+          ],
+          'node' => [
+            'type' => 'entity:node',
+            'load_latest_revision' => FALSE,
+          ],
+        ],
+      ],
+    ];
+  }
+
 }
 
 /**
