diff --git a/core/core.services.yml b/core/core.services.yml
index 7d7ff3e..8e900e1 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -891,6 +891,9 @@ services:
   entity.query.keyvalue:
     class: Drupal\Core\Entity\KeyValueStore\Query\QueryFactory
     arguments: ['@keyvalue']
+  entity.common_queries:
+    class: Drupal\Core\Entity\Query\Queries
+    arguments: ['@entity_type.manager']
   router.dumper:
     class: Drupal\Core\Routing\MatcherDumper
     arguments: ['@database', '@state']
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 2c62589..3415466 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Entity\Plugin\DataType\EntityReference;
+use Drupal\Core\Entity\Query\Queries;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Field\ChangedFieldItemList;
 use Drupal\Core\Language\Language;
@@ -354,6 +355,70 @@ public function getRevisionId() {
   /**
    * {@inheritdoc}
    */
+  public function createNewRevision($default_revision = TRUE) {
+    $new_revision = NULL;
+    $storage = $this->entityTypeManager()->getStorage($this->entityTypeId);
+
+    if ($this->languageManager()->isMultilingual() && $this->getTranslationLanguages() > 1) {
+      // Start from the default revision and merge the values of the latest
+      // translation affected revision for the active language. This allows to
+      // create pending revisions that can always be saved as the new default
+      // revision without reverting changes in other languages.
+      /** @var \Drupal\Core\Entity\ContentEntityBase $new_revision */
+      $new_revision = clone $storage->load($this->id());
+      $new_revision = $new_revision->hasTranslation($this->activeLangcode) ?
+        $new_revision->getTranslation($this->activeLangcode) : $new_revision->addTranslation($this->activeLangcode);
+
+      // TODO
+      $latest_affected_revision_id = Queries::get()->getLatestAffectedRevisionId($this);
+      if (isset($latest_affected_revision_id)) {
+        /** @var \Drupal\Core\Entity\ContentEntityBase $latest_revision */
+        $latest_revision = $storage->loadRevision($latest_affected_revision_id);
+        $latest_revision = $latest_revision->getTranslation($this->activeLangcode);
+      }
+      else {
+        $latest_revision = $this;
+      }
+
+      // TODO untranslatable fields.
+      foreach ($new_revision->getFieldDefinitions() as $field_name => $definition) {
+        if ($definition->isTranslatable()) {
+          $new_revision->set($field_name, $latest_revision->get($field_name)->getValue());
+        }
+      }
+
+      // TODO
+      foreach ($new_revision->getTranslationLanguages() as $langcode => $language) {
+        $new_revision
+          ->getTranslation($langcode)
+          ->setRevisionTranslationAffected($langcode == $this->activeLangcode);
+      }
+
+      // TODO
+      $new_revision->original = clone $new_revision;
+    }
+
+    // TODO
+    $latest_revision_id = Queries::get()->getLatestRevisionId($this);
+    if (!isset($new_revision)) {
+      $revision = $this->getRevisionId() == $latest_revision_id ? $this : $storage->loadRevision($latest_revision_id);
+      $new_revision = clone $revision;
+    }
+    else {
+      $revision_key = $this->getEntityType()->getKey('revision');
+      $new_revision->set($revision_key, $latest_revision_id);
+      $new_revision->updateLoadedRevisionId();
+    }
+
+    $new_revision->setNewRevision();
+    $new_revision->isDefaultRevision($default_revision);
+
+    return $new_revision;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function isTranslatable() {
     // Check that the bundle is translatable, the entity has a language defined
     // and if we have more than one language on the site.
@@ -713,6 +778,12 @@ public function onChange($name) {
         elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
           unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
         }
+        // If the revision identifier field is being populated with the original
+        // value, we need to make sure the "new revision" flag is reset
+        // accordingly.
+        if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId()) {
+          $this->newRevision = FALSE;
+        }
       }
     }
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 92bbbae..4fb330e 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -127,6 +127,17 @@ public function form(array $form, FormStateInterface $form_state) {
       $this->addRevisionableFormFields($form);
     }
 
