From 2ff30c7cc9c5118599e7d6554160b71e4ef5d5b3 Mon Sep 17 00:00:00 2001
From: Owen Bush <ojb@ukhhf.co.uk>
Date: Tue, 14 May 2019 16:37:21 -0600
Subject: [PATCH] Enable translation and revisioning for eventseries and
 eventinstance

---
 .../src/Form/RegistrantForm.php               |   2 +-
 recurring_events.links.action.yml             |   1 -
 recurring_events.links.task.yml               |  20 +-
 recurring_events.permissions.yml              |  10 +
 recurring_events.routing.yml                  |   4 +-
 src/Controller/EventController.php            |  79 ------
 src/Controller/EventInstanceController.php    | 222 ++++++++++++++++
 src/Controller/EventSeriesController.php      | 236 ++++++++++++++++++
 src/Entity/EventInstance.php                  |  94 +++----
 src/Entity/EventSeries.php                    |  94 +++----
 src/EventInstanceHtmlRouteProvider.php        | 163 ++++++++++++
 src/EventInstanceStorage.php                  |  58 +++++
 src/EventInstanceStorageInterface.php         |  61 +++++
 src/EventInstanceTranslationHandler.php       |  14 ++
 src/EventSeriesHtmlRouteProvider.php          | 163 ++++++++++++
 src/EventSeriesStorage.php                    |  58 +++++
 src/EventSeriesStorageInterface.php           |  61 +++++
 src/EventSeriesTranslationHandler.php         |  14 ++
 src/Form/EventInstanceForm.php                |  21 +-
 src/Form/EventInstanceRevisionDeleteForm.php  | 122 +++++++++
 src/Form/EventInstanceRevisionRevertForm.php  | 149 +++++++++++
 ...tInstanceRevisionRevertTranslationForm.php | 115 +++++++++
 src/Form/EventSeriesDeleteForm.php            |   2 +-
 src/Form/EventSeriesForm.php                  |  17 +-
 src/Form/EventSeriesRevisionDeleteForm.php    | 122 +++++++++
 src/Form/EventSeriesRevisionRevertForm.php    | 149 +++++++++++
 ...entSeriesRevisionRevertTranslationForm.php | 115 +++++++++
 27 files changed, 1981 insertions(+), 185 deletions(-)
 delete mode 100644 src/Controller/EventController.php
 create mode 100644 src/Controller/EventInstanceController.php
 create mode 100644 src/Controller/EventSeriesController.php
 create mode 100644 src/EventInstanceHtmlRouteProvider.php
 create mode 100644 src/EventInstanceStorage.php
 create mode 100644 src/EventInstanceStorageInterface.php
 create mode 100644 src/EventInstanceTranslationHandler.php
 create mode 100644 src/EventSeriesHtmlRouteProvider.php
 create mode 100644 src/EventSeriesStorage.php
 create mode 100644 src/EventSeriesStorageInterface.php
 create mode 100644 src/EventSeriesTranslationHandler.php
 create mode 100644 src/Form/EventInstanceRevisionDeleteForm.php
 create mode 100644 src/Form/EventInstanceRevisionRevertForm.php
 create mode 100644 src/Form/EventInstanceRevisionRevertTranslationForm.php
 create mode 100644 src/Form/EventSeriesRevisionDeleteForm.php
 create mode 100644 src/Form/EventSeriesRevisionRevertForm.php
 create mode 100644 src/Form/EventSeriesRevisionRevertTranslationForm.php

