diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
index 3294044..74d0b73 100644
--- a/core/modules/node/node.tokens.inc
+++ b/core/modules/node/node.tokens.inc
@@ -7,7 +7,6 @@
 
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\user\Entity\User;
 
@@ -86,6 +85,11 @@ function node_token_info() {
  * Implements hook_tokens().
  */
 function node_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+  if ($type != 'node' || empty($data['node'])) {
+    return [];
+  }
+
+  $replacements = array();
   $token_service = \Drupal::token();
 
   $url_options = array('absolute' => TRUE);
@@ -94,119 +98,110 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
     $langcode = $options['langcode'];
   }
   else {
-    $langcode = LanguageInterface::LANGCODE_DEFAULT;
+    $langcode = NULL;
   }
   $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 ? Html::escape($node->getType()) : $node->getType();
-          break;
-
-        case 'type-name':
-          $type_name = node_get_type_label($node);
-          $replacements[$original] = $sanitize ? Html::escape($type_name) : $type_name;
-          break;
-
-        case 'title':
-          $replacements[$original] = $sanitize ? Html::escape($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, $item->format, $length);
+  /** @var \Drupal\node\NodeInterface $node */
+  $node = \Drupal::entityManager()->getTranslationFromContext($data['node'], $langcode, ['operation' => 'node_tokens']);
+
+  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 ? Html::escape($node->getType()) : $node->getType();
+        break;
+
+      case 'type-name':
+        $type_name = node_get_type_label($node);
+        $replacements[$original] = $sanitize ? Html::escape($type_name) : $type_name;
+        break;
+
+      case 'title':
+        $replacements[$original] = $sanitize ? Html::escape($node->getTitle()) : $node->getTitle();
+        break;
+      case 'body':
+      case 'summary':
+        if ($node->hasField('body') && ($items = $node->get('body')) && !$items->isEmpty()) {
+          $item = $items[0];
+          // 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, $item->format, $length);
             }
-            $replacements[$original] = $output;
           }
-          break;
-
-        case 'langcode':
-          $replacements[$original] = $sanitize ? Html::escape($node->language()->getId()) : $node->language()->getId();
-          break;
-
-        case 'url':
-          $replacements[$original] = $node->url('canonical', $url_options);
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = $node->url('edit-form', $url_options);
-          break;
-
-        // Default values for the chained tokens handled below.
-        case 'author':
-          $account = $node->getOwner() ? $node->getOwner() : User::load(0);
-          $bubbleable_metadata->addCacheableDependency($account);
-          $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
-          break;
-
-        case 'created':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = format_date($node->getCreatedTime(), 'medium', '', NULL, $langcode);
-          break;
-
-        case 'changed':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = format_date($node->getChangedTime(), 'medium', '', NULL, $langcode);
-          break;
-      }
+          $replacements[$original] = $output;
+        }
+        break;
+
+      case 'langcode':
+        $replacements[$original] = $sanitize ? Html::escape($node->language()->getId()) : $node->language()->getId();
+        break;
+
+      case 'url':
+        $replacements[$original] = $node->url('canonical', $url_options);
+        break;
+
+      case 'edit-url':
+        $replacements[$original] = $node->url('edit-form', $url_options);
+        break;
+
+      // Default values for the chained tokens handled below.
+      case 'author':
+        $account = $node->getOwner() ? $node->getOwner() : User::load(0);
+        $bubbleable_metadata->addCacheableDependency($account);
+        $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
+        break;
+
+      case 'created':
+      case 'changed':
+        /** @var \Drupal\Core\Datetime\Entity\DateFormat $date_format */
+        $date_format = DateFormat::load('medium');
+        $date_raw = ($name === 'created' ? $node->getCreatedTime() : $node->getChangedTime());
+        $date_formatted = format_date($date_raw, $date_format->id(), '', NULL, $langcode);
+        $bubbleable_metadata->addCacheableDependency($date_format);
+        $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($date_formatted) : $date_formatted;
+        break;
     }
+  }
 
-    if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
-      $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options, $bubbleable_metadata);
-    }
+  if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
+    $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options, $bubbleable_metadata);
+  }
 
-    if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
-      $replacements += $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options, $bubbleable_metadata);
-    }
+  if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
+    $replacements += $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options, $bubbleable_metadata);
+  }
 
