diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index 37ed343..5d7145e 100644
--- a/core/lib/Drupal/Core/Entity/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -306,6 +306,14 @@ public function hasKey($key) {
   /**
    * {@inheritdoc}
    */
+  public function setKey($key, $value) {
+    $this->entity_keys[$key] = $value;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function id() {
     return $this->id;
   }
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
index a1c1bc9..d23e15c 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
@@ -141,6 +141,18 @@ public function getKey($key);
   public function hasKey($key);
 
   /**
+   * Sets a specific entity key.
+   *
+   * @param string $key
+   *   The name of the entity key.
+   * @param string $value
+   *   The new value of the key.
+   *
+   * @return $this
+   */
+  public function setKey($key, $value);
+
+  /**
    * Indicates whether entities should be statically cached.
    *
    * @return bool
diff --git a/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php
index 830d2ab..9bbde56 100644
--- a/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php
@@ -26,7 +26,7 @@ class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
   /**
    * The storage field definitions for this entity type.
    *
-   * @var \Drupal\Core\Field\FieldDefinitionInterface[]
+   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
    */
   protected $fieldStorageDefinitions;
 
diff --git a/core/modules/comment/src/Tests/CommentTestBase.php b/core/modules/comment/src/Tests/CommentTestBase.php
index 05f139d..bb4e55e 100644
--- a/core/modules/comment/src/Tests/CommentTestBase.php
+++ b/core/modules/comment/src/Tests/CommentTestBase.php
@@ -82,7 +82,7 @@ protected function setUp() {
     $this->container->get('comment.manager')->addDefaultField('node', 'article');
 
     // Create a test node authored by the web user.
-    $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->id()));
+    $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->id(), 'langcode' => 'en'));
   }
 
   /**
diff --git a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php
index 61fa85e..c4150f3 100644
--- a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php
+++ b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php
@@ -50,9 +50,9 @@ function testDifferentPermissions() {
     // - An article, which should be user-editable.
     // - A page, which should not be user-editable.
     // - A second article, which should also be user-editable.
-    $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
-    $node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
-    $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
+    $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en'));
+    $node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1, 'langcode' => 'en'));
+    $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en'));
 
     // Now, on the front page, all article nodes should have contextual links
     // placeholders, as should the view that contains them.
diff --git a/core/modules/file/src/FileViewsData.php b/core/modules/file/src/FileViewsData.php
index 1c6e524..5a977a2 100644
--- a/core/modules/file/src/FileViewsData.php
+++ b/core/modules/file/src/FileViewsData.php
@@ -7,118 +7,47 @@
 
 namespace Drupal\file;
 
-use Drupal\views\EntityViewsDataInterface;
+use Drupal\views\EntityViewsData;
 
 /**
  * Provides views data for the file entity type.
  */
