diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 5376644c54..66d78bd2ff 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -331,6 +331,26 @@ public function isDefaultRevision($new_value = NULL) {
     return $this->isNew() || $return;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isLatestRevision() {
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
+
+    return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isLatestAffectedRevision() {
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
+
+    return $this->getLoadedRevisionId() == $storage->getLatestAffectedRevisionId($this->id(), $this->language()->getId());
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -377,6 +397,14 @@ public function getRevisionId() {
     return $this->getEntityKey('revision');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isDefaultRevisionType() {
+    $revision_type = $this->get('revision_type')->value;
+    return isset($revision_type) && intval($revision_type) === 1;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -745,6 +773,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;
+        }
       }
     }
 
@@ -1380,10 +1414,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);
@@ -1404,4 +1443,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..2a44664eab 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -127,6 +127,18 @@ 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()) {
+      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+      $storage = $this->entityTypeManager->getStorage($entity_type->id());
+      $form['original_latest_revision_id'] = [
+        '#type' => 'hidden',
+        '#default_value' => $storage->getLatestRevisionId($this->entity->id()),
+      ];
+    }
+
     $form['footer'] = [
       '#type' => 'container',
       '#weight' => 99,
@@ -155,6 +167,15 @@ 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) {
+      // @todo Update the "latest_revision_id" field once we have introduced it
+      //   in https://www.drupal.org/project/drupal/issues/2784201.
+      $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 3053d23694..60320b31e5 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -85,6 +85,14 @@ public function isRevisionTranslationAffectedEnforced();
    */
   public function setRevisionTranslationAffectedEnforced($enforced);
 
+  /**
+   * Checks if this entity is the latest affected revision.
+   *
+   * @return bool
+   *   TRUE if the entity is the latest affected revision, FALSE otherwise.
+   */
+  public function isLatestAffectedRevision();
+
   /**
    * Gets the loaded Revision ID of the entity.
    *
@@ -106,4 +114,21 @@ public function getLoadedRevisionId();
    */
   public function updateLoadedRevisionId();
 
+  /**
+   * Checks whether the entity object was a default revision when it was saved.
+   *
+   * @return bool
+   *   TRUE if the entity object was a revision, FALSE otherwise.
+   */
+  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 a3bb20508f..42901ad3b0 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -148,6 +148,29 @@ protected function initFieldValues(ContentEntityInterface $entity, array $values
     $this->invokeHook('field_values_init', $entity);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTranslations(ContentEntityInterface $entity) {
+    if ($entity->isNew()) {
+      $result = FALSE;
+    }
+    else {
+      $query = $this->getQuery()
+        ->condition($this->entityType->getKey('id'), $entity->id())
+        ->condition($this->entityType->getKey('default_langcode'), FALSE)
+        ->range(0, 1);
+
+      if ($entity->getEntityType()->isRevisionable()) {
+        $query->allRevisions();
+      }
+
+      $result = (bool) $query->execute();
+    }
+
+    return $result;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -166,6 +189,117 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
     return $translation;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function createRevision(ContentEntityInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+    $new_revision = clone $entity;
+
+    // For translatable entities, create a merged revision of the active
+    // translation and the other translations in the default revision. This
+    // allows to create pending revisions that can always be saved as the new
+    // default revision without reverting changes in other languages.
+    if (!$entity->isNew() && !$entity->isDefaultRevision() && $entity->isTranslatable() && $this->hasTranslations($entity)) {
+      $active_langcode = $entity->language()->getId();
+
+      // By default we copy untranslatable field values from the default
+      // revision, unless they are configured to affect only the default
+      // translation. This way we can ensure we always have only one affected
+      // translation in pending revisions. This constraint is enforced by
+      // EntityUntranslatableFieldsConstraintValidator.
+      if (!isset($keep_untranslatable_fields)) {
+        $keep_untranslatable_fields = $entity->isDefaultTranslation() && $entity->isDefaultTranslationAffectedOnly();
+      }
+
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
+      $default_revision = $this->load($entity->id());
+      /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $definitions */
+      $definitions = $default_revision->getFieldDefinitions();
+      foreach ($default_revision->getTranslationLanguages() as $langcode => $language) {
+        if ($langcode == $active_langcode) {
+          continue;
+        }
+
+        $default_revision_translation = $default_revision->getTranslation($langcode);
+        $new_revision_translation = $new_revision->hasTranslation($langcode) ?
+          $new_revision->getTranslation($langcode) : $new_revision->addTranslation($langcode);
+
+        foreach ($definitions as $field_name => $definition) {
+          if (!$keep_untranslatable_fields || $definition->isTranslatable()) {
+            $new_revision_translation->set($field_name, $default_revision_translation->get($field_name)->getValue());
+          }
+        }
+
+        // Make sure the "revision_translation_affected" flag is recalculated.
+        $new_revision_translation->setRevisionTranslationAffected(NULL);
+
+        // No need to copy untranslatable field values more than once.
+        $keep_untranslatable_fields = TRUE;
+      }
+
+      // 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->entityType->getKey('revision'), $entity->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;
+    }
+
+    // Eventually mark the new revision as such.
+    $new_revision->setNewRevision();
+    $new_revision->isDefaultRevision($default);
+
+    // Actually make sure the current translation is marked as affected, even if
+    // there are no explicit changes, to be sure this revision can be related
+    // to the correct translation.
+    $new_revision->setRevisionTranslationAffected(TRUE);
+
+    return $new_revision;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLatestRevisionId($entity_id) {
+    if (!$this->entityType->isRevisionable()) {
+      return NULL;
+    }
+
+    $result = $this->getQuery()
+      ->latestRevision()
+      ->condition($this->entityType->getKey('id'), $entity_id)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    return key($result);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLatestAffectedRevisionId($entity_id, $langcode) {
+    if (!$this->entityType->isRevisionable()) {
+      return NULL;
+    }
+
+    if (!$this->entityType->isTranslatable()) {
+      return $this->getLatestRevisionId($entity_id);
+    }
+
+    $result = $this->getQuery()
+      ->allRevisions()
+      ->condition($this->entityType->getKey('id'), $entity_id)
+      ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
+      ->range(0, 1)
+      ->sort($this->entityType->getKey('revision'), 'DESC')
+      ->accessCheck(FALSE)
+      ->execute();
+
+    return key($result);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -334,6 +468,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;
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php
index 678937bdc0..5fa9a28072 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php
@@ -7,6 +7,25 @@
  */
 interface ContentEntityStorageInterface extends EntityStorageInterface, RevisionableStorageInterface {
 
+  /**
+   * Checks whether the specified has stored translations.
+   *
+   * A revisionable entity can have translations in a pending revision, hence
+   * the default revision may appear as not translated. This method allows to
+   * determine whether the entity has any translation in the storage and thus
+   * should be considered as multilingual.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity object to be checked.
+   *
+   * @return bool
+   *   TRUE if the entity has at least one translation in any revision, FALSE
+   *   otherwise.
+   *
+   * @see \Drupal\Core\Entity\ContentEntityInterface::getTranslationLanguages()
+   */
+  public function hasTranslations(ContentEntityInterface $entity);
+
   /**
    * Constructs a new entity translation object, without permanently saving it.
    *
@@ -23,6 +42,26 @@
    */
   public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []);
 
+  /**
+   * Creates a new revision starting off from the specified entity object.
+   *
+   * When dealing with a translatable entity, this will merge the default
+   * revision with the latest affected revision for the entity active language.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity object being modified.
+   * @param bool $default
+   *   (optional) Whether the new revision should be marked as default. Defaults
+   *   to TRUE.
+   * @param bool|null $keep_untranslatable_fields
+   *   (optional) Whether untranslatable field values should be kept or copied
+   *   from the default revision when generating a merged revision. Defaults to
+   *   FALSE.
+   *
+   * @return \Drupal\Core\Entity\ContentEntityInterface
+   *   A new entity revision object.
+   */
+  public function createRevision(ContentEntityInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL);
 
   /**
    * Creates an entity with sample field values.
@@ -40,4 +79,17 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
    */
   public function createWithSampleValues($bundle = FALSE, array $values = []);
 
+  /**
+   * Returns the latest affected revision identifier for an entity translation.
+   *
+   * @param int $entity_id
+   *   The entity identifier.
+   * @param string $langcode
+   *   The language code of the translation.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  public function getLatestAffectedRevisionId($entity_id, $langcode);
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManager.php b/core/lib/Drupal/Core/Entity/EntityFieldManager.php
index 63c35e9743..edd4fc8326 100644
--- a/core/lib/Drupal/Core/Entity/EntityFieldManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php
@@ -221,6 +221,17 @@ protected function buildBaseFieldDefinitions($entity_type_id) {
     }
 
     // Make sure that revisionable entity types are correctly defined.
+    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 and translatable entity types are correctly
+    // defined.
     if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
       // The 'revision_translation_affected' field should always be defined.
       // This field has been added unconditionally in Drupal 8.4.0 and it is
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/KeyValueStore/KeyValueContentEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
index 2c57405c19..4f92b36417 100644
--- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
@@ -18,6 +18,20 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
     //   https://www.drupal.org/node/2618436.
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTranslations(ContentEntityInterface $entity) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createRevision(ContentEntityInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+    return NULL;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -30,4 +44,18 @@ public function loadMultipleRevisions(array $revision_ids) {
     return [];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getLatestRevisionId($entity_id) {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLatestAffectedRevisionId($entity_id, $langcode) {
+    return NULL;
+  }
+
 }
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..6179295743 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,8 @@
 
 namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
 
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\RevisionableInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 
@@ -14,15 +16,39 @@ 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;
+
+      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+      $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
+
+      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.
+        // @todo Check the "latest_revision_id" field once we have introduced it
+        //   in https://www.drupal.org/project/drupal/issues/2784201.
+        $valid = $entity->original_latest_revision_id == $storage->getLatestRevisionId($entity->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.
+        // @todo Check the "latest_revision_id" field once we have introduced it
+        //   in https://www.drupal.org/project/drupal/issues/2784201.
+        $check_changed_time = $entity->original_latest_revision_id == $entity->getLoadedRevisionId();
+      }
+
+      if ($valid && $check_changed_time && $entity instanceof EntityChangedInterface) {
+        /** @var \Drupal\Core\Entity\EntityChangedInterface|\Drupal\Core\Entity\RevisionableInterface $stored_entity */
+        $stored_entity = $storage->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);
       }
     }
   }
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..d4bd32345f
--- /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', // FIXME
+    ];
+    $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys()));
+
+    return $fields;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/RevisionableInterface.php b/core/lib/Drupal/Core/Entity/RevisionableInterface.php
index 14690e3c54..e25bc25443 100644
--- a/core/lib/Drupal/Core/Entity/RevisionableInterface.php
+++ b/core/lib/Drupal/Core/Entity/RevisionableInterface.php
@@ -51,6 +51,14 @@ public function getRevisionId();
    */
   public function isDefaultRevision($new_value = NULL);
 