-    if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
-      $replacements += $token_service->generate('date', $changed_tokens, array('date' => $node->getChangedTime()), $options, $bubbleable_metadata);
-    }
+  if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
+    $replacements += $token_service->generate('date', $changed_tokens, array('date' => $node->getChangedTime()), $options, $bubbleable_metadata);
   }
 
   return $replacements;
diff --git a/core/modules/node/src/Tests/NodeTokenLanguageTest.php b/core/modules/node/src/Tests/NodeTokenLanguageTest.php
new file mode 100644
index 0000000..a7d6c5b
--- /dev/null
+++ b/core/modules/node/src/Tests/NodeTokenLanguageTest.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\NodeTokenLanguageTest.
+ */
+
+namespace Drupal\node\Tests;
+
+use Drupal\simpletest\WebTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\Entity\Node;
+use Drupal\user\Entity\User;
+
+/**
+ * Check if node tokens are multilingual.
+ *
+ * @group node
+ */
+class NodeTokenLanguageTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'language', 'content_translation'];
+
+  /**
+   * @var null
+   */
+  private $user = NULL;
+
+  protected function setUp() {
+    parent::setUp();
+
+    // Create Basic page node type.
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+
+    // Setup users.
+    $admin_user = $this->drupalCreateUser([
+      'administer languages',
+      'administer content types',
+      'access administration pages',
+      'create page content',
+      'edit own page content'
+    ]);
+    $this->drupalLogin($admin_user);
+
+    // Add a new language.
+    ConfigurableLanguage::createFromLangcode('it')->save();
+
+    // Enable URL language detection and selection.
+    $edit = ['language_interface[enabled][language-url]' => '1'];
+    $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
+
+    // Set "Basic page" content type to use multilingual support.
+    $edit = ['language_configuration[language_alterable]' => TRUE];
+    $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
+    $this->assertRaw(t('The content type %type has been updated.', ['%type' => 'Basic page']), 'Basic page content type has been updated.');
+
+    // Make node body translatable.
+    $field_storage = FieldStorageConfig::loadByName('node', 'body');
+    $field_storage->setTranslatable(TRUE);
+    $field_storage->save();
+  }
+
+  /**
+   * Test if node tokes are multilingual.
+   */
+  public function testMultilingualNodeTokens() {
+    // Setup users.
+    $this->user = $this->drupalCreateUser([
+      'create page content',
+      'edit own page content'
+    ]);
+    $this->drupalLogin($this->user);
+
+    // Create English "Basic page" node.
+    $langcode = 'en';
+    $fields = $this->getFieldsArray($langcode);
+
+    $node = Node::create($fields);
+    $node->save();
+
+    // Check that the node exists in the database.
+    $this->assertEqual($node->language()->getId(), $langcode, 'Node has langcode: ' . $langcode);
+    $this->assertEqual($node->title->value, $fields['title'], 'Title value matches.');
+    $this->assertEqual($node->body->value, $fields['body'], 'Body value matches.');
+    $this->assertEqual($node->getOwner()->getUsername(), User::load($fields['uid'])->getUsername(), 'Owner username matches ');
+
+    $this->assertEqual($node->created->value, $fields['created'], 'Created value matches');
+    $this->assertEqual($node->changed->value, $fields['changed'], 'Changed value matches');
+
+    // Perform unsanitized replacement for easy comparison.
+    $options = [
+      'langcode' => $langcode,
+      'sanitize' => FALSE,
+    ];
+    $token = '[node:title]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $node], $options);
+    $this->assertEqual($token_replacement, $node->title->value, $token . ' replaced successfully');
+
+    $token = '[node:body]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $node], $options);
+    $this->assertEqual($token_replacement, $node->body->value, $token . ' replaced successfully');
+
+    $token = '[node:author]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $node], $options);
+    $this->assertEqual($token_replacement, $node->getOwner()->getUsername(), $token . ' replaced successfully');
+
+    $token = '[node:created]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $node], $options);
+    $this->assertEqual($token_replacement, \Drupal::service('date.formatter')->format($node->created->value, 'medium', '', NULL, $langcode), $token . ' replaced successfully');
+
+    $token = '[node:changed]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $node], $options);
+    $this->assertEqual($token_replacement, \Drupal::service('date.formatter')->format($node->changed->value, 'medium', '', NULL, $langcode), $token . ' replaced successfully');
+
+
+    // Create italian translation with new field values.
+    $langcode = 'it';
+    $fields = $this->getFieldsArray($langcode);
+    $node->addTranslation($langcode, $fields);
+    $translation = $node->getTranslation($langcode);
+
+    // Check that the node translation exists in the database.
+    $this->assertEqual($translation->language()->getId(), $langcode, 'Node has langcode: ' . $langcode);
+    $this->assertEqual($translation->title->value, $fields['title'], 'Title value matches.');
+    $this->assertEqual($translation->body->value, $fields['body'], 'Body value matches.');
+    $this->assertEqual($translation->getOwner()->getUsername(), User::load($fields['uid'])->getUsername(), 'Owner username matches ');
+
+    $this->assertEqual($translation->created->value, $fields['created'], 'Created value matches');
+    $this->assertEqual($translation->changed->value, $fields['changed'], 'Changed value matches');
+
+    // Perform unsanitized replacement for easy comparison.
+    $options = [
+      'langcode' => $langcode,
+      'sanitize' => FALSE,
+    ];
+    $token = '[node:title]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $translation], $options);
+
+    $this->assertEqual($token_replacement, $translation->title->value, $token . ' replaced successfully');
+
+    $token = '[node:body]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $translation], $options);
+    $this->assertEqual($token_replacement, $translation->body->value, $token . ' replaced successfully');
+
+    $token = '[node:author]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $translation], $options);
+    $this->assertEqual($token_replacement, $translation->getOwner()->getUsername(), $token . ' replaced successfully');
+
+    $token = '[node:created]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $translation], $options);
+    $this->assertEqual($token_replacement, \Drupal::service('date.formatter')->format($translation->created->value, 'medium', '', NULL, $langcode), $token . ' replaced successfully');
+
+    $token = '[node:changed]';
+    $token_replacement = \Drupal::token()->replace($token, ['node' => $translation], $options);
+    $this->assertEqual($token_replacement, \Drupal::service('date.formatter')->format($translation->changed->value, 'medium', '', NULL, $langcode), $token . ' replaced successfully');
+  }
+
+  /**
+   * @param $langcode
+   *
+   * @return array
+   */
+  protected function getFieldsArray($langcode) {
+    return [
+      'title' => $langcode . $this->randomString(8),
+      'body' => $langcode . $this->randomString(16),
+      'type' => 'page',
+      'language' => $langcode,
+      'uid' => $this->user->id(),
+      'created' => REQUEST_TIME,
+      'changed' => REQUEST_TIME + 100
+    ];
+  }
+
+}
diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
index 3ec91d9..69f87d2 100644
--- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php
+++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\node\NodeTypeInterface;
 use Drupal\system\Tests\System\TokenReplaceUnitTestBase;
 
 /**
@@ -24,38 +25,55 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('node', 'filter');
+  public static $modules = [
+    'filter',
+    'node',
+  ];
+
+  /**
+   * @var NodeTypeInterface
+   */
+  protected $nodeType;
 
   /**
    * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
-    $this->installConfig(array('filter', 'node'));
+    $this->installConfig(static::$modules);
 
-    $node_type = entity_create('node_type', array('type' => 'article', 'name' => 'Article'));
-    $node_type->save();
-    node_add_body_field($node_type);
+    /** @var NodeTypeInterface $node_type */
+    $this->nodeType = entity_create('node_type', [
+      'type' => 'article',
+      'name' => 'My <strong>"&lt;Article&gt;"</strong>',
+    ]);
+    $this->nodeType->save();
+    node_add_body_field($this->nodeType);
   }
 
   /**
    * Creates a node, then tests the tokens generated from it.
    */
   function testNodeTokenReplacement() {
-    $url_options = array(
+    $url_options = [
       'absolute' => TRUE,
       'language' => $this->interfaceLanguage,
-    );
+    ];
 
     // Create a user and a node.
     $account = $this->createUser();
     /* @var $node \Drupal\node\NodeInterface */
     $node = entity_create('node', array(
-      'type' => 'article',
-      'tnid' => 0,
+      'type' => $this->nodeType->id(),
       'uid' => $account->id(),
       'title' => '<blink>Blinking Text</blink>',
-      'body' => array(array('value' => $this->randomMachineName(32), 'summary' => $this->randomMachineName(16), 'format' => 'plain_text')),
+      'body' => [
+        [
+          'value' => '<blink>Blinking Body</blink>',
+          'summary' => '<blink>Blinking Summary</blink>',
+          'format' => 'plain_text',
+        ],
+      ],
     ));
     $node->save();
 
@@ -63,8 +81,8 @@ function testNodeTokenReplacement() {
     $tests = array();
     $tests['[node:nid]'] = $node->id();
     $tests['[node:vid]'] = $node->getRevisionId();
-    $tests['[node:type]'] = 'article';
-    $tests['[node:type-name]'] = 'Article';
+    $tests['[node:type]'] = $this->nodeType->id();
+    $tests['[node:type-name]'] = Html::escape($this->nodeType->label());
     $tests['[node:title]'] = Html::escape($node->getTitle());
     $tests['[node:body]'] = $node->body->processed;
     $tests['[node:summary]'] = $node->body->summary_processed;
@@ -78,75 +96,106 @@ function testNodeTokenReplacement() {
     $tests['[node:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getChangedTime(), array('langcode' => $this->interfaceLanguage->getId()));
 
     $base_bubbleable_metadata = BubbleableMetadata::createFromObject($node);
-
-    $metadata_tests = [];
-    $metadata_tests['[node:nid]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:vid]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:type]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:type-name]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:title]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:body]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:summary]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:langcode]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:url]'] = $base_bubbleable_metadata;
-    $metadata_tests['[node:edit-url]'] = $base_bubbleable_metadata;
-    $bubbleable_metadata = clone $base_bubbleable_metadata;
-    $metadata_tests['[node:author]'] = $bubbleable_metadata->addCacheTags(['user:1']);
-    $metadata_tests['[node:author:uid]'] = $bubbleable_metadata;
-    $metadata_tests['[node:author:name]'] = $bubbleable_metadata;
-    $bubbleable_metadata = clone $base_bubbleable_metadata;
-    $metadata_tests['[node:created:since]'] = $bubbleable_metadata->setCacheMaxAge(0);
-    $metadata_tests['[node:changed:since]'] = $bubbleable_metadata;
+    $author_bubbleable_metadata = clone $base_bubbleable_metadata;
+    $date_bubbleable_metadata = clone $base_bubbleable_metadata;
+    $metadata_tests = [
+      '[node:nid]' => $base_bubbleable_metadata,
+      '[node:vid]' => $base_bubbleable_metadata,
+      '[node:type]' => $base_bubbleable_metadata,
+      '[node:type-name]' => $base_bubbleable_metadata,
+      '[node:title]' => $base_bubbleable_metadata,
+      '[node:body]' => $base_bubbleable_metadata,
+      '[node:summary]' => $base_bubbleable_metadata,
+      '[node:langcode]' => $base_bubbleable_metadata,
+      '[node:url]' => $base_bubbleable_metadata,
+      '[node:edit-url]' => $base_bubbleable_metadata,
+      '[node:author]' => $author_bubbleable_metadata->addCacheTags(['user:1']),
+      '[node:author:uid]' => $author_bubbleable_metadata,
+      '[node:author:name]' => $author_bubbleable_metadata,
+      '[node:created:since]' => $date_bubbleable_metadata->setCacheMaxAge(0),
+      '[node:changed:since]' => $date_bubbleable_metadata,
+    ];
 
     // Test to make sure that we generated something for each token.
     $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
 
-    foreach ($tests as $input => $expected) {
-      $bubbleable_metadata = new BubbleableMetadata();
-      $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata);
-      $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced.', array('%token' => $input)));
-      $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
-    }
+    $data = ['node' => $node];
+    $options = [
+      'langcode' => $this->interfaceLanguage->getId(),
+      'sanitize' => TRUE,
+    ];
+    $msg = 'Sanitized node token %token replaced with %output. Expected is %expected';
+    $this->replaceTokensAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests);
 
     // Generate and test unsanitized tokens.
+    $tests['[node:type-name]'] = $this->nodeType->label();
     $tests['[node:title]'] = $node->getTitle();
     $tests['[node:body]'] = $node->body->value;
     $tests['[node:summary]'] = $node->body->summary;
     $tests['[node:langcode]'] = $node->language()->getId();
+    $tests['[node:author]'] = $account->getUsername();
     $tests['[node:author:name]'] = $account->getUsername();
-
-    foreach ($tests as $input => $expected) {
-      $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced.', array('%token' => $input)));
-    }
+    $options['sanitize'] = FALSE;
+    $msg = 'Unsanitized node token %token replaced with %output. Expected is %expected';
+    $this->replaceTokens($tests, $data, $options, $msg);
 
     // Repeat for a node without a summary.
     $node = entity_create('node', array(
-      'type' => 'article',
+      'type' => $this->nodeType->id(),
       'uid' => $account->id(),
       'title' => '<blink>Blinking Text</blink>',
-      'body' => array(array('value' => $this->randomMachineName(32), 'format' => 'plain_text')),
+      'body' => [
+        [
+          'value' => '<blink>Blinking Body without Summary</blink>',
+          'format' => 'plain_text',
+        ],
+      ],
     ));
     $node->save();
 
     // Generate and test sanitized token - use full body as expected value.
-    $tests = array();
-    $tests['[node:summary]'] = $node->body->processed;
+    $tests = [
+      '[node:summary]' => $node->body->processed,
+    ];
 
     // Test to make sure that we generated something for each token.
     $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated for node without a summary.');
 
-    foreach ($tests as $input => $expected) {
-      $output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage));
-      $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced for node without a summary.', array('%token' => $input)));
-    }
+    $data = ['node' => $node];
+    $options['sanitize'] = TRUE;
+    $msg = 'Sanitized node token %token replaced with %output for node without a summary. Expected is %expected';
+    $this->replaceTokens($tests, $data, $options, $msg);
 
     // Generate and test unsanitized tokens.
     $tests['[node:summary]'] = $node->body->value;
 