diff --git a/modules/recurring_events_registration/src/Form/RegistrantForm.php b/modules/recurring_events_registration/src/Form/RegistrantForm.php
index 40988816..18569fcd 100644
--- a/modules/recurring_events_registration/src/Form/RegistrantForm.php
+++ b/modules/recurring_events_registration/src/Form/RegistrantForm.php
@@ -87,7 +87,7 @@ class RegistrantForm extends ContentEntityForm {
   }
 
   /**
-   * Construct a EventSeriesForm.
+   * Construct an RegistrantForm.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager service.
diff --git a/recurring_events.links.action.yml b/recurring_events.links.action.yml
index 9f3ca0df..dd54450e 100644
--- a/recurring_events.links.action.yml
+++ b/recurring_events.links.action.yml
@@ -3,4 +3,3 @@ entity.field_inheritance.add_form:
   title: 'Add Field inheritance'
   appears_on:
     - entity.field_inheritance.collection
-
diff --git a/recurring_events.links.task.yml b/recurring_events.links.task.yml
index 2b584d41..e5dbecb5 100644
--- a/recurring_events.links.task.yml
+++ b/recurring_events.links.task.yml
@@ -17,14 +17,21 @@ eventseries.delete_form:
   route_name:  entity.eventseries.delete_form
   base_route:  entity.eventseries.canonical
   title: Delete
-  weight: 
+  weight: 10
+
+# Event Series revisions link.
+entity.eventseries.version_history:
+  route_name: entity.eventseries.version_history
+  base_route: entity.eventseries.canonical
+  title: 'Revisions'
+  weight: 11
   
 # Event Series clone link.
 eventseries.clone_form:
   route_name:  entity.eventseries.clone_form
   base_route:  entity.eventseries.canonical
   title: Clone
-  weight: 11
+  weight: 12
 
 ### Event Instance local links.
 
@@ -47,12 +54,19 @@ eventinstance.delete_form:
   title: Delete
   weight: 10
 
+# Event Instance revisions link.
+entity.eventinstance.version_history:
+  route_name: entity.eventinstance.version_history
+  base_route: entity.eventinstance.canonical
+  title: 'Revisions'
+  weight: 11
+
 # Event Instance clone link.
 eventinstance.clone_form:
   route_name:  entity.eventinstance.clone_form
   base_route:  entity.eventinstance.canonical
   title: Clone
-  weight: 11
+  weight: 12
 
 # Event Series settings admin page.
 entity.eventseries.settings:
diff --git a/recurring_events.permissions.yml b/recurring_events.permissions.yml
index c01b3e97..e2be5ba2 100644
--- a/recurring_events.permissions.yml
+++ b/recurring_events.permissions.yml
@@ -23,6 +23,11 @@ delete own eventseries entity:
 clone eventseries entity:
   title: 'Clone eventseries entity'
   description: 'Clone existing event series entities'
+view all eventseries revisions:
+  title: 'View all eventseries revisions'
+revert all eventseries revisions:
+  title: 'Revert all eventseries revisions'
+  description: 'Role requires permission <em>view eventseries revisions</em> and <em>edit rights</em> for eventseries entities in question or <em>administer eventseries entities</em>.'
 administer eventseries entity:
   title: 'Administer eventseries entity'
   description: 'Make changes to the event series entity type.'
@@ -50,6 +55,11 @@ delete own eventinstance entity:
 clone eventinstance entity:
   title: 'Clone eventinstance entity'
   description: 'Clone existing event instance entities'
+view all eventinstance revisions:
+  title: 'View all eventinstance revisions'
+revert all eventinstance revisions:
+  title: 'Revert all eventinstance revisions'
+  description: 'Role requires permission <em>view eventinstance revisions</em> and <em>edit rights</em> for eventinstance entities in question or <em>administer eventinstance entities</em>.'
 administer eventinstance entity:
   title: 'Administer eventinstance entity'
   description: 'Make changes to the event instance entity type.'
diff --git a/recurring_events.routing.yml b/recurring_events.routing.yml
index e923ec6a..b054bd01 100644
--- a/recurring_events.routing.yml
+++ b/recurring_events.routing.yml
@@ -147,7 +147,7 @@ events.admin.overview:
   path: '/admin/structure/events'
   defaults:
     _title: 'Events Management'
-    _controller: '\Drupal\recurring_events\Controller\EventController::adminPage'
+    _controller: '\Drupal\recurring_events\Controller\EventSeriesController::adminPage'
   requirements:
     _permission: 'access administration pages'
 
@@ -156,7 +156,7 @@ events.admin.content:
   path: '/admin/content/events'
   defaults:
     _title: 'Events'
-    _controller: '\Drupal\recurring_events\Controller\EventController::contentPage'
+    _controller: '\Drupal\recurring_events\Controller\EventSeriesController::contentPage'
   requirements:
     _permission: 'access administration pages'
 
diff --git a/src/Controller/EventController.php b/src/Controller/EventController.php
deleted file mode 100644
index 3c71ae50..00000000
--- a/src/Controller/EventController.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-namespace Drupal\recurring_events\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Datetime\DateFormatterInterface;
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Render\RendererInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\system\SystemManager;
-
-/**
- * The EventController class.
- */
-class EventController extends ControllerBase implements ContainerInjectionInterface {
-
-  /**
-   * The date formatter service.
-   *
-   * @var \Drupal\Core\Datetime\DateFormatterInterface
-   */
-  protected $dateFormatter;
-
-  /**
-   * The renderer service.
-   *
-   * @var \Drupal\Core\Render\RendererInterface
-   */
-  protected $renderer;
-
-  /**
-   * System Manager Service.
-   *
-   * @var \Drupal\system\SystemManager
-   */
-  protected $systemManager;
-
-  /**
-   * Constructs a EventController object.
-   *
-   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
-   *   The date formatter service.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer service.
-   * @param \Drupal\system\SystemManager $systemManager
-   *   System manager service.
-   */
-  public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer, SystemManager $systemManager) {
-    $this->dateFormatter = $date_formatter;
-    $this->renderer = $renderer;
-    $this->systemManager = $systemManager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('date.formatter'),
-      $container->get('renderer'),
-      $container->get('system.manager')
-    );
-  }
-
-  /**
-   * The page callback for the admin overview page.
-   */
-  public function adminPage() {
-    return $this->systemManager->getBlockContents();
-  }
-
-  /**
-   * The page callback for the admin content page.
-   */
-  public function contentPage() {
-    return $this->systemManager->getBlockContents();
-  }
-
-}
diff --git a/src/Controller/EventInstanceController.php b/src/Controller/EventInstanceController.php
new file mode 100644
index 00000000..e09e0971
--- /dev/null
+++ b/src/Controller/EventInstanceController.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace Drupal\recurring_events\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\system\SystemManager;
+use Drupal\recurring_events\EventInterface;
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Url;
+
+/**
+ * The EventInstanceController class.
+ */
+class EventInstanceController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * System Manager Service.
+   *
+   * @var \Drupal\system\SystemManager
+   */
+  protected $systemManager;
+
+  /**
+   * Constructs a EventInstanceController object.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   * @param \Drupal\system\SystemManager $systemManager
+   *   System manager service.
+   */
+  public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer, SystemManager $systemManager) {
+    $this->dateFormatter = $date_formatter;
+    $this->renderer = $renderer;
+    $this->systemManager = $systemManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('date.formatter'),
+      $container->get('renderer'),
+      $container->get('system.manager')
+    );
+  }
+
+  /**
+   * Displays an eventinstance revision.
+   *
+   * @param int $eventinstance_revision
+   *   The Default entity  revision ID.
+   *
+   * @return array
+   *   An array suitable for drupal_render().
+   */
+  public function revisionShow($eventinstance_revision) {
+    $eventinstance = $this->entityManager()->getStorage('eventinstance')->loadRevision($eventinstance_revision);
+    $view_builder = $this->entityManager()->getViewBuilder('eventinstance');
+
+    return $view_builder->view($eventinstance);
+  }
+
+  /**
+   * Page title callback for an eventinstance revision.
+   *
+   * @param int $eventinstance_revision
+   *   The Default entity  revision ID.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function revisionPageTitle($eventinstance_revision) {
+    $eventinstance = $this->entityManager()->getStorage('eventinstance')->loadRevision($eventinstance_revision);
+    return $this->t('Revision of %title from %date', ['%title' => $eventinstance->label(), '%date' => format_date($eventinstance->getRevisionCreationTime())]);
+  }
+
+  /**
+   * Generates an overview table of older revisions of an eventinstance.
+   *
+   * @param \Drupal\recurring_events\EventInterface $eventinstance
+   *   A Default entity  object.
+   *
+   * @return array
+   *   An array as expected by drupal_render().
+   */
+  public function revisionOverview(EventInterface $eventinstance) {
+    $account = $this->currentUser();
+    $langcode = $eventinstance->language()->getId();
+    $langname = $eventinstance->language()->getName();
+    $languages = $eventinstance->getTranslationLanguages();
+    $has_translations = (count($languages) > 1);
+    $eventinstance_storage = $this->entityManager()->getStorage('eventinstance');
+
+    $build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $eventinstance->label()]) : $this->t('Revisions for %title', ['%title' => $eventinstance->label()]);
+    $header = [$this->t('Revision'), $this->t('Operations')];
+
+    $revert_permission = (($account->hasPermission("revert all eventinstance revisions") || $account->hasPermission('administer eventinstance entities')));
+    $delete_permission = (($account->hasPermission("delete all eventinstance revisions") || $account->hasPermission('administer eventinstance entities')));
+
+    $rows = [];
+
+    $vids = $eventinstance_storage->revisionIds($eventinstance);
+
+    $latest_revision = TRUE;
+
+    foreach (array_reverse($vids) as $vid) {
+      /** @var \Drupal\recurring_events\EventInterface $revision */
+      $revision = $eventinstance_storage->loadRevision($vid);
+      // Only show revisions that are affected by the language that is being
+      // displayed.
+      if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
+        $username = [
+          '#theme' => 'username',
+          '#account' => $revision->getRevisionUser(),
+        ];
+
+        // Use revision link to link to revisions that are not active.
+        $date = \Drupal::service('date.formatter')->format($revision->getRevisionCreationTime(), 'short');
+        if ($vid != $eventinstance->getRevisionId()) {
+          $link = $this->l($date, new Url('entity.eventinstance.revision', ['eventinstance' => $eventinstance->id(), 'eventinstance_revision' => $vid]));
+        }
+        else {
+          $link = $eventinstance->link($date);
+        }
+
+        $row = [];
+        $column = [
+          'data' => [
+            '#type' => 'inline_template',
+            '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
+            '#context' => [
+              'date' => $link,
+              'username' => \Drupal::service('renderer')->renderPlain($username),
+              'message' => ['#markup' => $revision->getRevisionLogMessage(), '#allowed_tags' => Xss::getHtmlTagList()],
+            ],
+          ],
+        ];
+        // @todo Simplify once https://www.drupal.org/node/2334319 lands.
+        $this->renderer->addCacheableDependency($column['data'], $username);
+        $row[] = $column;
+
+        if ($latest_revision) {
+          $row[] = [
+            'data' => [
+              '#prefix' => '<em>',
+              '#markup' => $this->t('Current revision'),
+              '#suffix' => '</em>',
+            ],
+          ];
+          foreach ($row as &$current) {
+            $current['class'] = ['revision-current'];
+          }
+          $latest_revision = FALSE;
+        }
+        else {
+          $links = [];
+          if ($revert_permission) {
+            $links['revert'] = [
+              'title' => $this->t('Revert'),
+              'url' => $has_translations ?
+              Url::fromRoute('entity.eventinstance.translation_revert', [
+                'eventinstance' => $eventinstance->id(),
+                'eventinstance_revision' => $vid,
+                'langcode' => $langcode,
+              ]) :
+              Url::fromRoute('entity.eventinstance.revision_revert', [
+                'eventinstance' => $eventinstance->id(),
+                'eventinstance_revision' => $vid,
+              ]),
+            ];
+          }
+
+          if ($delete_permission) {
+            $links['delete'] = [
+              'title' => $this->t('Delete'),
+              'url' => Url::fromRoute('entity.eventinstance.revision_delete', ['eventinstance' => $eventinstance->id(), 'eventinstance_revision' => $vid]),
+            ];
+          }
+
+          $row[] = [
+            'data' => [
+              '#type' => 'operations',
+              '#links' => $links,
+            ],
+          ];
+        }
+
+        $rows[] = $row;
+      }
+    }
+
+    $build['eventinstance_revisions_table'] = [
+      '#theme' => 'table',
+      '#rows' => $rows,
+      '#header' => $header,
+    ];
+
+    return $build;
+  }
+
+}
diff --git a/src/Controller/EventSeriesController.php b/src/Controller/EventSeriesController.php
new file mode 100644
index 00000000..455f4623
--- /dev/null
+++ b/src/Controller/EventSeriesController.php
@@ -0,0 +1,236 @@
+<?php
+
+namespace Drupal\recurring_events\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\system\SystemManager;
+use Drupal\recurring_events\EventInterface;
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Url;
+
+/**
+ * The EventSeriesController class.
+ */
+class EventSeriesController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * System Manager Service.
+   *
+   * @var \Drupal\system\SystemManager
+   */
+  protected $systemManager;
+
+  /**
+   * Constructs a EventSeriesController object.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   * @param \Drupal\system\SystemManager $systemManager
+   *   System manager service.
+   */
+  public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer, SystemManager $systemManager) {
+    $this->dateFormatter = $date_formatter;
+    $this->renderer = $renderer;
+    $this->systemManager = $systemManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('date.formatter'),
+      $container->get('renderer'),
+      $container->get('system.manager')
+    );
+  }
+
+  /**
+   * The page callback for the admin overview page.
+   */
+  public function adminPage() {
+    return $this->systemManager->getBlockContents();
+  }
+
+  /**
+   * The page callback for the admin content page.
+   */
+  public function contentPage() {
+    return $this->systemManager->getBlockContents();
+  }
+
+  /**
+   * Displays an eventseries revision.
+   *
+   * @param int $eventseries_revision
+   *   The Default entity  revision ID.
+   *
+   * @return array
+   *   An array suitable for drupal_render().
+   */
+  public function revisionShow($eventseries_revision) {
+    $eventseries = $this->entityManager()->getStorage('eventseries')->loadRevision($eventseries_revision);
+    $view_builder = $this->entityManager()->getViewBuilder('eventseries');
+
+    return $view_builder->view($eventseries);
+  }
+
+  /**
+   * Page title callback for an eventseries revision.
+   *
+   * @param int $eventseries_revision
+   *   The Default entity  revision ID.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function revisionPageTitle($eventseries_revision) {
+    $eventseries = $this->entityManager()->getStorage('eventseries')->loadRevision($eventseries_revision);
+    return $this->t('Revision of %title from %date', ['%title' => $eventseries->label(), '%date' => format_date($eventseries->getRevisionCreationTime())]);
+  }
+
+  /**
+   * Generates an overview table of older revisions of an eventseries.
+   *
+   * @param \Drupal\recurring_events\EventInterface $eventseries
+   *   A Default entity  object.
+   *
+   * @return array
+   *   An array as expected by drupal_render().
+   */
+  public function revisionOverview(EventInterface $eventseries) {
+    $account = $this->currentUser();
+    $langcode = $eventseries->language()->getId();
+    $langname = $eventseries->language()->getName();
+    $languages = $eventseries->getTranslationLanguages();
+    $has_translations = (count($languages) > 1);
+    $eventseries_storage = $this->entityManager()->getStorage('eventseries');
+
+    $build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $eventseries->label()]) : $this->t('Revisions for %title', ['%title' => $eventseries->label()]);
+    $header = [$this->t('Revision'), $this->t('Operations')];
+
+    $revert_permission = (($account->hasPermission("revert all eventseries revisions") || $account->hasPermission('administer eventseries entities')));
+    $delete_permission = (($account->hasPermission("delete all eventseries revisions") || $account->hasPermission('administer eventseries entities')));
+
+    $rows = [];
+
+    $vids = $eventseries_storage->revisionIds($eventseries);
+
+    $latest_revision = TRUE;
+
+    foreach (array_reverse($vids) as $vid) {
+      /** @var \Drupal\recurring_events\EventInterface $revision */
+      $revision = $eventseries_storage->loadRevision($vid);
+      // Only show revisions that are affected by the language that is being
+      // displayed.
+      if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
+        $username = [
+          '#theme' => 'username',
+          '#account' => $revision->getRevisionUser(),
+        ];
+
+        // Use revision link to link to revisions that are not active.
+        $date = \Drupal::service('date.formatter')->format($revision->getRevisionCreationTime(), 'short');
+        if ($vid != $eventseries->getRevisionId()) {
+          $link = $this->l($date, new Url('entity.eventseries.revision', ['eventseries' => $eventseries->id(), 'eventseries_revision' => $vid]));
+        }
+        else {
+          $link = $eventseries->link($date);
+        }
+
+        $row = [];
+        $column = [
+          'data' => [
+            '#type' => 'inline_template',
+            '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
+            '#context' => [
+              'date' => $link,
+              'username' => \Drupal::service('renderer')->renderPlain($username),
+              'message' => ['#markup' => $revision->getRevisionLogMessage(), '#allowed_tags' => Xss::getHtmlTagList()],
+            ],
+          ],
+        ];
+        // @todo Simplify once https://www.drupal.org/node/2334319 lands.
+        $this->renderer->addCacheableDependency($column['data'], $username);
+        $row[] = $column;
+
+        if ($latest_revision) {
+          $row[] = [
+            'data' => [
+              '#prefix' => '<em>',
+              '#markup' => $this->t('Current revision'),
+              '#suffix' => '</em>',
+            ],
+          ];
+          foreach ($row as &$current) {
+            $current['class'] = ['revision-current'];
+          }
+          $latest_revision = FALSE;
+        }
+        else {
+          $links = [];
+          if ($revert_permission) {
+            $links['revert'] = [
+              'title' => $this->t('Revert'),
+              'url' => $has_translations ?
+              Url::fromRoute('entity.eventseries.translation_revert', [
+                'eventseries' => $eventseries->id(),
+                'eventseries_revision' => $vid,
+                'langcode' => $langcode,
+              ]) :
+              Url::fromRoute('entity.eventseries.revision_revert', [
+                'eventseries' => $eventseries->id(),
+                'eventseries_revision' => $vid,
+              ]),
+            ];
+          }
+
+          if ($delete_permission) {
+            $links['delete'] = [
+              'title' => $this->t('Delete'),
+              'url' => Url::fromRoute('entity.eventseries.revision_delete', ['eventseries' => $eventseries->id(), 'eventseries_revision' => $vid]),
+            ];
+          }
+
+          $row[] = [
+            'data' => [
+              '#type' => 'operations',
+              '#links' => $links,
+            ],
+          ];
+        }
+
+        $rows[] = $row;
+      }
+    }
+
+    $build['eventseries_revisions_table'] = [
+      '#theme' => 'table',
+      '#rows' => $rows,
+      '#header' => $header,
+    ];
+
+    return $build;
+  }
+
+}
diff --git a/src/Entity/EventInstance.php b/src/Entity/EventInstance.php
index 57e74c06..cdb17a97 100644
--- a/src/Entity/EventInstance.php
+++ b/src/Entity/EventInstance.php
@@ -72,10 +72,11 @@ use Drupal\user\UserInterface;
  *   id = "eventinstance",
  *   label = @Translation("Event Instance entity"),
  *   handlers = {
- *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "storage" = "Drupal\recurring_events\EventInstanceStorage",
  *     "list_builder" = "Drupal\recurring_events\EventInstanceListBuilder",
  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
  *     "views_data" = "Drupal\views\EntityViewsData",
+ *     "translation" = "Drupal\recurring_events\EventInstanceTranslationHandler",
  *     "form" = {
  *       "edit" = "Drupal\recurring_events\Form\EventInstanceForm",
  *       "delete" = "Drupal\recurring_events\Form\EventInstanceDeleteForm",
@@ -83,6 +84,9 @@ use Drupal\user\UserInterface;
  *       "clone" = "Drupal\recurring_events\Form\EventInstanceCloneForm",
  *     },
  *     "access" = "Drupal\recurring_events\EventInstanceAccessControlHandler",
+ *     "route_provider" = {
+ *       "html" = "Drupal\recurring_events\EventInstanceHtmlRouteProvider",
+ *     },
  *   },
  *   base_table = "eventinstance",
  *   data_table = "eventinstance_field_data",
