diff --git a/TaxonomyTokenLanguageTest.php b/TaxonomyTokenLanguageTest.php
new file mode 100644
index 0000000..2f0923a
--- /dev/null
+++ b/TaxonomyTokenLanguageTest.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\taxonomy\Tests\TaxonomyTokenLanguageTest.
+ */
+
+namespace Drupal\taxonomy\Tests;
+
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\taxonomy\Entity\Term;
+
+/**
+ * Check if taxonomy tokens are multilingual.
+ *
+ * @group taxonomy
+ */
+class TaxonomyTokenLanguageTest extends TaxonomyTestBase {
+
+  /**
+   * Vocabulary for testing.
+   *
+   * @var \Drupal\taxonomy\VocabularyInterface
+   */
+  protected $vocabulary;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'language', 'content_translation'];
+
+  protected function setUp() {
+    parent::setUp();
+
+    // Create an administrative user.
+    $this->drupalLogin($this->drupalCreateUser(['administer taxonomy']));
+
+    // Create a vocabulary to which the terms will be assigned.
+    $this->vocabulary = $this->createVocabulary();
+
+    // Add a new language.
+    ConfigurableLanguage::createFromLangcode('it')->save();
+  }
+
+  /**
+   * Test if taxonomy token are multilingual.
+   */
+  public function testMultilingualTaxonomyTokens() {
+    // Configure the vocabulary to not hide the language selector.
+    // TODO do we need this?
+    $edit = [
+      'default_language[language_alterable]' => TRUE,
+    ];
+    $this->drupalPostForm('admin/structure/taxonomy/manage/' . $this->vocabulary->id(), $edit, t('Save'));
+
+    // Create Term.
+    $langcode = 'en';
+    $fields = $this->getFieldsArray($langcode);
+    $term = $this->createTerm($this->vocabulary, $fields);
+
+    $this->verifyTokensCorrectlyReplaced($term);
+
+    $langcode = 'it';
+    $fields = $this->getFieldsArray($langcode);
+    $term = $term->addTranslation($langcode, $fields);
+
+    $this->verifyTokensCorrectlyReplaced($term);
+  }
+
+  /**
+   * Populates an array with field values for a given language.
+   *
+   * @param $langcode
+   *
+   * @return array
+   */
+  protected function getFieldsArray($langcode) {
+    return [
+      'name' => $langcode . $this->randomString(8),
+      'description' => $langcode . $this->randomString(16),
+      'langcode' => $langcode,
+    ];
+  }
+
+  /**
+   * Verifies if tokens were correctly replaced.
+   *
+   * @param \Drupal\taxonomy\Entity\Term $term
+   */
+  protected function verifyTokensCorrectlyReplaced(Term $term) {
+    // Perform unsanitized replacement for easy comparison.
+    $options = ['langcode' => $term->language()->getId(), 'sanitize' => FALSE];
+
+    $tokens = [
+      '[term:name]' => [
+        'expected' => $term->name->value,
+        'actual' => \Drupal::token()->replace('[term:name]', ['term' => $term], $options)
+      ],
+      '[term:description]' => [
+        'expected' => $term->description->value,
+        'actual' => $token_replacement = \Drupal::token()->replace('[term:description]', ['term' => $term], $options)
+      ],
+      '[term:url]' => [
+        'expected' => $term->url('canonical', array('absolute' => TRUE)),
+        'actual' => \Drupal::token()->replace('[term:url]', ['term' => $term], $options)
+      ]
+    ];
+
+    foreach ($tokens as $token => $replacements) {
+      $this->assertEqual($replacements['expected'], $replacements['actual'], $token . ' replaced successfully');
+    }
+  }
+}
diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
index 3294044..23f6062 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;
 