-    foreach ($tests as $input => $expected) {
-      $output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage, 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced for node without a summary.', array('%token' => $input)));
+    $options['sanitize'] = FALSE;
+    $msg = 'Unsanitized node token %token replaced with %output for node without a summary. Expected is %expected';
+    $this->replaceTokens($tests, $data, $options, $msg);
+  }
+
+  protected function replaceTokensAndCheckMetadata(array $tests, array $data, array $options, $message, array $metadata_tests) {
+    foreach ($tests as $token => $expected) {
+      $bubbleable_metadata = new BubbleableMetadata();
+      $output = $this->tokenService->replace($token, $data, $options, $bubbleable_metadata);
+      $this->assertEqual($output, $expected, format_string($message, [
+        '%token' => $token,
+        '%output' => $output,
+        '%expected' => $expected,
+      ]));
+
+      $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]);
+    }
+  }
+
+  protected function replaceTokens(array $tests, array $data, array $options, $message) {
+    foreach ($tests as $token => $expected) {
+      $output = $this->tokenService->replace($token, $data, $options);
+      $this->assertEqual($output, $expected, format_string($message, [
+        '%token' => $token,
+        '%output' => $output,
+        '%expected' => $expected,
+      ]));
     }
   }
 
diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php b/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php
index 34971db..c7c4f93 100644
--- a/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php
+++ b/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php
@@ -18,10 +18,16 @@
 
   /**
    * Returns a new vocabulary with random properties.
+   *
+   * @param array $values
+   *   (optional) An array of values to set, keyed by property name.
+   *
+   * @return \Drupal\taxonomy\VocabularyInterface
+   *   The new taxonomy vocabulary object.
    */