@@ -109,9 +113,12 @@ use Drupal\user\UserInterface;
  *     "edit-form" = "/events/{eventinstance}/edit",
  *     "delete-form" = "/events/{eventinstance}/delete",
  *     "collection" = "/admin/content/events/instances",
+ *     "clone-form" = "/events/{eventinstance}/clone",
  *     "version-history" = "/events/{eventinstance}/revisions",
  *     "revision" = "/events/{eventinstance}/revisions/{eventinstance_revision}/view",
- *     "clone-form" = "/events/{eventinstance}/clone",
+ *     "revision_revert" = "/events/{eventinstance}/revisions/{eventinstance_revision}/revert",
+ *     "revision_delete" = "/events/{eventinstance}/revisions/{eventinstance_revision}/delete",
+ *     "translation_revert" = "/events/{eventinstance}/revisions/{eventinstance_revision}/revert/{langcode}",
  *   },
  *   field_ui_base_route = "eventinstance.settings",
  * )
@@ -131,7 +138,7 @@ use Drupal\user\UserInterface;
  * we want. In our case we want to provide access to the standard fields about
  * creation and changed time stamps.
  *
- * Our interface (see EventSeriesInterface) also exposes the
+ * Our interface (see EventInterface) also exposes the
  * EntityOwnerInterface. This allows us to provide methods for setting
  * and providing ownership information.
  *