-class FileViewsData implements EntityViewsDataInterface {
+class FileViewsData extends EntityViewsData {
 
   /**
    * {@inheritdoc}
    */
   public function getViewsData() {
-    $data = array();
-    // Sets 'group' index for file_managed table.
-    $data['file_managed']['table']['group']  = t('File');
+    $data = parent::getViewsData();
 
-    // Advertise this table as a possible base table.
-    $data['file_managed']['table']['base'] = array(
-      'field' => 'fid',
-      'title' => t('File'),
-      'help' => t("Files maintained by Drupal and various modules."),
-      'defaults' => array(
-        'field' => 'filename'
-      ),
-    );
-    $data['file_managed']['table']['entity type'] = 'file';
+    // @TODO There is no corresponding information in entity metadata.
+    $data['file_managed']['table']['base']['help'] = t('Files maintained by Drupal and various modules.');
+    $data['file_managed']['table']['base']['defaults']['field'] = 'filename';
     $data['file_managed']['table']['wizard_id'] = 'file_managed';
 
-    // Describes fid field in file_managed table.
-    $data['file_managed']['fid'] = array(
-      'title' => t('File ID'),
-      'help' => t('The ID of the file.'),
-      'field' => array(
-        'id' => 'file',
-      ),
-      'argument' => array(
-        'id' => 'file_fid',
-        // The field to display in the summary.
-        'name field' => 'filename',
-        'numeric' => TRUE,
-      ),
-      'filter' => array(
-        'id' => 'numeric',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'relationship' => array(
-        'title' => t('File usage'),
-        'help' => t('Relate file entities to their usage.'),
-        'id' => 'standard',
-        'base' => 'file_usage',
-        'base field' => 'fid',
-        'field' => 'fid',
-        'label' => t('File usage'),
-      ),
+    $data['file_managed']['fid']['field']['id'] ='file';
+    $data['file_managed']['fid']['argument'] = array(
+      'id' => 'file_fid',
+      // The field to display in the summary.
+      'name field' => 'filename',
+      'numeric' => TRUE,
     );
-
-    // Describes filename field in file_managed table.
-    $data['file_managed']['filename'] = array(
-      'title' => t('Name'),
-      'help' => t('The name of the file.'),
-      'field' => array(
-        'id' => 'file',
-       ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'string',
-      ),
-      'argument' => array(
-        'id' => 'string',
-      ),
+    $data['file_managed']['fid']['relationship'] = array(
+      'title' => t('File usage'),
+      'help' => t('Relate file entities to their usage.'),
+      'id' => 'standard',
+      'base' => 'file_usage',
+      'base field' => 'fid',
+      'field' => 'fid',
+      'label' => t('File usage'),
     );
 
-    // Describes uri field in file_managed table.
-    $data['file_managed']['uri'] = array(
-      'title' => t('Path'),
-      'help' => t('The path of the file.'),
-      'field' => array(
-        'id' => 'file_uri',
-       ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'string',
-      ),
-      'argument' => array(
-        'id' => 'string',
-      ),
-    );
+    $data['file_managed']['filename']['field']['id'] = 'file';
 
-    // Describes filemime field in file_managed table.
-    $data['file_managed']['filemime'] = array(
-      'title' => t('Mime type'),
-      'help' => t('The mime type of the file.'),
-      'field' => array(
-        'id' => 'file_filemime',
-       ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'string',
-      ),
-      'argument' => array(
-        'id' => 'string',
-      ),
-    );
+    $data['file_managed']['uri']['field']['id'] = 'file_uri';
+
+    $data['file_managed']['filemime']['field']['id'] = 'file_filemime';
 
-    // Describes extension field in file_managed table.
     $data['file_managed']['extension'] = array(
       'title' => t('Extension'),
       'help' => t('The extension of the file.'),
@@ -129,79 +58,14 @@ public function getViewsData() {
        ),
     );
 
-    // Describes filesize field in file_managed table.
-    $data['file_managed']['filesize'] = array(
-      'title' => t('Size'),
-      'help' => t('The size of the file.'),
-      'field' => array(
-        'id' => 'file_size',
-       ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'numeric',
-      ),
-    );
-
-    // Describes status field in file_managed table.
-    $data['file_managed']['status'] = array(
-      'title' => t('Status'),
-      'help' => t('The status of the file.'),
-      'field' => array(
-        'id' => 'file_status',
-       ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'file_status',
-      ),
-    );
-
-    // Describes created field in file_managed table.
-    $data['file_managed']['created'] = array(
-      'title' => t('Upload date'),
-      'help' => t('The date the file was uploaded.'),
-      'field' => array(
-        'id' => 'date',
-      ),
-      'sort' => array(
-        'id' => 'date',
-      ),
-      'filter' => array(
-        'id' => 'date',
-      ),
-    );
+    $data['file_managed']['filesize']['field']['id'] = 'file_size';
 
-    // Describes changed field in file_managed table.
-    $data['file_managed']['changed'] = array(
-      'title' => t('Modified date'),
-      'help' => t('The date the file was last changed.'),
-      'field' => array(
-        'id' => 'date',
-      ),
-      'sort' => array(
-        'id' => 'date',
-      ),
-      'filter' => array(
-        'id' => 'date',
-      ),
-    );
+    $data['file_managed']['status']['field']['id'] = 'file_status';
+    $data['file_managed']['status']['filter']['id'] = 'file_status';
 
-    // Describes uid field in file_managed table.
-    $data['file_managed']['uid'] = array(
-      'title' => t('User who uploaded'),
-      'help' => t('The user that uploaded the file.'),
-      'relationship' => array(
-        'title' => t('User who uploaded'),
-        'label' => t('User who uploaded'),
-        'base' => 'users',
-        'base field' => 'uid',
-      ),
-    );
+    $data['file_managed']['uid']['relationship']['title'] = t('User who uploaded');
+    $data['file_managed']['uid']['relationship']['label'] = t('User who uploaded');
 
-    // Sets 'group' index for file_usage table.
     $data['file_usage']['table']['group']  = t('File Usage');
 
     // Provide field-type-things to several base tables; on the core files table
@@ -499,4 +363,3 @@ public function getViewsData() {
   }
 
 }
-
diff --git a/core/modules/file/src/Tests/FileFieldRSSContentTest.php b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
index db265c2..42b88ce 100644
--- a/core/modules/file/src/Tests/FileFieldRSSContentTest.php
+++ b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
@@ -51,7 +51,7 @@ function testFileFieldRSSContent() {
 
     // Create a new node with a file field set. Promote to frontpage
     // needs to be set so this node will appear in the RSS feed.
-    $node = $this->drupalCreateNode(array('type' => $type_name, 'promote' => 1));
+    $node = $this->drupalCreateNode(array('type' => $type_name, 'promote' => 1, 'langcode' => 'en'));
     $test_file = $this->getTestFile('text');
 
     // Create a new node with the uploaded file.
diff --git a/core/modules/node/src/NodeViewsData.php b/core/modules/node/src/NodeViewsData.php
index 6d69a58..008d175 100644
--- a/core/modules/node/src/NodeViewsData.php
+++ b/core/modules/node/src/NodeViewsData.php
@@ -7,169 +7,48 @@
 
 namespace Drupal\node;
 
+use Drupal\views\EntityViewsData;
 use Drupal\views\EntityViewsDataInterface;
 
 /**
  * Provides the views data for the node entity type.
  */
-class NodeViewsData implements EntityViewsDataInterface {
+class NodeViewsData extends EntityViewsData implements EntityViewsDataInterface {
 
   /**
    * {@inheritdoc}
    */
   public function getViewsData() {
-    // Define the base group of this table. Fields that don't have a group defined
-    // will go into this field by default.
-    $data['node']['table']['group'] = t('Content');
+    $data = parent::getViewsData();
 
-    // Advertise this table as a possible base table.
-    $data['node']['table']['base'] = array(
-      'field' => 'nid',
-      'title' => t('Content'),
-      'weight' => -10,
-      'access query tag' => 'node_access',
-      'defaults' => array(
-        'field' => 'title',
-      ),
-    );
-    $data['node']['table']['entity type'] = 'node';
+    $data['node']['table']['base']['weight'] = -10;
+    $data['node']['table']['base']['access query tag'] = 'node_access';
     $data['node']['table']['wizard_id'] = 'node';
 
-    $data['node_field_data']['table']['group'] = t('Content');
-    $data['node_field_data']['table']['entity type'] = 'node';
-    $data['node_field_data']['table']['join']['node'] = array(
-      'type' => 'INNER',
-      'left_field' => 'nid',
-      'field' => 'nid',
-    );
+    $data['node']['nid']['field']['id'] = 'node';
+    $data['node']['nid']['field']['argument'] = [
+      'id' => 'node_nid',
+      'name field' => 'title',
+      'numeric' => TRUE,
+      'validate type' => 'nid',
+    ];
 
-    $data['node']['nid'] = array(
-      'title' => t('Nid'),
-      'help' => t('The node ID.'),
-      'field' => array(
-        'id' => 'node',
-      ),
-      'argument' => array(
-        'id' => 'node_nid',
-        'name field' => 'title',
-        'numeric' => TRUE,
-        'validate type' => 'nid',
-      ),
-      'filter' => array(
-        'id' => 'numeric',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-    );
+    $data['node_field_data']['title']['field']['id'] = 'node';
+    $data['node_field_data']['title']['field']['link_to_node default'] = TRUE;
 
-    // This definition has more items in it than it needs to as an example.
-    $data['node_field_data']['title'] = array(
-      'title' => t('Title'),
-      'help' => t('The content title.'),
-      'field' => array(
-        // This is the real field which could be left out since it is the same.
-        'field' => 'title',
-        // This is the UI group which could be left out since it is the same.
-        'group' => t('Content'),
-        'id' => 'node',
-        'link_to_node default' => TRUE,
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'string',
-      ),
-      'argument' => array(
-        'id' => 'string',
-      ),
-    );
+    $data['node_field_data']['type']['field']['id'] = 'node_type';
+    $data['node_field_data']['type']['argument']['id'] = 'node_type';
 
-    $data['node_field_data']['created'] = array(
-      'title' => t('Post date'),
-      'help' => t('The date the content was posted.'),
-      'field' => array(
-        'id' => 'date',
-      ),
-      'sort' => array(
-        'id' => 'date'
-      ),
-      'filter' => array(
-        'id' => 'date',
-      ),
-    );
+    $data['node_field_data']['langcode']['help'] = t('The language of the content or translation.');
+    $data['node_field_data']['langcode']['field']['id'] = 'node_language';
 
-    $data['node_field_data']['changed'] = array(
-      'title' => t('Updated date'),
-      'help' => t('The date the content was last updated.'),
-      'field' => array(
-        'id' => 'date',
-      ),
-      'sort' => array(
-        'id' => 'date'
-      ),
-      'filter' => array(
-        'id' => 'date',
-      ),
-    );
-
-    $data['node_field_data']['type'] = array(
-      'title' => t('Type'),
-      'help' => t('The content type (for example, "blog entry", "forum post", "story", etc).'),
-      'field' => array(
-        'id' => 'node_type',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'bundle',
-      ),
-      'argument' => array(
-        'id' => 'node_type',
-      ),
-    );
-
-    if (\Drupal::moduleHandler()->moduleExists('language')) {
-      $data['node_field_data']['langcode'] = array(
-        'title' => t('Translation language'),
-        'help' => t('The language of the content or translation.'),
-        'field' => array(
-          'id' => 'node_language',
-        ),
-        'filter' => array(
-          'id' => 'language',
-        ),
-        'argument' => array(
-          'id' => 'language',
-        ),
-        'sort' => array(
-          'id' => 'standard',
-        ),
-      );
-    }
-
-    $data['node_field_data']['status'] = array(
-      'title' => t('Published status'),
-      'help' => t('Whether or not the content is published.'),
-      'field' => array(
-        'id' => 'boolean',
-        'output formats' => array(
-          'published-notpublished' => array(t('Published'), t('Not published')),
-        ),
-      ),
-      'filter' => array(
-        'id' => 'boolean',
-        'label' => t('Published status'),
-        'type' => 'yes-no',
-        // Use status = 1 instead of status <> 0 in WHERE statement.
-        'use_equal' => TRUE,
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-    );
+    $data['node_field_data']['status']['field']['output formats'] = [
+      'published-notpublished' => array(t('Published'), t('Not published')),
+    ];
+    $data['node_field_data']['status']['filter']['label'] = t('Published status');
+    $data['node_field_data']['status']['filter']['type'] = 'yes-no';
+    // Use status = 1 instead of status <> 0 in WHERE statement.
+    $data['node_field_data']['status']['filter']['use_equal'] = TRUE;
 
     $data['node_field_data']['status_extra'] = array(
       'title' => t('Published status or admin user'),
@@ -181,55 +60,27 @@ public function getViewsData() {
       ),
     );
 
-    $data['node_field_data']['promote'] = array(
-      'title' => t('Promoted to front page status'),
-      'help' => t('Whether or not the content is promoted to the front page.'),
-      'field' => array(
-        'id' => 'boolean',
-        'output formats' => array(
-          'promoted-notpromoted' => array(t('Promoted'), t('Not promoted')),
-        ),
-      ),
-      'filter' => array(
-        'id' => 'boolean',
-        'label' => t('Promoted to front page status'),
-        'type' => 'yes-no',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-    );
+    $data['node_field_data']['promote']['field']['output formats'] = [
+      'promoted-notpromoted' => array(t('Promoted'), t('Not promoted')),
+    ];
+    $data['node_field_data']['promote']['filter']['label'] = t('Promoted to front page status');
+    $data['node_field_data']['promote']['filter']['type'] = 'yes-no';
+
+    $data['node_field_data']['sticky']['field']['output formats'] = [
+      'sticky' => array(t('Sticky'), t('Not sticky')),
+    ];
+    $data['node_field_data']['sticky']['filter']['label'] = t('Sticky status');
+    $data['node_field_data']['sticky']['filter']['type'] = 'yes-no';
+    $data['node_field_data']['sticky']['sort']['help'] = t('Whether or not the content is sticky. To list sticky content first, set this to descending.');
 
-    $data['node_field_data']['sticky'] = array(
-      'title' => t('Sticky status'),
-      'help' => t('Whether or not the content is sticky.'),
+    $data['node']['translation_link'] = array(
+      'title' => t('Translation link'),
+      'help' => t('Provide a link to the translations overview for nodes.'),
       'field' => array(
-        'id' => 'boolean',
-        'output formats' => array(
-          'sticky' => array(t('Sticky'), t('Not sticky')),
-        ),
-      ),
-      'filter' => array(
-        'id' => 'boolean',
-        'label' => t('Sticky status'),
-        'type' => 'yes-no',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-        'help' => t('Whether or not the content is sticky. To list sticky content first, set this to descending.'),
+        'id' => 'content_translation_link',
       ),
     );
 
-    if (\Drupal::moduleHandler()->moduleExists('content_translation')) {
-      $data['node']['translation_link'] = array(
-        'title' => t('Translation link'),
-        'help' => t('Provide a link to the translations overview for nodes.'),
-        'field' => array(
-          'id' => 'content_translation_link',
-        ),
-      );
-    }
-
     $data['node']['view_node'] = array(
       'field' => array(
         'title' => t('Link to content'),
@@ -264,6 +115,7 @@ public function getViewsData() {
 
     // Bogus fields for aliasing purposes.
 
+    // @Todo Add similar support to any date field.
     $data['node_field_data']['created_fulldate'] = array(
       'title' => t('Created date'),
       'help' => t('Date in the form of CCYYMMDD.'),
@@ -372,27 +224,12 @@ public function getViewsData() {
       ),
     );
 
-    $data['node_field_data']['uid'] = array(
-      'title' => t('Author uid'),
-      'help' => t('The user authoring the content. If you need more fields than the uid add the content: author relationship'),
-      'relationship' => array(
-        'title' => t('Content author'),
-        'help' => t('Relate content to the user who created it.'),
-        'id' => 'standard',
-        'base' => 'users',
-        'field' => 'uid',
-        'label' => t('author'),
-      ),
-      'filter' => array(
-        'id' => 'user_name',
-      ),
-      'argument' => array(
-        'id' => 'numeric',
-      ),
-      'field' => array(
-        'id' => 'user',
-      ),
-    );
+    $data['node_field_data']['uid']['help'] = t('The user authoring the content. If you need more fields than the uid add the content: author relationship');
+    $data['node_field_data']['uid']['filter']['id'] = 'user_name';
+    $data['node_field_data']['uid']['field']['id'] = 'user';
+    $data['node_field_data']['uid']['relationship']['title'] = t('Content author');
+    $data['node_field_data']['uid']['relationship']['help'] = t('Relate content to the user who created it.');
+    $data['node_field_data']['uid']['relationship']['label'] = t('author');
 
     $data['node']['node_listing_empty'] = array(
       'title' => t('Empty Node Frontpage behavior'),
@@ -402,83 +239,36 @@ public function getViewsData() {
       ),
     );
 
-    $data['node_field_data']['uid_revision'] = array(
-      'title' => t('User has a revision'),
-      'help' => t('All nodes where a certain user has a revision'),
-      'real field' => 'nid',
-      'filter' => array(
-        'id' => 'node_uid_revision',
-      ),
-      'argument' => array(
-        'id' => 'node_uid_revision',
-      ),
-    );
+    $data['node_field_data']['uid_revision']['title'] = t('User has a revision');
+    $data['node_field_data']['uid_revision']['help'] = t('All nodes where a certain user has a revision');
+    $data['node_field_data']['uid_revision']['real field'] = 'nid';
+    $data['node_field_data']['uid_revision']['filter']['id'] = 'node_uid_revision';
+    $data['node_field_data']['uid_revision']['argument']['id'] = 'node_uid_revision';
 
-    $data['node_revision']['table']['entity type'] = 'node';
-    // Define the base group of this table. Fields that don't have a group defined
-    // will go into this field by default.
-    $data['node_revision']['table']['group']  = t('Content revision');
     $data['node_revision']['table']['wizard_id'] = 'node_revision';
 
     // Advertise this table as a possible base table.
-    $data['node_revision']['table']['base'] = array(
-      'field' => 'vid',
-      'title' => t('Content revision'),
-      'help' => t('Content revision is a history of changes to content.'),
-      'defaults' => array(
-        'field' => 'title',
-      ),
-    );
-
-    // For other base tables, explain how we join.
-    $data['node_revision']['table']['join'] = array(
-      'node' => array(
-        'left_field' => 'vid',
-        'field' => 'vid',
-      ),
-    );
-
-    $data['node_revision']['nid'] = array(
-      'title' => t('Nid'),
-      'help' => t('The revision NID of the content revision.'),
-      'field' => array(
-        'id' => 'standard',
-      ),
-      'argument' => array(
-        'id' => 'node_nid',
-        'numeric' => TRUE,
-      ),
-      'filter' => array(
-        'id' => 'numeric',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'relationship' => array(
-        'id' => 'standard',
-        'base' => 'node',
-        'base field' => 'nid',
-        'title' => t('Content'),
-        'label' => t('Get the actual content from a content revision.'),
-      ),
-    );
+    $data['node_revision']['table']['base']['help'] = t('Content revision is a history of changes to content.');
+    $data['node_revision']['table']['base']['defaults']['title'] = 'title';
+
+    $data['node_revision']['nid']['argument'] = [
+      'id' => 'node_nid',
+      'numeric' => TRUE,
+    ];
+    // @todo the NID field needs different behaviour on revision/non-revision
+    //   tables. It would be neat if this could be encoded in the base field
+    //   definition.
+    $data['node_revision']['nid']['relationship']['id'] = 'standard';
+    $data['node_revision']['nid']['relationship']['base'] = 'node';
+    $data['node_revision']['nid']['relationship']['base field'] = 'nid';
+    $data['node_revision']['nid']['relationship']['title'] = t('Content');
+    $data['node_revision']['nid']['relationship']['label'] = t('Get the actual content from a content revision.');
 
     $data['node_revision']['vid'] = array(
-      'title' => t('Vid'),
-      'help' => t('The revision ID of the content revision.'),
-      'field' => array(
-        'id' => 'standard',
-      ),
       'argument' => array(
         'id' => 'node_vid',
         'numeric' => TRUE,
       ),
-      'filter' => array(
-        'id' => 'numeric',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
       'relationship' => array(
         'id' => 'standard',
         'base' => 'node',
@@ -486,119 +276,29 @@ public function getViewsData() {
         'title' => t('Content'),
         'label' => t('Get the actual content from a content revision.'),
       ),
-    );
+    ) + $data['node_revision']['vid'];
 
-    if (\Drupal::moduleHandler()->moduleExists('language')) {
-      $data['node_revision']['langcode'] = array(
-        'title' => t('Original language'),
-        'help' => t('The language the original content is in.'),
-        'field' => array(
-          'id' => 'node_language',
-        ),
-        'filter' => array(
-          'id' => 'language',
-        ),
-        'argument' => array(
-          'id' => 'language',
-        ),
-        'sort' => array(
-          'id' => 'standard',
-        ),
-      );
-    }
+    $data['node_revision']['langcode']['help'] = t('The language the original content is in.');
+    $data['node_revision']['langcode']['field']['id'] = 'node_language';
 
-    $data['node_revision']['revision_log'] = array(
-      'title' => t('Log message'),
-      'help' => t('The log message entered when the revision was created.'),
-      'field' => array(
-        'id' => 'xss',
-      ),
-      'filter' => array(
-        'id' => 'string',
-      ),
-    );
+    $data['node_revision']['revision_log']['field']['id'] = 'xss';
 
-    $data['node_revision']['revision_uid'] = array(
-      'title' => t('User'),
-      'help' => t('Relate a content revision to the user who created the revision.'),
-      'relationship' => array(
-        'id' => 'standard',
-        'base' => 'users',
-        'base field' => 'uid',
-        'label' => t('revision user'),
-      ),
-    );
+    $data['node_revision']['revision_uid']['help'] = t('Relate a content revision to the user who created the revision.');
+    $data['node_revision']['revision_uid']['relationship']['label'] = t('revision user');
 
-    $data['node_field_revision']['table']['entity type'] = 'node';
-    // Define the base group of this table. Fields that don't have a group defined
-    // will go into this field by default.
-    $data['node_field_revision']['table']['group']  = t('Content revision');
     $data['node_field_revision']['table']['wizard_id'] = 'node_field_revision';
 
-    // For other base tables, explain how we join.
-    $data['node_field_revision']['table']['join'] = array(
-      'node' => array(
-        'left_field' => 'vid',
-        'field' => 'vid',
-      ),
-      'node_revision' => array(
-        'left_field' => 'vid',
-        'field' => 'vid',
-      ),
-    );
+    $data['node_field_revision']['table']['join']['node']['left_field'] = 'vid';
+    $data['node_field_revision']['table']['join']['node']['field'] = 'vid';
 
-    $data['node_field_revision']['status'] = array(
-      'title' => t('Published'),
-      'help' => t('Whether or not the content is published.'),
-      'field' => array(
-        'id' => 'boolean',
-        'output formats' => array(
-          'published-notpublished' => array(t('Published'), t('Not published')),
-        ),
-      ),
-      'filter' => array(
-        'id' => 'boolean',
-        'label' => t('Published'),
-        'type' => 'yes-no',
-        // Use status = 1 instead of status <> 0 in WHERE statement.
-        'use_equal' => TRUE,
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-    );
-
-    $data['node_field_revision']['title'] = array(
-      'title' => t('Title'),
-      'help' => t('The content title.'),
-      'field' => array(
-        'field' => 'title',
-        'id' => 'node_revision',
-      ),
-      'sort' => array(
-        'id' => 'standard',
-      ),
-      'filter' => array(
-        'id' => 'string',
-      ),
-      'argument' => array(
-        'id' => 'string',
-      ),
-    );
+    $data['node_field_revision']['status']['field']['output formats'] = [
+      'published-notpublished' => [t('Published'), t('Not published')],
+    ];
+    $data['node_field_revision']['status']['filter']['label'] = t('Published');
+    $data['node_field_revision']['status']['filter']['type'] = 'yes-no';
+    $data['node_field_revision']['status']['filter']['use_equal'] = TRUE;
 
-    $data['node_field_revision']['changed'] = array(
-      'title' => t('Updated date'),
-      'help' => t('The date the content was last updated.'),
-      'field' => array(
-        'id' => 'date',
-      ),
-      'sort' => array(
-        'id' => 'date'
-      ),
-      'filter' => array(
-        'id' => 'date',
-      ),
-    );
+    $data['node_field_revision']['title']['field']['id'] = 'node_revision';
 
     $data['node_revision']['link_to_revision'] = array(
       'field' => array(
@@ -727,6 +427,4 @@ public function getViewsData() {
     return $data;
   }
 
-
 }
-
diff --git a/core/modules/node/src/Tests/NodeLoadMultipleTest.php b/core/modules/node/src/Tests/NodeLoadMultipleTest.php
index 3cea6a1..fbeb16b 100644
--- a/core/modules/node/src/Tests/NodeLoadMultipleTest.php
+++ b/core/modules/node/src/Tests/NodeLoadMultipleTest.php
@@ -31,10 +31,10 @@ protected function setUp() {
    * Creates four nodes and ensures that they are loaded correctly.
    */
   function testNodeMultipleLoad() {
-    $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
-    $node2 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
-    $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 0));
-    $node4 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0));
+    $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en'));
+    $node2 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en'));
+    $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 0, 'langcode' => 'en'));
+    $node4 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0, 'langcode' => 'en'));
 
     // Confirm that promoted nodes appear in the default node listing.
     $this->drupalGet('node');
diff --git a/core/modules/node/src/Tests/NodeRSSContentTest.php b/core/modules/node/src/Tests/NodeRSSContentTest.php
index e4b7cb3..6a1c830 100644
--- a/core/modules/node/src/Tests/NodeRSSContentTest.php
+++ b/core/modules/node/src/Tests/NodeRSSContentTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
    */
   function testNodeRSSContent() {
     // Create a node.
-    $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
+    $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en'));
 
     $this->drupalGet('rss.xml');
 
diff --git a/core/modules/system/entity.api.php b/core/modules/system/entity.api.php
index f4bbfd2..f7411c6 100644
--- a/core/modules/system/entity.api.php
+++ b/core/modules/system/entity.api.php
@@ -310,7 +310,8 @@
  *     config entities will use \Drupal\Core\Config\Entity\ConfigEntityStorage.
  *     You can extend one of these classes to provide custom behavior.
  *   - views_data: A class implementing \Drupal\views\EntityViewsDataInterface
- *     to provide views data for the entity type.
+ *     to provide views data for the entity type. You can autogenerate most of
+ *     the views data by extending \Drupal\views\EntityViewsData.
  * - For content entities, the annotation will refer to a number of database
  *   tables and their fields. These annotation properties, such as 'base_table',
  *   'data_table', 'entity_keys', etc., are documented on
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php
index 3993dcf..1b065af 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php
@@ -29,7 +29,8 @@
  *       "default" = "Drupal\entity_test\EntityTestForm",
  *       "delete" = "Drupal\entity_test\EntityTestDeleteForm"
  *     },
- *     "translation" = "Drupal\content_translation\ContentTranslationHandler"
+ *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ *     "views_data" = "Drupal\views\EntityViewsData"
  *   },
  *   base_table = "entity_test",
  *   fieldable = TRUE,
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php
index fa66635..4e03fd1 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php
@@ -24,7 +24,8 @@
  *       "default" = "Drupal\entity_test\EntityTestForm",
  *       "delete" = "Drupal\entity_test\EntityTestDeleteForm"
  *     },
- *     "translation" = "Drupal\content_translation\ContentTranslationHandler"
+ *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ *     "views_data" = "Drupal\views\EntityViewsData"
  *   },
  *   base_table = "entity_test_mul",
  *   data_table = "entity_test_mul_property_data",
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php
index 50afa32..13aca32 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php
@@ -23,7 +23,8 @@
  *       "default" = "Drupal\entity_test\EntityTestForm",
  *       "delete" = "Drupal\entity_test\EntityTestDeleteForm"
  *     },
- *     "translation" = "Drupal\content_translation\ContentTranslationHandler"
+ *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ *     "views_data" = "Drupal\views\EntityViewsData"
  *   },
  *   base_table = "entity_test_mulrev",
  *   data_table = "entity_test_mulrev_property_data",
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php
index ebb39f2..f97d2fd 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php
@@ -23,7 +23,8 @@
  *       "default" = "Drupal\entity_test\EntityTestForm",
  *       "delete" = "Drupal\entity_test\EntityTestDeleteForm"
  *     },
- *     "translation" = "Drupal\content_translation\ContentTranslationHandler"
+ *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ *     "views_data" = "Drupal\views\EntityViewsData"
  *   },
  *   base_table = "entity_test_rev",
  *   revision_table = "entity_test_rev_revision",
diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php
new file mode 100644
index 0000000..9a86f77
--- /dev/null
+++ b/core/modules/views/src/EntityViewsData.php
@@ -0,0 +1,444 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\EntityViewsData.
+ */
+
+namespace Drupal\views;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityType;
+use Drupal\Core\Entity\EntityHandlerInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
+use Drupal\Core\Entity\Sql\TableMappingInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\TypedData\TypedDataManager;
+use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides generic views integration for entities.
+ */
+class EntityViewsData implements EntityHandlerInterface, EntityViewsDataInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * Entity type for this views controller instance.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * The storage used for this entity type.
+   *
+   * @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The translation manager.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface
+   */
+  protected $translationManager;
+
+  /**
+   * The typed data manager.
+   *
+   * @var \Drupal\Core\TypedData\TypedDataManager
+   */
+  protected $typedDataManager;
+
+  /**
+   * The field storage definitions for all base fields of the entity type.
+   *
+   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+   */
+  protected $fieldStorageDefinitions;
+
+  /**
+   * Constructs an EntityViewsData object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type to provide views integration for.
+   * @param \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage_controller
+   *   The storage controller used for this entity type.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
+   *   The translation manager.
+   */
+  function __construct(EntityTypeInterface $entity_type, SqlEntityStorageInterface $storage_controller, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, TranslationInterface $translation_manager, TypedDataManager $typed_data_manager) {
+    $this->entityType = $entity_type;
+    $this->entityManager = $entity_manager;
+    $this->storage = $storage_controller;
+    $this->moduleHandler = $module_handler;
+    $this->setStringTranslation($translation_manager);
+    $this->typedDataManager = $typed_data_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage($entity_type->id()),
+      $container->get('entity.manager'),
+      $container->get('module_handler'),
+      $container->get('string_translation'),
+      $container->get('typed_data_manager')
+    );
+  }
+
+  /**
+   * Gets the field storage definitions.
+   *
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+   */
+  protected function getFieldStorageDefinitions() {
+    if (!isset($this->fieldStorageDefinitions)) {
+      $this->fieldStorageDefinitions = $this->entityManager->getFieldStorageDefinitions($this->entityType->id());
+    }
+    return $this->fieldStorageDefinitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = [];
+
+    // @todo In theory we should use the data table as base table, as this would
+    //   save one pointless join (and one more for every relationship).
+    $base_table = $this->entityType->getBaseTable();
+    $base_field = $this->entityType->getKey('id');
+    $data_table = $this->entityType->getDataTable();
+    $revision_table = $this->entityType->getRevisionTable();
+    $revision_data_table = $this->entityType->getRevisionDataTable();
+    $revision_field = $this->entityType->getKey('revision');
+
+    // Setup base information of the views data.
+    $data[$base_table]['table']['entity type'] = $this->entityType->id();
+    $data[$base_table]['table']['group'] = $this->entityType->getLabel();
+    $data[$base_table]['table']['base'] = [
+      'field' => $base_field,
+      'title' => $this->entityType->getLabel(),
+    ];
+
+    if ($label_key = $this->entityType->getKey('label')) {
+      if ($data_table) {
+        $data[$base_table]['table']['base']['defaults'] = array(
+          'field' => $label_key,
+          'table' => $data_table,
+        );
+      }
+      else {
+        $data[$base_table]['table']['base']['defaults'] = array(
+          'field' => $label_key,
+        );
+      }
+    }
+
+    // Setup relations to the revisions/property data.
+    if ($data_table) {
+      $data[$data_table]['table']['join'][$base_table] = [
+        'left_field' => $base_field,
+        'field' => $base_field,
+        'type' => 'INNER'
+      ];
+      $data[$data_table]['table']['entity type'] = $this->entityType->id();
+      $data[$data_table]['table']['group'] = $this->entityType->getLabel();
+    }
+    if ($revision_table) {
+      $data[$revision_table]['table']['entity type'] = $this->entityType->id();
+      $data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]);
+      $data[$revision_table]['table']['base'] = array(
+        'field' => $revision_field,
+        'title' => $this->t('@entity_type revisions', array('@entity_type' => $this->entityType->getLabel())),
+      );
+      // Join the revision table to the base table.
+      $data[$revision_table]['table']['join'][$base_table] = array(
+        'left_field' => $revision_field,
+        'field' => $revision_field,
+        'type' => 'INNER',
+      );
+
+      if ($revision_data_table) {
+        $data[$revision_data_table]['table']['entity type'] = $this->entityType->id();
+        $data[$revision_data_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]);
+
+        $data[$revision_data_table]['table']['join'][$revision_table] = array(
+          'left_field' => $revision_field,
+          'field' => $revision_field,
+          'type' => 'INNER',
+        );
+      }
+    }
+
+    // Load all typed data definitions of all fields. This should cover each of
+    // the entity base, revision, data tables.
+    $field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityType->id());
+    if ($table_mapping = $this->storage->getTableMapping()) {
+      // Iterate over each table we have so far and collect field data for each.
+      // Based on whether the field is in the field_definitions provided by the
+      // entity manager.
+      // @todo We should better just rely on information coming from the entity
+      //   storage.
+      foreach ($table_mapping->getTableNames() as $table) {
+        foreach ($table_mapping->getFieldNames($table) as $field_name) {
+          $this->mapFieldDefinition($table, $field_name, $field_definitions[$field_name], $table_mapping, $data[$table]);
+        }
+      }
+    }
+
+    return $data;
+  }
+
+  /**
+   * Puts the views data for a single field onto the views data.
+   *
+   * @param string $field_name
+   *   The name of the field to handle.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition defined in Entity::baseFieldDefinitions()
+   * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
+   *   The table mapping information
+   * @param array $table_data
+   *   A reference to a specific entity table (for example data_table) inside
+   *   the views data.
+   */
+  protected function mapFieldDefinition($table, $field_name, FieldDefinitionInterface $field_definition, TableMappingInterface $table_mapping, &$table_data) {
+    // Create a dummy instance to retrieve property definitions.
+    $field_column_mapping = $table_mapping->getColumnNames($field_name);
+    $field_schema = $this->getFieldStorageDefinitions()[$field_name]->getSchema();
+
+    $field_definition_type = $field_definition->getType();
+    // Add all properties to views table data.
+    $first = TRUE;
+    foreach ($field_column_mapping as $field_column_name => $schema_field_name) {
+      $schema = $field_schema['columns'][$field_column_name];
+      // We want to both have an entry in the views data for the actual field,
+      // but also each additional schema field, for example the file
+      // description.
+      // @todo Introduce a concept of the "main" schema field for a field item.
+      //   This would be the FID for a file reference for example.
+      if ($first) {
+        $first = FALSE;
+        $table_data[$field_name] = $this->mapSingleFieldViewsData($table, $field_name, $field_definition_type, $schema_field_name, $field_definition, TRUE);
+      }
+      else {
+        $table_data["$field_name.$field_column_name"] = $this->mapSingleFieldViewsData($table, $field_name, $schema['type'], $schema_field_name, $field_definition, FALSE);
+      }
+    }
+  }
+
+  /**
+   * Provides the views data for a given data type and schema field.
+   *
+   * @param string $data_type
+   *   The data type to generate views data for, for example "int". The data
+   *   type comes directly from the schema definition of each field item.
+   * @param string $schema_field_name
+   *   The schema field name.
+   * @param bool $first
+   *   Is it the first column of the schema.
+   *
+   * @return array
+   *   The modified views data field definition.
+   */
+  protected function mapSingleFieldViewsData($table, $field_name, $data_type, $schema_field_name, FieldDefinitionInterface $field_definition, $first) {
+    $views_field = array();
+
+    // Provide a nicer, less verbose label for the first field.
+    if ($first) {
+      $views_field['title'] = $field_definition->getLabel();
+    }
+    else {
+      $views_field['title'] = $field_definition->getLabel() . " ($schema_field_name)";
+    }
+
+    if ($description = $field_definition->getDescription()) {
+      $views_field['help'] = $description;
+    }
+
+    // @todo Allow field types to customize this.
+    switch ($data_type) {
+      case 'int':
+      case 'integer':
+      case 'smallint':
+      case 'tinyint':
+      case 'mediumint':
+      case 'float':
+      case 'double':
+      case 'decimal':
+        $views_field['field']['id'] = 'numeric';
+        $views_field['argument']['id'] = 'numeric';
+        $views_field['filter']['id'] = 'numeric';
+        $views_field['sort']['id'] = 'standard';
+        break;
+      case 'char':
+      case 'string':
+      case 'varchar':
+      case 'tinytext':
+      case 'text':
+      case 'mediumtext':
+      case 'longtext':
+        $views_field['field']['id'] = 'standard';
+        $views_field['argument']['id'] = 'string';
+        $views_field['filter']['id'] = 'string';
+        $views_field['sort']['id'] = 'standard';
+        break;
+      case 'boolean':
+        $views_field['field']['id'] = 'boolean';
+        $views_field['argument']['id'] = 'numeric';
+        $views_field['filter']['id'] = 'boolean';
+        $views_field['sort']['id'] = 'standard';
+        break;
+      case 'uuid':
+        $views_field['field']['id'] = 'standard';
+        $views_field['argument']['id'] = 'string';
+        $views_field['filter']['id'] = 'string';
+        $views_field['sort']['id'] = 'standard';
+        break;
+      case 'language':
+        $views_field['field']['id'] = 'language';
+        $views_field['argument']['id'] = 'language';
+        $views_field['filter']['id'] = 'language';
+        $views_field['sort']['id'] = 'standard';
+        break;
+      case 'created':
+      case 'changed':
+      $views_field['field']['id'] = 'date';
+      $views_field['argument']['id'] = 'date';
+      $views_field['filter']['id'] = 'date';
+      $views_field['sort']['id'] = 'date';
+        break;
+      case 'entity_reference':
+        // @todo Should the actual field handler respect that this is just renders a number
+        // @todo Create an optional entity field handler, that can render the
+        //   entity.
+        $views_field['field']['id'] = 'standard';
+        $views_field['argument']['id'] = 'standard';
+        $views_field['filter']['id'] = 'standard';
+        $views_field['sort']['id'] = 'standard';
+        break;
+      case 'uri':
+        $views_field['field']['id'] = 'standard';
+        $views_field['argument']['id'] = 'string';
+        $views_field['filter']['id'] = 'string';
+        $views_field['sort']['id'] = 'standard';
+        break;
+      default:
+        $views_field['field']['id'] = 'standard';
+        $views_field['argument']['id'] = 'standard';
+        $views_field['filter']['id'] = 'standard';
+        $views_field['sort']['id'] = 'standard';
+    }
+
+    $process_method = 'processViewsDataFor' . Container::camelize($data_type);
+    if (method_exists($this, $process_method)) {
+      $this->{$process_method}($table, $field_name, $field_definition, $views_field);
+    }
+
+    return $views_field;
+  }
+
+  /**
+   * Processes the views data for a language field.
+   *
+   * @param string $table
+   *   The table the language field is added to.
+   * @param string $field_name
+   *   The field name of the language field.
+   * @param array $views_field
+   *   The views field data.
+   */
+  protected function processViewsDataForLanguage($table, $field_name, FieldDefinitionInterface $field_definition, array &$views_field) {
+    // Apply special titles for the langcode field.
+    if ($field_name == 'langcode') {
+      if ($table == $this->entityType->getDataTable() || $table == $this->entityType->getBaseTable()) {
+        $views_field['title'] = $this->t('Translation language');
+      }
+      if ($table == $this->entityType->getRevisionDataTable() || $table == $this->entityType->getRevisionTable()) {
+        $views_field['title'] =  $this->t('Original language');
+      }
+    }
+  }
+
+  /**
+   * Processes the views data for an entity reference field.
+   *
+   * @param string $table
+   *   The table the language field is added to.
+   * @param string $field_name
+   *   The field name of the language field.
+   * @param array $views_field
+   *   The views field data.
+   */
+  protected function processViewsDataForEntityReference($table, $field_name, FieldDefinitionInterface $field_definition, array &$views_field) {
+    if ($entity_type_id = $field_definition->getItemDefinition()->getSetting('target_type')) {
+      $entity_type = $this->entityManager->getDefinition($entity_type_id);
+      if ($entity_type instanceof ContentEntityType) {
+        $views_field['relationship'] = [
+          'base' => $this->getViewsTableForEntityType($entity_type),
+          'base field' => $entity_type->getKey('id'),
+          'label' => $entity_type->getLabel(),
+          'title' => $entity_type->getLabel(),
+          'id' => 'standard',
+        ];
+        $views_field['field']['id'] = 'numeric';
+        $views_field['argument']['id'] = 'numeric';
+        $views_field['filter']['id'] = 'numeric';
+        $views_field['sort']['id'] = 'standard';
+      }
+      else {
+        $views_field['field']['id'] = 'standard';
+        $views_field['argument']['id'] = 'string';
+        $views_field['filter']['id'] = 'string';
+        $views_field['sort']['id'] = 'standard';
+      }
+    }
+
+    if ($field_name == $this->entityType->getKey('bundle')) {
+      // @todo Use the other bundle handlers, once
+      //   https://www.drupal.org/node/2322949 is in.
+      $views_field['filter']['id'] = 'bundle';
+    }
+  }
+
+  /**
+   * Gets the table of an entity type to be used as base table in views.
+   *
+   * @todo Given that the base_table is pretty much useless as you often have to
+   *   join to the data table anyway, it could make a lot of sense to start with
+   *   the data table right from the beginning.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return string
+   *   The name of the base table in views.
+   */
+  protected function getViewsTableForEntityType(EntityTypeInterface $entity_type) {
+    return $entity_type->getBaseTable();
+  }
+
+}
diff --git a/core/modules/views/tests/Drupal/views/Tests/EntityViewsDataTest.php b/core/modules/views/tests/Drupal/views/Tests/EntityViewsDataTest.php
new file mode 100644
index 0000000..4d6c8c8
--- /dev/null
+++ b/core/modules/views/tests/Drupal/views/Tests/EntityViewsDataTest.php
@@ -0,0 +1,626 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\Tests\EntityViewsDataTest.
+ */
+
+namespace Drupal\views\Tests {
+
+use Drupal\Core\Config\Entity\ConfigEntityType;
+use Drupal\Core\Entity\ContentEntityType;
+use Drupal\Core\Entity\EntityType;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\LanguageItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\UuidItem;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\entity_test\Entity\EntityTestMul;
+use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\Tests\UnitTestCase;
+use Drupal\views\EntityViewsData;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+  /**
+ * @coversDefaultClass \Drupal\views\EntityViewsData
+ * @group Views
+ */
+class EntityViewsDataTest extends UnitTestCase {
+
+  /**
+   * Entity info to use in this test.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected $baseEntityType;
+
+  /**
+   * The mocked entity storage.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityDatabaseStorage|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $entityStorage;
+
+  /**
+   * The mocked entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $entityManager;
+
+  /**
+   * The mocked module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The mocked translation manager.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $translationManager;
+
+  /**
+   * The tested entity views controller.
+   *
+   * @var \Drupal\views\Tests\TestEntityViewsData
+   */
+  protected $viewsData;
+  protected $typedDataManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->entityStorage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+
+    $this->baseEntityType = new EntityType([
+      'base_table' => 'entity_test',
+      'id' => 'entity_test',
+      'label' => 'Entity test',
+      'entity_keys' => ['id' => 'id'],
+    ]);
+
+    $this->translationManager = $this->getStringTranslationStub();
+    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->typedDataManager = $this->getMockBuilder('Drupal\Core\TypedData\TypedDataManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->viewsData = new TestEntityViewsData($this->baseEntityType, $this->entityStorage, $this->entityManager, $this->moduleHandler, $this->translationManager, $this->typedDataManager);
+
+    $field_type_manager = $this->getMockBuilder('Drupal\Core\Field\FieldTypePluginManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $field_type_manager->expects($this->any())
+      ->method('getDefaultSettings')
+      ->willReturn([]);
+    $field_type_manager->expects($this->any())
+      ->method('getDefaultInstanceSettings')
+      ->willReturn([]);
+
+    $container = new ContainerBuilder();
+    $container->set('plugin.manager.field.field_type', $field_type_manager);
+    $container->set('entity.manager', $this->entityManager);
+    \Drupal::setContainer($container);
+  }
+
+  /**
+   * Tests base tables.
+   */
+  public function testBaseTables() {
+    $data = $this->viewsData->getViewsData();
+
+    $this->assertEquals('entity_test', $data['entity_test']['table']['entity type']);
+    $this->assertEquals('Entity test', $data['entity_test']['table']['group']);
+
+    $this->assertEquals('id', $data['entity_test']['table']['base']['field']);
+    $this->assertEquals('Entity test', $data['entity_test']['table']['base']['title']);
+
+    $this->assertFalse(isset($data['entity_test']['table']['defaults']));
+
+    $this->assertFalse(isset($data['entity_test_mul_property_data']));
+    $this->assertFalse(isset($data['revision_table']));
+    $this->assertFalse(isset($data['revision_data_table']));
+  }
+
+
+  /**
+   * Tests data_table support.
+   */
+  public function testDataTable() {
+    $entity_type = $this->baseEntityType->set('data_table', 'entity_test_mul_property_data')
+      ->set('id', 'entity_test_mul')
+      ->setKey('label', 'label');
+
+    $this->viewsData->setEntityType($entity_type);
+
+    // Tests the join definition between the base and the data table.
+    $data = $this->viewsData->getViewsData();
+    $field_views_data = $data['entity_test_mul_property_data'];
+
+    $this->assertEquals('entity_test_mul', $data['entity_test_mul_property_data']['table']['entity type']);
+    $this->assertEquals('Entity test', $data['entity_test_mul_property_data']['table']['group']);
+    $this->assertEquals(['field' => 'label', 'table' => 'entity_test_mul_property_data'], $data['entity_test']['table']['base']['defaults']);
+
+    // Ensure the join information is set up properly.
+    $this->assertCount(1, $field_views_data['table']['join']);
+    $this->assertEquals(['entity_test' => ['left_field' => 'id', 'field' => 'id', 'type' => 'INNER']], $field_views_data['table']['join']);
+    $this->assertFalse(isset($data['revision_table']));
+    $this->assertFalse(isset($data['revision_data_table']));
+  }
+
+  /**
+   * Tests revision table support.
+   */
+  public function testRevisionTable() {
+    $entity_type = $this->baseEntityType
+      ->set('revision_table', 'entity_test_mulrev_revision')
+      ->set('revision_data_table', 'entity_test_mulrev_property_revision')
+      ->set('id', 'entity_test_mulrev')
+      ->setKey('revision', 'revision_id')
+    ;
+    $this->viewsData->setEntityType($entity_type);
+
+    $data = $this->viewsData->getViewsData();
+
+    $this->assertEquals('entity_test_mulrev', $data['entity_test_mulrev_revision']['table']['entity type']);
+    $this->assertEquals('entity_test_mulrev', $data['entity_test_mulrev_property_revision']['table']['entity type']);
+    $this->assertEquals('Entity test revision', $data['entity_test_mulrev_revision']['table']['group']);
+
+    // Ensure the join information is set up properly.
+    // Tests the join definition between the base and the revision table.
+    $revision_data = $data['entity_test_mulrev_revision'];
+    $this->assertCount(1, $revision_data['table']['join']);
+    $this->assertEquals(['entity_test' => ['left_field' => 'revision_id', 'field' => 'revision_id', 'type' => 'INNER']], $revision_data['table']['join']);
+    $revision_data = $data['entity_test_mulrev_property_revision'];
+    $this->assertCount(1, $revision_data['table']['join']);
+    $this->assertEquals(['entity_test_mulrev_revision' => ['left_field' => 'revision_id', 'field' => 'revision_id', 'type' => 'INNER']], $revision_data['table']['join']);
+    $this->assertFalse(isset($data['data_table']));
+  }
+
+  /**
+   * Helper method to mock all store definitions.
+   */
+  protected function setupFieldStorageDefinition() {
+    $id_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $id_field_storage_definition->expects($this->any())
+      ->method('getSchema')
+      ->willReturn(IntegerItem::schema($id_field_storage_definition));
+    $uuid_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $uuid_field_storage_definition->expects($this->any())
+      ->method('getSchema')
+      ->willReturn(UuidItem::schema($uuid_field_storage_definition));
+    $type_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $type_field_storage_definition->expects($this->any())
+      ->method('getSchema')
+      ->willReturn(StringItem::schema($type_field_storage_definition));
+    $langcode_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $langcode_field_storage_definition->expects($this->any())
+      ->method('getSchema')
+      ->willReturn(LanguageItem::schema($langcode_field_storage_definition));
+    $name_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $name_field_storage_definition->expects($this->any())
+      ->method('getSchema')
+      ->willReturn(StringItem::schema($name_field_storage_definition));
+
+    // Setup the user_id entity reference field.
+    $this->entityManager->expects($this->any())
+      ->method('getDefinition')
+      ->willReturnMap([
+          ['user', TRUE, static::userEntityInfo()],
+        ]
+      );
+    $user_id_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $user_id_field_storage_definition->expects($this->any())
+      ->method('getSetting')
+      ->with('target_type')
+      ->willReturn('user');
+    $user_id_field_storage_definition->expects($this->any())
+      ->method('getSchema')
+      ->willReturn(EntityReferenceItem::schema($user_id_field_storage_definition));
+
+    $revision_id_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $revision_id_field_storage_definition->expects($this->any())
+      ->method('getSchema')
+      ->willReturn(IntegerItem::schema($revision_id_field_storage_definition));
+
+    $this->entityManager->expects($this->any())
+      ->method('getFieldStorageDefinitions')
+      ->willReturn([
+        'id' => $id_field_storage_definition,
+        'uuid' => $uuid_field_storage_definition,
+        'type' => $type_field_storage_definition,
+        'langcode' => $langcode_field_storage_definition,
+        'name' => $name_field_storage_definition,
+        'user_id' => $user_id_field_storage_definition,
+        'revision_id' => $revision_id_field_storage_definition,
+      ]);
+  }
+
+  /**
+   * Tests fields on the base table.
+   */
+  public function testBaseTableFields() {
+    $base_field_definitions = EntityTest::baseFieldDefinitions($this->baseEntityType);
+    $this->entityManager->expects($this->once())
+      ->method('getBaseFieldDefinitions')
+      ->with('entity_test')
+      ->willReturn($base_field_definitions);
+
+    // Setup the table mapping.
+    $table_mapping = $this->getMock('Drupal\Core\Entity\Sql\TableMappingInterface');
+    $table_mapping->expects($this->any())
+      ->method('getTableNames')
+      ->willReturn(['entity_test']);
+    $table_mapping->expects($this->any())
+      ->method('getColumnNames')
+      ->willReturnMap([
+        ['id', ['value' => 'id']],
+        ['uuid', ['value' => 'uuid']],
+        ['type', ['value' => 'type']],
+        ['langcode', ['value' => 'langcode']],
+        ['name', ['value' => 'name']],
+        ['user_id', ['target_id' => 'user_id']],
+      ]);
+    $table_mapping->expects($this->any())
+      ->method('getFieldNames')
+      ->willReturnMap([
+        ['entity_test', ['id', 'uuid', 'type', 'langcode', 'name', 'user_id']]
+      ]);
+
+    $this->entityStorage->expects($this->once())
+      ->method('getTableMapping')
+      ->willReturn($table_mapping);
+
+    $this->setupFieldStorageDefinition();
+
+    $this->viewsData->setSchemaFields(['entity_test' => ['id', 'uuid', 'type', 'langcode', 'name', 'user_id']]);
+    $data = $this->viewsData->getViewsData();
+
+    $this->assertNumericField($data['entity_test']['id']);
+    $this->assertUuidField($data['entity_test']['uuid']);
+    $this->assertStringField($data['entity_test']['type']);
+
+    $this->assertLanguageField($data['entity_test']['langcode']);
+    $this->assertEquals('Translation language', $data['entity_test']['langcode']['title']);
+
+    $this->assertStringField($data['entity_test']['name']);
+
+    $this->assertEntityReferenceField($data['entity_test']['user_id']);
+    $relationship = $data['entity_test']['user_id']['relationship'];
+    $this->assertEquals('users', $relationship['base']);
+    $this->assertEquals('uid', $relationship['base field']);
+  }
+
+  /**
+   * Tests fields on the data table.
+   */
+  public function testDataTableFields() {
+    $entity_type = $this->baseEntityType
+      ->set('data_table', 'entity_test_mul_property_data')
+      ->set('base_table', 'entity_test_mul')
+      ->set('id', 'entity_test_mul')
+      ->setKey('bundle', 'type')
+    ;
+    $base_field_definitions = EntityTestMul::baseFieldDefinitions($this->baseEntityType);
+    $base_field_definitions['type'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel('entity test type')
+      ->setSettings(array('target_type' => 'entity_test_bundle'))
+      ->setTranslatable(TRUE);
+    $entity_test_type = new ConfigEntityType(['id' => 'entity_test_bundle']);
+    $user_entity_type = new ContentEntityType(['id' => 'user', 'base_table' => 'users', 'entity_keys' => ['id' => 'uid']]);
+    $this->entityManager->expects($this->any())
+      ->method('getDefinition')
+      ->willReturnMap([
+        ['entity_test_bundle', TRUE, $entity_test_type],
+        ['user', TRUE, $user_entity_type],
+      ]);
+
+    $this->entityManager->expects($this->once())
+      ->method('getBaseFieldDefinitions')
+      ->with('entity_test_mul')
+      ->willReturn($base_field_definitions);
+
+    $this->viewsData->setSchemaFields([
+      'entity_test_mul' => ['id', 'uuid', 'type', 'langcode'],
+      'entity_test_mul_property_data' => ['id', 'langcode', 'name', 'user_id'],
+    ]);
+
+    ;
+    $this->viewsData->setEntityType($entity_type);
+
+     // Setup the table mapping.
+    $table_mapping = $this->getMock('Drupal\Core\Entity\Sql\TableMappingInterface');
+    $table_mapping->expects($this->any())
+      ->method('getTableNames')
+      ->willReturn(['entity_test_mul', 'entity_test_mul_property_data']);
+    $table_mapping->expects($this->any())
+      ->method('getColumnNames')
+      ->willReturnMap([
+        ['id', ['value' => 'id']],
+        ['uuid', ['value' => 'uuid']],
+        ['type', ['value' => 'type']],
+        ['langcode', ['value' => 'langcode']],
+        ['name', ['value' => 'name']],
+        ['user_id', ['target_id' => 'user_id']],
+      ]);
+    $table_mapping->expects($this->any())
+      ->method('getFieldNames')
+      ->willReturnMap([
+        ['entity_test_mul', ['id', 'uuid', 'type', 'langcode']],
+        ['entity_test_mul_property_data', ['id', 'langcode', 'name', 'user_id']],
+      ]);
+
+    $this->entityStorage->expects($this->once())
+      ->method('getTableMapping')
+      ->willReturn($table_mapping);
+
+    $this->setupFieldStorageDefinition();
+
+    $data = $this->viewsData->getViewsData();
+
+    // Check the base fields.
+    $this->assertNumericField($data['entity_test_mul']['id']);
+    $this->assertUuidField($data['entity_test_mul']['uuid']);
+
+    $this->assertBundleField($data['entity_test_mul']['type']);
+    $this->assertFalse(isset($data['entity_test_mul']['type']['relationship']));
+
+    $this->assertLanguageField($data['entity_test_mul']['langcode']);
+    // Also ensure that field_data only fields don't appear on the base table.
+    $this->assertFalse(isset($data['entity_test_mul']['name']));
+    $this->assertFalse(isset($data['entity_test_mul']['user_id']));
+
+    // Check the data fields.
+    $this->assertNumericField($data['entity_test_mul_property_data']['id']);
+
+    $this->assertLanguageField($data['entity_test_mul_property_data']['langcode']);
+    $this->assertEquals('Translation language', $data['entity_test_mul_property_data']['langcode']['title']);
+
+    $this->assertStringField($data['entity_test_mul_property_data']['name']);
+
+    $this->assertEntityReferenceField($data['entity_test_mul_property_data']['user_id']);
+    $relationship = $data['entity_test_mul_property_data']['user_id']['relationship'];
+    $this->assertEquals('users', $relationship['base']);
+    $this->assertEquals('uid', $relationship['base field']);
+  }
+
+  /**
+   * Tests fields on the revision table.
+   */
+  public function testRevisionTableFields() {
+    $entity_type = $this->baseEntityType
+      ->set('base_table', 'entity_test_mulrev')
+      ->set('revision_table', 'entity_test_mulrev_revision')
+      ->set('data_table', 'entity_test_mulrev_property_data')
+      ->set('revision_data_table', 'entity_test_mulrev_property_revision')
+      ->set('id', 'entity_test_mulrev');
+    $base_field_definitions = EntityTestMulRev::baseFieldDefinitions($this->baseEntityType);
+    $this->entityManager->expects($this->once())
+      ->method('getBaseFieldDefinitions')
+      ->with('entity_test_mulrev')
+      ->willReturn($base_field_definitions);
+
+    $this->viewsData->setSchemaFields([
+      'entity_test_mulrev' => ['id', 'revision_id', 'uuid', 'type'],
+      'entity_test_mulrev_revision' => ['id', 'revision_id', 'langcode'],
+      'entity_test_mulrev_property_data' => ['id', 'revision_id', 'langcode', 'name', 'user_id'],
+      'entity_test_mulrev_property_revision' => ['id', 'revision_id', 'langcode', 'name', 'user_id'],
+    ]);
+
+    ;
+    $this->viewsData->setEntityType($entity_type);
+
+     // Setup the table mapping.
+    $table_mapping = $this->getMock('Drupal\Core\Entity\Sql\TableMappingInterface');
+    $table_mapping->expects($this->any())
+      ->method('getTableNames')
+      ->willReturn(['entity_test_mulrev', 'entity_test_mulrev_revision', 'entity_test_mulrev_property_data', 'entity_test_mulrev_property_revision']);
+    $table_mapping->expects($this->any())
+      ->method('getColumnNames')
+      ->willReturnMap([
+        ['id', ['value' => 'id']],
+        ['uuid', ['value' => 'uuid']],
+        ['type', ['value' => 'type']],
+        ['langcode', ['value' => 'langcode']],
+        ['name', ['value' => 'name']],
+        ['user_id', ['target_id' => 'user_id']],
+        ['revision_id', ['value' => 'id']],
+      ]);
+    $table_mapping->expects($this->any())
+      ->method('getFieldNames')
+      ->willReturnMap([
+        ['entity_test_mulrev', ['id', 'revision_id', 'uuid', 'type']],
+        ['entity_test_mulrev_revision', ['id', 'revision_id', 'langcode']],
+        ['entity_test_mulrev_property_data', ['id', 'revision_id', 'langcode', 'name', 'user_id']],
+        ['entity_test_mulrev_property_revision', ['id', 'revision_id', 'langcode', 'name', 'user_id']],
+      ]);
+
+    $this->entityStorage->expects($this->once())
+      ->method('getTableMapping')
+      ->willReturn($table_mapping);
+
+    $this->setupFieldStorageDefinition();
+
+    $data = $this->viewsData->getViewsData();
+
+    // Check the base fields.
+    $this->assertNumericField($data['entity_test_mulrev']['id']);
+    $this->assertNumericField($data['entity_test_mulrev']['revision_id']);
+    $this->assertUuidField($data['entity_test_mulrev']['uuid']);
+    $this->assertStringField($data['entity_test_mulrev']['type']);
+
+    // Also ensure that field_data only fields don't appear on the base table.
+    $this->assertFalse(isset($data['entity_test_mulrev']['name']));
+    $this->assertFalse(isset($data['entity_test_mulrev']['langcode']));
+    $this->assertFalse(isset($data['entity_test_mulrev']['user_id']));
+
+    // Check the revision fields.
+    $this->assertNumericField($data['entity_test_mulrev_revision']['id']);
+    $this->assertNumericField($data['entity_test_mulrev_revision']['revision_id']);
+
+    $this->assertLanguageField($data['entity_test_mulrev_revision']['langcode']);
+    $this->assertEquals('Original language', $data['entity_test_mulrev_revision']['langcode']['title']);
+
+    // Also ensure that field_data only fields don't appear on the revision table.
+    $this->assertFalse(isset($data['entity_test_mulrev_revision']['name']));
+    $this->assertFalse(isset($data['entity_test_mulrev_revision']['user_id']));
+
+    // Check the data fields.
+    $this->assertNumericField($data['entity_test_mulrev_property_data']['id']);
+    $this->assertLanguageField($data['entity_test_mulrev_property_data']['langcode']);
+    $this->assertStringField($data['entity_test_mulrev_property_data']['name']);
+
+    $this->assertEntityReferenceField($data['entity_test_mulrev_property_data']['user_id']);
+    $relationship = $data['entity_test_mulrev_property_data']['user_id']['relationship'];
+    $this->assertEquals('users', $relationship['base']);
+    $this->assertEquals('uid', $relationship['base field']);
+
+    // Check the property data fields.
+    $this->assertNumericField($data['entity_test_mulrev_property_revision']['id']);
+
+    $this->assertLanguageField($data['entity_test_mulrev_property_revision']['langcode']);
+    $this->assertEquals('Original language', $data['entity_test_mulrev_property_revision']['langcode']['title']);
+
+    $this->assertStringField($data['entity_test_mulrev_property_revision']['name']);
+
+    $this->assertEntityReferenceField($data['entity_test_mulrev_property_revision']['user_id']);
+    $relationship = $data['entity_test_mulrev_property_revision']['user_id']['relationship'];
+    $this->assertEquals('users', $relationship['base']);
+    $this->assertEquals('uid', $relationship['base field']);
+  }
+
+  /**
+   * Tests views data for a string field.
+   *
+   * @param $data
+   *   The views data to check.
+   */
+  protected function assertStringField($data) {
+    $this->assertEquals('standard', $data['field']['id']);
+    $this->assertEquals('string', $data['filter']['id']);
+    $this->assertEquals('string', $data['argument']['id']);
+    $this->assertEquals('standard', $data['sort']['id']);
+  }
+
+  /**
+   * Tests views data for a UUID field.
+   *
+   * @param array $data
+   *   The views data to check.
+   */
+  protected function assertUuidField($data) {
+    // @todo Can we provide additional support for UUIDs in views?
+    $this->assertEquals('standard', $data['field']['id']);
+    $this->assertEquals('string', $data['filter']['id']);
+    $this->assertEquals('string', $data['argument']['id']);
+    $this->assertEquals('standard', $data['sort']['id']);
+  }
+
+  /**
+   * Tests views data for a numeric field.
+   *
+   * @param array $data
+   *   The views data to check.
+   */
+  protected function assertNumericField($data) {
+    $this->assertEquals('numeric', $data['field']['id']);
+    $this->assertEquals('numeric', $data['filter']['id']);
+    $this->assertEquals('numeric', $data['argument']['id']);
+    $this->assertEquals('standard', $data['sort']['id']);
+  }
+
+  /**
+   * Tests views data for a language field.
+   *
+   * @param array $data
+   *   The views data to check.
+   */
+  protected function assertLanguageField($data) {
+    $this->assertEquals('language', $data['field']['id']);
+    $this->assertEquals('language', $data['filter']['id']);
+    $this->assertEquals('language', $data['argument']['id']);
+    $this->assertEquals('standard', $data['sort']['id']);
+  }
+
+  /**
+   * Tests views data for a entity reference field.
+   */
+  protected function assertEntityReferenceField($data) {
+    $this->assertEquals('numeric', $data['field']['id']);
+    $this->assertEquals('numeric', $data['filter']['id']);
+    $this->assertEquals('numeric', $data['argument']['id']);
+    $this->assertEquals('standard', $data['sort']['id']);
+  }
+
+  /**
+   * Tests views data for a bundle field.
+   */
+  protected function assertBundleField($data) {
+    $this->assertEquals('standard', $data['field']['id']);
+    $this->assertEquals('bundle', $data['filter']['id']);
+    $this->assertEquals('string', $data['argument']['id']);
+    $this->assertEquals('standard', $data['sort']['id']);
+  }
+
+  /**
+   * Returns entity info for the user entity.
+   *
+   * @return array
+   */
+  protected static function userEntityInfo() {
+    return new ContentEntityType([
+      'id' => 'user',
+      'class' => 'Drupal\user\Entity\User',
+      'label' => 'User',
+      'base_table' => 'users',
+      'entity_keys' => [
+        'id' => 'uid',
+        'uuid' => 'uuid',
+      ],
+    ]);
+  }
+
+}
+
+class TestEntityViewsData extends EntityViewsData {
+
+  protected $schemaFields = [];
+
+  public function setSchemaFields($fields) {
+    $this->schemaFields = $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalSchemaFieldsSql($table) {
+    return isset($this->schemaFields[$table]) ? $this->schemaFields[$table] : [];
+  }
+
+  public function setEntityType(EntityTypeInterface $entity_type) {
+    $this->entityType = $entity_type;
+  }
+
+}
+
+}
+
+namespace {
+  use Drupal\Component\Utility\String;
+
+  if (!function_exists('t')) {
+    function t($string, array $args = []) {
+      return String::format($string, $args);
+    }
+  }
+}
diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php
index c7e5133..5534df6 100644
--- a/core/modules/views/views.api.php
+++ b/core/modules/views/views.api.php
@@ -26,9 +26,10 @@
  *   by implementing hook_views_data_alter(). To provide views data for an
  *   entity, create a class implementing
  *   \Drupal\views\EntityViewsDataInterface and reference this in the
- *   "views_data" annotation in the entity class. See the
- *   @link entity_api Entity API topic @endlink for more information about
- *   entities.
+ *   "views_data" annotation in the entity class. You can autogenerate big parts
+ *   of the ingration if you extend the \Drupal\views\EntityViewsData base
+ *   class. See the @link entity_api Entity API topic @endlink for more
+ *   information about entities.
  * - Implement hooks: A few operations in Views can be influenced by hooks.
  *   See the @link Views hooks topic @endlink for a list.
  * - Theming: See the @link views_templates Views templates topic @endlink
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php
index 4ff2f23..7109a62 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php
@@ -76,6 +76,19 @@ public function providerTestGetKeys() {
   }
 
   /**
+   * Tests the setKey() method.
+   *
+   * @covers ::setKey
+   */
+  public function testSetKey() {
+    $entity_type = $this->setUpEntityType([]);
+
+    $this->assertFalse($entity_type->getKey('id'));
+    $this->assertSame($entity_type, $entity_type->setKey('id', 'new_value'));
+    $this->assertEquals('new_value', $entity_type->getKey('id'));
+  }
+
+  /**
    * Tests the isRevisionable() method.
    */
   public function testIsRevisionable() {