-  function createVocabulary() {
+  function createVocabulary(array $values = array()) {
     // Create a vocabulary.
-    $vocabulary = entity_create('taxonomy_vocabulary', array(
+    $vocabulary = entity_create('taxonomy_vocabulary', $values + array(
       'name' => $this->randomMachineName(),
       'description' => $this->randomMachineName(),
       'vid' => Unicode::strtolower($this->randomMachineName()),
diff --git a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
index 4fcb0f1..1759140 100644
--- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
+++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
@@ -23,7 +23,7 @@ class TokenReplaceTest extends TaxonomyTestBase {
   /**
    * The vocabulary used for creating terms.
    *
-   * @var \Drupal\taxonomy\VocabularyInterface
+   * @var \Drupal\taxonomy\Entity\Vocabulary
    */
   protected $vocabulary;
 
@@ -34,10 +34,22 @@ class TokenReplaceTest extends TaxonomyTestBase {
    */
   protected $fieldName;
 
+  /**
+   * Token service.
+   *
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $tokenService;
+
   protected function setUp() {
     parent::setUp();
+
+    $this->tokenService = \Drupal::token();
+
     $this->drupalLogin($this->drupalCreateUser(['administer taxonomy', 'bypass node access']));
-    $this->vocabulary = $this->createVocabulary();
+    $this->vocabulary = $this->createVocabulary([
+      'name' => 'V1 <strong>"&lt;name&gt;"</strong>',
+    ]);
     $this->fieldName = 'taxonomy_' . $this->vocabulary->id();
 
     $handler_settings = array(
@@ -64,23 +76,28 @@ protected function setUp() {
    * Creates some terms and a node, then tests the tokens generated from them.
    */
   function testTaxonomyTokenReplacement() {
-    $token_service = \Drupal::token();
     $language_interface = \Drupal::languageManager()->getCurrentLanguage();
 
-    // Create two taxonomy terms.
-    $term1 = $this->createTerm($this->vocabulary);
-    $term2 = $this->createTerm($this->vocabulary);
+    // Create two taxonomy terms with unsafe names.
+    $term1 = $this->createTerm($this->vocabulary, [
+      'name' => 'T1 <script>"&lt;name&gt;"</script>',
+    ]);
+    $term2 = $this->createTerm($this->vocabulary, [
+      'name' => 'T2 <strong>"&lt;name&gt;"</strong>',
+    ]);
 
     // Edit $term2, setting $term1 as parent.
-    $edit = array();
-    $edit['name[0][value]'] = '<blink>Blinking Text</blink>';
-    $edit['parent[]'] = array($term1->id());
+    $edit = [
+      'name[0][value]' => '<blink>Blinking Text</blink>',
+      'parent[]' => [$term1->id()],
+    ];
     $this->drupalPostForm('taxonomy/term/' . $term2->id() . '/edit', $edit, t('Save'));
 
     // Create node with term2.
-    $edit = array();
-    $node = $this->drupalCreateNode(array('type' => 'article'));
-    $edit[$this->fieldName . '[]'] = $term2->id();
+    $node = $this->drupalCreateNode(['type' => 'article']);
+    $edit = [
+      $this->fieldName . '[]' => $term2->id(),
+    ];
     $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
 
     // Generate and test sanitized tokens for term1.
@@ -90,29 +107,35 @@ function testTaxonomyTokenReplacement() {
     $tests['[term:description]'] = $term1->description->processed;
     $tests['[term:url]'] = $term1->url('canonical', array('absolute' => TRUE));
     $tests['[term:node-count]'] = 0;
+    $tests['[term:parent]'] = '[term:parent]';
     $tests['[term:parent:name]'] = '[term:parent:name]';
-    $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label());
+    $tests['[term:parent:url]'] = '[term:parent:url]';
     $tests['[term:vocabulary]'] = Html::escape($this->vocabulary->label());
+    $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label());
 
     $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1);
 
-    $metadata_tests = array();
-    $metadata_tests['[term:tid]'] = $base_bubbleable_metadata;
-    $metadata_tests['[term:name]'] = $base_bubbleable_metadata;
-    $metadata_tests['[term:description]'] = $base_bubbleable_metadata;
-    $metadata_tests['[term:url]'] = $base_bubbleable_metadata;
-    $metadata_tests['[term:node-count]'] = $base_bubbleable_metadata;
-    $metadata_tests['[term:parent:name]'] = $base_bubbleable_metadata;
     $bubbleable_metadata = clone $base_bubbleable_metadata;
-    $metadata_tests['[term:vocabulary:name]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags());
-    $metadata_tests['[term:vocabulary]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags());
-
-    foreach ($tests as $input => $expected) {
-      $bubbleable_metadata = new BubbleableMetadata();
-      $output = $token_service->replace($input, array('term' => $term1), array('langcode' => $language_interface->getId()), $bubbleable_metadata);
-      $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input)));
-      $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
-    }
+    $metadata_tests = [
+      '[term:tid]' => $base_bubbleable_metadata,
+      '[term:name]' => $base_bubbleable_metadata,
+      '[term:description]' => $base_bubbleable_metadata,
+      '[term:url]' => $base_bubbleable_metadata,
+      '[term:node-count]' => $base_bubbleable_metadata,
+      '[term:parent]' => $base_bubbleable_metadata,
+      '[term:parent:name]' => $base_bubbleable_metadata,
+      '[term:parent:url]' => $base_bubbleable_metadata,
+      '[term:vocabulary]' => $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()),
+      '[term:vocabulary:name]' => $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()),
+    ];
+
+    $data = ['term' => $term1];
+    $options = [
+      'langcode' => $language_interface->getId(),
+      'sanitize' => TRUE,
+    ];
+    $msg = 'Sanitized taxonomy term 1 token %token replaced with %output. Expected is %expected';
+    $this->replaceTokensAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests);
 
     // Generate and test sanitized tokens for term2.
     $tests = array();
@@ -121,29 +144,32 @@ function testTaxonomyTokenReplacement() {
     $tests['[term:description]'] = $term2->description->processed;
     $tests['[term:url]'] = $term2->url('canonical', array('absolute' => TRUE));
     $tests['[term:node-count]'] = 1;
+    $tests['[term:parent]'] = Html::escape($term1->getName());
     $tests['[term:parent:name]'] = Html::escape($term1->getName());
     $tests['[term:parent:url]'] = $term1->url('canonical', array('absolute' => TRUE));
     $tests['[term:parent:parent:name]'] = '[term:parent:parent:name]';
+    $tests['[term:vocabulary]'] = Html::escape($this->vocabulary->label());
     $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label());
 
     // Test to make sure that we generated something for each token.
     $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
 
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId()));
-      $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input)));
-    }
+    $data = ['term' => $term2];
+    $options['sanitize'] = TRUE;
+    $msg = 'Sanitized taxonomy term 2 token %token replaced with %output. Expected is %expected';
+    $this->replaceTokens($tests, $data, $options, $msg);
 
     // Generate and test unsanitized tokens.
     $tests['[term:name]'] = $term2->getName();
     $tests['[term:description]'] = $term2->getDescription();
