diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index d755c6f7e6..33e85550d7 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -20,6 +20,10 @@
  */
 abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface {
 
+  use EntityChangesDetectionTrait {
+    getFieldsToSkipFromTranslationChangesCheck as traitGetFieldsToSkipFromTranslationChangesCheck;
+  }
+
   /**
    * The plain data values of the contained fields.
    *
@@ -1356,17 +1360,7 @@ public static function bundleFieldDefinitions(EntityTypeInterface $entity_type,
    *   An array of field names.
    */
   protected function getFieldsToSkipFromTranslationChangesCheck() {
-    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
-    $entity_type = $this->getEntityType();
-    // A list of known revision metadata fields which should be skipped from
-    // the comparision.
-    $fields = [
-      $entity_type->getKey('revision'),
-      'revision_translation_affected',
-    ];
-    $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys()));
-
-    return $fields;
+    return $this->traitGetFieldsToSkipFromTranslationChangesCheck($this);
   }
 
   /**
@@ -1406,10 +1400,15 @@ public function hasTranslationChanges() {
     // The list of fields to skip from the comparision.
     $skip_fields = $this->getFieldsToSkipFromTranslationChangesCheck();
 
+    // We also check 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);
@@ -1430,4 +1429,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..e2be1013af 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -127,6 +127,8 @@ public function form(array $form, FormStateInterface $form_state) {
       $this->addRevisionableFormFields($form);
     }
 
+    $this->addLatestRevisionIdWidget($form);
+
     $form['footer'] = [
       '#type' => 'container',
       '#weight' => 99,
@@ -139,6 +141,30 @@ public function form(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
+  /**
+   * Adds an hidden input field storing the (current) latest revision ID.
+   *
+   * @param array $form
+   *   A form array reference.
+   *
+   * @internal This widget and the related property should be considered
+   *   internal. They will be replaced by a regular field in
+   *   https://www.drupal.org/project/drupal/issues/2784201.
+   */
+  protected function addLatestRevisionIdWidget(array& $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()),
+      ];
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -155,6 +181,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 f43bc3b453..61604ba6a5 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -41,4 +41,13 @@ public function getLoadedRevisionId();
    */
   public function updateLoadedRevisionId();
 
