+ /**
+ * {@inheritdoc}
+ */
+ public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ $new_revision = clone $entity;
+
+ $original_keep_untranslatable_fields = $keep_untranslatable_fields;
+
+ // 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->isAnyRevisionTranslated($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());
+ $translation_languages = $default_revision->getTranslationLanguages();
+ foreach ($translation_languages 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;
+ }
+
+ // Make sure we do not inadvertently recreate removed translations.
+ foreach (array_diff_key($new_revision->getTranslationLanguages(), $translation_languages) as $langcode => $language) {
+ // Allow a new revision to be created for the active language.
+ if ($langcode !== $active_langcode) {
+ $new_revision->removeTranslation($langcode);
+ }
+ }
+
+ // The "original" property is used in various places to detect changes in
+ // field values with respect to the stored ones. If the property is not
+ // defined, the stored version is loaded explicitly. Since the merged
+ // revision generated here is not stored anywhere, we need to populate the
+ // "original" property manually, so that changes can be properly detected.
+ $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);
+
+ // Notify modules about the new revision.
+ $arguments = [$new_revision, $entity, $original_keep_untranslatable_fields];
+ $this->moduleHandler()->invokeAll($this->entityTypeId . '_revision_create', $arguments);
+ $this->moduleHandler()->invokeAll('entity_revision_create', $arguments);
+
+ 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}
+ */
+ public function getLatestRevisionId($entity_id) {
+ if (!$this->entityType->isRevisionable()) {
+ return NULL;
+ }
+
+ if (!isset($this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT])) {
+ $result = $this->getQuery()
+ ->latestRevision()
+ ->condition($this->entityType->getKey('id'), $entity_id)
+ ->accessCheck(FALSE)
+ ->execute();
+
+ $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT] = key($result);
+ }
+
+ return $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
+ if (!$this->entityType->isRevisionable()) {
+ return NULL;
+ }
+
+ if (!$this->entityType->isTranslatable()) {
+ return $this->getLatestRevisionId($entity_id);
+ }
+
+ if (!isset($this->latestRevisionIds[$entity_id][$langcode])) {
+ $result = $this->getQuery()
+ ->allRevisions()
+ ->condition($this->entityType->getKey('id'), $entity_id)
+ ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
+ ->range(0, 1)
+ ->sort($this->entityType->getKey('revision'), 'DESC')
+ ->accessCheck(FALSE)
+ ->execute();
+
+ $this->latestRevisionIds[$entity_id][$langcode] = key($result);
+ }
+ return $this->latestRevisionIds[$entity_id][$langcode];
+ }
+