+    $tests['[term:parent]'] = $term1->getName();
     $tests['[term:parent:name]'] = $term1->getName();
+    $tests['[term:vocabulary]'] = $this->vocabulary->label();
     $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
 
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy term token %token replaced.', array('%token' => $input)));
-    }
+    $options['sanitize'] = FALSE;
+    $msg = 'Unsanitized taxonomy term 2 token %token replaced with %output. Expected is %expected';
+    $this->replaceTokens($tests, $data, $options, $msg);
 
     // Generate and test sanitized tokens.
     $tests = array();
@@ -156,18 +182,42 @@ function testTaxonomyTokenReplacement() {
     // Test to make sure that we generated something for each token.
     $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
 
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId()));
-      $this->assertEqual($output, $expected, format_string('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
-    }
+    $data = ['vocabulary' => $this->vocabulary];
+    $options['sanitize'] = TRUE;
+    $msg = 'Sanitized taxonomy vocabulary token %token replaced with %output. Expected is %expected';
+    $this->replaceTokens($tests, $data, $options, $msg);
 
     // Generate and test unsanitized tokens.
     $tests['[vocabulary:name]'] = $this->vocabulary->label();
     $tests['[vocabulary:description]'] = $this->vocabulary->getDescription();
 
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
+    $options['sanitize'] = FALSE;
+    $msg = 'Unsanitized taxonomy vocabulary token %token replaced with %output. Expected is %expected.';
+    $this->replaceTokens($tests, $data, $options, $msg);
+  }
+
+  protected function replaceTokensAndCheckMetadata(array $tests, array $data, array $options, $message, array $metadata_tests) {
+    foreach ($tests as $token => $expected) {
+      $bubbleable_metadata = new BubbleableMetadata();
+      $output = $this->tokenService->replace($token, $data, $options, $bubbleable_metadata);
+      $this->assertEqual($output, $expected, format_string($message, [
+        '%token' => $token,
+        '%output' => $output,
+        '%expected' => $expected,
+      ]));
+
+      $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]);
+    }
+  }
+
+  protected function replaceTokens(array $tests, array $data, array $options, $message) {
+    foreach ($tests as $token => $expected) {
+      $output = $this->tokenService->replace($token, $data, $options);
+      $this->assertEqual($output, $expected, format_string($message, [
+        '%token' => $token,
+        '%output' => $output,
+        '%expected' => $expected,
+      ]));
     }
   }
 }
