diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
index 81fa3f0..17c529d 100644
--- a/core/modules/node/node.tokens.inc
+++ b/core/modules/node/node.tokens.inc
@@ -6,7 +6,6 @@
  */
 
 use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\user\Entity\User;
 
@@ -93,13 +92,16 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
     $langcode = $options['langcode'];
   }
   else {
-    $langcode = LanguageInterface::LANGCODE_DEFAULT;
+    $langcode = NULL;
   }
-  $replacements = array();
+  $replacements = [];
+
+  $sanitize = !empty($options['sanitize']);
 
   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) {
@@ -127,8 +129,7 @@ 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];
             // If the summary was requested and is not empty, use it.
             if ($name == 'summary' && !empty($item->summary)) {
@@ -181,15 +182,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..e5f83e3
--- /dev/null
+++ b/core/modules/node/src/Tests/NodeTokenLanguageTest.php
@@ -0,0 +1,165 @@
+<?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;
+
+/**
+ * Check if node tokens are multilingual.
+ *
+ * @group node
+ */
+class NodeTokenLanguageTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'language', 'content_translation'];
+
+  /**
+   * Users for testing different languages.
+   *
+   * @var null
+   */
+  private $users = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  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->assertTokenReplacement($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->assertTokenReplacement($translation);
+  }
+
+  /**
+   * Populates an array with field values for a given language.
+   *
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return array
+   *   The fields of node entity.
+   */
+  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,
+    ];
+  }
+
+  /**
+   * Asserts if tokens are correctly replaced.
+   *
+   * @param \Drupal\node\Entity\Node $node
+   *   The node for which to test token replacement.
+   */
+  protected function assertTokenReplacement(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 bc6a049..b6f326f 100644
--- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php
+++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
@@ -13,6 +13,8 @@
 use Drupal\system\Tests\System\TokenReplaceUnitTestBase;
 
 /**
+ * Generates text using placeholders.
+ *
  * Generates text using placeholders for dummy content to check node token
  * replacement.
  *
@@ -42,7 +44,7 @@ protected function setUp() {
   /**
    * Creates a node, then tests the tokens generated from it.
    */
-  function testNodeTokenReplacement() {
+  public function testNodeTokenReplacement() {
     $url_options = array(
       'absolute' => TRUE,
       'language' => $this->interfaceLanguage,
@@ -51,13 +53,19 @@ function testNodeTokenReplacement() {
     // Create a user and a node.
     $account = $this->createUser();
     /* @var $node \Drupal\node\NodeInterface */
-    $node = entity_create('node', array(
+    $node = entity_create('node', [
       'type' => 'article',
       'tnid' => 0,
       'uid' => $account->id(),
       'title' => '<blink>Blinking Text</blink>',
-      'body' => [['value' => 'Regular NODE body for the test.', 'summary' => 'Fancy NODE summary.', 'format' => 'plain_text']],
-    ));
+      'body' => [
+        [
+          'value' => $this->randomMachineName(32),
+          'summary' => $this->randomMachineName(16),
+          'format' => 'plain_text',
+        ],
+      ],
+    ]);
     $node->save();
 
     // Generate and test tokens.
diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php b/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php
index 34971db..8a76546 100644
--- a/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php
+++ b/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php
@@ -18,16 +18,22 @@
 
   /**
    * 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 + [
       'name' => $this->randomMachineName(),
       'description' => $this->randomMachineName(),
       'vid' => Unicode::strtolower($this->randomMachineName()),
       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
       'weight' => mt_rand(0, 10),
-    ));
+    ]);
     $vocabulary->save();
     return $vocabulary;
   }
@@ -47,17 +53,18 @@ function createVocabulary() {
   function createTerm(Vocabulary $vocabulary, $values = array()) {
     $filter_formats = filter_formats();
     $format = array_pop($filter_formats);
-    $term = entity_create('taxonomy_term', $values + array(
+    $term = entity_create('taxonomy_term', $values + [
       'name' => $this->randomMachineName(),
-      'description' => array(
+      'description' => [
         'value' => $this->randomMachineName(),
         // Use the first available text format.
         'format' => $format->id(),
-      ),
+      ],
       'vid' => $vocabulary->id(),
       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-    ));
+    ]);
     $term->save();
     return $term;
   }
+
 }
diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTokenLanguageTest.php b/core/modules/taxonomy/src/Tests/TaxonomyTokenLanguageTest.php
new file mode 100644
index 0000000..cc23d4d
--- /dev/null
+++ b/core/modules/taxonomy/src/Tests/TaxonomyTokenLanguageTest.php
@@ -0,0 +1,121 @@
+<?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'];
+
+  /**
+   * {@inheritdoc}
+   */
+  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.
+    $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 string $langcode
+   *   The language code.
+   *
+   * @return array
+   *   The fields of a term.
+   */
+  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
+   *   The term that needs to be checked.
+   */
+  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/taxonomy/src/Tests/TokenReplaceTest.php b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
index 1e00a8e..9e50077 100644
--- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
+++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
@@ -7,10 +7,14 @@
 
 namespace Drupal\taxonomy\Tests;
 
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 
 /**
+ * Generates text using placeholders.
+ *
  * Generates text using placeholders for dummy content to check taxonomy token
  * replacement.
  *
@@ -21,7 +25,7 @@ class TokenReplaceTest extends TaxonomyTestBase {
   /**
    * The vocabulary used for creating terms.
    *
-   * @var \Drupal\taxonomy\VocabularyInterface
+   * @var \Drupal\taxonomy\Entity\Vocabulary
    */
   protected $vocabulary;
 
@@ -32,10 +36,23 @@ class TokenReplaceTest extends TaxonomyTestBase {
    */
   protected $fieldName;
 
+  /**
+   * Token service.
+   *
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $tokenService;
+
+  /**
+   * {@inheritdoc}
+   */
   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(
@@ -61,24 +78,29 @@ protected function setUp() {
   /**
    * Creates some terms and a node, then tests the tokens generated from them.
    */
-  function testTaxonomyTokenReplacement() {
-    $token_service = \Drupal::token();
+  public function testTaxonomyTokenReplacement() {
     $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.
@@ -88,52 +110,72 @@ 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:parent:url]'] = '[term:parent:url]';
     $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
     $tests['[term:vocabulary]'] = $this->vocabulary->label();
 
+    $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->assertTokenReplacementAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests);
 
     // Generate and test sanitized tokens for term2.
     $tests = array();
     $tests['[term:tid]'] = $term2->id();
     $tests['[term:name]'] = $term2->getName();
+    $tests['[term:description]'] = $term2->getDescription();
+    $tests['[term:parent]'] = $term1->getName();
     $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]'] = $term1->getName();
+    $tests['[term:vocabulary]'] = $this->vocabulary->label();
     $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]'] = $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->assertTokenReplacement($tests, $data, $options, $msg);
+
+    $data = ['vocabulary' => $this->vocabulary];
+    $options['sanitize'] = TRUE;
+    $msg = 'Sanitized taxonomy vocabulary token %token replaced with %output. Expected is %expected';
+    $this->assertTokenReplacement($tests, $data, $options, $msg);
+    $options['sanitize'] = FALSE;
+    $msg = 'Unsanitized taxonomy vocabulary token %token replaced with %output. Expected is %expected.';
+    $this->assertTokenReplacement($tests, $data, $options, $msg);
 
     // Generate and test sanitized tokens.
-    $tests = array();
+    $tests = [];
     $tests['[vocabulary:vid]'] = $this->vocabulary->id();
     $tests['[vocabulary:name]'] = $this->vocabulary->label();
     $tests['[vocabulary:description]'] = $this->vocabulary->getDescription();
@@ -143,10 +185,71 @@ 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)));
+    $options['sanitize'] = FALSE;
+    $msg = 'Unsanitized taxonomy term 2 token %token replaced with %output. Expected is %expected';
+    $this->assertTokenReplacement($tests, $data, $options, $msg);
+  }
+
+  /**
+   * Checks token replacement and metadata.
+   *
+   * Asserts 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 assertTokenReplacementAndCheckMetadata(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]);
+    }
+  }
+
+  /**
+   * Asserts 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 assertTokenReplacement(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 20251f2..fafea47 100644
--- a/core/modules/taxonomy/taxonomy.tokens.inc
+++ b/core/modules/taxonomy/taxonomy.tokens.inc
@@ -92,13 +92,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;
+  }
   $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']);
+  $sanitize = !empty($options['sanitize']);
 
+  /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
     foreach ($tokens as $name => $original) {
       switch ($name) {
         case 'tid':
@@ -116,7 +135,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':
@@ -128,15 +147,18 @@ 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] = $sanitize ? Html::escape($vocabulary->label()) : $vocabulary->label();
           $replacements[$original] = $vocabulary->label();
           break;
 
         case 'parent':
           if ($parents = $taxonomy_storage->loadParents($term->id())) {
-            $parent = array_pop($parents);
-            $bubbleable_metadata->addCacheableDependency($parent);
+            $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();
             $replacements[$original] = $parent->getName();
           }
           break;
@@ -148,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) {