@@ -94,15 +93,14 @@ 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'];
+    $node = \Drupal::entityManager()
+      ->getTranslationFromContext($data['node'], $langcode, ['operation' => 'node_tokens']);
 
     foreach ($tokens as $name => $original) {
       switch ($name) {
@@ -130,10 +128,8 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
 
         case 'body':
         case 'summary':
-          $translation = \Drupal::entityManager()->getTranslationFromContext($node, $langcode, array('operation' => 'node_tokens'));
-          if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) {
+          if ($node->hasField('body') && ($items = $node->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;
@@ -183,15 +179,14 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
           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':
+          /** @var \Drupal\Core\Datetime\Entity\DateFormat $date_format */
           $date_format = DateFormat::load('medium');
+          $date_raw = ($name === 'created' ? $node->getCreatedTime() : $node->getChangedTime());
+          $date_formatted = \Drupal::service('date.formatter')
+            ->format($date_raw, $date_format->id(), '', NULL, $langcode);
           $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = format_date($node->getChangedTime(), 'medium', '', NULL, $langcode);
+          $replacements[$original] = $sanitize ? Html::escape($date_formatted) : $date_formatted;
           break;
       }
     }
diff --git a/core/modules/node/src/Tests/NodeTokenLanguageTest.php b/core/modules/node/src/Tests/NodeTokenLanguageTest.php
new file mode 100644
index 0000000..fe3f2cb
--- /dev/null
+++ b/core/modules/node/src/Tests/NodeTokenLanguageTest.php
@@ -0,0 +1,159 @@
+<?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 $users = NULL;
+
+  protected function setUp() {
+    parent::setUp();
+
+    // Create Basic page node type.
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+
+    // Setup user.
+    $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.
+    foreach (['en', 'it'] as $langcode) {
+      $this->users[$langcode] = $this->drupalCreateUser([
+        'create page content',
+        'edit own page content'
+      ]);
+    }
+
+    // Create English "Basic page" node.
+    $langcode = 'en';
+    $this->drupalLogin($this->users[$langcode]);
+    $fields = $this->getFieldsArray($langcode);
+
+    $node = Node::create($fields);
+    $node->save();
+
+    $this->testTokenReplacement($node);
+
+    // Create italian translation with new field values.
+    $langcode = 'it';
+    $this->drupalLogin($this->users[$langcode]);
+    $fields =  $this->getFieldsArray($langcode);
+    $node->addTranslation($langcode, $fields);
+    $translation = $node->getTranslation($langcode);
+
+    $this->testTokenReplacement($translation);
+  }
+
+  /**
+   * Populates an array with field values for a given language.
+   *
+   * @param $langcode
+   *
+   * @return array
+   */
+  protected function getFieldsArray($langcode) {
+    $time = REQUEST_TIME - rand(0,1000);
+    return [
+      'title' => $langcode . $this->randomString(8),
+      'body' => $langcode . $this->randomString(16),
+      'type' => 'page',
+      'language' => $langcode,
+      'uid' => $this->users[$langcode]->id(),
+      'created' => $time,
+      'changed' => $time + 100,
+    ];
+  }
+
+  /**
+   * Tests if tokens are correctly replaced.
+   *
+   * @param \Drupal\node\Entity\Node $node
+   *   The node for which to test token replacement.
+   */
+  protected function testTokenReplacement(Node $node) {
+    // Perform unsanitized replacement for easy comparison.
+    $options = ['langcode' => $node->language()->getId(), 'sanitize' => FALSE];
+
+    $tokens = [
+      '[node:title]' => [
+        'expected' => $node->title->value,
+        'actual' => \Drupal::token()->replace('[node:title]', ['node' => $node], $options)
+      ],
+      '[node:body]' => [
+        'expected' => $node->body->value,
+        'actual' => $token_replacement = \Drupal::token()->replace('[node:body]', ['node' => $node], $options)
+      ],
+      '[node:author]' => [
+        'expected' => $node->getOwner()->getUsername(),
+        'actual' => \Drupal::token()->replace('[node:author]', ['node' => $node], $options)
+      ],
+      '[node:created]' => [
+        'expected' => \Drupal::service('date.formatter')->format($node->created->value, 'medium', '', NULL, $options['langcode']),
+        'actual' => \Drupal::token()->replace('[node:created]', ['node' => $node], $options)
+      ],
+      '[node:changed]' => [
+        'expected' => \Drupal::service('date.formatter')->format($node->changed->value, 'medium', '', NULL, $options['langcode']),
+        'actual' => \Drupal::token()->replace('[node:changed]', ['node' => $node], $options),
+      ],
+    ];
+
+    foreach ($tokens as $token => $replacements) {
+      $this->assertEqual($replacements['expected'], $replacements['actual'], $token . ' replaced successfully');
+    }
+  }
+
+}
diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
index 3ec91d9..59a6718 100644
--- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php
+++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
@@ -8,7 +8,9 @@
 namespace Drupal\node\Tests;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\node\NodeTypeInterface;
 use Drupal\system\Tests\System\TokenReplaceUnitTestBase;
 
 /**
@@ -24,38 +26,52 @@ 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 +79,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,7 +94,8 @@ function testNodeTokenReplacement() {
     $tests['[node:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getChangedTime(), array('langcode' => $this->interfaceLanguage->getId()));
 
     $base_bubbleable_metadata = BubbleableMetadata::createFromObject($node);
-
+    $author_bubbleable_metadata = clone $base_bubbleable_metadata;
+    $date_bubbleable_metadata = clone $base_bubbleable_metadata;
     $metadata_tests = [];
     $metadata_tests['[node:nid]'] = $base_bubbleable_metadata;
     $metadata_tests['[node:vid]'] = $base_bubbleable_metadata;
@@ -90,63 +107,125 @@ function testNodeTokenReplacement() {
     $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;
+    $metadata_tests['[node:author]'] = $author_bubbleable_metadata->addCacheTags(['user:1']);
+    $metadata_tests['[node:author:uid]'] = $author_bubbleable_metadata;
+    $metadata_tests['[node:author:name]'] = $author_bubbleable_metadata;
+    $metadata_tests['[node:created:since]'] = $date_bubbleable_metadata->setCacheMaxAge(0);
+    $metadata_tests['[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->testTokenReplacementAndCheckMetadata($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->testTokenReplacement($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->testTokenReplacement($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->testTokenReplacement($tests, $data, $options, $msg);
+  }
+
+  /**
+   * Tests if tokens are correctly replaced and verifies that the correct
+   * metadata has been set.
+   *
+   * @param array $tests
+   * Expected results keyed by their respective tokens.
+   * @param array $data
+   * The data to perform the replacement on. @see Token::replace().
+   * @param array $options
+   * Aditional replacement options. @see Token::replace().
+   * @param $message
+   * The message to display with the assertion. Can contain replacement tokens:
+   * - %token for the token name
+   * - %output for the value returned by the token replacement service
+   * - %expected for the expected result
+   * @param array $metadata_tests
+   * The metadata to verify. Keyed by the relevant token.
+   */
+  protected function testTokenReplacementAndCheckMetadata(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, SafeMarkup::format($message, [
+        '%token' => $token,
+        '%output' => $output,
+        '%expected' => $expected,
+      ]));
+
+      $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]);
+    }
+  }
+
+  /**
+   * Tests if tokens are correctly replaced.
+   *
+   * @param array $tests
+   * Expected results keyed by their respective tokens.
+   * @param array $data
+   * The data to perform the replacement on. @see Token::replace().
+   * @param array $options
+   * Aditional replacement options. @see Token::replace().
+   * @param $message
+   * The message to display with the assertion. Can contain replacement tokens:
+   * - %token for the token name
+   * - %output for the value returned by the token replacement service
+   * - %expected for the expected result
+   */
+  protected function testTokenReplacement(array $tests, array $data, array $options, $message) {
+    foreach ($tests as $token => $expected) {
+      $output = $this->tokenService->replace($token, $data, $options);
+      $this->assertEqual($output, $expected, SafeMarkup::format($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..18f005e 100644
--- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
+++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\taxonomy\Tests;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Render\BubbleableMetadata;
@@ -23,7 +24,7 @@ class TokenReplaceTest extends TaxonomyTestBase {
   /**
    * The vocabulary used for creating terms.
    *
-   * @var \Drupal\taxonomy\VocabularyInterface
+   * @var \Drupal\taxonomy\Entity\Vocabulary
    */
   protected $vocabulary;
 
@@ -34,10 +35,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 +77,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 +108,34 @@ 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();
+    $bubbleable_metadata = clone $base_bubbleable_metadata;
+    $metadata_tests = [];
     $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]'] = $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:parent:url]'] = $base_bubbleable_metadata;
     $metadata_tests['[term:vocabulary]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags());
+    $metadata_tests['[term:vocabulary:name]'] = $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]);
-    }
+    $data = ['term' => $term1];
+    $options = [
+      'langcode' => $language_interface->getId(),
+      'sanitize' => TRUE,
+    ];
+    $msg = 'Sanitized taxonomy term 1 token %token replaced with %output. Expected is %expected';
+    $this->testTokenReplacementAndCheckMetadata($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->testTokenReplacement($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->testTokenReplacement($tests, $data, $options, $msg);
 
     // Generate and test sanitized tokens.
     $tests = array();
@@ -156,18 +182,75 @@ 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->testTokenReplacement($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->testTokenReplacement($tests, $data, $options, $msg);
+  }
+
+  /**
+   * Tests if tokens are correctly replaced and verifies that the correct
+   * metadata has been set.
+   *
+   * @param array $tests
+   * Expected results keyed by their respective tokens.
+   * @param array $data
+   * The data to perform the replacement on. @see Token::replace().
+   * @param array $options
+   * Aditional replacement options. @see Token::replace().
+   * @param $message
+   * The message to display with the assertion. Can contain replacement tokens:
+   * - %token for the token name
+   * - %output for the value returned by the token replacement service
+   * - %expected for the expected result
+   * @param array $metadata_tests
+   * The metadata to verify. Keyed by the relevant token.
+   */
+  protected function testTokenReplacementAndCheckMetadata(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, SafeMarkup::format($message, [
+        '%token' => $token,
+        '%output' => $output,
+        '%expected' => $expected,
+      ]));
+
+      $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]);
+    }
+  }
+
+  /**
+   * Tests if tokens are correctly replaced.
+   *
+   * @param array $tests
+   * Expected results keyed by their respective tokens.
+   * @param array $data
+   * The data to perform the replacement on. @see Token::replace().
+   * @param array $options
+   * Aditional replacement options. @see Token::replace().
+   * @param $message
+   * The message to display with the assertion. Can contain replacement tokens:
+   * - %token for the token name
+   * - %output for the value returned by the token replacement service
+   * - %expected for the expected result
+   */
+  protected function testTokenReplacement(array $tests, array $data, array $options, $message) {
+    foreach ($tests as $token => $expected) {
+      $output = $this->tokenService->replace($token, $data, $options);
+      $this->assertEqual($output, $expected, SafeMarkup::format($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..73d0a5c 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,7 +136,7 @@ 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':
@@ -129,16 +148,17 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
           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 +169,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) {