diff --git a/core/modules/taxonomy/taxonomy.tokens.inc b/core/modules/taxonomy/taxonomy.tokens.inc
index daf690b..3f41b82 100644
--- a/core/modules/taxonomy/taxonomy.tokens.inc
+++ b/core/modules/taxonomy/taxonomy.tokens.inc
@@ -94,13 +94,32 @@ function taxonomy_token_info() {
  * Implements hook_tokens().
  */
 function taxonomy_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+  // Return if the $type is not supported or the required data is missing.
+  if (($type != 'term' && $type != 'vocabulary')
+    || ($type == 'term' && empty($data['term']))
+    || ($type == 'vocabulary' && empty($data['vocabulary']))
+  ) {
+    return [];
+  }
+
+  $replacements = [];
   $token_service = \Drupal::token();
 
-  $replacements = array();
+  $url_options = ['absolute' => TRUE];
+  if (isset($options['langcode'])) {
+    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+    $langcode = $options['langcode'];
+  }
+  else {
+    $langcode = NULL;
+  }
   $sanitize = !empty($options['sanitize']);
+
+  /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
   $taxonomy_storage = \Drupal::entityManager()->getStorage('taxonomy_term');
-  if ($type == 'term' && !empty($data['term'])) {
-    $term = $data['term'];
+  if ($type == 'term') {
+    /** @var \Drupal\taxonomy\TermInterface $term */
+    $term = \Drupal::entityManager()->getTranslationFromContext($data['term'], $langcode, ['operation' => 'term_tokens']);
 
     foreach ($tokens as $name => $original) {
       switch ($name) {
@@ -117,28 +136,30 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
           break;
 
         case 'url':
-          $replacements[$original] = $term->url('canonical', array('absolute' => TRUE));
+          $replacements[$original] = $term->url('canonical', $url_options);
           break;
 
         case 'node-count':
-          $query = db_select('taxonomy_index');
-          $query->condition('tid', $term->id());
-          $query->addTag('term_node_count');
-          $count = $query->countQuery()->execute()->fetchField();
-          $replacements[$original] = $count;
+          $replacements[$original] = db_select('taxonomy_index')
+            ->condition('tid', $term->id())
+            ->addTag('term_node_count')
+            ->countQuery()
+            ->execute()
+            ->fetchField();
           break;
 
         case 'vocabulary':
+          /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
           $vocabulary = Vocabulary::load($term->bundle());
           $bubbleable_metadata->addCacheableDependency($vocabulary);
-          $replacements[$original] = Html::escape($vocabulary->label());
+          $replacements[$original] = $sanitize ? Html::escape($vocabulary->label()) : $vocabulary->label();
           break;
 
         case 'parent':
           if ($parents = $taxonomy_storage->loadParents($term->id())) {
-            $parent = array_pop($parents);
-            $bubbleable_metadata->addCacheableDependency($parent);
-            $replacements[$original] = Html::escape($parent->getName());
+            $term_first_parent = \Drupal::entityManager()->getTranslationFromContext(array_pop($parents), $langcode, ['operation' => 'term_tokens']);
+            $bubbleable_metadata->addCacheableDependency($term_first_parent);
+            $replacements[$original] = $sanitize ? Html::escape($term_first_parent->getName()) : $term_first_parent->getName();
           }
           break;
       }
@@ -149,14 +170,16 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
       $replacements += $token_service->generate('vocabulary', $vocabulary_tokens, array('vocabulary' => $vocabulary), $options, $bubbleable_metadata);
     }
 
-    if (($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'parent')) && $parents = $taxonomy_storage->loadParents($term->id())) {
-      $parent = array_pop($parents);
-      $replacements += $token_service->generate('term', $vocabulary_tokens, array('term' => $parent), $options, $bubbleable_metadata);
+    if (($term_parent_tokens = $token_service->findWithPrefix($tokens, 'parent'))
+      && $term_parents = $taxonomy_storage->loadParents($term->id())
+    ) {
+      $term_first_parent = array_pop($term_parents);
+      $replacements += $token_service->generate('term', $term_parent_tokens, array('term' => $term_first_parent), $options, $bubbleable_metadata);
     }
   }
-
-  elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) {
-    $vocabulary = $data['vocabulary'];
+  elseif ($type == 'vocabulary') {
+    /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
+    $vocabulary = \Drupal::entityManager()->getTranslationFromContext($data['vocabulary'], $langcode, ['operation' => 'vocabulary_tokens']);
 
     foreach ($tokens as $name => $original) {
       switch ($name) {