+  /**
+   * Checks if this entity is the latest revision.
+   *
+   * @return bool
+   *   TRUE if the entity is the latest revision, FALSE otherwise.
+   */
+  public function isLatestRevision();
+
   /**
    * Acts on a revision before it gets saved.
    *
diff --git a/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
index 871fcc5435..87bea271c4 100644
--- a/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
@@ -40,4 +40,15 @@ public function loadMultipleRevisions(array $revision_ids);
    */
   public function deleteRevision($revision_id);
 
+  /**
+   * Returns the latest revision identifier for an entity.
+   *
+   * @param int|string $entity_id
+   *   The entity identifier.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  public function getLatestRevisionId($entity_id);
+
 }
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 1f6b42b609..2f91fc23b0 100644
--- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
+++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
@@ -3,10 +3,11 @@
 namespace Drupal\Core\ParamConverter;
 
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityStorageInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
-use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\TypedData\TranslatableInterface;
 use Symfony\Component\Routing\Route;
 
@@ -72,7 +73,7 @@ public function convert($value, $definition, $name, array $defaults) {
     // "load_latest_revision" flag, load the latest revision.
     if ($entity instanceof ContentEntityInterface && !empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) {
       $latest_revision_id = $this->getLatestRevisionId($storage, $entity_definition, $value);
-      if ($entity->getLoadedRevisionId() !== $latest_revision_id) {
+      if (!$entity->getLoadedRevisionId() !== $latest_revision_id) {
         $entity = $storage->loadRevision($latest_revision_id);
       }
     }
@@ -89,28 +90,48 @@ public function convert($value, $definition, $name, array $defaults) {
   /**
    * Get the latest revision ID.
    *
-   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   * @param \Drupal\Core\Entity\ContentEntityStorageInterface $storage
    *   The entity storage.
-   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_definition
-   *   The entity definition.
-   * @param mixed $value
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param int $entity_id
    *   The raw value.
    *
    * @return int
-   *   The latest revision ID for a given entity.
+   *   The latest (affected) revision ID for a given entity.
    */