+    // Store the revision ID at the time the form is built, so it can be used
+    // later to detect concurrent edits.
+    $entity_type = $this->entity->getEntityType();
+    if ($entity_type->isRevisionable()) {
+      $revision_key = $entity_type->getKey('revision');
+      $form[$revision_key] = [
+        '#type' => 'hidden',
+        '#default_value' => $this->entity->getRevisionId(),
+      ];
+    }
+
     $form['footer'] = [
       '#type' => 'container',
       '#weight' => 99,
@@ -155,6 +166,14 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     $entity = parent::buildEntity($form, $form_state);
 
+    // We did not save the entity yet, so we expect the loaded revision ID to
+    // match the revision ID. However this may not be the case if the entity was
+    // saved in another form. Thus we need to make sure the loaded revision ID
+    // actually matches the revision ID.
+    if ($this->entity->getEntityType()->isRevisionable()) {
+      $entity->updateLoadedRevisionId();
+    }
+
     // 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 d045152..6b757ff 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -71,4 +71,11 @@ public function getLoadedRevisionId();
    */
   public function updateLoadedRevisionId();
 
+  /**
+   * TODO
+   * @param bool $default_revision
+   * @return static
+   */
+  public function createNewRevision($default_revision = TRUE);
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 874a341..ee245d1 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -323,6 +323,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/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
index b1fee2a..9b9d3bd 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
@@ -2,6 +2,10 @@
 
 namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
 
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Query\Queries;
+use Drupal\Core\Entity\RevisionableInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 
@@ -14,17 +18,44 @@ class EntityChangedConstraintValidator extends ConstraintValidator {
    * {@inheritdoc}
    */
   public function validate($entity, Constraint $constraint) {
-    if (isset($entity)) {
-      /** @var \Drupal\Core\Entity\EntityInterface $entity */
-      if (!$entity->isNew()) {
-        $saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
+    if (isset($entity) && !$entity->isNew()) {
+      $valid = TRUE;
+
+      if ($entity instanceof RevisionableInterface) {
+        // If the entity is revisionable, we need to make sure that no new
+        // revision was created while the entity was being changed.
+        $latest_revision_id = $this->getLatestRevisionId($entity);
+        $loaded_revision_id = $entity->getLoadedRevisionId();
+        $valid = $loaded_revision_id == $latest_revision_id;
+      }
+
+      if ($valid && $entity instanceof EntityChangedInterface) {
+        /** @var \Drupal\Core\Entity\EntityChangedInterface|\Drupal\Core\Entity\RevisionableInterface $stored_entity */
+        $stored_entity = \Drupal::entityTypeManager()
+          ->getStorage($entity->getEntityTypeId())
+          ->loadUnchanged($entity->id());
         // A change to any other translation must add a violation to the current
         // translation because there might be untranslatable shared fields.
-        if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) {
-          $this->context->addViolation($constraint->message);
-        }
+        $valid = $stored_entity->getChangedTimeAcrossTranslations() <= $entity->getChangedTimeAcrossTranslations();
+      }
+
+      if (!$valid) {
+        $this->context->addViolation($constraint->message);
       }
     }
   }
 
+  /**
+   * Returns the latest revision identifier for the specified entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   An entity object.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  protected function getLatestRevisionId(EntityInterface $entity) {
+    return Queries::get()->getLatestRevisionId($entity);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Query/Queries.php b/core/lib/Drupal/Core/Entity/Query/Queries.php
new file mode 100644
index 0000000..30d5803
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Query/Queries.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\Core\Entity\Query;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+
+/**
+ * Implements a collection of common entity queries.
+ *
+ * @internal
+ */
+class Queries {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Returns an instance of the Queries class.
+   *
+   * @return static
+   */
+  public static function get() {
+    return \Drupal::service('entity.common_queries');
+  }
+
+  /**
+   * Queries constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Returns the latest revision identifier for the specified entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   An entity object.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  public function getLatestRevisionId(EntityInterface $entity) {
+    $entity_type = $entity->getEntityType();
+    if (!$entity instanceof RevisionableInterface || $entity->isNew() || !$entity_type->isRevisionable()) {
+      return NULL;
+    }
+
+    $id_key = $entity_type->getKey('id');
+
+    $result = $this->entityTypeManager
+      ->getStorage($entity->getEntityTypeId())
+      ->getQuery()
+      ->latestRevision()
+      ->condition($id_key, $entity->id())
+      ->execute();
+
+    return key($result);
+  }
+
+  /**
+   * Returns the latest revision identifier for the specified entity. TODO
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   An entity object.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  public function getLatestAffectedRevisionId(ContentEntityInterface $entity) {
+    $entity_type = $entity->getEntityType();
+    if (!$entity instanceof RevisionableInterface || $entity->isNew() || !$entity_type->isRevisionable()) {
+      return NULL;
+    }
+
+    $id_key = $entity_type->getKey('id');
+    $revision_key = $entity_type->getKey('revision');
+    $rta_key = $entity_type->getKey('revision_translation_affected');
+
+    $result = $this->entityTypeManager
+      ->getStorage($entity->getEntityTypeId())
+      ->getQuery()
+      ->allRevisions()
+      ->condition($id_key, $entity->id())
+      ->condition($rta_key, 1, '=', $entity->language()->getId())
+      ->range(0, 1)
+      ->sort($revision_key, 'DESC')
+      ->execute();
+
+    return key($result);
+  }
+
+}
diff --git a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
index f2c6917..1f36200 100644
--- a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
+++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
@@ -32,6 +32,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    * {@inheritdoc}
    */
   public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) {
+    // FIXME
     // This is probably not necessary if configuration is setup correctly.
     $entity->setNewRevision(TRUE);
     $entity->isDefaultRevision($default_revision);
diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php
index 58eaa9f..451e013 100644
--- a/core/modules/content_moderation/src/ModerationInformation.php
+++ b/core/modules/content_moderation/src/ModerationInformation.php
@@ -128,7 +128,8 @@ public function getAffectedRevisionTranslation(ContentEntityInterface $entity) {
    * {@inheritdoc}
    */
   public function isPendingRevisionAllowed(ContentEntityInterface $entity) {
-    return !(!$entity->isRevisionTranslationAffected() && count($entity->getTranslationLanguages()) > 1 && $this->hasPendingRevision($entity));
+    // return !(!$entity->isRevisionTranslationAffected() && count($entity->getTranslationLanguages()) > 1 && $this->hasPendingRevision($entity));
+    return TRUE; // FIXME
   }
 
   /**
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 0000000..1416f14
--- /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 0000000..8e76e0d
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test/content_moderation_test.module
@@ -0,0 +1,20 @@
+<?php
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * {@inheritdoc}
+ */
+function content_moderation_test_form_node_moderated_content_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  if (\Drupal::state()->get('content_moderation_test.concurrent_edit')) {
+    /** @var \Drupal\Core\Entity\ContentEntityForm $form_object */
+    $form_object = $form_state->getFormObject();
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $form_object->getEntity();
+    // We simulate a concurrent edit by setting the revision ID at the time the
+    // form is instantiated to the previous revision ID.
+    $form['vid']['#default_value'] = $entity->getRevisionId() - 1;
+  }
+}
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
index 326eb7c..1c4d07e 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\content_moderation\Functional;
 
+use Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraint;
 use Drupal\workflows\Entity\Workflow;
 
 /**
@@ -21,6 +22,7 @@ class ModerationFormTest extends ModerationStateTestBase {
     'content_moderation',
     'locale',
     'content_translation',
+    'content_moderation_test',
   ];
 
   /**
@@ -525,4 +527,41 @@ public function testWorkflowInUse() {
     }
   }
 
+  /**
+   * Tests that concurrent edits of the same draft are not allowed.
+   */
+  public function testConcurrentDraftEdit() {
+    // Create new moderated content in published status.
+    $title = 'Some moderated content';
+    $edit = [
+      'title[0][value]' => $title,
+      'body[0][value]' => 'First version of the content.',
+      'moderation_state[0][state]' => 'published',
+    ];
+    $this->drupalPostForm('node/add/moderated_content', $edit, t('Save'));
+
+    $node = $this->drupalGetNodeByTitle($title);
+    $edit_path = sprintf('node/%d/edit', $node->id());
+
+    // Create a draft.
+    $edit = [
+      'body[0][value]' => 'Second version of the content.',
+      'moderation_state[0][state]' => 'draft',
+    ];
+    $this->drupalPostForm($edit_path, $edit, t('Save'));
+    $revision_id = $this->getLatestRevisionId($node);
+
+    // Simulate a concurrent draft post: we expect to see a validation error in
+    // this case.
+    \Drupal::state()->set('content_moderation_test.concurrent_edit', 1);
+    $edit = [
+      'body[0][value]' => 'Third version of the content.',
+      'moderation_state[0][state]' => 'draft',
+    ];
+    $this->drupalPostForm($edit_path, $edit, t('Save'));
+    $this->assertEquals($revision_id, $this->getLatestRevisionId($node));
+    $this->assertSession()
+      ->responseContains((new EntityChangedConstraint())->message);
+  }
+
 }
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php
index 92f51a8..457aac5 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\content_moderation\Functional;
 
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\user\Entity\Role;
@@ -151,4 +152,26 @@ protected function grantUserPermissionToCreateContentOfType(AccountInterface $ac
     $role->save();
   }
 
+  /**
+   * Returns the latest revision identifier for the specified node.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   A content entity object.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  protected function getLatestRevisionId(ContentEntityInterface $entity) {
+    $id_key = $entity->getEntityType()->getKey('id');
+
+    $result = \Drupal::entityTypeManager()
+      ->getStorage($entity->getEntityTypeId())
+      ->getQuery()
+      ->latestRevision()
+      ->condition($id_key, $entity->id())
+      ->execute();
+
+    return key($result);
+  }
+
 }
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
index 2fefc01..9b898c6 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
@@ -199,14 +199,16 @@ public function testInvalidStateMultilingual() {
     $this->assertCount(1, $violations);
     $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
 
-    // From the default french published revision, there should be none.
-    $node_fr = Node::load($node->id())->getTranslation('fr');
-    $this->assertEquals('published', $node_fr->moderation_state->value);
-    $node_fr->moderation_state = 'archived';
-    $violations = $node_fr->validate();
-    $this->assertCount(0, $violations);
-
-    // From the latest french revision, there should also be no violation.
+    // @todo Creating a new revision from a past revision is currently not
+    //   supported. Evaluate this again in https://www.drupal.org/node/2860097.
+    // // From the default French published revision, there should be none.
+    // $node_fr = Node::load($node->id())->getTranslation('fr');
+    // $this->assertEquals('published', $node_fr->moderation_state->value);
+    // $node_fr->moderation_state = 'archived';
+    // $violations = $node_fr->validate();
+    // $this->assertCount(0, $violations);
+
+    // From the latest French revision, there should be no violation.
     $node_fr = $node->getTranslation('fr');
     $this->assertEquals('published', $node_fr->moderation_state->value);
     $node_fr->moderation_state = 'archived';
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 8dbd170..e0a0b43 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -413,10 +413,6 @@ function content_translation_entity_presave(EntityInterface $entity) {
     // If we are creating a new translation we need to use the source language
     // as original language, since source values are the only ones available to
     // compare against.
-    if (!isset($entity->original)) {
-      $entity->original = \Drupal::entityTypeManager()
-        ->getStorage($entity->entityType())->loadUnchanged($entity->id());
-    }
     $langcode = $entity->language()->getId();
     /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
     $manager = \Drupal::service('content_translation.manager');
diff --git a/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
index 0c68d8b..2b1efcd 100644
--- a/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
+++ b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
@@ -101,6 +101,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $node_rev
    * {@inheritdoc}
    */
   protected function prepareRevertedRevision(NodeInterface $revision, FormStateInterface $form_state) {
+    // FIXME
     $revert_untranslated_fields = $form_state->getValue('revert_untranslated_fields');
 
     /** @var \Drupal\node\NodeInterface $default_revision */
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 0000000..0a88890
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\entity_test\Entity\EntityTestMulRevChanged;
+
+/**
+ * Tests concurrent entity saves and revisions.
+ *
+ * @group Entity
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraintValidator
+ */
+class ContentEntityConcurrentRevisionSaveTest extends EntityKernelTestBase {
+
+  const SAVE_NO_REVISION = 0x0;
+  const SAVE_NEW_REVISION = 0x1;
+  const SAVE_PENDING_REVISION = 0x3;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['user', 'system', 'field', 'text', 'filter', 'entity_test'];
+
+  /**
+   * The EntityTestMulRevChanged entity type storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Counter used to generate meaningful entity labels.
+   *
+   * @var int
+   */
+  protected $saveCounter;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('entity_test_mulrev_changed');
+    $this->storage = $this->entityManager->getStorage('entity_test_mulrev_changed');
+    $this->saveCounter = 1;
+  }
+
+  /**
+   * Data provider for testConcurrentSave().
+   */
+  public function dataTestConcurrentSave() {
+    return [
+      'pending revision - pending revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_PENDING_REVISION]],
+      'no revision - pending revision' => [[static::SAVE_NO_REVISION, static::SAVE_PENDING_REVISION]],
+      'new revision - pending revision' => [[static::SAVE_NEW_REVISION, static::SAVE_PENDING_REVISION]],
+      'pending revision - no revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_NO_REVISION]],
+      'no revision - no revision' => [[static::SAVE_NO_REVISION, static::SAVE_NO_REVISION]],
+      'new revision - no revision' => [[static::SAVE_NEW_REVISION, static::SAVE_NO_REVISION]],
+      'pending revision - new revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_NEW_REVISION]],
+      'no revision - new revision' => [[static::SAVE_NO_REVISION, static::SAVE_NEW_REVISION]],
+      'new revision - new revision' => [[static::SAVE_NEW_REVISION, static::SAVE_NEW_REVISION]],
+    ];
+  }
+
+  /**
+   * Tests concurrent entity saves with various save types.
+   *
+   * @param int[] $type
+   *   An array of save types as defined by the related ::SAVE_* constants. One
+   *   for each of the two concurrent saves.
+   *
+   * @covers ::validate
+   *
+   * @dataProvider dataTestConcurrentSave
+   */
+  public function testConcurrentSave(array $type) {
+    $user = $this->createUser();
+
+    // Create a test entity.
+    $entity = EntityTestMulRevChanged::create([
+      'not_translatable' => $this->randomString(),
+      'user_id' => $user->id(),
+      'language' => 'en',
+    ]);
+    $this->doEntitySave($entity, static::SAVE_NO_REVISION, FALSE);
+
+    // Perform two "concurrent" saves and verify the second one triggers a
+    // validation error.
+    $entity_other_instance = clone $entity;
+    $this->doEntitySave($entity, $type[0], FALSE);
+    $this->doEntitySave($entity_other_instance, $type[1], TRUE);
+  }
+
+  /**
+   * Performs an entity save of the specified "type".
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity to be saved.
+   * @param int $type
+   *   The save type, as defined by the ::SAVE_* constants.
+   * @param bool $error_expected
+   *   Flag indicating whether a validation error is expected.
+   */
+  protected function doEntitySave(ContentEntityInterface $entity, $type, $error_expected) {
+    // Set a meaningful entity label, useful for debugging.
+    $entity->set('name', 'Title ' . ($this->saveCounter++));
+    if ($type & static::SAVE_NEW_REVISION) {
+      $entity->setNewRevision();
+    }
+    if ($type === static::SAVE_PENDING_REVISION) {
+      $entity->isDefaultRevision(FALSE);
+    }
+    $violations = $entity->validate();
+    $this->assertEquals($error_expected, (bool) $violations->count());
+    $entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php
new file mode 100644
index 0000000..3e8c268
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDecoupledTranslationRevisionsTest.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\entity_test\Entity\EntityTestMulRevChanged;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\user\Entity\User;
+
+/**
+ * Test decoupled translation revisions.
+ *
+ * @group entity
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase
+ */
+class EntityDecoupledTranslationRevisionsTest extends EntityKernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'system',
+    'entity_test',
+    'language',
+    'content_translation',
+  ];
+
+  /**
+   * The entity storage.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * The translations of the test entity.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityInterface[] $translations
+   */
+  protected $translations;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity_type_id = 'entity_test_mulrev_changed';
+    $this->installEntitySchema($entity_type_id);
+    $this->storage = $this->container->get('entity_type.manager')
+      ->getStorage($entity_type_id);
+
+    $this->installConfig(['language']);
+    ConfigurableLanguage::createFromLangcode('it')->save();
+
+    $values = [
+      'name' => $this->randomString(),
+      'status' => 1,
+    ];
+    User::create($values)->save();
+  }
+
+  /**
+   * Data provider for ::testDecoupledDefaultRevisions.
+   */
+  public function dataTestDecoupledDefaultRevisions() {
+    $sets = [];
+
+    $sets['Alternate languages - No default translation'] = [[
+      ['en', TRUE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', FALSE],
+      ['it', FALSE],
+      ['en', TRUE],
+      ['it', TRUE],
+    ]];
+
+    return $sets;
+  }
+
+  /**
+   * Test decoupled default revisions.
+   *
+   * @param array[] $sequence
+   *   An array with arrays of arguments for the ::doSaveNewRevision() method as
+   *   values. Every child array corresponds to a method invocation.
+   *
+   * @covers ::createNewRevision
+   *
+   * @dataProvider dataTestDecoupledDefaultRevisions
+   */
+  public function testDecoupledDefaultRevisions($sequence) {
+    $revision_id = NULL;
+    foreach ($sequence as $step) {
+      list($langcode, $default_revision) = $step;
+      $revision_id = $this->doSaveNewRevision($langcode, $default_revision);
+    }
+    $this->assertEquals(count($sequence), $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.
+   *
+   * @return int
+   *   The new revision identifier.
+   */
+  protected function doSaveNewRevision($active_langcode, $default_revision) {
+    if (!isset($this->translations)) {
+      $this->translations[$active_langcode] = EntityTestMulRevChanged::create();
+    }
+    if (!isset($this->translations[$active_langcode])) {
+      $this->translations[$active_langcode] = reset($this->translations)->addTranslation($active_langcode);
+    }
+    $entity = &$this->translations[$active_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 current revision should be
+    // match the stored one.
+    if (!$entity->isNew()) {
+      $previous_label = NULL;
+      if (!$entity->isNewTranslation()) {
+        $previous_label = $this->generateNewEntityLabel($entity);
+      }
+      $entity = $entity->createNewRevision($default_revision);
+      $this->assertEquals($previous_label, $entity->label());
+    }
+
+    // Perform a change and store it. We expect the revision to always be valid.
+    $label = $this->generateNewEntityLabel($entity, TRUE);
+    $entity->set('name', $label);
+    $violations = $entity->validate();
+    foreach ($violations as $violation) {
+      print '' . $violation;
+    }
+    $this->assertEmpty($violations);
+    $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;
+      $this->assertEquals($rta_expected, $translation->isRevisionTranslationAffected());
+      $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());
+    }
+
+    return $entity->getRevisionId();
+  }
+
+  /**
+   * Generates a new label for the specified revision.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $revision
+   *   An entity object.
+   * @param bool $next
+   *   (optional) Whether the label describes the current revision or the one
+   *   to be created.
+   *
+   * @return string
+   *   A revision label.
+   */
+  protected function generateNewEntityLabel(ContentEntityInterface $revision, $next = FALSE) {
+    $language_name = $revision->language()->getName();
+    $revision_type = $revision->isDefaultRevision() ? 'Default' : 'Pending';
+    $revision_id = $revision->getLoadedRevisionId() + (bool) $next;
+    return sprintf('%s (%s revision %d)', $language_name, $revision_type, $revision_id);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
index a3b99fe..5a1d5fa 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
@@ -167,4 +167,39 @@ public function testSaveInHookEntityInsert() {
 
   }
 
+  /**
+   * Tests manual revert of the revision ID value.
+   *
+   * @covers \Drupal\Core\Entity\ContentEntityBase::getRevisionId
+   * @covers \Drupal\Core\Entity\ContentEntityBase::getLoadedRevisionId
+   * @covers \Drupal\Core\Entity\ContentEntityBase::setNewRevision
+   * @covers \Drupal\Core\Entity\ContentEntityBase::isNewRevision
+   */
+  public function testNewRevisionRevert() {
+    $entity = EntityTestMulRev::create(['name' => 'EntityLoadedRevisionTest']);
+    $entity->save();
+
+    // Check that revision ID field is reset while the loaded revision ID is
+    // preserved when flagging a new revision.
+    $revision_id = $entity->getRevisionId();
+    $entity->setNewRevision();
+    $this->assertNull($entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+    $this->assertTrue($entity->isNewRevision());
+
+    // Check that manually reverting the new revision state by assigning a value
+    // to the revision ID field works as expected.
+    $key = $entity->getEntityType()->getKey('revision');
+    $entity->set($key, $revision_id);
+    $this->assertEquals($revision_id, $entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+    $this->assertFalse($entity->isNewRevision());
+
+    // Check that flagging a new revision again works correctly.
+    $entity->setNewRevision();
+    $this->assertNull($entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+    $this->assertTrue($entity->isNewRevision());
+  }
+
 }