@@ -156,6 +163,22 @@ class EventInstance extends EditorialContentEntityBase implements EventInterface
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function urlRouteParameters($rel) {
+    $uri_route_parameters = parent::urlRouteParameters($rel);
+
+    if ($rel === 'revision_revert' && $this instanceof RevisionableInterface) {
+      $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
+    }
+    elseif ($rel === 'revision_delete' && $this instanceof RevisionableInterface) {
+      $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
+    }
+
+    return $uri_route_parameters;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -178,36 +201,6 @@ class EventInstance extends EditorialContentEntityBase implements EventInterface
     }
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
-    parent::preSaveRevision($storage, $record);
-
-    if (!$this->isNewRevision() && isset($this->original) && (!isset($record->revision_log) || $record->revision_log === '')) {
-      // If we are updating an existing eventinstance without adding a new
-      // revision, we need to make sure $entity->revision_log is reset whenever
-      // it is empty. Therefore, this code allows us to avoid clobbering an
-      // existing log entry with an empty one.
-      $record->revision_log = $this->original->revision_log->value;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRevisionAuthor() {
-    return $this->getRevisionUser();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRevisionAuthorId($uid) {
-    $this->setRevisionUserId($uid);
-    return $this;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -230,19 +223,6 @@ class EventInstance extends EditorialContentEntityBase implements EventInterface
     return $this;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTimeAcrossTranslations() {
-    $changed = $this->getUntranslated()->getChangedTime();
-    foreach ($this->getTranslationLanguages(FALSE) as $language) {
-      $translation_changed = $this->getTranslation($language->getId())
-        ->getChangedTime();
-      $changed = max($translation_changed, $changed);
-    }
-    return $changed;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -273,6 +253,21 @@ class EventInstance extends EditorialContentEntityBase implements EventInterface
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionAuthor() {
+    return $this->getRevisionUser();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionAuthorId($uid) {
+    $this->setRevisionUserId($uid);
+    return $this;
+  }
+
   /**
    * {@inheritdoc}
    *
@@ -379,6 +374,13 @@ class EventInstance extends EditorialContentEntityBase implements EventInterface
       ])
       ->setDisplayConfigurable('form', TRUE);
 
+    $fields['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Revision translation affected'))
+      ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
+      ->setReadOnly(TRUE)
+      ->setRevisionable(TRUE)
+      ->setTranslatable(TRUE);
+
     return $fields;
   }
 
diff --git a/src/Entity/EventSeries.php b/src/Entity/EventSeries.php
index bffe20a8..e9ac8857 100644
--- a/src/Entity/EventSeries.php
+++ b/src/Entity/EventSeries.php
@@ -72,10 +72,11 @@ use Drupal\user\UserInterface;
  *   id = "eventseries",
  *   label = @Translation("Event series entity"),
  *   handlers = {
- *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "storage" = "Drupal\recurring_events\EventSeriesStorage",
  *     "list_builder" = "Drupal\recurring_events\EventSeriesListBuilder",
  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
  *     "views_data" = "Drupal\views\EntityViewsData",
+ *     "translation" = "Drupal\recurring_events\EventSeriesTranslationHandler",
  *     "form" = {
  *       "add" = "Drupal\recurring_events\Form\EventSeriesForm",
  *       "edit" = "Drupal\recurring_events\Form\EventSeriesForm",
@@ -83,6 +84,9 @@ use Drupal\user\UserInterface;
  *       "clone" = "Drupal\recurring_events\Form\EventSeriesCloneForm",
  *     },
  *     "access" = "Drupal\recurring_events\EventSeriesAccessControlHandler",
+ *     "route_provider" = {
+ *       "html" = "Drupal\recurring_events\EventSeriesHtmlRouteProvider",
+ *     },
  *   },
  *   base_table = "eventseries",
  *   data_table = "eventseries_field_data",
@@ -110,9 +114,12 @@ use Drupal\user\UserInterface;
  *     "edit-form" = "/events/series/{eventseries}/edit",
  *     "delete-form" = "/events/series/{eventseries}/delete",
  *     "collection" = "/admin/content/events/series",
+ *     "clone-form" = "/events/series/{eventseries}/clone",
  *     "version-history" = "/events/series/{eventseries}/revisions",
  *     "revision" = "/events/series/{eventseries}/revisions/{eventseries_revision}/view",
- *     "clone-form" = "/events/series/{eventseries}/clone",
+ *     "revision_revert" = "/events/series/{eventseries}/revisions/{eventseries_revision}/revert",
+ *     "revision_delete" = "/events/series/{eventseries}/revisions/{eventseries_revision}/delete",
+ *     "translation_revert" = "/events/series/{eventseries}/revisions/{eventseries_revision}/revert/{langcode}",
  *   },
  *   field_ui_base_route = "eventseries.settings",
  * )
@@ -131,7 +138,7 @@ use Drupal\user\UserInterface;
  * we want. In our case we want to provide access to the standard fields about
  * creation and changed time stamps.
  *
- * Our interface (see EventSeriesInterface) also exposes the
+ * Our interface (see EventInterface) also exposes the
  * EntityOwnerInterface. This allows us to provide methods for setting
  * and providing ownership information.
  *
@@ -156,6 +163,22 @@ class EventSeries extends EditorialContentEntityBase implements EventInterface {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function urlRouteParameters($rel) {
+    $uri_route_parameters = parent::urlRouteParameters($rel);
+
+    if ($rel === 'revision_revert' && $this instanceof RevisionableInterface) {
+      $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
+    }
+    elseif ($rel === 'revision_delete' && $this instanceof RevisionableInterface) {
+      $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
+    }
+
+    return $uri_route_parameters;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -178,36 +201,6 @@ class EventSeries extends EditorialContentEntityBase implements EventInterface {
     }
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
-    parent::preSaveRevision($storage, $record);
-
-    if (!$this->isNewRevision() && isset($this->original) && (!isset($record->revision_log) || $record->revision_log === '')) {
-      // If we are updating an existing event without adding a new
-      // revision, we need to make sure $entity->revision_log is reset whenever
-      // it is empty. Therefore, this code allows us to avoid clobbering an
-      // existing log entry with an empty one.
-      $record->revision_log = $this->original->revision_log->value;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRevisionAuthor() {
-    return $this->getRevisionUser();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRevisionAuthorId($uid) {
-    $this->setRevisionUserId($uid);
-    return $this;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -230,19 +223,6 @@ class EventSeries extends EditorialContentEntityBase implements EventInterface {
     return $this;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTimeAcrossTranslations() {
-    $changed = $this->getUntranslated()->getChangedTime();
-    foreach ($this->getTranslationLanguages(FALSE) as $language) {
-      $translation_changed = $this->getTranslation($language->getId())
-        ->getChangedTime();
-      $changed = max($translation_changed, $changed);
-    }
-    return $changed;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -273,6 +253,21 @@ class EventSeries extends EditorialContentEntityBase implements EventInterface {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionAuthor() {
+    return $this->getRevisionUser();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionAuthorId($uid) {
+    $this->setRevisionUserId($uid);
+    return $this;
+  }
+
   /**
    * {@inheritdoc}
    *
@@ -469,6 +464,13 @@ class EventSeries extends EditorialContentEntityBase implements EventInterface {
       ])
       ->setDisplayConfigurable('form', TRUE);
 
+    $fields['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Revision translation affected'))
+      ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
+      ->setReadOnly(TRUE)
+      ->setRevisionable(TRUE)
+      ->setTranslatable(TRUE);
+
     return $fields;
   }
 
diff --git a/src/EventInstanceHtmlRouteProvider.php b/src/EventInstanceHtmlRouteProvider.php
new file mode 100644
index 00000000..af1ceaf8
--- /dev/null
+++ b/src/EventInstanceHtmlRouteProvider.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for eventinstance entities.
+ *
+ * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
+ * @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
+ */
+class EventInstanceHtmlRouteProvider extends AdminHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $collection = parent::getRoutes($entity_type);
+
+    $entity_type_id = $entity_type->id();
+
+    if ($history_route = $this->getHistoryRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.version_history", $history_route);
+    }
+
+    if ($revision_route = $this->getRevisionRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision", $revision_route);
+    }
+
+    if ($revert_route = $this->getRevisionRevertRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision_revert", $revert_route);
+    }
+
+    if ($delete_route = $this->getRevisionDeleteRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision_delete", $delete_route);
+    }
+
+    if ($translation_route = $this->getRevisionTranslationRevertRoute($entity_type)) {
+      $collection->add("{$entity_type_id}.revision_revert_translation_confirm", $translation_route);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Gets the version history route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getHistoryRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('version-history')) {
+      $route = new Route($entity_type->getLinkTemplate('version-history'));
+      $route
+        ->setDefaults([
+          '_title' => "{$entity_type->getLabel()} revisions",
+          '_controller' => '\Drupal\recurring_events\Controller\EventInstanceController::revisionOverview',
+        ])
+        ->setRequirement('_permission', 'access eventinstance revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision')) {
+      $route = new Route($entity_type->getLinkTemplate('revision'));
+      $route
+        ->setDefaults([
+          '_controller' => '\Drupal\recurring_events\Controller\EventInstanceController::revisionShow',
+          '_title_callback' => '\Drupal\recurring_events\Controller\EventInstanceController::revisionPageTitle',
+        ])
+        ->setRequirement('_permission', 'access eventinstance revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision revert route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionRevertRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision_revert')) {
+      $route = new Route($entity_type->getLinkTemplate('revision_revert'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\recurring_events\Form\EventInstanceRevisionRevertForm',
+          '_title' => 'Revert to earlier revision',
+        ])
+        ->setRequirement('_permission', 'revert all eventinstance revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision delete route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionDeleteRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision_delete')) {
+      $route = new Route($entity_type->getLinkTemplate('revision_delete'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\recurring_events\Form\EventInstanceRevisionDeleteForm',
+          '_title' => 'Delete earlier revision',
+        ])
+        ->setRequirement('_permission', 'delete all eventinstance revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision translation revert route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionTranslationRevertRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('translation_revert')) {
+      $route = new Route($entity_type->getLinkTemplate('translation_revert'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\recurring_events\Form\EventInstanceRevisionRevertTranslationForm',
+          '_title' => 'Revert to earlier revision of a translation',
+        ])
+        ->setRequirement('_permission', 'revert all eventinstance revisions');
+
+      return $route;
+    }
+  }
+
+}
diff --git a/src/EventInstanceStorage.php b/src/EventInstanceStorage.php
new file mode 100644
index 00000000..37abf099
--- /dev/null
+++ b/src/EventInstanceStorage.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\recurring_events\EventInterface;
+
+/**
+ * Defines the storage handler class for eventinstance entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * eventinstance entities.
+ *
+ * @ingroup recurring_events
+ */
+class EventInstanceStorage extends SqlContentEntityStorage implements EventInstanceStorageInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revisionIds(EventInterface $entity) {
+    return $this->database->query(
+      'SELECT vid FROM {eventinstance_revision} WHERE id=:id ORDER BY vid',
+      [':id' => $entity->id()]
+    )->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function userRevisionIds(AccountInterface $account) {
+    return $this->database->query(
+      'SELECT vid FROM {eventinstance_field_revision} WHERE uid = :uid ORDER BY vid',
+      [':uid' => $account->id()]
+    )->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countDefaultLanguageRevisions(EventInterface $entity) {
+    return $this->database->query('SELECT COUNT(*) FROM {eventinstance_field_revision} WHERE id = :id AND default_langcode = 1', [':id' => $entity->id()])
+      ->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearRevisionsLanguage(LanguageInterface $language) {
+    return $this->database->update('eventinstance_revision')
+      ->fields(['langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])
+      ->condition('langcode', $language->getId())
+      ->execute();
+  }
+
+}
diff --git a/src/EventInstanceStorageInterface.php b/src/EventInstanceStorageInterface.php
new file mode 100644
index 00000000..2a6e586e
--- /dev/null
+++ b/src/EventInstanceStorageInterface.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\recurring_events\EventInterface;
+
+/**
+ * Defines the storage handler class for eventinstance entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * eventinstance entities.
+ *
+ * @ingroup recurring_events
+ */
+interface EventInstanceStorageInterface extends ContentEntityStorageInterface {
+
+  /**
+   * Gets a list of eventinstance revision IDs for a specific eventinstance.
+   *
+   * @param \Drupal\recurring_events\EventInterface $entity
+   *   The eventinstance entity.
+   *
+   * @return int[]
+   *   Eventinstance revision IDs (in ascending order).
+   */
+  public function revisionIds(EventInterface $entity);
+
+  /**
+   * Gets a list of revision IDs having a given user as eventinstance author.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user entity.
+   *
+   * @return int[]
+   *   Eventinstance revision IDs (in ascending order).
+   */
+  public function userRevisionIds(AccountInterface $account);
+
+  /**
+   * Counts the number of revisions in the default language.
+   *
+   * @param \Drupal\recurring_events\EventInterface $entity
+   *   The eventinstance entity.
+   *
+   * @return int
+   *   The number of revisions in the default language.
+   */
+  public function countDefaultLanguageRevisions(EventInterface $entity);
+
+  /**
+   * Unsets the language for all eventinstance with the given language.
+   *
+   * @param \Drupal\Core\Language\LanguageInterface $language
+   *   The language object.
+   */
+  public function clearRevisionsLanguage(LanguageInterface $language);
+
+}
diff --git a/src/EventInstanceTranslationHandler.php b/src/EventInstanceTranslationHandler.php
new file mode 100644
index 00000000..ce1f6810
--- /dev/null
+++ b/src/EventInstanceTranslationHandler.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\content_translation\ContentTranslationHandler;
+
+/**
+ * Defines the translation handler for eventinstance.
+ */
+class EventInstanceTranslationHandler extends ContentTranslationHandler {
+
+  // Override here the needed methods from ContentTranslationHandler.
+
+}
diff --git a/src/EventSeriesHtmlRouteProvider.php b/src/EventSeriesHtmlRouteProvider.php
new file mode 100644
index 00000000..6ad9d61b
--- /dev/null
+++ b/src/EventSeriesHtmlRouteProvider.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for eventseries entities.
+ *
+ * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
+ * @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
+ */
+class EventSeriesHtmlRouteProvider extends AdminHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $collection = parent::getRoutes($entity_type);
+
+    $entity_type_id = $entity_type->id();
+
+    if ($history_route = $this->getHistoryRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.version_history", $history_route);
+    }
+
+    if ($revision_route = $this->getRevisionRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision", $revision_route);
+    }
+
+    if ($revert_route = $this->getRevisionRevertRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision_revert", $revert_route);
+    }
+
+    if ($delete_route = $this->getRevisionDeleteRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision_delete", $delete_route);
+    }
+
+    if ($translation_route = $this->getRevisionTranslationRevertRoute($entity_type)) {
+      $collection->add("{$entity_type_id}.revision_revert_translation_confirm", $translation_route);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Gets the version history route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getHistoryRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('version-history')) {
+      $route = new Route($entity_type->getLinkTemplate('version-history'));
+      $route
+        ->setDefaults([
+          '_title' => "{$entity_type->getLabel()} revisions",
+          '_controller' => '\Drupal\recurring_events\Controller\EventSeriesController::revisionOverview',
+        ])
+        ->setRequirement('_permission', 'access eventseries revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision')) {
+      $route = new Route($entity_type->getLinkTemplate('revision'));
+      $route
+        ->setDefaults([
+          '_controller' => '\Drupal\recurring_events\Controller\EventSeriesController::revisionShow',
+          '_title_callback' => '\Drupal\recurring_events\Controller\EventSeriesController::revisionPageTitle',
+        ])
+        ->setRequirement('_permission', 'access eventseries revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision revert route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionRevertRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision_revert')) {
+      $route = new Route($entity_type->getLinkTemplate('revision_revert'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\recurring_events\Form\EventSeriesRevisionRevertForm',
+          '_title' => 'Revert to earlier revision',
+        ])
+        ->setRequirement('_permission', 'revert all eventseries revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision delete route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionDeleteRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision_delete')) {
+      $route = new Route($entity_type->getLinkTemplate('revision_delete'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\recurring_events\Form\EventSeriesRevisionDeleteForm',
+          '_title' => 'Delete earlier revision',
+        ])
+        ->setRequirement('_permission', 'delete all eventseries revisions');
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision translation revert route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionTranslationRevertRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('translation_revert')) {
+      $route = new Route($entity_type->getLinkTemplate('translation_revert'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\recurring_events\Form\EventSeriesRevisionRevertTranslationForm',
+          '_title' => 'Revert to earlier revision of a translation',
+        ])
+        ->setRequirement('_permission', 'revert all eventseries revisions');
+
+      return $route;
+    }
+  }
+
+}
diff --git a/src/EventSeriesStorage.php b/src/EventSeriesStorage.php
new file mode 100644
index 00000000..76f60bb8
--- /dev/null
+++ b/src/EventSeriesStorage.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\recurring_events\EventInterface;
+
+/**
+ * Defines the storage handler class for eventseries entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * eventseries entities.
+ *
+ * @ingroup recurring_events
+ */
+class EventSeriesStorage extends SqlContentEntityStorage implements EventSeriesStorageInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revisionIds(EventInterface $entity) {
+    return $this->database->query(
+      'SELECT vid FROM {eventseries_revision} WHERE id=:id ORDER BY vid',
+      [':id' => $entity->id()]
+    )->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function userRevisionIds(AccountInterface $account) {
+    return $this->database->query(
+      'SELECT vid FROM {eventseries_field_revision} WHERE uid = :uid ORDER BY vid',
+      [':uid' => $account->id()]
+    )->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countDefaultLanguageRevisions(EventInterface $entity) {
+    return $this->database->query('SELECT COUNT(*) FROM {eventseries_field_revision} WHERE id = :id AND default_langcode = 1', [':id' => $entity->id()])
+      ->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearRevisionsLanguage(LanguageInterface $language) {
+    return $this->database->update('eventseries_revision')
+      ->fields(['langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])
+      ->condition('langcode', $language->getId())
+      ->execute();
+  }
+
+}
diff --git a/src/EventSeriesStorageInterface.php b/src/EventSeriesStorageInterface.php
new file mode 100644
index 00000000..4d05919b
--- /dev/null
+++ b/src/EventSeriesStorageInterface.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\recurring_events\EventInterface;
+
+/**
+ * Defines the storage handler class for eventseries entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * eventseries entities.
+ *
+ * @ingroup recurring_events
+ */
+interface EventSeriesStorageInterface extends ContentEntityStorageInterface {
+
+  /**
+   * Gets a list of eventseries revision IDs for a specific eventseries.
+   *
+   * @param \Drupal\recurring_events\EventInterface $entity
+   *   The eventseries entity.
+   *
+   * @return int[]
+   *   Eventseries revision IDs (in ascending order).
+   */
+  public function revisionIds(EventInterface $entity);
+
+  /**
+   * Gets a list of revision IDs having a given user as eventseries author.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user entity.
+   *
+   * @return int[]
+   *   Eventseries revision IDs (in ascending order).
+   */
+  public function userRevisionIds(AccountInterface $account);
+
+  /**
+   * Counts the number of revisions in the default language.
+   *
+   * @param \Drupal\recurring_events\EventInterface $entity
+   *   The eventseries entity.
+   *
+   * @return int
+   *   The number of revisions in the default language.
+   */
+  public function countDefaultLanguageRevisions(EventInterface $entity);
+
+  /**
+   * Unsets the language for all eventseries with the given language.
+   *
+   * @param \Drupal\Core\Language\LanguageInterface $language
+   *   The language object.
+   */
+  public function clearRevisionsLanguage(LanguageInterface $language);
+
+}
diff --git a/src/EventSeriesTranslationHandler.php b/src/EventSeriesTranslationHandler.php
new file mode 100644
index 00000000..f2e1d817
--- /dev/null
+++ b/src/EventSeriesTranslationHandler.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\content_translation\ContentTranslationHandler;
+
+/**
+ * Defines the translation handler for eventseries.
+ */
+class EventSeriesTranslationHandler extends ContentTranslationHandler {
+
+  // Override here the needed methods from ContentTranslationHandler.
+
+}
diff --git a/src/Form/EventInstanceForm.php b/src/Form/EventInstanceForm.php
index e161317a..e4894729 100644
--- a/src/Form/EventInstanceForm.php
+++ b/src/Form/EventInstanceForm.php
@@ -33,7 +33,7 @@ class EventInstanceForm extends ContentEntityForm {
   }
 
   /**
-   * Construct a EventSeriesForm.
+   * Construct an EventInstanceForm.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager service.
@@ -89,11 +89,22 @@ class EventInstanceForm extends ContentEntityForm {
    * {@inheritdoc}
    */
   public function save(array $form, FormStateInterface $form_state) {
-    $form_state->setRedirect('entity.eventinstance.collection');
-    parent::save($form, $form_state);
-
     $entity = $this->getEntity();
 
+    // Save as a new revision if requested to do so.
+    if (!$form_state->isValueEmpty('new_revision') && $form_state->getValue('new_revision') != FALSE) {
+      $entity->setNewRevision();
+
+      // If a new revision is created, save the current user as revision author.
+      $entity->setRevisionCreationTime(REQUEST_TIME);
+      $entity->setRevisionUserId(\Drupal::currentUser()->id());
+    }
+    else {
+      $entity->setNewRevision(FALSE);
+    }
+
+    parent::save($form, $form_state);
+
     if ($entity->isDefaultTranslation()) {
       $message = t('Event instance of %label has been saved.', [
         '%label' => $entity->getEventSeries()->title->value,
@@ -106,6 +117,8 @@ class EventInstanceForm extends ContentEntityForm {
       ]);
     }
     $this->messenger->addMessage($message);
+
+    $form_state->setRedirect('entity.eventinstance.canonical', ['eventinstance' => $entity->id()]);
   }
 
 }
diff --git a/src/Form/EventInstanceRevisionDeleteForm.php b/src/Form/EventInstanceRevisionDeleteForm.php
new file mode 100644
index 00000000..00cd43d8
--- /dev/null
+++ b/src/Form/EventInstanceRevisionDeleteForm.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\recurring_events\Form;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for deleting a eventinstance revision.
+ *
+ * @ingroup recurring_events
+ */
+class EventInstanceRevisionDeleteForm extends ConfirmFormBase {
+
+  /**
+   * The eventinstance revision.
+   *
+   * @var \Drupal\recurring_events\EventInterface
+   */
+  protected $revision;
+
+  /**
+   * The eventinstance storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $eventInstanceStorage;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs a new EventInstanceRevisionDeleteForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The entity storage.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, Connection $connection) {
+    $this->eventInstanceStorage = $entity_storage;
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $entity_manager = $container->get('entity.manager');
+    return new static(
+      $entity_manager->getStorage('eventinstance'),
+      $container->get('database')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'eventinstance_revision_delete_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to delete the revision from %revision-date?', ['%revision-date' => format_date($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.eventinstance.version_history', ['eventinstance' => $this->revision->id()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $eventinstance_revision = NULL) {
+    $this->revision = $this->eventInstanceStorage->loadRevision($eventinstance_revision);
+    $form = parent::buildForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->eventInstanceStorage->deleteRevision($this->revision->getRevisionId());
+
+    $this->logger('content')->notice('eventinstance: deleted %title revision %revision.', ['%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
+    drupal_set_message(t('Revision from %revision-date of eventinstance %title has been deleted.', ['%revision-date' => format_date($this->revision->getRevisionCreationTime()), '%title' => $this->revision->label()]));
+    $form_state->setRedirect(
+      'entity.eventinstance.canonical',
+       ['eventinstance' => $this->revision->id()]
+    );
+    if ($this->connection->query('SELECT COUNT(DISTINCT vid) FROM {eventinstance_field_revision} WHERE id = :id', [':id' => $this->revision->id()])->fetchField() > 1) {
+      $form_state->setRedirect(
+        'entity.eventinstance.version_history',
+         ['eventinstance' => $this->revision->id()]
+      );
+    }
+  }
+
+}
diff --git a/src/Form/EventInstanceRevisionRevertForm.php b/src/Form/EventInstanceRevisionRevertForm.php
new file mode 100644
index 00000000..605cc4e5
--- /dev/null
+++ b/src/Form/EventInstanceRevisionRevertForm.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Drupal\recurring_events\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\recurring_events\EventInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for reverting an eventinstance revision.
+ *
+ * @ingroup recurring_events
+ */
+class EventInstanceRevisionRevertForm extends ConfirmFormBase {
+
+
+  /**
+   * The eventinstance revision.
+   *
+   * @var \Drupal\recurring_events\EventInterface
+   */
+  protected $revision;
+
+  /**
+   * The eventinstance storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $eventSeriesStorage;
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Constructs a new EventInstanceRevisionRevertForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The eventinstance storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, DateFormatterInterface $date_formatter) {
+    $this->eventSeriesStorage = $entity_storage;
+    $this->dateFormatter = $date_formatter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('eventinstance'),
+      $container->get('date.formatter')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'eventinstance_revision_revert_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.eventinstance.version_history', ['eventinstance' => $this->revision->id()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Revert');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $eventinstance_revision = NULL) {
+    $this->revision = $this->eventSeriesStorage->loadRevision($eventinstance_revision);
+    $form = parent::buildForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // The revision timestamp will be updated when the revision is saved. Keep
+    // the original one for the confirmation message.
+    $original_revision_timestamp = $this->revision->getRevisionCreationTime();
+
+    $this->revision = $this->prepareRevertedRevision($this->revision, $form_state);
+    $this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]);
+    $this->revision->save();
+
+    $this->logger('content')->notice('eventinstance: reverted %title revision %revision.', ['%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
+    drupal_set_message(t('eventinstance %title has been reverted to the revision from %revision-date.', ['%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)]));
+    $form_state->setRedirect(
+      'entity.eventinstance.version_history',
+      ['eventinstance' => $this->revision->id()]
+    );
+  }
+
+  /**
+   * Prepares a revision to be reverted.
+   *
+   * @param \Drupal\recurring_events\EventInterface $revision
+   *   The revision to be reverted.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\recurring_events\EventInterface
+   *   The prepared revision ready to be stored.
+   */
+  protected function prepareRevertedRevision(EventInterface $revision, FormStateInterface $form_state) {
+    $revision->setNewRevision();
+    $revision->isDefaultRevision(TRUE);
+    $revision->setRevisionCreationTime(REQUEST_TIME);
+
+    return $revision;
+  }
+
+}
diff --git a/src/Form/EventInstanceRevisionRevertTranslationForm.php b/src/Form/EventInstanceRevisionRevertTranslationForm.php
new file mode 100644
index 00000000..3df7c5bb
--- /dev/null
+++ b/src/Form/EventInstanceRevisionRevertTranslationForm.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\recurring_events\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\recurring_events\EventInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides form to revert an eventinstance revision for a single translation.
+ *
+ * @ingroup recurring_events
+ */
+class EventInstanceRevisionRevertTranslationForm extends EventInstanceRevisionRevertForm {
+
+
+  /**
+   * The language to be reverted.
+   *
+   * @var string
+   */
+  protected $langcode;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new EventInstanceRevisionRevertTranslationForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The eventinstance storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, DateFormatterInterface $date_formatter, LanguageManagerInterface $language_manager) {
+    parent::__construct($entity_storage, $date_formatter);
+    $this->languageManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('eventinstance'),
+      $container->get('date.formatter'),
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'eventinstance_revision_revert_translation_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to revert @language translation to the revision from %revision-date?', ['@language' => $this->languageManager->getLanguageName($this->langcode), '%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $eventinstance_revision = NULL, $langcode = NULL) {
+    $this->langcode = $langcode;
+    $form = parent::buildForm($form, $form_state, $eventinstance_revision);
+
+    $form['revert_untranslated_fields'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Revert content shared among translations'),
+      '#default_value' => FALSE,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareRevertedRevision(EventInterface $revision, FormStateInterface $form_state) {
+    $revert_untranslated_fields = $form_state->getValue('revert_untranslated_fields');
+
+    /** @var \Drupal\recurring_events\EventInterface $default_revision */
+    $latest_revision = $this->eventSeriesStorage->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);
+    $revision->setRevisionCreationTime(REQUEST_TIME);
+
+    return $latest_revision_translation;
+  }
+
+}
diff --git a/src/Form/EventSeriesDeleteForm.php b/src/Form/EventSeriesDeleteForm.php
index b4ee5425..9589033e 100644
--- a/src/Form/EventSeriesDeleteForm.php
+++ b/src/Form/EventSeriesDeleteForm.php
@@ -49,7 +49,7 @@ class EventSeriesDeleteForm extends ContentEntityDeleteForm {
   }
 
   /**
-   * Construct a EventSeriesDeleteForm.
+   * Construct an EventSeriesDeleteForm.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager service.
diff --git a/src/Form/EventSeriesForm.php b/src/Form/EventSeriesForm.php
index f7f3637a..d266d4b7 100644
--- a/src/Form/EventSeriesForm.php
+++ b/src/Form/EventSeriesForm.php
@@ -67,7 +67,7 @@ class EventSeriesForm extends ContentEntityForm {
   }
 
   /**
-   * Construct a EventSeriesForm.
+   * Construct an EventSeriesForm.
    *
    * @param \Drupal\recurring_events\EventCreationService $creation_service
    *   The event creation service.
@@ -211,10 +211,21 @@ class EventSeriesForm extends ContentEntityForm {
    * {@inheritdoc}
    */
   public function save(array $form, FormStateInterface $form_state) {
-    $form_state->setRedirect('entity.eventseries.collection');
     $entity = $this->getEntity();
     $original = NULL;
 
+    // Save as a new revision if requested to do so.
+    if (!$form_state->isValueEmpty('new_revision') && $form_state->getValue('new_revision') != FALSE) {
+      $entity->setNewRevision();
+
+      // If a new revision is created, save the current user as revision author.
+      $entity->setRevisionCreationTime(REQUEST_TIME);
+      $entity->setRevisionUserId(\Drupal::currentUser()->id());
+    }
+    else {
+      $entity->setNewRevision(FALSE);
+    }
+
     if (!$entity->isNew()) {
       $original = $this->storage->loadUnchanged($entity->id());
     }
@@ -223,6 +234,8 @@ class EventSeriesForm extends ContentEntityForm {
       '%name' => $entity->title->value,
     ]));
 
+    $form_state->setRedirect('entity.eventseries.canonical', ['eventseries' => $entity->id()]);
+
     $this->creationService->saveEvent($entity, $form_state, $original);
     parent::save($form, $form_state);
   }
diff --git a/src/Form/EventSeriesRevisionDeleteForm.php b/src/Form/EventSeriesRevisionDeleteForm.php
new file mode 100644
index 00000000..7362644e
--- /dev/null
+++ b/src/Form/EventSeriesRevisionDeleteForm.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\recurring_events\Form;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for deleting an eventseries revision.
+ *
+ * @ingroup recurring_events
+ */
+class EventSeriesRevisionDeleteForm extends ConfirmFormBase {
+
+  /**
+   * The eventseries revision.
+   *
+   * @var \Drupal\recurring_events\EventInterface
+   */
+  protected $revision;
+
+  /**
+   * The eventseries storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $eventSeriesStorage;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs a new EventSeriesRevisionDeleteForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The entity storage.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, Connection $connection) {
+    $this->eventSeriesStorage = $entity_storage;
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $entity_manager = $container->get('entity.manager');
+    return new static(
+      $entity_manager->getStorage('eventseries'),
+      $container->get('database')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'eventseries_revision_delete_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to delete the revision from %revision-date?', ['%revision-date' => format_date($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.eventseries.version_history', ['eventseries' => $this->revision->id()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $eventseries_revision = NULL) {
+    $this->revision = $this->eventSeriesStorage->loadRevision($eventseries_revision);
+    $form = parent::buildForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->eventSeriesStorage->deleteRevision($this->revision->getRevisionId());
+
+    $this->logger('content')->notice('eventseries: deleted %title revision %revision.', ['%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
+    drupal_set_message(t('Revision from %revision-date of eventseries %title has been deleted.', ['%revision-date' => format_date($this->revision->getRevisionCreationTime()), '%title' => $this->revision->label()]));
+    $form_state->setRedirect(
+      'entity.eventseries.canonical',
+       ['eventseries' => $this->revision->id()]
+    );
+    if ($this->connection->query('SELECT COUNT(DISTINCT vid) FROM {eventseries_field_revision} WHERE id = :id', [':id' => $this->revision->id()])->fetchField() > 1) {
+      $form_state->setRedirect(
+        'entity.eventseries.version_history',
+         ['eventseries' => $this->revision->id()]
+      );
+    }
+  }
+
+}
diff --git a/src/Form/EventSeriesRevisionRevertForm.php b/src/Form/EventSeriesRevisionRevertForm.php
new file mode 100644
index 00000000..8da8d5ab
--- /dev/null
+++ b/src/Form/EventSeriesRevisionRevertForm.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Drupal\recurring_events\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\recurring_events\EventInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for reverting an eventseries revision.
+ *
+ * @ingroup recurring_events
+ */
+class EventSeriesRevisionRevertForm extends ConfirmFormBase {
+
+
+  /**
+   * The eventseries revision.
+   *
+   * @var \Drupal\recurring_events\EventInterface
+   */
+  protected $revision;
+
+  /**
+   * The eventseries storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $eventSeriesStorage;
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Constructs a new EventSeriesRevisionRevertForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The eventseries storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, DateFormatterInterface $date_formatter) {
+    $this->eventSeriesStorage = $entity_storage;
+    $this->dateFormatter = $date_formatter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('eventseries'),
+      $container->get('date.formatter')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'eventseries_revision_revert_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.eventseries.version_history', ['eventseries' => $this->revision->id()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Revert');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $eventseries_revision = NULL) {
+    $this->revision = $this->eventSeriesStorage->loadRevision($eventseries_revision);
+    $form = parent::buildForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // The revision timestamp will be updated when the revision is saved. Keep
+    // the original one for the confirmation message.
+    $original_revision_timestamp = $this->revision->getRevisionCreationTime();
+
+    $this->revision = $this->prepareRevertedRevision($this->revision, $form_state);
+    $this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]);
+    $this->revision->save();
+
+    $this->logger('content')->notice('eventseries: reverted %title revision %revision.', ['%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
+    drupal_set_message(t('eventseries %title has been reverted to the revision from %revision-date.', ['%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)]));
+    $form_state->setRedirect(
+      'entity.eventseries.version_history',
+      ['eventseries' => $this->revision->id()]
+    );
+  }
+
+  /**
+   * Prepares a revision to be reverted.
+   *
+   * @param \Drupal\recurring_events\EventInterface $revision
+   *   The revision to be reverted.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\recurring_events\EventInterface
+   *   The prepared revision ready to be stored.
+   */
+  protected function prepareRevertedRevision(EventInterface $revision, FormStateInterface $form_state) {
+    $revision->setNewRevision();
+    $revision->isDefaultRevision(TRUE);
+    $revision->setRevisionCreationTime(REQUEST_TIME);
+
+    return $revision;
+  }
+
+}
diff --git a/src/Form/EventSeriesRevisionRevertTranslationForm.php b/src/Form/EventSeriesRevisionRevertTranslationForm.php
new file mode 100644
index 00000000..5a194ef1
--- /dev/null
+++ b/src/Form/EventSeriesRevisionRevertTranslationForm.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\recurring_events\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\recurring_events\EventInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides form to revert an eventseries revision for a single translation.
+ *
+ * @ingroup recurring_events
+ */
+class EventSeriesRevisionRevertTranslationForm extends EventSeriesRevisionRevertForm {
+
+
+  /**
+   * The language to be reverted.
+   *
+   * @var string
+   */
+  protected $langcode;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new EventSeriesRevisionRevertTranslationForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The eventseries storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, DateFormatterInterface $date_formatter, LanguageManagerInterface $language_manager) {
+    parent::__construct($entity_storage, $date_formatter);
+    $this->languageManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('eventseries'),
+      $container->get('date.formatter'),
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'eventseries_revision_revert_translation_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to revert @language translation to the revision from %revision-date?', ['@language' => $this->languageManager->getLanguageName($this->langcode), '%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $eventseries_revision = NULL, $langcode = NULL) {
+    $this->langcode = $langcode;
+    $form = parent::buildForm($form, $form_state, $eventseries_revision);
+
+    $form['revert_untranslated_fields'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Revert content shared among translations'),
+      '#default_value' => FALSE,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareRevertedRevision(EventInterface $revision, FormStateInterface $form_state) {
+    $revert_untranslated_fields = $form_state->getValue('revert_untranslated_fields');
+
+    /** @var \Drupal\recurring_events\EventInterface $default_revision */
+    $latest_revision = $this->eventSeriesStorage->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);
+    $revision->setRevisionCreationTime(REQUEST_TIME);
+
+    return $latest_revision_translation;
+  }
+
+}
-- 
GitLab