-  protected function getLatestRevisionId(EntityStorageInterface $storage, EntityTypeInterface $entity_definition, $value) {
+  protected function getLatestRevisionId(ContentEntityStorageInterface $storage, EntityTypeInterface $entity_type, $entity_id) {
+    $latest_revision_id = NULL;
+    // TODO Inject this.
+    $language_manager = \Drupal::languageManager();
+
     // @todo, replace this query with a standardized way of getting the
     //   latest revision in https://www.drupal.org/node/2784201.
-    $result = $storage
-      ->getQuery()
-      ->latestRevision()
-      ->condition($entity_definition->getKey('id'), $value)
-      // The entity converter is not concerned with access checking, skip the
-      // access check when looking up the latest revision.
-      ->accessCheck(FALSE)
-      ->execute();
-    return key($result);
+    if ($entity_type->isTranslatable() && $language_manager->isMultilingual()) {
+      $langcode = $language_manager
+        ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
+        ->getId();
+      $latest_revision_id = $storage->getLatestAffectedRevisionId($entity_id, $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 = $latest_revision_id ? $storage->loadRevision($latest_revision_id) : NULL;
+      if (!$revision || ($revision->isDefaultRevisionType() && !$revision->isDefaultRevision())) {
+        $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 = $storage->getLatestRevisionId($entity_id);
+    }
+
+    return (string) $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..6f338cdcab 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);
 
@@ -245,23 +244,19 @@ public function entityTranslationDelete(EntityInterface $translation) {
    * @see EntityFieldManagerInterface::getExtraFields()
    */
   public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     if (!$this->moderationInfo->isModeratedEntity($entity)) {
       return;
     }
-    if (!$this->moderationInfo->isLatestRevision($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)) {
+    // The moderation form should not be displayed when viewing old revisions.
+    // However it should be displayed if the latest affected revision is a
+    // pending revision.
+    if (!$entity->isDefaultRevision() && (($entity->isDefaultRevisionType() && $this->moderationInfo->isDefaultRevisionPublished($entity)) || !$this->moderationInfo->isLatestAffectedRevision($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 666ee2647f..e48157de2d 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,37 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
     return $fields;
   }
 
+  /**
+   * Replaces the entity form entity object with a proper revision object.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being edited.
+   * @param string $operation
+   *   The entity form operation.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The 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()) {
+      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+      $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
+      // Generate a proper revision object for the current entity. This allows
+      // to correctly handle translatable entities having pending revisions.
+      $new_revision = $storage->createRevision($entity,FALSE);
+      // Restore the revision ID as other modules may expect to find it still
+      // populated. The entity object wil be marked as a new revision on
+      // presave.
+      $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 +324,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 +351,23 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id
     }
   }
 
+  /**
+   * Checks whether the entity form should get content moderation.
+   *
+   * @param \Drupal\Core\Form\FormInterface $form_object
+   *   The form object.
+   * @param string $operation
+   *   The form operation.
+   *
+   * @return bool
+   *   TRUE if the form should get form moderation, FALSE otherwise.
+   */
+  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 507ef54485..98f6fdf2b9 100644
--- a/core/modules/content_moderation/src/Form/EntityModerationForm.php
+++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php
@@ -138,6 +138,9 @@ public function buildForm(array $form, FormStateInterface $form_state, ContentEn
   public function submitForm(array &$form, FormStateInterface $form_state) {
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     $entity = $form_state->get('entity');
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
+    $entity = $storage->createRevision($entity, $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 0fc9af4d2c..2edcddae99 100644
--- a/core/modules/content_moderation/src/ModerationInformation.php
+++ b/core/modules/content_moderation/src/ModerationInformation.php
@@ -124,23 +124,34 @@ 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) {
+    return $entity->isLatestAffectedRevision();
   }
 
   /**
    * {@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)) {
+      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+      $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
+      $latest_revision_id = $storage->getLatestAffectedRevisionId($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 = $storage->loadRevision($latest_revision_id);
+        $result = !$latest_revision->isDefaultRevisionType();
+      }
+    }
+    return $result;
   }
 
   /**
@@ -148,8 +159,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();
   }
@@ -159,7 +169,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 since the statically cached object may
+    // be 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..761d7158a0 100644
--- a/core/modules/content_moderation/src/ModerationInformationInterface.php
+++ b/core/modules/content_moderation/src/ModerationInformationInterface.php
@@ -101,29 +101,28 @@ public function getDefaultRevisionId($entity_type_id, $entity_id);
   public function getAffectedRevisionTranslation(ContentEntityInterface $entity);
 
   /**
-   * Determines if pending revisions are allowed.
-   *
-   * @internal
+   * Determines if an entity is a latest revision.
    *
    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
-   *   The content entity.
+   *   A revisionable content entity.
    *
    * @return bool
-   *   If pending revisions are allowed.
+   *   TRUE if the specified object is the latest revision of its entity,
+   *   FALSE otherwise.
    */
-  public function isPendingRevisionAllowed(ContentEntityInterface $entity);
+  public function isLatestRevision(ContentEntityInterface $entity);
 
   /**
-   * Determines if an entity is a latest revision.
+   * Determines if an entity is the latest affected revision.
    *
    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
    *   A revisionable content entity.
    *
    * @return bool
-   *   TRUE if the specified object is the latest revision of its entity,
-   *   FALSE otherwise.
+   *   TRUE if the specified object is the latest affected revision for the
+   *   entity language, FALSE otherwise.
    */
-  public function isLatestRevision(ContentEntityInterface $entity);
+  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..2421c4d6b8 100644
--- a/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
+++ b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
@@ -2,11 +2,9 @@
 
 namespace Drupal\content_moderation\ParamConverter;
 
-use Drupal\Core\Entity\EntityInterface;
+use Drupal\content_moderation\ModerationInformationInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\ParamConverter\EntityConverter;
-use Drupal\Core\TypedData\TranslatableInterface;
-use Drupal\content_moderation\ModerationInformationInterface;
 use Symfony\Component\Routing\Route;
 
 /**
@@ -83,23 +81,8 @@ protected function isEditFormPage(Route $route) {
    * {@inheritdoc}
    */
   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;
-      }
-    }
-
-    return $entity;
+    // @todo Deprecate this in https://www.drupal.org/node/2924812.
+    return parent::convert($value, $definition, $name, $defaults);
   }
 
 }
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..20853d9f4c
--- /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['original_latest_revision_id']['#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 13fd0c0571..b93ea622e7 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',
   ];
 
   /**
@@ -295,14 +297,6 @@ public function testContentTranslationNodeForm() {
     $this->drupalGet($latest_version_path, ['language' => $french]);
     $this->assertTrue($this->xpath('//ul[@class="entity-moderation-form"]'));
 
-    // It should not be possible to add a new english revision.
-    $this->drupalGet($edit_path);
-    $this->assertSession()->fieldNotExists('moderation_state[0][state]');
-    $this->assertSession()->pageTextContains('Unable to save this Moderated content.');
-
-    $this->clickLink('Publish');
-    $this->assertSession()->fieldValueEquals('body[0][value]', 'Third version of the content.');
-
     $this->drupalGet($edit_path);
     $this->clickLink('Delete');
     $this->assertSession()->buttonExists('Delete');
@@ -323,7 +317,7 @@ public function testContentTranslationNodeForm() {
     $this->drupalGet($latest_version_path, ['language' => $french]);
     $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
 
-    // Now we can publish the english (revision 5).
+    // Publish the english pending revision (revision 5).
     $this->drupalGet($edit_path);
     $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
@@ -336,13 +330,13 @@ public function testContentTranslationNodeForm() {
     $this->drupalGet($latest_version_path);
     $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
 
-    // Make sure we're allowed to create a pending french revision.
+    // Make sure we are allowed to create a pending french revision.
     $this->drupalGet($edit_path, ['language' => $french]);
     $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 
-    // Add a english pending revision (revision 6).
+    // Add an english pending revision (revision 6).
     $this->drupalGet($edit_path);
     $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
@@ -354,16 +348,10 @@ public function testContentTranslationNodeForm() {
 
     $this->drupalGet($latest_version_path);
     $this->assertTrue($this->xpath('//ul[@class="entity-moderation-form"]'));
-
-    // Make sure we're not allowed to create a pending french revision.
-    $this->drupalGet($edit_path, ['language' => $french]);
-    $this->assertSession()->fieldNotExists('moderation_state[0][state]');
-    $this->assertSession()->pageTextContains('Unable to save this Moderated content.');
-
     $this->drupalGet($latest_version_path, ['language' => $french]);
     $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
 
-    // We should be able to publish the english pending revision (revision 7)
+    // Publish the english pending revision (revision 7)
     $this->drupalGet($edit_path);
     $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
@@ -376,44 +364,17 @@ public function testContentTranslationNodeForm() {
     $this->drupalGet($latest_version_path);
     $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
 
-    // Make sure we're allowed to create a pending french revision.
+    // Make sure we are allowed to create a pending french revision.
     $this->drupalGet($edit_path, ['language' => $french]);
     $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 
-    // Make sure we're allowed to create a pending english revision.
-    $this->drupalGet($edit_path);
-    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
-    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
-    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
-
-    // Create new moderated content. (revision 1).
-    $this->drupalPostForm('node/add/moderated_content', [
-      'title[0][value]' => 'Second moderated content',
-      'body[0][value]' => 'First version of the content.',
-      'moderation_state[0][state]' => 'published',
-    ], t('Save'));
-
-    $node = $this->drupalGetNodeByTitle('Second moderated content');
-    $this->assertTrue($node->language(), 'en');
-    $edit_path = sprintf('node/%d/edit', $node->id());
-    $translate_path = sprintf('node/%d/translations/add/en/fr', $node->id());
-
-    // Add a pending revision (revision 2).
+    // Make sure we are allowed to create a pending english revision.
     $this->drupalGet($edit_path);
     $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
     $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
-    $this->drupalPostForm(NULL, [
-      'body[0][value]' => 'Second version of the content.',
-      'moderation_state[0][state]' => 'draft',
-    ], t('Save'));
-
-    // It shouldn't be possible to translate as we have a pending revision.
-    $this->drupalGet($translate_path);
-    $this->assertSession()->fieldNotExists('moderation_state[0][state]');
-    $this->assertSession()->pageTextContains('Unable to save this Moderated content.');
 
     // Create new moderated content (revision 1).
     $this->drupalPostForm('node/add/moderated_content', [
@@ -444,11 +405,6 @@ public function testContentTranslationNodeForm() {
       'moderation_state[0][state]' => 'draft',
     ], t('Save (this translation)'));
 
-    // Editing the original translation should not be possible.
-    $this->drupalGet($edit_path);
-    $this->assertSession()->fieldNotExists('moderation_state[0][state]');
-    $this->assertSession()->pageTextContains('Unable to save this Moderated content.');
-
     // Updating and publishing the french translation is still possible.
     $this->drupalGet($edit_path, ['language' => $french]);
     $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
@@ -530,4 +486,43 @@ 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'));
+    /** @var \Drupal\node\NodeStorageInterface $node_storage */
+    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
+    $revision_id = $node_storage->getLatestRevisionId($node->id());
+
+    // 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, $node_storage->getLatestRevisionId($node->id()));
+    $this->assertSession()
+      ->responseContains((new EntityChangedConstraint())->message);
+  }
+
 }
diff --git a/core/modules/content_translation/config/schema/content_translation.schema.yml b/core/modules/content_translation/config/schema/content_translation.schema.yml
index 808a6e73e8..8cf6e2c8c4 100644
--- a/core/modules/content_translation/config/schema/content_translation.schema.yml
+++ b/core/modules/content_translation/config/schema/content_translation.schema.yml
@@ -18,3 +18,17 @@ language.content_settings.*.*.third_party.content_translation:
     enabled:
       type: boolean
       label: 'Content translation enabled'
+
+content_translation.settings:
+  type: config_object
+  label: 'Content translation settings'
+  mapping:
+    untranslatable_fields_hide:
+      type: sequence
+      label: 'Entity types'
+      sequence:
+        type: sequence
+        label: 'Bundles'
+        sequence:
+          type: boolean
+          label: 'Hide shared fields on translation forms'
diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc
index bf33ba0af0..2f8b834fe5 100644
--- a/core/modules/content_translation/content_translation.admin.inc
+++ b/core/modules/content_translation/content_translation.admin.inc
@@ -83,6 +83,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
     return;
   }
 
+  /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
   $content_translation_manager = \Drupal::service('content_translation.manager');
   $default = $form['entity_types']['#default_value'];
   foreach ($default as $entity_type_id => $enabled) {
@@ -94,6 +95,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
 
   $entity_manager = Drupal::entityManager();
   $bundle_info_service = \Drupal::service('entity_type.bundle.info');
+  $config = \Drupal::configFactory()->get('content_translation.settings');
   foreach ($form['#labels'] as $entity_type_id => $label) {
     $entity_type = $entity_manager->getDefinition($entity_type_id);
     $storage_definitions = $entity_type instanceof ContentEntityTypeInterface ? $entity_manager->getFieldStorageDefinitions($entity_type_id) : [];
@@ -110,6 +112,22 @@ function _content_translation_form_language_content_settings_form_alter(array &$
         continue;
       }
 
+      // Displayed the "shared fields widgets" toggle.
+      $key = 'untranslatable_fields_hide.' . $entity_type_id . '.' . $bundle;
+      $form['settings'][$entity_type_id][$bundle]['settings']['untranslatable_fields_hide'] = [
+        '#type' => 'checkbox',
+        '#title' => t('Hide shared fields on translation forms'),
+        '#description' => t('This will prevent editors from changing field values shared among translations when editing content translations.'),
+        '#default_value' => $config->get($key),
+        '#states' => [
+          'visible' => [
+            ':input[name="settings[' . $entity_type_id . '][' . $bundle . '][translatable]"]' => [
+              'checked' => TRUE,
+            ],
+          ],
+        ],
+      ];
+
       $fields = $entity_manager->getFieldDefinitions($entity_type_id, $bundle);
       if ($fields) {
         foreach ($fields as $field_name => $definition) {
@@ -317,6 +335,9 @@ function content_translation_form_language_content_settings_validate(array $form
  * @see content_translation_admin_settings_form_validate()
  */
 function content_translation_form_language_content_settings_submit(array $form, FormStateInterface $form_state) {
+  /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
+  $manager = \Drupal::service('content_translation.manager');
+  $config = \Drupal::configFactory()->getEditable('content_translation.settings');
   $entity_types = $form_state->getValue('entity_types');
   $settings = &$form_state->getValue('settings');
 
@@ -347,7 +368,11 @@ function content_translation_form_language_content_settings_submit(array $form,
       }
       if (isset($bundle_settings['translatable'])) {
         // Store whether a bundle has translation enabled or not.
-        \Drupal::service('content_translation.manager')->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
+        $manager->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
+
+        // TODO Should we use the content translation manager for this too?
+        $key = 'untranslatable_fields_hide.' . $entity_type_id . '.' . $bundle;
+        $config->set($key, (bool) $bundle_settings['settings']['untranslatable_fields_hide']);
 
         // Save translation_sync settings.
         if (!empty($bundle_settings['columns'])) {
@@ -367,8 +392,10 @@ function content_translation_form_language_content_settings_submit(array $form,
       }
     }
   }
+
+  $config->save();
+
   // Ensure entity and menu router information are correctly rebuilt.
   \Drupal::entityManager()->clearCachedDefinitions();
   \Drupal::service('router.builder')->setRebuildNeeded();
-
 }
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 6a0c229adb..0cd43a0a06 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -161,9 +161,14 @@ function content_translation_entity_type_alter(array &$entity_types) {
  * Implements hook_entity_bundle_info_alter().
  */
 function content_translation_entity_bundle_info_alter(&$bundles) {
-  foreach ($bundles as $entity_type => &$info) {
+  /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
+  $manager = \Drupal::service('content_translation.manager');
+  $config = \Drupal::configFactory()->get('content_translation.settings');
+  foreach ($bundles as $entity_type_id => &$info) {
     foreach ($info as $bundle => &$bundle_info) {
-      $bundle_info['translatable'] = \Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle);
+      $bundle_info['translatable'] = $manager->isEnabled($entity_type_id, $bundle);
+      $key = 'untranslatable_fields_hide.' . $entity_type_id . '.' . $bundle;
+      $bundle_info['untranslatable_fields.default_translation_affected'] = $config->get($key);
     }
   }
 }
@@ -319,6 +324,11 @@ function content_translation_form_alter(array &$form, FormStateInterface $form_s
       }
     }
 
+    // The footer region, if defined, may contain multilingual widgets so we
+    // need to always display it.
+    if (isset($form['footer'])) {
+      $form['footer']['#multilingual'] = TRUE;
+    }
   }
 }
 
@@ -413,10 +423,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/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php
index 7e6a7ca3ea..d76b5c3645 100644
--- a/core/modules/content_translation/src/ContentTranslationHandler.php
+++ b/core/modules/content_translation/src/ContentTranslationHandler.php
@@ -512,6 +512,13 @@ public function entityFormSharedElements($element, FormStateInterface $form_stat
       $ignored_types = array_flip(['actions', 'value', 'hidden', 'vertical_tabs', 'token', 'details']);
     }
 
+    /** @var \Drupal\Core\Entity\ContentEntityForm $form_object */
+    $form_object = $form_state->getFormObject();
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $form_object->getEntity();
+    $display_translatability_clue = !$entity->isDefaultTranslationAffectedOnly();
+    $hide_shared_fields = $entity->isDefaultTranslationAffectedOnly() && !$entity->isDefaultTranslation();
+
     foreach (Element::children($element) as $key) {
       if (!isset($element[$key]['#type'])) {
         $this->entityFormSharedElements($element[$key], $form_state, $form);
@@ -526,8 +533,10 @@ public function entityFormSharedElements($element, FormStateInterface $form_stat
           // If we are displaying a multilingual entity form we need to provide
           // translatability clues, otherwise the shared form elements should be
           // hidden.
-          if (!$form_state->get(['content_translation', 'translation_form'])) {
-            $this->addTranslatabilityClue($element[$key]);
+          if (!$hide_shared_fields && !$form_state->get(['content_translation', 'translation_form'])) {
+            if ($display_translatability_clue) {
+              $this->addTranslatabilityClue($element[$key]);
+            }
           }
           else {
             $element[$key]['#access'] = FALSE;
diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php
index 190778d42c..2e95da94c4 100644
--- a/core/modules/content_translation/src/Controller/ContentTranslationController.php
+++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
@@ -87,6 +87,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.
@@ -99,6 +100,9 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
 
     $rows = [];
     $show_source_column = FALSE;
+    $default_revision = $entity;
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->entityTypeManager()->getStorage($entity_type_id);
 
     if ($this->languageManager()->isMultilingual()) {
       // Determine whether the current entity is translatable.
@@ -121,6 +125,16 @@ 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 = $storage->getLatestAffectedRevisionId($entity->id(), $langcode);
+          $entity = $latest_revision_id ? $storage->loadRevision($latest_revision_id) : $default_revision;
+          $translations = $entity->getTranslationLanguages();
+        }
+
         $add_url = new Url(
           "entity.$entity_type_id.content_translation_add",
           [
@@ -330,8 +344,19 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
    *   A processed form array ready to be rendered.
    */
   public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     $entity = $route_match->getParameter($entity_type_id);
 
+    // In case of a pending revision, make sure we load the latest affected
+    // revision for the source language, otherwise the form initial values may
+    // not be up to date.
+    if (!$entity->isDefaultRevision()) {
+      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+      $storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId());
+      $revision_id = $storage->getLatestAffectedRevisionId($entity->id(), $source->getId());
+      $entity = $storage->loadRevision($revision_id);
+    }
+
     // @todo Exploit the upcoming hook_entity_prepare() when available.
     // See https://www.drupal.org/node/1810394.
     $this->prepareTranslation($entity, $source, $target);
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/content_translation/tests/src/Functional/ContentTranslationTestBase.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationTestBase.php
index ec7f87dd1b..d3e81a6b4a 100644
--- a/core/modules/content_translation/tests/src/Functional/ContentTranslationTestBase.php
+++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationTestBase.php
@@ -138,7 +138,7 @@ protected function getEditorPermissions() {
    * Returns an array of permissions needed for the administrator.
    */
   protected function getAdministratorPermissions() {
-    return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer content translation']);
+    return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer languages', 'administer content translation']);
   }
 
   /**
diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php
new file mode 100644
index 0000000000..fa0dd01b60
--- /dev/null
+++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\Tests\content_translation\Functional;
+
+use Drupal\Core\Url;
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * Tests the untranslatable fields behaviors.
+ *
+ * @group content_translation
+ */
+class ContentTranslationUntranslatableFieldsTest extends ContentTranslationTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['language', 'content_translation', 'entity_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Configure one field as untranslatable.
+    $this->drupalLogin($this->administrator);
+    $edit = [
+      'settings[' . $this->entityTypeId . '][' . $this->bundle . '][fields][' . $this->fieldName . ']' => 0,
+    ];
+    $this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
+
+    /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
+    $entity_field_manager = $this->container->get('entity_field.manager');
+    $entity_field_manager->clearCachedFieldDefinitions();
+    $definitions = $entity_field_manager->getFieldDefinitions($this->entityTypeId, $this->bundle);
+    $this->assertFalse($definitions[$this->fieldName]->isTranslatable());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditorPermissions() {
+    return array_merge(parent::getTranslatorPermissions(), ['administer entity_test content', 'view test entity']);
+  }
+
+  /**
+   * Tests that hiding untransltable field widgets works correctly.
+   */
+  public function testHiddenWidgets() {
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+    $entity_type_manager = $this->container->get('entity_type.manager');
+    $id = $this->createEntity([], 'en');
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $entity_type_manager
+      ->getStorage($this->entityTypeId)
+      ->load($id);
+
+    // Check that the untranslatable field widget is displayed on the edit form
+    // and not translatability clue is displayed yet.
+    $this->drupalGet($entity->toUrl('edit-form'));
+    $field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
+    $this->assertNotEmpty($this->xpath($field_xpath));
+    $clue_xpath = '//label[@for="edit-' . strtr($this->fieldName, '_', '-') . '-0-value"]/span[text()="(all languages)"]';
+    $this->assertEmpty($this->xpath($clue_xpath));
+
+    // Add a translation and check that the untranslatable field widget is
+    // displayed on the translation and edit forms along with the
+    // translatability clue.
+    $add_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
+      $entity->getEntityTypeId() => $entity->id(),
+      'source' => 'en',
+      'target' => 'it'
+    ]);
+    $this->drupalGet($add_url);
+    $this->assertNotEmpty($this->xpath($field_xpath));
+    $this->assertNotEmpty($this->xpath($clue_xpath));
+    $this->drupalPostForm(NULL, [], 'Save');
+
+    // Check that the widget is displayed along with its clue in the edit form
+    // for both languages.
+    $this->drupalGet($entity->toUrl('edit-form'));
+    $this->assertNotEmpty($this->xpath($field_xpath));
+    $this->assertNotEmpty($this->xpath($clue_xpath));
+    $it_language = ConfigurableLanguage::load('it');
+    $this->drupalGet($entity->toUrl('edit-form', ['language' => $it_language]));
+    $this->assertNotEmpty($this->xpath($field_xpath));
+    $this->assertNotEmpty($this->xpath($clue_xpath));
+
+    // Configure untranslatable field widgets to be hidden on non-default
+    // language edit forms.
+    $edit = [
+      'settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][untranslatable_fields_hide]' => 1,
+    ];
+    $this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
+
+    // Verify that the widget is displayed in the default language edit form,
+    // but no clue is displayed.
+    $this->drupalGet($entity->toUrl('edit-form'));
+    $field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
+    $this->assertNotEmpty($this->xpath($field_xpath));
+    $this->assertEmpty($this->xpath($clue_xpath));
+
+    // Verify no widget is displayed on the non-default language edit form.
+    $this->drupalGet($entity->toUrl('edit-form', ['language' => $it_language]));
+    $this->assertEmpty($this->xpath($field_xpath));
+    $this->assertEmpty($this->xpath($clue_xpath));
+  }
+}
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index 48401475fc..8804b18a70 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -173,6 +173,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 +188,17 @@ 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()) {
+
+        // We treat also the latest affected revision as current revision, if it
+        // was the default revision, as its values for the current language will
+        // be the same of the current default revision in this case.
+        $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 +217,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/NodeRevisionRevertForm.php b/core/modules/node/src/Form/NodeRevisionRevertForm.php
index fc6fe023f5..e18ad77610 100644
--- a/core/modules/node/src/Form/NodeRevisionRevertForm.php
+++ b/core/modules/node/src/Form/NodeRevisionRevertForm.php
@@ -28,7 +28,7 @@ class NodeRevisionRevertForm extends ConfirmFormBase {
   /**
    * The node storage.
    *
-   * @var \Drupal\Core\Entity\EntityStorageInterface
+   * @var \Drupal\node\NodeStorageInterface
    */
   protected $nodeStorage;
 
diff --git a/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
index 0c68d8b254..a6a4d85d8a 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->getTranslation($this->langcode)->isDefaultTranslation(),
+      '#access' => !$default_translation_affected,
     ];
 
     return $form;
@@ -101,24 +106,9 @@ 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');
+    $translation = $revision->getTranslation($this->langcode);
+    return $this->nodeStorage->createRevision($translation, 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/src/Tests/NodeRevisionsTest.php b/core/modules/node/src/Tests/NodeRevisionsTest.php
index da5b260537..066193ddab 100644
--- a/core/modules/node/src/Tests/NodeRevisionsTest.php
+++ b/core/modules/node/src/Tests/NodeRevisionsTest.php
@@ -64,6 +64,9 @@ protected function setUp() {
     $field = FieldConfig::create($field_definition);
     $field->save();
 
+    // Enable translation for page nodes.
+    \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
+
     // Create and log in user.
     $web_user = $this->drupalCreateUser(
       [
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/quickedit/quickedit.module b/core/modules/quickedit/quickedit.module
index 3d27050078..999445b377 100644
--- a/core/modules/quickedit/quickedit.module
+++ b/core/modules/quickedit/quickedit.module
@@ -11,7 +11,6 @@
  * entities, enabling them for in-place editing.
  */
 
-use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -130,10 +129,10 @@ function quickedit_preprocess_page_title(&$variables) {
 function quickedit_preprocess_field(&$variables) {
   $variables['#cache']['contexts'][] = 'user.permissions';
   $element = $variables['element'];
-  /** @var $entity \Drupal\Core\Entity\EntityInterface */
+  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
   $entity = $element['#object'];
 
-  if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !_quickedit_entity_is_latest_revision($entity)) {
+  if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !$entity->isLatestRevision()) {
     return;
   }
 
@@ -157,41 +156,11 @@ function quickedit_preprocess_field(&$variables) {
  * Implements hook_entity_view_alter().
  */
 function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
   $build['#cache']['contexts'][] = 'user.permissions';
-  if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !_quickedit_entity_is_latest_revision($entity)) {
+  if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !$entity->isLatestRevision()) {
     return;
   }
 
   $build['#attributes']['data-quickedit-entity-id'] = $entity->getEntityTypeId() . '/' . $entity->id();
 }
-
-/**
- * Check if a loaded entity is the latest revision.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- *   The entity to check.
- *
- * @return bool
- *   TRUE if the loaded entity is the latest revision, FALSE otherwise.
- *
- * @todo Remove this method once better support for pending revisions is added
- * to core https://www.drupal.org/node/2784201.
- *
- * @internal
- */
-function _quickedit_entity_is_latest_revision(ContentEntityInterface $entity) {
-  $entity_type_manager = \Drupal::entityTypeManager();
-  $entity_definition = $entity_type_manager->getDefinition($entity->getEntityTypeId());
-  if (!$entity_definition->isRevisionable()) {
-    return TRUE;
-  }
-  $revision_ids = $entity_type_manager
-    ->getStorage($entity->getEntityTypeId())
-    ->getQuery()
-    ->allRevisions()
-    ->condition($entity_definition->getKey('id'), $entity->id())
-    ->sort($entity_definition->getKey('revision'), 'DESC')
-    ->range(0, 1)
-    ->execute();
-  return $entity->getLoadedRevisionId() == array_keys($revision_ids)[0];
-}
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 5d7329feb9..2a20988222 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 1875d004a4..9a5183ccea 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
@@ -212,6 +212,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..5c42985a64 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -129,7 +129,11 @@ public function testNormalize() {
       'revision_translation_affected' => [
         ['value' => TRUE],
       ],
+      'revision_type' => [
+        ['value' => TRUE],
+      ],
       'non_rev_field' => [],
+      'not_translatable' => [],
       'field_test_text' => [
         [
           'value' => $this->values['field_test_text']['value'],
@@ -201,6 +205,8 @@ 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>',
+      'not_translatable' => '<not_translatable/>',
+      '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 ef6f124f51..750f050082 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -2056,3 +2056,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/EntityTestMulRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php
index f01675f602..511d34c9c3 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\entity_test\Entity;
 
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+
 /**
  * Defines the test entity class.
  *
@@ -47,4 +50,17 @@
  */
 class EntityTestMulRev extends EntityTestRev {
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['not_translatable'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Non translatable'))
+      ->setDescription(t('A non-translatable string field'));
+
+    return $fields;
+  }
+
 }
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/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
index 8b544c4abd..863f95b332 100644
--- a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
+++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
@@ -212,4 +212,50 @@ public function testEntityRevisionParamConverter() {
     $this->assertNoText('pending revision - en');
   }
 
+  /**
+   * 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 after manually restoring the original revision ID, the entity
+    // is stored without creating a new revision.
+    $key = $entity->getEntityType()->getKey('revision');
+    $entity->set($key, $revision_id);
+    $entity->save();
+    $this->assertEquals($revision_id, $entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+
+    // Check that manually restoring the original revision ID causes the "new
+    // revision" state to be reverted.
+    $entity->setNewRevision();
+    $this->assertNull($entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+    $this->assertTrue($entity->isNewRevision());
+    $entity->set($key, $revision_id);
+    $this->assertFalse($entity->isNewRevision());
+    $this->assertEquals($revision_id, $entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+
+    // 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/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..e342ddfdc9
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
@@ -0,0 +1,123 @@
+<?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\ContentEntityStorageInterface
+   */
+  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.
+    // @todo Remove this line once the "latest_revision_id" field is introduced
+    //   in https://www.drupal.org/project/drupal/issues/2784201.
+    $entity->original_latest_revision_id = $this->storage->getLatestRevisionId($entity->id());
+    $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..34b07ee858
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php
@@ -0,0 +1,506 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\user\Entity\User;
+
+/**
+ * Test decoupled translation revisions.
+ *
+ * @group entity
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\ContentEntityStorageBase
+ */
+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[]
+   */
+  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';
+    $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 ::createRevision
+   *
+   * @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 - Untranslatable fields affect all revisions'] = [
+      [
+        ['en', TRUE, TRUE],
+        ['it', FALSE, TRUE, FALSE],
+        ['en', FALSE, TRUE, FALSE],
+        ['en', TRUE, TRUE],
+        ['it', TRUE, TRUE],
+        ['en', FALSE],
+        ['it', FALSE],
+        ['en', TRUE],
+        ['it', TRUE],
+      ],
+      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 ::createRevision
+   * @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 = EntityTestMulRev::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.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   */
+  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] = EntityTestMulRev::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 = $this->storage->getLatestAffectedRevisionId($entity->id(), $entity->language()->getId());
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_affected_revision */
+      $latest_affected_revision = isset($latest_affected_revision_id) ?
+        $this->storage->loadRevision($latest_affected_revision_id) : $this->storage->load($entity->id());
+      $translation = $latest_affected_revision->hasTranslation($active_langcode) ?
+        $latest_affected_revision->getTranslation($active_langcode) : $latest_affected_revision->addTranslation($active_langcode);
+      $entity = $this->storage->createRevision($translation, $default_revision);
+      $this->assertEquals($default_revision, $entity->isDefaultRevision());
+      $this->assertEquals($translation->getLoadedRevisionId(), $entity->getLoadedRevisionId());
+      $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);
+    }
+
+    $violations = $entity->validate();
+    $messages = [];
+    foreach ($violations as $violation) {
+      /** \Symfony\Component\Validator\ConstraintViolationInterface */
+      $messages[] = $violation->getMessage();
+    }
+    $this->assertEquals($valid, !$violations->count(), $this->formatMessage('Validation does not match the expected result: %s', implode(', ', $messages)));
+
+    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 ? $this->storage->getLatestRevisionId($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) {
+    $args = func_get_args();
+    array_shift($args);
+    $params = array_merge($args, $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);
+  }
+
+  /**
+   * Tests that internal properties are preserved while creating a new revision.
+   */
+  public function testInternalProperties() {
+    $entity = EntityTestMulRev::create();
+    $this->doTestInternalProperties($entity);
+
+    $entity = EntityTestMulRev::create();
+    $entity->save();
+    $this->doTestInternalProperties($entity);
+
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRev $translation */
+    $translation = EntityTestMulRev::create()->addTranslation('it');
+    $translation->save();
+    $this->doTestInternalProperties($translation);
+  }
+
+  /**
+   * Checks that internal properties are preserved for the specified entity.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   An entity object.
+   */
+  protected function doTestInternalProperties(ContentEntityInterface $entity) {
+    $this->assertFalse($entity->isValidationRequired());
+    $entity->setValidationRequired(TRUE);
+    $this->assertTrue($entity->isValidationRequired());
+    $new_revision = $this->storage->createRevision($entity);
+    $this->assertTrue($new_revision->isValidationRequired());
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
index d0f5a1cd7b..e49eeb3d85 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/EntityRevisionTranslationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
index e82d77e713..66128fb65e 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\KernelTests\Core\Entity;
 
+use Drupal\entity_test\Entity\EntityTestMul;
 use Drupal\entity_test\Entity\EntityTestMulRev;
 use Drupal\language\Entity\ConfigurableLanguage;
 
@@ -23,9 +24,11 @@ class EntityRevisionTranslationTest extends EntityKernelTestBase {
   protected function setUp() {
     parent::setUp();
 
-    // Enable an additional language.
+    // Enable some additional languages.
     ConfigurableLanguage::createFromLangcode('de')->save();
+    ConfigurableLanguage::createFromLangcode('it')->save();
 
+    $this->installEntitySchema('entity_test_mul');
     $this->installEntitySchema('entity_test_mulrev');
   }
 
@@ -186,4 +189,72 @@ public function testSetNewRevision() {
     }
   }
 
+  /**
+   * Tests that revision translations are correctly detected.
+   *
+   * @covers \Drupal\Core\Entity\ContentEntityStorageBase::hasTranslations
+   */
+  public function testHasStoredTranslations() {
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->entityManager->getStorage('entity_test_mul');
+
+    // Check that a non-revisionable new entity is handled correctly.
+    $entity = EntityTestMul::create();
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasTranslations($entity));
+    $entity->addTranslation('it');
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasTranslations($entity));
+
+    // Check that a non-revisionable not translated entity is handled correctly.
+    $entity = EntityTestMul::create();
+    $entity->save();
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasTranslations($entity));
+
+    // Check that a non-revisionable translated entity is handled correctly.
+    $entity->addTranslation('it');
+    $entity->save();
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertTrue($storage->hasTranslations($entity));
+
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->entityManager->getStorage('entity_test_mulrev');
+
+    // Check that a revisionable new entity is handled correctly.
+    $entity = EntityTestMulRev::create();
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasTranslations($entity));
+    $entity->addTranslation('it');
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasTranslations($entity));
+
+    // Check that a revisionable not translated entity is handled correctly.
+    $entity = EntityTestMulRev::create();
+    $entity->save();
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasTranslations($entity));
+
+    // Check that a revisionable translated pending revision is handled
+    // correctly.
+    $new_revision = $storage->createRevision($entity, FALSE);
+    $new_revision->addTranslation('it');
+    $new_revision->save();
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $storage->loadUnchanged($entity->id());
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertNotEmpty($new_revision->getTranslationLanguages(FALSE));
+    $this->assertTrue($storage->hasTranslations($entity));
+
+    // Check that a revisionable translated default revision is handled
+    // correctly.
+    $new_revision->isDefaultRevision(TRUE);
+    $new_revision->save();
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $storage->loadUnchanged($entity->id());
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertNotEmpty($new_revision->getTranslationLanguages(FALSE));
+    $this->assertTrue($storage->hasTranslations($entity));
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionsTest.php
similarity index 64%
rename from core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
rename to core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionsTest.php
index a3b99fe0c9..aded19cced 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionsTest.php
@@ -8,9 +8,11 @@
 /**
  * Tests the loaded Revision of an entity.
  *
+ * @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase
+ *
  * @group entity
  */
-class EntityLoadedRevisionTest extends EntityKernelTestBase {
+class EntityRevisionsTest extends EntityKernelTestBase {
 
   /**
    * Modules to enable.
@@ -164,7 +166,92 @@ public function testSaveInHookEntityInsert() {
     $loadedRevisionId = \Drupal::state()->get('entity_test.loadedRevisionId');
     $this->assertEquals($entity->getLoadedRevisionId(), $loadedRevisionId);
     $this->assertEquals($entity->getRevisionId(), $entity->getLoadedRevisionId());
+  }
+
+  /**
+   * Tests that latest revisions are working as expected.
+   *
+   * @covers ::isLatestRevision
+   */
+  public function testIsLatestRevision() {
+    // Create a basic EntityTestMulRev entity and save it.
+    $entity = EntityTestMulRev::create();
+    $entity->save();
+    $this->assertTrue($entity->isLatestRevision());
+
+    // Load the created entity and create a new pending revision.
+    $pending_revision = EntityTestMulRev::load($entity->id());
+    $pending_revision->setNewRevision(TRUE);
+    $pending_revision->isDefaultRevision(FALSE);
+
+    // The pending revision should still be marked as the latest one before it
+    // is saved.
+    $this->assertTrue($pending_revision->isLatestRevision());
+    $pending_revision->save();
+    $this->assertTrue($pending_revision->isLatestRevision());
+
+    // Load the default revision and check that it is not marked as the latest
+    // revision.
+    $default_revision = EntityTestMulRev::load($entity->id());
+    $this->assertFalse($default_revision->isLatestRevision());
+  }
+
+  /**
+   * Tests that latest affected revisions are working as expected.
+   *
+   * Latest affected revisions behave as latest revisions for monolingual
+   * entities.
+   *
+   * @covers ::isLatestAffectedRevision
+   */
+  public function testIsLatestAffectedRevision() {
+    ConfigurableLanguage::createFromLangcode('it')->save();
+
+    // Create a basic EntityTestMulRev entity and save it.
+    $entity = EntityTestMulRev::create();
+    $entity->setName($this->randomString());
+    $entity->save();
+    $this->assertTrue($entity->isLatestAffectedRevision());
+
+    // Load the created entity and create a new pending revision.
+    $pending_revision = EntityTestMulRev::load($entity->id());
+    $pending_revision->setName($this->randomString());
+    $pending_revision->setNewRevision(TRUE);
+    $pending_revision->isDefaultRevision(FALSE);
+
+    // The pending revision should still be marked as the latest affected one
+    // before it is saved.
+    $this->assertTrue($pending_revision->isLatestAffectedRevision());
+    $pending_revision->save();
+    $this->assertTrue($pending_revision->isLatestAffectedRevision());
+
+    // Load the default revision and check that it is not marked as the latest
+    // (affected) revision.
+    $default_revision = EntityTestMulRev::load($entity->id());
+    $this->assertFalse($default_revision->isLatestRevision());
+    $this->assertFalse($default_revision->isLatestAffectedRevision());
 
+    // Add a translation in a new pending revision and verify that both the
+    // English and Italian revisions are the latest affected revisions for their
+    // respective languages, while the English revision is not the latest
+    // revision.
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRev $en_revision */
+    $en_revision = clone $pending_revision;
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRev $it_revision */
+    $it_revision = $pending_revision->addTranslation('it');
+    $it_revision->setName($this->randomString());
+    $it_revision->setNewRevision(TRUE);
+    $it_revision->isDefaultRevision(FALSE);
+    // @todo Remove this once the "original" property works with revisions. See
+    //   https://www.drupal.org/project/drupal/issues/2859042.
+    $it_revision->original = $this->entityManager
+      ->getStorage($it_revision->getEntityTypeId())
+      ->loadRevision($it_revision->getLoadedRevisionId());
+    $it_revision->save();
+    $this->assertTrue($it_revision->isLatestRevision());
+    $this->assertTrue($it_revision->isLatestAffectedRevision());
+    $this->assertFalse($en_revision->isLatestRevision());
+    $this->assertTrue($en_revision->isLatestAffectedRevision());
   }
 
 }
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
index 34b53cb97e..aa8541d732 100644
--- a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php
@@ -80,8 +80,12 @@ public function testEntityWithPendingRevision() {
     $entity = EntityTestMulRev::create();
     $entity->save();
 
-    $entity->isDefaultRevision(FALSE);
-    $entity->setNewRevision(TRUE);
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->container->get('entity_type.manager')
+      ->getStorage($entity->getEntityTypeId());
+
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $storage->createRevision($entity,FALSE);
     $entity->save();
 
     $converted = $this->converter->convert(1, [