+  /**
+   * 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 7f36c11a67..7bc95ec043 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\TranslationStatusInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -148,6 +149,37 @@ protected function initFieldValues(ContentEntityInterface $entity, array $values
     $this->invokeHook('field_values_init', $entity);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasStoredTranslations(TranslatableInterface $entity) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    if ($entity->isNew()) {
+      return FALSE;
+    }
+
+    if ($entity instanceof TranslationStatusInterface) {
+      foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
+        if ($entity->getTranslationStatus($langcode) === TranslationStatusInterface::TRANSLATION_EXISTING) {
+          return TRUE;
+        }
+      }
+    }
+
+    $query = $this->getQuery()
+      ->condition($this->entityType->getKey('id'), $entity->id())
+      ->condition($this->entityType->getKey('default_langcode'), FALSE)
+      ->accessCheck(FALSE)
+      ->range(0, 1);
+
+    if ($entity->getEntityType()->isRevisionable()) {
+      $query->allRevisions();
+    }
+
+    $result = $query->execute();
+    return !empty($result);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -166,6 +198,96 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
     return $translation;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $new_revision = clone $entity;
+
+    // For translatable entities, create a merged revision of the active
+    // translation and the other translations in the default revision. This
+    // permits the creation of 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->hasStoredTranslations($entity)) {
+      $active_langcode = $entity->language()->getId();
+      $skipped_field_names = array_flip($this->getRevisionTranslationMergeSkippedFieldNames());
+
+      // 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());
+      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);
+
+        /** @var \Drupal\Core\Field\FieldItemListInterface[] $sync_items */
+        $sync_items = array_diff_key(
+          $keep_untranslatable_fields ? $default_revision_translation->getTranslatableFields() : $default_revision_translation->getFields(),
+          $skipped_field_names
+        );
+        foreach ($sync_items as $field_name => $items) {
+          $new_revision_translation->set($field_name, $items->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;
+      }
+
+      // Populate the "original" property with the current values, given that
+      // the new revision is not stored anywhere. This way we can detect changes
+      // properly.
+      $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;
+  }
+
+  /**
+   * Returns an array of field names to skip when merging revision translations.
+   *
+   * @return array
+   *   An array of field names.
+   */
+  protected function getRevisionTranslationMergeSkippedFieldNames() {
+    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+    $entity_type = $this->getEntityType();
+
+    // A list of known revision metadata fields which should be skipped from
+    // the comparision.
+    $field_names = [
+      $entity_type->getKey('revision'),
+      $entity_type->getKey('revision_translation_affected'),
+    ];
+    $field_names = array_merge($field_names, array_values($entity_type->getRevisionMetadataKeys()));
+
+    return $field_names;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -375,6 +497,13 @@ protected function doSave($id, EntityInterface $entity) {
     }
 
     $this->populateAffectedRevisionTranslations($entity);
+
+    // Populate the "revision_default" flag.
+    if ($entity instanceof RevisionLogInterface) {
+      $revision_default_key = $this->entityType->getRevisionMetadataKey('revision_default');
+      $entity->set($revision_default_key, $entity->isDefaultRevision());
+    }
+
     $this->doSaveFieldItems($entity);
 
     return $return;
@@ -410,6 +539,14 @@ protected function doPreSave(EntityInterface $entity) {
       $entity->updateLoadedRevisionId();
     }
 
+    // Make sure that the original entity is the revision the current entity was
+    // originated from, otherwise we would always compare the current values
+    // with the default values, which is not correct for pending revisions.
+    // TODO Add test coverage.
+    if (!isset($entity->original) && $this->entityType->isRevisionable()) {
+      $entity->original = $this->loadRevision($entity->getLoadedRevisionId());
+    }
+
     $id = parent::doPreSave($entity);
 
     if (!$entity->isNew()) {
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php
index 0e26c3bb51..143ff3757b 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityType.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
@@ -54,19 +54,22 @@ protected function checkStorageClass($class) {
   public function getRevisionMetadataKeys($include_backwards_compatibility_field_names = TRUE) {
     // Provide backwards compatibility in case the revision metadata keys are
     // not defined in the entity annotation.
-    if (!$this->revision_metadata_keys && $include_backwards_compatibility_field_names) {
-      $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
-      if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
-        @trigger_error('The revision_user revision metadata key is not set.', E_USER_DEPRECATED);
-        $this->revision_metadata_keys['revision_user'] = $revision_user;
-      }
-      if ((isset($base_fields['revision_timestamp']) && $revision_timestamp = 'revision_timestamp') || (isset($base_fields['revision_created'])) && $revision_timestamp = 'revision_created') {
-        @trigger_error('The revision_created revision metadata key is not set.', E_USER_DEPRECATED);
-        $this->revision_metadata_keys['revision_created'] = $revision_timestamp;
-      }
-      if ((isset($base_fields['revision_log']) && $revision_log = 'revision_log') || (isset($base_fields['revision_log_message']) && $revision_log = 'revision_log_message')) {
-        @trigger_error('The revision_log_message revision metadata key is not set.', E_USER_DEPRECATED);
-        $this->revision_metadata_keys['revision_log_message'] = $revision_log;
+    if ($include_backwards_compatibility_field_names) {
+      $required_keys = [
+        'revision_user' => 'revision_uid',
+        'revision_created' => 'revision_timestamp',
+        'revision_log_message' => 'revision_log',
+        'revision_default' => 'revision_default',
+      ];
+      $missing_keys = array_diff_key($required_keys, $this->revision_metadata_keys);
+      if ($missing_keys) {
+        $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
+        foreach ($missing_keys as $key => $field_name) {
+          if (isset($base_fields[$field_name]) || (isset($base_fields[$key]) && $field_name = $key)) {
+            @trigger_error("The $key revision metadata key is not set.", E_USER_DEPRECATED);
+            $this->revision_metadata_keys[$key] = $field_name;
+          }
+        }
       }
     }
     return $this->revision_metadata_keys;
diff --git a/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php b/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php
new file mode 100644
index 0000000000..e598e8c089
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Provides helper methods to detect changes in an entity object.
+ *
+ * @internal This may be replaced by a proper entity comparison handler.
+ */
+trait EntityChangesDetectionTrait {
+
+  /**
+   * Returns an array of field names to skip when checking for changes.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   A content entity object.
+   *
+   * @return string[]
+   *   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'),
+    ];
+    $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys()));
+
+    return $fields;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index 19a52ca0fa..f5204f8241 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 b0725251e5..6aa18ff840 100644
--- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Entity\TranslatableInterface;
 
 /**
  * Provides a key value backend for content entities.
@@ -18,6 +20,20 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
     //   https://www.drupal.org/node/2618436.
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasStoredTranslations(TranslatableInterface $entity) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+    return NULL;
+  }
+
   /**
    * {@inheritdoc}
    */
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 28d81ba6e4..47a30b29de 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,9 @@
 
 namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
 
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\RevisionableInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 
@@ -10,34 +13,121 @@
  */
 class EntityChangedConstraintValidator extends ConstraintValidator {
 
+  /**
+   * Flag determining whether changed time validation should be performed.
+   *
+   * @var bool
+   */
+  protected $checkChangedTime;
+
   /**
    * {@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());
-        // Ensure that all the entity translations are the same as or newer
-        // than their current version in the storage in order to avoid
-        // reverting other changes. In fact the entity object that is being
-        // saved might contain an older entity translation when different
-        // translations are being concurrently edited.
-        if ($saved_entity) {
-          $common_translation_languages = array_intersect_key($entity->getTranslationLanguages(), $saved_entity->getTranslationLanguages());
-          foreach (array_keys($common_translation_languages) as $langcode) {
-            // Merely comparing the latest changed timestamps across all
-            // translations is not sufficient since other translations may have
-            // been edited and saved in the meanwhile. Therefore, compare the
-            // changed timestamps of each entity translation individually.
-            if ($saved_entity->getTranslation($langcode)->getChangedTime() > $entity->getTranslation($langcode)->getChangedTime()) {
-              $this->context->addViolation($constraint->message);
-              break;
-            }
+    if ($entity instanceof EntityInterface && !$entity->isNew()) {
+      $valid = $this->validateChangedRevisions($entity);
+      if ($this->checkChangedTime) {
+        $valid = $this->validateChangedTime($entity);
+      }
+      if (!$valid) {
+        $this->context->addViolation($constraint->message);
+      }
+    }
+  }
+
+  /**
+   * Performs validation based on revision changes.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being validated.
+   *
+   * @return bool
+   *   TRUE if validation is successful, FALSE otherwise.
+   */
+  protected function validateChangedRevisions(EntityInterface $entity) {
+    $valid = TRUE;
+    $this->checkChangedTime = TRUE;
+
+    if ($entity instanceof RevisionableInterface && $this->isRevisionValidationEnabled($entity)) {
+      /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
+      $storage = \Drupal::entityTypeManager()
+        ->getStorage($entity->getEntityTypeId());
+
+      if (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 one. 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.
+        $this->checkChangedTime = $valid && $entity->original_latest_revision_id == $entity->getLoadedRevisionId();
+      }
+    }
+
+    return $valid;
+  }
+
+  /**
+   * Checks whether this validator applies to the specified bundle.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   An entity object.
+   *
+   * @return bool
+   *   TRUE if the bundle is enabled, FALSE otherwise.
+   */
+  protected function isRevisionValidationEnabled(EntityInterface $entity) {
+    $bundle_name = $entity->bundle();
+    $bundle_info = \Drupal::service('entity_type.bundle.info')
+      ->getBundleInfo($entity->getEntityTypeId());
+    return !empty($bundle_info[$bundle_name]['entity_validation_constraints.EntityChanged.changed_revision']);
+  }
+
+  /**
+   * Performs validation based on timestamp changes.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being validated.
+   *
+   * @return bool
+   *   TRUE if validation is successful, FALSE otherwise.
+   */
+  protected function validateChangedTime(EntityInterface $entity) {
+    $valid = TRUE;
+
+    if ($entity instanceof EntityChangedInterface && !$entity->isNew()) {
+      /** @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\EntityChangedInterface $saved_entity */
+      $saved_entity = \Drupal::entityTypeManager()
+        ->getStorage($entity->getEntityTypeId())
+        ->loadUnchanged($entity->id());
+
+      // Ensure that all the entity translations are the same as or newer
+      // than their current version in the storage in order to avoid
+      // reverting other changes. In fact the entity object that is being
+      // saved might contain an older entity translation when different
+      // translations are being concurrently edited.
+      if ($saved_entity) {
+        $common_translation_languages = array_intersect_key($entity->getTranslationLanguages(), $saved_entity->getTranslationLanguages());
+        foreach (array_keys($common_translation_languages) as $langcode) {
+          // Merely comparing the latest changed timestamps across all
+          // translations is not sufficient since other translations may have
+          // been edited and saved in the meanwhile. Therefore, compare the
+          // changed timestamps of each entity translation individually.
+          if ($saved_entity->getTranslation($langcode)->getChangedTime() > $entity->getTranslation($langcode)->getChangedTime()) {
+            $valid = FALSE;
+            break;
           }
         }
       }
     }
+
+    return $valid;
   }
 
 }
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..0044b6b4fb
--- /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 = 'Non translatable fields can only be changed when updating the current revision or the original language.';
+
+}
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..39ed9e00a4
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangesDetectionTrait;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\ChangedFieldItemList;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the EntityChanged constraint.
+ */
+class EntityUntranslatableFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  use EntityChangesDetectionTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs an EntityUntranslatableFieldsConstraintValidator object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($entity, Constraint $constraint) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+
+    // Untranslatable field 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. The
+    // only case where changes in 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 field 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 :
+      $this->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;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php b/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php
index f935a8b510..20463d9caf 100644
--- a/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php
+++ b/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\user\UserInterface;
 
 /**
@@ -49,6 +50,12 @@ public static function revisionLogBaseFieldDefinitions(EntityTypeInterface $enti
         ],
       ]);
 
+    $fields[static::getRevisionMetadataKey($entity_type, 'revision_default')] = BaseFieldDefinition::create('boolean')
+      ->setLabel(new TranslatableMarkup('Default revision'))
+      ->setDescription(new TranslatableMarkup('A flag indicating whether this was a default revision when it was saved.'))
+      ->setTranslatable(FALSE)
+      ->setRevisionable(TRUE);
+
     return $fields;
   }
 
@@ -133,9 +140,31 @@ protected static function getRevisionMetadataKey(EntityTypeInterface $entity_typ
       'revision_created' => 'revision_created',
       'revision_user' => 'revision_user',
       'revision_log_message' => 'revision_log_message',
+      'revision_default' => 'revision_default',
     ];
 
     return $revision_metadata_keys[$key];
   }
 
+  /**
+   * Implements \Drupal\Core\Entity\RevisionLogInterface::wasDefaultRevision().
+   */
+  public function wasDefaultRevision() {
+    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+    $entity_type = $this->getEntityType();
+    if (!$entity_type->isRevisionable()) {
+      throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support revisions.");
+    }
+
+    $revision_default_key = $entity_type->getRevisionMetadataKey('revision_default');
+    $value = $this->isNew() ? $this->isDefaultRevision() : $this->get($revision_default_key)->value;
+
+    if (isset($value)) {
+      return (bool) $value;
+    }
+    else {
+      throw new \LogicException("Missing data for the \"{$revision_default_key}\" field on the entity {$this->getEntityTypeId()}:{$this->id()}.");
+    }
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/RevisionLogInterface.php b/core/lib/Drupal/Core/Entity/RevisionLogInterface.php
index becf97c162..806c300072 100644
--- a/core/lib/Drupal/Core/Entity/RevisionLogInterface.php
+++ b/core/lib/Drupal/Core/Entity/RevisionLogInterface.php
@@ -81,4 +81,16 @@ public function getRevisionLogMessage();
    */
   public function setRevisionLogMessage($revision_log_message);
 
+  /**
+   * 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.
+   *
+   * @throws \LogicException
+   *   If the entity type is not revisionable or the default revision data is
+   *   missing.
+   */
+  public function wasDefaultRevision();
+
 }
diff --git a/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
index f92d419aa5..d23808b1b6 100644
--- a/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
@@ -7,6 +7,20 @@
  */
 interface RevisionableStorageInterface {
 
+  /**
+   * Creates a new revision starting off from the specified entity object.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $entity
+   *   The revisionable entity object being modified.
+   * @param bool $default
+   *   (optional) Whether the new revision should be marked as default. Defaults
+   *   to TRUE.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface
+   *   A new entity revision object.
+   */
+  public function createRevision(RevisionableInterface $entity, $default = TRUE);
+
   /**
    * Loads a specific entity revision.
    *
diff --git a/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php b/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php
index 1a6b73784e..a09613ec16 100644
--- a/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php
@@ -7,6 +7,45 @@
  */
 interface TranslatableRevisionableStorageInterface extends TranslatableStorageInterface, RevisionableStorageInterface {
 
+  /**
+   * 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 active translation of the passed entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $entity
+   *   The revisionable 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.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface
+   *   A new translatable entity revision object.
+   */
+  public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL);
+
+  /**
+   * Checks whether the specified entity has stored translations.
+   *
+   * A revisionable entity can have translations in a pending revision, hence
+   * the default revision may appear as not translated. This determines whether
+   * the entity has any translation in the storage and thus should be considered
+   * as multilingual.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $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\TypedData\TranslatableInterface::getTranslationLanguages()
+   */
+  public function hasStoredTranslations(TranslatableInterface $entity);
+
   /**
    * Returns the latest revision affecting the specified translation.
    *
diff --git a/core/lib/Drupal/Core/Entity/TranslatableStorageInterface.php b/core/lib/Drupal/Core/Entity/TranslatableStorageInterface.php
index c9d93fef4f..aa6ba6befa 100644
--- a/core/lib/Drupal/Core/Entity/TranslatableStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/TranslatableStorageInterface.php
@@ -27,4 +27,21 @@
    */
   public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []);
 
+  /**
+   * Checks whether the specified entity has stored translations.
+   *
+   * This determines whether the entity has any translation in the storage and
+   * thus should be considered as multilingual.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
+   *   The entity object to be checked.
+   *
+   * @return bool
+   *   TRUE if the entity has at least one translation in the storage, FALSE
+   *   otherwise.
+   *
+   * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
+   */
+  public function hasStoredTranslations(TranslatableInterface $entity);
+
 }
diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
index f76b327a86..89e4d113ca 100644
--- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
+++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
@@ -3,8 +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\EntityTypeInterface;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\TypedData\TranslatableInterface;
 use Symfony\Component\Routing\Route;
 
@@ -70,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()) {
       /** @var \Drupal\Core\Entity\ContentEntityStorageInterface  $storage */
-      $latest_revision_id = $storage->getLatestRevisionId($value);
+      $latest_revision_id = $this->getLatestRevisionId($storage, $entity_definition, $value);
       // We explicitly perform a loose equality check, since a revision ID may
       // be returned as an integer or a string.
       if ($entity->getLoadedRevisionId() != $latest_revision_id) {
@@ -87,6 +90,53 @@ public function convert($value, $definition, $name, array $defaults) {
     return $entity;
   }
 
+  /**
+   * Get the latest revision ID.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityStorageInterface $storage
+   *   The entity storage.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param int $entity_id
+   *   The raw value.
+   *
+   * @return int
+   *   The latest (affected) revision ID for a given entity.
+   */
+  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.
+    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;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php
index 3c8858ff88..eb10f97108 100644
--- a/core/modules/block_content/src/Entity/BlockContent.php
+++ b/core/modules/block_content/src/Entity/BlockContent.php
@@ -56,7 +56,8 @@
  *   revision_metadata_keys = {
  *     "revision_user" = "revision_user",
  *     "revision_created" = "revision_created",
- *     "revision_log_message" = "revision_log"
+ *     "revision_log_message" = "revision_log",
+ *     "revision_default" = "revision_default",
  *   },
  *   bundle_entity_type = "block_content_type",
  *   field_ui_base_route = "entity.block_content_type.edit_form",
diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module
index 5a7af0e2c9..722ba78008 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().
  */
@@ -248,7 +257,9 @@ function content_moderation_entity_bundle_info_alter(&$bundles) {
     foreach ($plugin->getEntityTypes() as $entity_type_id) {
       foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
         if (isset($bundles[$entity_type_id][$bundle_id])) {
-          $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
+          $bundle_info = &$bundles[$entity_type_id][$bundle_id];
+          $bundle_info['workflow'] = $workflow->id();
+          $bundle_info['entity_validation_constraints.EntityChanged.changed_revision'] = TRUE;
         }
       }
     }
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 7e3e513307..37b1646805 100644
--- a/core/modules/content_moderation/src/ModerationInformation.php
+++ b/core/modules/content_moderation/src/ModerationInformation.php
@@ -130,23 +130,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;
   }
 
   /**
@@ -154,8 +165,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();
   }
@@ -165,7 +175,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 b5263639e2..6b4a9c2117 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..73676724bd 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,9 @@ language.content_settings.*.*.third_party.content_translation:
     enabled:
       type: boolean
       label: 'Content translation enabled'
+    bundle_settings:
+      type: sequence
+      label: 'Content translation bundle settings'
+      sequence:
+        type: string
+        label: 'Bundle settings values'
diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc
index bf33ba0af0..d71b150b90 100644
--- a/core/modules/content_translation/content_translation.admin.inc
+++ b/core/modules/content_translation/content_translation.admin.inc
@@ -5,6 +5,7 @@
  * The content translation administration forms.
  */
 
+use Drupal\content_translation\BundleSettingsInterface;
 use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -83,6 +84,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) {
@@ -110,6 +112,23 @@ function _content_translation_form_language_content_settings_form_alter(array &$
         continue;
       }
 
+      // Displayed the "shared fields widgets" toggle.
+      if ($content_translation_manager instanceof BundleSettingsInterface) {
+        $settings = $content_translation_manager->getBundleSettings($entity_type_id, $bundle);
+        $form['settings'][$entity_type_id][$bundle]['settings']['content_translation']['untranslatable_fields_hide'] = [
+          '#type' => 'checkbox',
+          '#title' => t('Hide non translatable fields on translation forms'),
+          '#default_value' => !empty($settings['untranslatable_fields_hide']),
+          '#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 +336,8 @@ 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 $content_translation_manager */
+  $content_translation_manager = \Drupal::service('content_translation.manager');
   $entity_types = $form_state->getValue('entity_types');
   $settings = &$form_state->getValue('settings');
 
@@ -347,7 +368,12 @@ 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']);
+        $content_translation_manager->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
+
+        // Store any other bundle settings.
+        if ($content_translation_manager instanceof BundleSettingsInterface) {
+          $content_translation_manager->setBundleSettings($entity_type_id, $bundle, $bundle_settings['settings']['content_translation']);
+        }
 
         // Save translation_sync settings.
         if (!empty($bundle_settings['columns'])) {
@@ -367,8 +393,8 @@ function content_translation_form_language_content_settings_submit(array $form,
       }
     }
   }
+
   // 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..f9c0e3a246 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -5,6 +5,7 @@
  * Allows entities to be translated into different languages.
  */
 
+use Drupal\content_translation\BundleSettingsInterface;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\ContentEntityFormInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
@@ -161,9 +162,15 @@ 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 $content_translation_manager */
+  $content_translation_manager = \Drupal::service('content_translation.manager');
+  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'] = $content_translation_manager->isEnabled($entity_type_id, $bundle);
+      if ($content_translation_manager instanceof BundleSettingsInterface) {
+        $settings = $content_translation_manager->getBundleSettings($entity_type_id, $bundle);
+        $bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']);
+      }
     }
   }
 }
@@ -319,6 +326,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 +425,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/BundleSettingsInterface.php b/core/modules/content_translation/src/BundleSettingsInterface.php
new file mode 100644
index 0000000000..f02522927e
--- /dev/null
+++ b/core/modules/content_translation/src/BundleSettingsInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\content_translation;
+
+/**
+ * Interface providing support for content translation bundle settings.
+ */
+interface BundleSettingsInterface {
+
+  /**
+   * Returns settings for the specified bundle.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param string $bundle
+   *   The bundle name.
+   *
+   * @return array
+   *   An associative array of values keyed by setting name.
+   */
+  public function getBundleSettings($entity_type_id, $bundle);
+
+  /**
+   * Sets settings for the specified bundle.
+   *
+   * @param array $settings
+   *   An associative array of values keyed by setting name.
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param string $bundle
+   *   The bundle name.
+   */
+  public function setBundleSettings($entity_type_id, $bundle, array $settings);
+
+}
diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php
index 7e6a7ca3ea..71e9ba86f9 100644
--- a/core/modules/content_translation/src/ContentTranslationHandler.php
+++ b/core/modules/content_translation/src/ContentTranslationHandler.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\EntityChangesDetectionTrait;
 use Drupal\Core\Entity\EntityHandlerInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
@@ -13,8 +14,10 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\user\Entity\User;
 use Drupal\user\EntityOwnerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -25,7 +28,10 @@
  * @ingroup entity_api
  */
 class ContentTranslationHandler implements ContentTranslationHandlerInterface, EntityHandlerInterface {
+
+  use EntityChangesDetectionTrait;
   use DependencySerializationTrait;
+  use StringTranslationTrait;
 
   /**
    * The type of the entity being translated.
@@ -70,6 +76,13 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
    */
   protected $fieldStorageDefinitions;
 
+  /**
+   * The messenger service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
   /**
    * Initializes an instance of the content translation controller.
    *
@@ -83,14 +96,17 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
    *   The entity manager.
    * @param \Drupal\Core\Session\AccountInterface $current_user
    *   The current user.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger service.
    */
-  public function __construct(EntityTypeInterface $entity_type, LanguageManagerInterface $language_manager, ContentTranslationManagerInterface $manager, EntityManagerInterface $entity_manager, AccountInterface $current_user) {
+  public function __construct(EntityTypeInterface $entity_type, LanguageManagerInterface $language_manager, ContentTranslationManagerInterface $manager, EntityManagerInterface $entity_manager, AccountInterface $current_user, MessengerInterface $messenger) {
     $this->entityTypeId = $entity_type->id();
     $this->entityType = $entity_type;
     $this->languageManager = $language_manager;
     $this->manager = $manager;
     $this->currentUser = $current_user;
     $this->fieldStorageDefinitions = $entity_manager->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
+    $this->messenger = $messenger;
   }
 
   /**
@@ -102,7 +118,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
       $container->get('language_manager'),
       $container->get('content_translation.manager'),
       $container->get('entity.manager'),
-      $container->get('current_user')
+      $container->get('current_user'),
+      \Drupal::messenger()
     );
   }
 
@@ -269,6 +286,8 @@ public function getSourceLangcode(FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+
     $form_object = $form_state->getFormObject();
     $form_langcode = $form_object->getFormLangcode($form_state);
     $entity_langcode = $entity->getUntranslated()->language()->getId();
@@ -512,6 +531,20 @@ 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_untranslatable_fields = $entity->isDefaultTranslationAffectedOnly() && !$entity->isDefaultTranslation();
+    $translation_form = $form_state->get(['content_translation', 'translation_form']);
+    $display_warning = FALSE;
+
+    // We use field definitions to identify untranslatable field widgets to be
+    // hidden. Field that are not involved in translation changes checks should
+    // not be affected by this logic (the "revision_log" field, for instance).
+    $field_definitions = array_diff_key($entity->getFieldDefinitions(), array_flip($this->getFieldsToSkipFromTranslationChangesCheck($entity)));
+
     foreach (Element::children($element) as $key) {
       if (!isset($element[$key]['#type'])) {
         $this->entityFormSharedElements($element[$key], $form_state, $form);
@@ -524,10 +557,17 @@ public function entityFormSharedElements($element, FormStateInterface $form_stat
         // Elements are considered to be non multilingual by default.
         if (empty($element[$key]['#multilingual'])) {
           // 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]);
+          // translatability clues, otherwise the non-multilingual form elements
+          // should be hidden.
+          if (!$translation_form) {
+            if ($display_translatability_clue) {
+              $this->addTranslatabilityClue($element[$key]);
+            }
+            // Hide widgets for untranslatable fields.
+            if ($hide_untranslatable_fields && isset($field_definitions[$key])) {
+              $element[$key]['#access'] = FALSE;
+              $display_warning = TRUE;
+            }
           }
           else {
             $element[$key]['#access'] = FALSE;
@@ -536,6 +576,11 @@ public function entityFormSharedElements($element, FormStateInterface $form_stat
       }
     }
 
+    if ($display_warning && !$form_state->isSubmitted() && !$form_state->isRebuilding()) {
+      $url = $entity->getUntranslated()->toUrl('edit-form')->toString();
+      $this->messenger->addWarning($this->t('Fields that apply to all languages are hidden to avoid conflicting changes. <a href=":url">Edit them on the original language form</a>.', [':url' => $url]));
+    }
+
     return $element;
   }
 
diff --git a/core/modules/content_translation/src/ContentTranslationManager.php b/core/modules/content_translation/src/ContentTranslationManager.php
index da8bd1c618..28acd42815 100644
--- a/core/modules/content_translation/src/ContentTranslationManager.php
+++ b/core/modules/content_translation/src/ContentTranslationManager.php
@@ -8,7 +8,7 @@
 /**
  * Provides common functionality for content translation.
  */
-class ContentTranslationManager implements ContentTranslationManagerInterface {
+class ContentTranslationManager implements ContentTranslationManagerInterface, BundleSettingsInterface {
 
   /**
    * The entity type manager.
@@ -105,6 +105,23 @@ public function isEnabled($entity_type_id, $bundle = NULL) {
     return $enabled;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setBundleSettings($entity_type_id, $bundle, array $settings) {
+    $config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
+    $config->setThirdPartySetting('content_translation', 'bundle_settings', $settings)
+      ->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBundleSettings($entity_type_id, $bundle) {
+    $config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
+    return $config->getThirdPartySetting('content_translation', 'bundle_settings', []);
+  }
+
   /**
    * Loads a content language config entity based on the entity type and bundle.
    *
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..75ed9255fe
--- /dev/null
+++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php
@@ -0,0 +1,118 @@
+<?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 untranslatable 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 no 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 translatability
+    // clues.
+    $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][content_translation][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));
+
+    // Verify a warning is displayed.
+    $this->assertSession()->pageTextContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
+    $edit_path = $entity->toUrl('edit-form')->toString();
+    $link_xpath = '//a[@href=:edit_path and text()="Edit them on the original language form"]';
+    $elements = $this->xpath($link_xpath, [':edit_path' => $edit_path]);
+    $this->assertNotEmpty($elements);
+  }
+}
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index dc8e0dcf0f..310d807d76 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -64,6 +64,7 @@
  *     "revision_user" = "revision_user",
  *     "revision_created" = "revision_created",
  *     "revision_log_message" = "revision_log_message",
+ *     "revision_default" = "revision_default",
  *   },
  *   bundle_entity_type = "media_type",
  *   permission_granularity = "entity_type",
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/Entity/Node.php b/core/modules/node/src/Entity/Node.php
index 368ce1b596..2a1907085a 100644
--- a/core/modules/node/src/Entity/Node.php
+++ b/core/modules/node/src/Entity/Node.php
@@ -62,7 +62,8 @@
  *   revision_metadata_keys = {
  *     "revision_user" = "revision_uid",
  *     "revision_created" = "revision_timestamp",
- *     "revision_log_message" = "revision_log"
+ *     "revision_log_message" = "revision_log",
+ *     "revision_default" = "revision_default",
  *   },
  *   bundle_entity_type = "node_type",
  *   field_ui_base_route = "entity.node_type.edit_form",
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 2bb7f4a2af..aec5d43089 100644
--- a/core/modules/node/src/NodeViewBuilder.php
+++ b/core/modules/node/src/NodeViewBuilder.php
@@ -89,7 +89,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/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
index 5766817940..f5b3fdcc15 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
@@ -122,6 +122,11 @@ protected function getExpectedNormalizedEntity() {
           'value' => TRUE,
         ],
       ],
+      'revision_default' => [
+        [
+          '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..430b731be7 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_default' => [
+        [
+          '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 bf0ba7a591..fc827ace16 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_default' => [
+        [
+          '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 5df9d75db0..ad9ee9e3d2 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -154,6 +154,7 @@ public function testNormalize() {
         ['value' => TRUE],
       ],
       'non_rev_field' => [],
+      'not_translatable' => [],
       'field_test_text' => [
         [
           'value' => $this->values['field_test_text']['value'],
@@ -226,6 +227,7 @@ public function testSerialize() {
       'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
       'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
       'revision_translation_affected' => '<revision_translation_affected><value>1</value></revision_translation_affected>',
+      'not_translatable' => '<not_translatable/>',
       '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><processed><![CDATA[<p>' . $this->values['field_test_text']['value'] . '</p>]]></processed></field_test_text>',
     ];
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index ef6f124f51..e1b9358bcd 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -2056,3 +2056,40 @@ function system_update_8403() {
     }
   }
 }
+
+/**
+ * Add the 'revision_default' field to all relevant entity types.
+ */
+function system_update_8501() {
+  /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
+  $field_manager = \Drupal::service('entity_field.manager');
+  $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+
+  // Get a list of entity types to process.
+  /** @var \Drupal\Core\Entity\ContentEntityTypeInterface[] $definitions */
+  $definitions = array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) use ($field_manager, $definition_update_manager) {
+    $entity_type_id = $entity_type->id();
+    if ($entity_type = $definition_update_manager->getEntityType($entity_type_id)) {
+      return $entity_type instanceof ContentEntityTypeInterface
+        && $entity_type->isRevisionable()
+        && ($field_name = $entity_type->getRevisionMetadataKey('revision_default'))
+        && isset($field_manager->getFieldStorageDefinitions($entity_type_id)[$field_name])
+        && !$definition_update_manager->getFieldStorageDefinition($field_name, $entity_type_id);
+    }
+    return FALSE;
+  });
+
+  $storage_definition = BaseFieldDefinition::create('boolean')
+    ->setLabel(t('Default revision'))
+    ->setDescription(t('A flag indicating whether this was a default revision when it was saved.'))
+    ->setTranslatable(FALSE)
+    ->setRevisionable(TRUE)
+    ->setInitialValue(TRUE);
+
+  // Install the 'revision_default' field.
+  foreach ($definitions as $entity_type_id => $entity_type) {
+    $field_name = $entity_type->getRevisionMetadataKey('revision_default');
+    $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..7d3f393fec 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,30 @@ function entity_test_entity_bundle_info() {
   return $bundles;
 }
 
+/**
+ * Implements hook_entity_bundle_info_alter().
+ */
+function entity_test_entity_bundle_info_alter(&$bundles) {
+  $state = \Drupal::state();
+  $entity_test_bundle_info = $state->get('entity_test.bundle_info');
+  if ($entity_test_bundle_info) {
+    $bundles = array_merge_recursive($bundles, $entity_test_bundle_info);
+  }
+  $entity_info = \Drupal::entityTypeManager()->getDefinitions();
+  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 2bfdf41419..80f265259d 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
@@ -3,6 +3,7 @@
 namespace Drupal\entity_test\Entity;
 
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
 
 /**
  * Defines the test entity class.
@@ -53,7 +54,14 @@ class EntityTestMulRev extends EntityTestRev {
    * {@inheritdoc}
    */
   public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-    return parent::baseFieldDefinitions($entity_type) + \Drupal::state()->get($entity_type->id() . '.additional_base_field_definitions', []);
+    $fields = parent::baseFieldDefinitions($entity_type) + \Drupal::state()->get($entity_type->id() . '.additional_base_field_definitions', []);
+
+    $fields['not_translatable'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Non translatable'))
+      ->setDescription(t('A non-translatable string field'))
+      ->setRevisionable(TRUE);
+
+    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/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
new file mode 100644
index 0000000000..9feb106ce4
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
@@ -0,0 +1,231 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\entity_test\Entity\EntityTestMulRevChanged;
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * 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', 'language'];
+
+  /**
+   * 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->installConfig(['language']);
+    ConfigurableLanguage::createFromLangcode('it')->save();
+  }
+
+  /**
+   * 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) {
+    $this->saveCounter = 1;
+
+    // When we do not create a new revision in the first of the two concurrent
+    // saves, only the changed timestamp can be used to detect concurrent edits.
+    // Validate this logic by applying only the related validation.
+    $changed_revision_validation = [TRUE];
+    if ($type[0] === static::SAVE_NO_REVISION) {
+      $changed_revision_validation[] = FALSE;
+    }
+
+    foreach ($changed_revision_validation as $enabled) {
+      // Set the "changed_revision" validation status.
+      $bundle_info = [
+        'entity_test_mulrev_changed' => [
+          'entity_test_mulrev_changed' => [
+            'entity_validation_constraints.EntityChanged.changed_revision' => $enabled,
+          ]
+        ],
+      ];
+      $this->state->set('entity_test.bundle_info', $bundle_info);
+      $this->bundleInfo->clearCachedBundles();
+
+      // Create a test entity.
+      $user = $this->createUser();
+      $entity = EntityTestMulRevChanged::create([
+        'not_translatable' => $this->randomString(),
+        'user_id' => $user->id(),
+        'language' => 'en',
+      ]);
+      $this->doEntitySave($entity, static::SAVE_NO_REVISION, 0);
+
+      // 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], 0);
+      $this->doEntitySave($entity_other_instance, $type[1], 1);
+    }
+  }
+
+  /**
+   * 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 int $expected_violation_count
+   *   The number of validation expected violations.
+   */
+  protected function doEntitySave(ContentEntityInterface $entity, $type, $expected_violation_count) {
+    // 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($expected_violation_count, $violations->count(), 'Wrong violations count with constraints');
+    $entity->save();
+  }
+
+  /**
+   * Tests that multilingual edits do not trigger validation errors.
+   *
+   * @covers ::validate
+   */
+  public function testMultilingualRevisionEdit() {
+    // Enable validation of revision changes.
+    $bundle_info = [
+      'entity_test_mulrev_changed' => [
+        'entity_test_mulrev_changed' => [
+          'entity_validation_constraints.EntityChanged.changed_revision' => TRUE,
+        ]
+      ],
+    ];
+    $this->state->set('entity_test.bundle_info', $bundle_info);
+    $this->bundleInfo->clearCachedBundles();
+
+    // Create a new entity.
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $entity */
+    $entity = EntityTestMulRevChanged::create([
+      'user_id' => $this->createUser()->id(),
+      'name' => 'Test 1.1 EN',
+    ]);
+    $violations = $entity->validate();
+    $this->assertEmpty($violations->count());
+    $entity->save();
+
+    // Create a new Italian translation in a new pending revision.
+    $entity = $this->loadLatestTranslationAffectingRevision($entity->id(), 'en');
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $translation */
+    $translation = $entity->addTranslation('it');
+    $translation->setNewRevision(TRUE);
+    $translation->isDefaultRevision(FALSE);
+    $translation->setName('Test 1.2 IT');
+    $violations = $translation->validate();
+    $this->assertEmpty($violations->count());
+    $translation->save();
+
+    // Start from the latest translation-affecting revision and create an new
+    // default revision for the default language.
+    $entity = $this->loadLatestTranslationAffectingRevision($entity->id(), 'en');
+    $entity->setNewRevision(TRUE);
+    $entity->isDefaultRevision(TRUE);
+    $entity->setName('Test 1.3 EN');
+    $violations = $entity->validate();
+    $this->assertEmpty($violations->count());
+    $entity->save();
+
+    // Start from the latest translation-affecting revision and create an new
+    // pending revision for the Italian translation.
+    $entity = $this->loadLatestTranslationAffectingRevision($entity->id(), 'it');
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $translation */
+    $translation = $entity->getTranslation('it');
+    $translation->setNewRevision(TRUE);
+    $translation->isDefaultRevision(FALSE);
+    $translation->setName('Test 1.4 IT');
+    $violations = $translation->validate();
+    $this->assertEmpty($violations->count());
+    $translation->save();
+  }
+
+  /**
+   * Loads the latest translation-affecting revision for the specified language.
+   *
+   * @param int $entity_id
+   *   The entity ID.
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return \Drupal\entity_test\Entity\EntityTestMulRevChanged
+   *   A revision entity object.
+   */
+  protected function loadLatestTranslationAffectingRevision($entity_id, $langcode) {
+    $revision_id = $this->storage->getLatestTranslationAffectedRevisionId($entity_id, $langcode);
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $entity */
+    $entity = $this->storage->loadRevision($revision_id);
+    // @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);
+    return $entity;
+  }
+
+}
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..5afbd07bd7
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php
@@ -0,0 +1,518 @@
+<?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 type 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->getLatestTranslationAffectedRevisionId($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.'));
+    }
+
+    // Check that the previous untranslatable field value is loaded in the new
+    // revision as expected. When we are dealing with a non default translation
+    // the expected value is always the one stored in the default revision, as
+    // untranslatable fields can only be changed in the default translation or
+    // in the default revision, depending on the configured mode.
+    $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.'));
+    }
+    elseif (!$entity->isDefaultTranslation()) {
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
+      $default_revision = $this->storage->loadUnchanged($entity->id());
+      $expected_value = $default_revision->get('not_translatable')->value;
+      $this->assertEquals($expected_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 to analyze test
+      // failures.
+      $prev = 0;
+      if (isset($previous_untranslatable_field_value)) {
+        preg_match('/^\d+ -> (\d+)$/', $previous_untranslatable_field_value, $matches);
+        $prev = $matches[1];
+      }
+      $value = $prev . ' -> ' . ($entity->getLoadedRevisionId() + 1);
+      $entity->set('not_translatable', $value);
+      $previous_untranslatable_field_value = $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/EntityKernelTestBase.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityKernelTestBase.php
index f7108d53b1..39ed42a4e4 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityKernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityKernelTestBase.php
@@ -26,6 +26,13 @@
    */
   protected $entityManager;
 
+  /**
+   * The entity bundle info service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleInfo;
+
   /**
    * A list of generated identifiers.
    *
@@ -45,6 +52,7 @@ protected function setUp() {
 
     $this->entityManager = $this->container->get('entity.manager');
     $this->state = $this->container->get('state');
+    $this->bundleInfo = $this->container->get('entity_type.bundle.info');
 
     $this->installSchema('system', 'sequences');
 
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
index e82d77e713..ba2a29a2a5 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,93 @@ public function testSetNewRevision() {
     }
   }
 
+  /**
+   * Tests that revision translations are correctly detected.
+   *
+   * @covers \Drupal\Core\Entity\ContentEntityStorageBase::hasStoredTranslations
+   */
+  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->hasStoredTranslations($entity));
+    $entity->addTranslation('it');
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasStoredTranslations($entity));
+
+    // Check that not yet stored translations are handled correctly.
+    $entity = EntityTestMul::create();
+    $entity->save();
+    $entity->addTranslation('it');
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasStoredTranslations($entity));
+
+    // Check that removed translations are handled correctly.
+    $entity->save();
+    $entity->removeTranslation('it');
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertTrue($storage->hasStoredTranslations($entity));
+    $entity->save();
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasStoredTranslations($entity));
+    $entity->addTranslation('de');
+    $entity->removeTranslation('de');
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasStoredTranslations($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->hasStoredTranslations($entity));
+
+    // Check that a non-revisionable translated entity is handled correctly.
+    $entity->addTranslation('it');
+    $entity->save();
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertTrue($storage->hasStoredTranslations($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->hasStoredTranslations($entity));
+    $entity->addTranslation('it');
+    $this->assertNotEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasStoredTranslations($entity));
+
+    // Check that a revisionable not translated entity is handled correctly.
+    $entity = EntityTestMulRev::create();
+    $entity->save();
+    $this->assertEmpty($entity->getTranslationLanguages(FALSE));
+    $this->assertFalse($storage->hasStoredTranslations($entity));
+
+    // Check that a revisionable translated pending revision is handled
+    // correctly.
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $new_revision */
+    $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->hasStoredTranslations($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->hasStoredTranslations($entity));
+  }
+
 }
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/Entity/RevisionableContentEntityBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/RevisionableContentEntityBaseTest.php
index 73b3254630..8bdd656ad6 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/RevisionableContentEntityBaseTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/RevisionableContentEntityBaseTest.php
@@ -5,7 +5,6 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog;
-use Drupal\KernelTests\KernelTestBase;
 use Drupal\user\Entity\User;
 use Drupal\user\UserInterface;
 
@@ -13,7 +12,7 @@
  * @coversDefaultClass \Drupal\Core\Entity\RevisionableContentEntityBase
  * @group Entity
  */
-class RevisionableContentEntityBaseTest extends KernelTestBase {
+class RevisionableContentEntityBaseTest extends EntityKernelTestBase {
 
   /**
    * {@inheritdoc}
@@ -25,10 +24,7 @@ class RevisionableContentEntityBaseTest extends KernelTestBase {
    */
   protected function setUp() {
     parent::setUp();
-
     $this->installEntitySchema('entity_test_mul_revlog');
-    $this->installEntitySchema('user');
-    $this->installSchema('system', 'sequences');
   }
 
   /**
@@ -89,6 +85,63 @@ public function testRevisionableContentEntity() {
     $this->assertItemsTableCount(3, $definition);
   }
 
+  /**
+   * Tests the behavior of the "revision_default" flag.
+   *
+   * @covers \Drupal\Core\Entity\RevisionLogEntityTrait::wasDefaultRevision
+   */
+  public function testWasDefaultRevision() {
+    $entity_type_id = 'entity_test_mul_revlog';
+    $entity = EntityTestMulWithRevisionLog::create([
+      'type' => $entity_type_id,
+    ]);
+
+    // Checks that in a new entity ::wasDefaultRevision() always matches
+    // ::isDefaultRevision().
+    $this->assertEquals($entity->isDefaultRevision(), $entity->wasDefaultRevision());
+    $entity->isDefaultRevision(FALSE);
+    $this->assertEquals($entity->isDefaultRevision(), $entity->wasDefaultRevision());
+
+    // Check that a new entity is always flagged as a default revision on save,
+    // regardless of its default revision status.
+    $entity->save();
+    $this->assertTrue($entity->wasDefaultRevision());
+
+    // Check that a pending revision is not flagged as default.
+    $entity->setNewRevision();
+    $entity->isDefaultRevision(FALSE);
+    $entity->save();
+    $this->assertFalse($entity->wasDefaultRevision());
+
+    // Check that a default revision is flagged as such.
+    $entity->setNewRevision();
+    $entity->isDefaultRevision(TRUE);
+    $entity->save();
+    $this->assertTrue($entity->wasDefaultRevision());
+
+    // Check that a manually set value for the "revision_default" flag is
+    // ignored on save.
+    $entity->setNewRevision();
+    $entity->isDefaultRevision(FALSE);
+    $entity->set('revision_default', TRUE);
+    $this->assertTrue($entity->wasDefaultRevision());
+    $entity->save();
+    $this->assertFalse($entity->wasDefaultRevision());
+
+    // Check that the default revision status was stored correctly.
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    foreach ([TRUE, FALSE, TRUE, FALSE] as $index => $expected) {
+      /** @var \Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog $revision */
+      $revision = $storage->loadRevision($index + 1);
+      $this->assertEquals($expected, $revision->wasDefaultRevision());
+    }
+
+    // Check that the default revision is flagged correctly.
+    /** @var \Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog $entity */
+    $entity = $storage->loadUnchanged($entity->id());
+    $this->assertTrue($entity->wasDefaultRevision());
+  }
+
   /**
    * Asserts the ammount of items on entity related tables.
    *
diff --git a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php
index d2fd2f5241..f142d0c71c 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, [
diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
index 9ec5430697..1b401b98b5 100644
--- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
+++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
@@ -69,6 +69,7 @@ public static function getSkippedDeprecations() {
       'The revision_user revision metadata key is not set.',
       'The revision_created revision metadata key is not set.',
       'The revision_log_message revision metadata key is not set.',
+      'The revision_default revision metadata key is not set.',
       'The "entity.query" service relies on the deprecated "Drupal\Core\Entity\Query\QueryFactory" class. It should either be deprecated or its implementation upgraded.',
       'MigrateCckField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Annotation\MigrateField instead.',
       'MigrateCckFieldPluginManager is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Annotation\MigrateFieldPluginManager instead.',
