diff --git a/core/core.services.yml b/core/core.services.yml
index 36376e2..a2b94d6 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -801,7 +801,10 @@ services:
       - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
   token:
     class: Drupal\Core\Utility\Token
-    arguments: ['@module_handler', '@cache.discovery', '@language_manager']
+    arguments: ['@module_handler', '@cache.discovery', '@language_manager', '@plugin.manager.token_type']
+  plugin.manager.token_type:
+    class: Drupal\Core\Token\TokenTypeManager
+    parent: default_plugin_manager
   batch.storage:
     class: Drupal\Core\Batch\BatchStorage
     arguments: ['@database']
diff --git a/core/lib/Drupal/Core/Annotation/TokenType.php b/core/lib/Drupal/Core/Annotation/TokenType.php
new file mode 100644
index 0000000..88d14ec
--- /dev/null
+++ b/core/lib/Drupal/Core/Annotation/TokenType.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\token\Annotation\Token
+ */
+
+namespace Drupal\Core\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines an Token annotation object.
+ *
+ * Plugin Namespace: Plugin\Token
+ *
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class TokenType extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the token plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+}
diff --git a/core/lib/Drupal/Core/Token/TokenTypeBag.php b/core/lib/Drupal/Core/Token/TokenTypeBag.php
new file mode 100644
index 0000000..e78dab8
--- /dev/null
+++ b/core/lib/Drupal/Core/Token/TokenTypeBag.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Token\TokenTypeBag
+ */
+
+namespace Drupal\Core\Token;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Plugin\DefaultPluginBag;
+
+/**
+ * A plugin bag for token types.
+ */
+class TokenTypeBag extends DefaultPluginBag {
+
+  /**
+   * All possible token type plugin IDs.
+   *
+   * @var array
+   */
+  protected $definitions;
+
+  /**
+   * Gets token type definitions and creates an instance for each token type.
+   *
+   * @return array
+   *   An array of instantiated token type plugins.
+   */
+  public function getAll() {
+    // Retrieve all available token type plugin definitions.
+    if (!$this->definitions) {
+      $this->definitions = $this->manager->getDefinitions();
+    }
+
+    foreach ($this->definitions as $plugin_id => $definition) {
+      if (!isset($this->pluginInstances[$plugin_id])) {
+        $this->initializePlugin($plugin_id);
+      }
+    }
+    return $this->pluginInstances;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializePlugin($instance_id) {
+    $configuration = $this->manager->getDefinition($instance_id);
+    // Merge the actual configuration into the default configuration.
+    if (isset($this->configurations[$instance_id])) {
+      $configuration = NestedArray::mergeDeep($configuration, $this->configurations[$instance_id]);
+    }
+    $this->configurations[$instance_id] = $configuration;
+    parent::initializePlugin($instance_id);
+  }
+}
diff --git a/core/lib/Drupal/Core/Token/TokenTypeInterface.php b/core/lib/Drupal/Core/Token/TokenTypeInterface.php
new file mode 100644
index 0000000..5e6f5a7
--- /dev/null
+++ b/core/lib/Drupal/Core/Token/TokenTypeInterface.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\token\TokenTypeInterface
+ */
+
+namespace Drupal\Core\Token;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Provides an interface for token type plugins.
+ */
+interface TokenTypeInterface extends PluginInspectionInterface {
+
+  /**
+   * Provide information about available placeholder tokens and token types.
+   *
+   * Tokens are placeholders that can be put into text by using the syntax
+   * [type:token], where type is the machine-readable name of a token type, and
+   * token is the machine-readable name of a token within this group. This hook
+   * provides a list of types and tokens to be displayed on text editing screens,
+   * so that people editing text can see what their token options are.
+   *
+   * The actual token replacement is done by
+   * \Drupal\Core\Utility\Token::replace(), which invokes hook_tokens(). Your
+   * module will need to implement that hook in order to generate token
+   * replacements from the tokens defined here.
+   *
+   * @return
+   *   An associative array of available tokens and token types. The outer array
+   *   has two components:
+   *   - types: An associative array of token types (groups). Each token type is
+   *     an associative array with the following components:
+   *     - name: The translated human-readable short name of the token type.
+   *     - description (optional): A translated longer description of the token
+   *       type.
+   *     - needs-data: The type of data that must be provided to
+   *       \Drupal\Core\Utility\Token::replace() in the $data argument (i.e., the
+   *       key name in $data) in order for tokens of this type to be used in the
+   *       $text being processed. For instance, if the token needs a node object,
+   *       'needs-data' should be 'node', and to use this token in
+   *       \Drupal\Core\Utility\Token::replace(), the caller needs to supply a
+   *       node object as $data['node']. Some token data can also be supplied
+   *       indirectly; for instance, a node object in $data supplies a user object
+   *       (the author of the node), allowing user tokens to be used when only
+   *       a node data object is supplied.
+   *   - tokens: An associative array of tokens. The outer array is keyed by the
+   *     group name (the same key as in the types array). Within each group of
+   *     tokens, each token item is keyed by the machine name of the token, and
+   *     each token item has the following components:
+   *     - name: The translated human-readable short name of the token.
+   *     - description (optional): A translated longer description of the token.
+   *     - type (optional): A 'needs-data' data type supplied by this token, which
+   *       should match a 'needs-data' value from another token type. For example,
+   *       the node author token provides a user object, which can then be used
+   *       for token replacement data in \Drupal\Core\Utility\Token::replace()
+   *       without having to supply a separate user object.
+   *
+   * @see hook_token_info_alter()
+   * @see hook_tokens()
+   */
+  public function getTokenInfo();
+
+  /**
+   * Provide replacement values for placeholder tokens.
+   *
+   * This hook is invoked when someone calls
+   * \Drupal\Core\Utility\Token::replace(). That function first scans the text for
+   * [type:token] patterns, and splits the needed tokens into groups by type.
+   * Then hook_tokens() is invoked on each token-type group, allowing your module
+   * to respond by providing replacement text for any of the tokens in the group
+   * that your module knows how to process.
+   *
+   * A module implementing this hook should also implement hook_token_info() in
+   * order to list its available tokens on editing screens.
+   *
+   * @param $type
+   *   The machine-readable name of the type (group) of token being replaced, such
+   *   as 'node', 'user', or another type defined by a hook_token_info()
+   *   implementation.
+   * @param $tokens
+   *   An array of tokens to be replaced. The keys are the machine-readable token
+   *   names, and the values are the raw [type:token] strings that appeared in the
+   *   original text.
+   * @param $data
+   *   (optional) An associative array of data objects to be used when generating
+   *   replacement values, as supplied in the $data parameter to
+   *   \Drupal\Core\Utility\Token::replace().
+   * @param $options
+   *   (optional) An associative array of options for token replacement; see
+   *   \Drupal\Core\Utility\Token::replace() for possible values.
+   *
+   * @return
+   *   An associative array of replacement values, keyed by the raw [type:token]
+   *   strings from the original text.
+   *
+   * @see hook_token_info()
+   * @see hook_tokens_alter()
+   */
+  public function getTokens($type, $tokens, array $data = array(), array $options = array());
+
+}
diff --git a/core/lib/Drupal/Core/Token/TokenTypeManager.php b/core/lib/Drupal/Core/Token/TokenTypeManager.php
new file mode 100644
index 0000000..03c0584
--- /dev/null
+++ b/core/lib/Drupal/Core/Token/TokenTypeManager.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\token\TokenTypeManager
+ */
+
+namespace Drupal\Core\Token;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Provides a manager for Token Type plugins.
+ */
+class TokenTypeManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new class instance.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/TokenType', $namespaces, $module_handler, 'Drupal\Core\Annotation\TokenType');
+    $this->alterInfo('token_info');
+    $this->setCacheBackend($cache_backend, 'token_info');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Utility/Token.php b/core/lib/Drupal/Core/Utility/Token.php
index d03d645..e81eed6 100644
--- a/core/lib/Drupal/Core/Utility/Token.php
+++ b/core/lib/Drupal/Core/Utility/Token.php
@@ -11,6 +11,9 @@
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Token\TokenTypeBag;
+use Drupal\Core\Token\TokenTypeManager;
+use Drupal\Core\Token\TokenTypeManagerInterface;
 
 /**
  * Drupal placeholder/token replacement system.
@@ -89,6 +92,14 @@ class Token {
   protected $tokenInfo;
 
   /**
+   * The token types.
+   *
+   * @var array
+   *   The array of token type plugin instances.
+   */
+  protected $tokenTypes;
+
+  /**
    * The module handler service.
    *
    * @var \Drupal\Core\Extension\ModuleHandlerInterface
@@ -96,6 +107,13 @@ class Token {
   protected $moduleHandler;
 
   /**
+   * The token type manager service.
+   *
+   * @var \Drupal\Core\Token\TokenTypeManager
+   */
+  protected $tokenTypeManager;
+
+  /**
    * Constructs a new class instance.
    *
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
@@ -104,11 +122,14 @@ class Token {
    *   The token cache.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
+   * @param \Drupal\Core\Token\TokenTypeManager $token_type_manager
+   *   The token type manager service.
    */
-  public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
+  public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TokenTypeManager $token_type_manager) {
     $this->cache = $cache;
     $this->languageManager = $language_manager;
     $this->moduleHandler = $module_handler;
+    $this->tokenTypeManager = $token_type_manager;
   }
 
   /**
@@ -244,7 +265,13 @@ public function scan($text) {
    */
   public function generate($type, array $tokens, array $data = array(), array $options = array()) {
     $options += array('sanitize' => TRUE);
-    $replacements = $this->moduleHandler->invokeAll('tokens', array($type, $tokens, $data, $options));
+    $replacements = array();
+    $token_types = $this->getTokenTypes();
+    /** @var \Drupal\Core\Token\TokenTypeInterface $token_type */
+    foreach ($token_types as $token_type) {
+      $replacements += $token_type->getTokens($type, $tokens, $data, $options);
+    }
+    $replacements += $this->moduleHandler->invokeAll('tokens', array($type, $tokens, $data, $options));
 
     // Allow other modules to alter the replacements.
     $context = array(
@@ -317,7 +344,12 @@ public function getInfo() {
         $this->tokenInfo = $cache->data;
       }
       else {
-        $this->tokenInfo = $this->moduleHandler->invokeAll('token_info');
+        $this->tokenInfo = array();
+        /** @var \Drupal\Core\Token\TokenTypeInterface $token_type */
+        foreach ($this->getTokenTypes() as $token_type) {
+          $this->tokenInfo += $token_type->getTokenInfo();
+        }
+        $this->tokenInfo += $this->moduleHandler->invokeAll('token_info');
         $this->moduleHandler->alter('token_info', $this->tokenInfo);
         $this->cache->set($cache_id, $this->tokenInfo, CacheBackendInterface::CACHE_PERMANENT, array(
           static::TOKEN_INFO_CACHE_TAG => TRUE,
@@ -350,4 +382,18 @@ public function resetInfo() {
       static::TOKEN_INFO_CACHE_TAG => TRUE,
     ));
   }
+
+  /**
+   * Gets the token types.
+   *
+   * @return array
+   *   The initialized token type plugins.
+   */
+  public function getTokenTypes() {
+    if (!isset($this->tokenTypes)) {
+      $token_type_bag = new TokenTypeBag($this->tokenTypeManager);
+      $this->tokenTypes = $token_type_bag->getAll();
+    }
+    return $this->tokenTypes;
+  }
 }
diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
deleted file mode 100644
index 4e0f300..0000000
--- a/core/modules/node/node.tokens.inc
+++ /dev/null
@@ -1,205 +0,0 @@
-<?php
-
-/**
- * @file
- * Builds placeholder replacement tokens for node-related data.
- */
-
-use Drupal\Component\Utility\String;
-use Drupal\Core\Language\LanguageInterface;
-
-/**
- * Implements hook_token_info().
- */
-function node_token_info() {
-  $type = array(
-    'name' => t('Nodes'),
-    'description' => t('Tokens related to individual content items, or "nodes".'),
-    'needs-data' => 'node',
-  );
-
-  // Core tokens for nodes.
-  $node['nid'] = array(
-    'name' => t("Content ID"),
-    'description' => t('The unique ID of the content item, or "node".'),
-  );
-  $node['vid'] = array(
-    'name' => t("Revision ID"),
-    'description' => t("The unique ID of the node's latest revision."),
-  );
-  $node['type'] = array(
-    'name' => t("Content type"),
-  );
-  $node['type-name'] = array(
-    'name' => t("Content type name"),
-    'description' => t("The human-readable name of the node type."),
-  );
-  $node['title'] = array(
-    'name' => t("Title"),
-  );
-  $node['body'] = array(
-    'name' => t("Body"),
-    'description' => t("The main body text of the node."),
-  );
-  $node['summary'] = array(
-    'name' => t("Summary"),
-    'description' => t("The summary of the node's main body text."),
-  );
-  $node['langcode'] = array(
-    'name' => t('Language code'),
-    'description' => t('The language code of the language the node is written in.'),
-  );
-  $node['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the node."),
-  );
-  $node['edit-url'] = array(
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the node's edit page."),
-  );
-
-  // Chained tokens for nodes.
-  $node['created'] = array(
-    'name' => t("Date created"),
-    'type' => 'date',
-  );
-  $node['changed'] = array(
-    'name' => t("Date changed"),
-    'description' => t("The date the node was most recently updated."),
-    'type' => 'date',
-  );
-  $node['author'] = array(
-    'name' => t("Author"),
-    'type' => 'user',
-  );
-
-  return array(
-    'types' => array('node' => $type),
-    'tokens' => array('node' => $node),
-  );
-}
-
-/**
- * Implements hook_tokens().
- */
-function node_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $token_service = \Drupal::token();
-
-  $url_options = array('absolute' => TRUE);
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = LanguageInterface::LANGCODE_DEFAULT;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'node' && !empty($data['node'])) {
-    /** @var \Drupal\node\NodeInterface $node */
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Simple key values on the node.
-        case 'nid':
-          $replacements[$original] = $node->id();
-          break;
-
-        case 'vid':
-          $replacements[$original] = $node->getRevisionId();
-          break;
-
-        case 'type':
-          $replacements[$original] = $sanitize ? String::checkPlain($node->getType()) : $node->getType();
-          break;
-
-        case 'type-name':
-          $type_name = node_get_type_label($node);
-          $replacements[$original] = $sanitize ? String::checkPlain($type_name) : $type_name;
-          break;
-
-        case 'title':
-          $replacements[$original] = $sanitize ? String::checkPlain($node->getTitle()) : $node->getTitle();
-          break;
-
-        case 'body':
-        case 'summary':
-          $translation = \Drupal::entityManager()->getTranslationFromContext($node, $langcode, array('operation' => 'node_tokens'));
-          if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) {
-            $item = $items[0];
-            $field_definition = \Drupal::entityManager()->getFieldDefinitions('node', $node->bundle())['body'];
-            // If the summary was requested and is not empty, use it.
-            if ($name == 'summary' && !empty($item->summary)) {
-              $output = $sanitize ? $item->summary_processed : $item->summary;
-            }
-            // Attempt to provide a suitable version of the 'body' field.
-            else {
-              $output = $sanitize ? $item->processed : $item->value;
-              // A summary was requested.
-              if ($name == 'summary') {
-                // Generate an optionally trimmed summary of the body field.
-
-                // Get the 'trim_length' size used for the 'teaser' mode, if
-                // present, or use the default trim_length size.
-                $display_options = entity_get_display('node', $node->getType(), 'teaser')->getComponent('body');
-                if (isset($display_options['settings']['trim_length'])) {
-                  $length = $display_options['settings']['trim_length'];
-                }
-                else {
-                  $settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_summary_or_trimmed');
-                  $length = $settings['trim_length'];
-                }
-
-                $output = text_summary($output, $field_definition->getSetting('text_processing') ? $item->format : NULL, $length);
-              }
-            }
-            $replacements[$original] = $output;
-          }
-          break;
-
-        case 'langcode':
-          $replacements[$original] = $sanitize ? String::checkPlain($node->language()->id) : $node->language()->id;
-          break;
-
-        case 'url':
-          $replacements[$original] = url('node/' . $node->id(), $url_options);
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = url('node/' . $node->id() . '/edit', $url_options);
-          break;
-
-        // Default values for the chained tokens handled below.
-        case 'author':
-          $account = $node->getOwner() ? $node->getOwner() : user_load(0);
-          $replacements[$original] = $sanitize ? String::checkPlain($account->label()) : $account->label();
-          break;
-
-        case 'created':
-          $replacements[$original] = format_date($node->getCreatedTime(), 'medium', '', NULL, $langcode);
-          break;
-
-        case 'changed':
-          $replacements[$original] = format_date($node->getChangedTime(), 'medium', '', NULL, $langcode);
-          break;
-      }
-    }
-
-    if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
-      $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options);
-    }
-
-    if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
-      $replacements += $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options);
-    }
-
-    if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
-      $replacements += $token_service->generate('date', $changed_tokens, array('date' => $node->getChangedTime()), $options);
-    }
-  }
-
-  return $replacements;
-}
diff --git a/core/modules/node/src/Plugin/TokenType/Node.php b/core/modules/node/src/Plugin/TokenType/Node.php
new file mode 100644
index 0000000..7810c91
--- /dev/null
+++ b/core/modules/node/src/Plugin/TokenType/Node.php
@@ -0,0 +1,221 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\token\Plugin\Token\Node
+ */
+
+namespace Drupal\node\Plugin\TokenType;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Component\Utility\String;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Token\TokenTypeInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @TokenType(
+ *   id = "node_token_type",
+ *   label = @Translation("Node"),
+ * )
+ */
+class Node extends PluginBase implements TokenTypeInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTokenInfo() {
+    $type = array(
+      'name' => t('Nodes'),
+      'description' => t('Tokens related to individual content items, or "nodes".'),
+      'needs-data' => 'node',
+    );
+
+    // Core tokens for nodes.
+    $node['nid'] = array(
+      'name' => t("Content ID"),
+      'description' => t('The unique ID of the content item, or "node".'),
+    );
+    $node['vid'] = array(
+      'name' => t("Revision ID"),
+      'description' => t("The unique ID of the node's latest revision."),
+    );
+    $node['type'] = array(
+      'name' => t("Content type"),
+    );
+    $node['type-name'] = array(
+      'name' => t("Content type name"),
+      'description' => t("The human-readable name of the node type."),
+    );
+    $node['title'] = array(
+      'name' => t("Title"),
+    );
+    $node['body'] = array(
+      'name' => t("Body"),
+      'description' => t("The main body text of the node."),
+    );
+    $node['summary'] = array(
+      'name' => t("Summary"),
+      'description' => t("The summary of the node's main body text."),
+    );
+    $node['langcode'] = array(
+      'name' => t('Language code'),
+      'description' => t('The language code of the language the node is written in.'),
+    );
+    $node['url'] = array(
+      'name' => t("URL"),
+      'description' => t("The URL of the node."),
+    );
+    $node['edit-url'] = array(
+      'name' => t("Edit URL"),
+      'description' => t("The URL of the node's edit page."),
+    );
+
+    // Chained tokens for nodes.
+    $node['created'] = array(
+      'name' => t("Date created"),
+      'type' => 'date',
+    );
+    $node['changed'] = array(
+      'name' => t("Date changed"),
+      'description' => t("The date the node was most recently updated."),
+      'type' => 'date',
+    );
+    $node['author'] = array(
+      'name' => t("Author"),
+      'type' => 'user',
+    );
+
+    return array(
+      'types' => array('node' => $type),
+      'tokens' => array('node' => $node),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTokens($type, $tokens, array $data = array(), array $options = array()) {
+    $token_service = \Drupal::token();
+
+    $url_options = array('absolute' => TRUE);
+    if (isset($options['langcode'])) {
+      $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+      $langcode = $options['langcode'];
+    }
+    else {
+      $langcode = LanguageInterface::LANGCODE_DEFAULT;
+    }
+    $sanitize = !empty($options['sanitize']);
+
+    $replacements = array();
+
+    if ($type == 'node' && !empty($data['node'])) {
+      /** @var \Drupal\node\NodeInterface $node */
+      $node = $data['node'];
+
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          // Simple key values on the node.
+          case 'nid':
+            $replacements[$original] = $node->id();
+            break;
+
+          case 'vid':
+            $replacements[$original] = $node->getRevisionId();
+            break;
+
+          case 'type':
+            $replacements[$original] = $sanitize ? String::checkPlain($node->getType()) : $node->getType();
+            break;
+
+          case 'type-name':
+            $type_name = node_get_type_label($node);
+            $replacements[$original] = $sanitize ? String::checkPlain($type_name) : $type_name;
+            break;
+
+          case 'title':
+            $replacements[$original] = $sanitize ? String::checkPlain($node->getTitle()) : $node->getTitle();
+            break;
+
+          case 'body':
+          case 'summary':
+            $translation = \Drupal::entityManager()->getTranslationFromContext($node, $langcode, array('operation' => 'node_tokens'));
+            if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) {
+              $item = $items[0];
+              $field_definition = \Drupal::entityManager()->getFieldDefinitions('node', $node->bundle())['body'];
+              // If the summary was requested and is not empty, use it.
+              if ($name == 'summary' && !empty($item->summary)) {
+                $output = $sanitize ? $item->summary_processed : $item->summary;
+              }
+              // Attempt to provide a suitable version of the 'body' field.
+              else {
+                $output = $sanitize ? $item->processed : $item->value;
+                // A summary was requested.
+                if ($name == 'summary') {
+                  // Generate an optionally trimmed summary of the body field.
+
+                  // Get the 'trim_length' size used for the 'teaser' mode, if
+                  // present, or use the default trim_length size.
+                  $display_options = entity_get_display('node', $node->getType(), 'teaser')->getComponent('body');
+                  if (isset($display_options['settings']['trim_length'])) {
+                    $length = $display_options['settings']['trim_length'];
+                  }
+                  else {
+                    $settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_summary_or_trimmed');
+                    $length = $settings['trim_length'];
+                  }
+
+                  $output = text_summary($output, $field_definition->getSetting('text_processing') ? $item->format : NULL, $length);
+                }
+              }
+              $replacements[$original] = $output;
+            }
+            break;
+
+          case 'langcode':
+            $replacements[$original] = $sanitize ? String::checkPlain($node->language()->id) : $node->language()->id;
+            break;
+
+          case 'url':
+            $replacements[$original] = url('node/' . $node->id(), $url_options);
+            break;
+
+          case 'edit-url':
+            $replacements[$original] = url('node/' . $node->id() . '/edit', $url_options);
+            break;
+
+          // Default values for the chained tokens handled below.
+          case 'author':
+            $account = $node->getOwner() ? $node->getOwner() : user_load(0);
+            $replacements[$original] = $sanitize ? String::checkPlain($account->label()) : $account->label();
+            break;
+
+          case 'created':
+            $replacements[$original] = format_date($node->getCreatedTime(), 'medium', '', NULL, $langcode);
+            break;
+
+          case 'changed':
+            $replacements[$original] = format_date($node->getChangedTime(), 'medium', '', NULL, $langcode);
+            break;
+        }
+      }
+
+      if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
+        $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options);
+      }
+
+      if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
+        $replacements += $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options);
+      }
+
+      if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
+        $replacements += $token_service->generate('date', $changed_tokens, array('date' => $node->getChangedTime()), $options);
+      }
+    }
+
+    return $replacements;
+  }
+
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
index 3736f0b..9873543 100644
--- a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
@@ -45,6 +45,14 @@ class TokenTest extends UnitTestCase {
    */
   protected $token;
 
+
+  /**
+   * The token type manager.
+   *
+   * @var \Drupal\Core\Token\TokenTypeManager
+   */
+  protected $tokenTypeManager;
+
   /**
    * {@inheritdoc}
    */
@@ -55,7 +63,11 @@ protected function setUp() {
 
     $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
 
-    $this->token = new Token($this->moduleHandler, $this->cache, $this->languageManager);
+    $this->tokenTypeManager = $this->getMockBuilder('\Drupal\Core\Token\TokenTypeManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->token = new Token($this->moduleHandler, $this->cache, $this->languageManager, $this->tokenTypeManager);
   }
 
   /**
@@ -94,6 +106,10 @@ public function testGetInfo() {
       ->method('alter')
       ->with('token_info', $token_info);
 
+    $this->tokenTypeManager->expects($this->once())
+      ->method('getDefinitions')
+      ->will($this->returnValue(array()));
+
     // Get the information for the first time. The cache should be checked, the
     // hooks invoked, and the info should be set to the cache should.
     $this->token->getInfo();
