diff --git a/core/modules/toolbar/js/views/ToolbarVisualView.js b/core/modules/toolbar/js/views/ToolbarVisualView.js
index 872aa98..3459404 100644
--- a/core/modules/toolbar/js/views/ToolbarVisualView.js
+++ b/core/modules/toolbar/js/views/ToolbarVisualView.js
@@ -267,7 +267,8 @@
       //   (3) The orientation of the tray is vertical.
       if (!this.model.get('areSubtreesLoaded') && typeof $activeTab.data('drupal-subtrees') !== 'undefined' && orientation === 'vertical') {
         var subtreesHash = drupalSettings.toolbar.subtreesHash;
-        var endpoint = Drupal.url('toolbar/subtrees/' + subtreesHash);
+        var langcode = drupalSettings.toolbar.langcode;
+        var endpoint = Drupal.url('toolbar/subtrees/' + subtreesHash + '/' + langcode);
         var cachedSubtreesHash = localStorage.getItem('Drupal.toolbar.subtreesHash');
         var cachedSubtrees = JSON.parse(localStorage.getItem('Drupal.toolbar.subtrees'));
         var isVertical = this.model.get('orientation') === 'vertical';
diff --git a/core/modules/toolbar/src/Controller/ToolbarController.php b/core/modules/toolbar/src/Controller/ToolbarController.php
index ecccc1e..5a8842a 100644
--- a/core/modules/toolbar/src/Controller/ToolbarController.php
+++ b/core/modules/toolbar/src/Controller/ToolbarController.php
@@ -22,7 +22,7 @@ class ToolbarController extends ControllerBase {
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    */
   public function subtreesJsonp() {
-    list($subtrees, $cacheability) = toolbar_get_rendered_subtrees();
+    $subtrees = toolbar_get_rendered_subtrees();
     $response = new JsonResponse($subtrees);
     $response->setCallback('Drupal.toolbar.setSubtrees.resolve');
 
@@ -46,12 +46,14 @@ public function subtreesJsonp() {
    *
    * @param string $hash
    *   The hash of the toolbar subtrees.
+   * @param string $langcode
+   *   The langcode of the requested site, NULL if none given.
    *
    * @return \Drupal\Core\Access\AccessResultInterface
    *   The access result.
    */
-  public function checkSubTreeAccess($hash) {
-    return AccessResult::allowedIf($this->currentUser()->hasPermission('access toolbar') && $hash == _toolbar_get_subtrees_hash()[0])->cachePerPermissions();
+  public function checkSubTreeAccess($hash, $langcode) {
+    return AccessResult::allowedIf($this->currentUser()->hasPermission('access toolbar') && $hash == _toolbar_get_subtrees_hash($langcode))->cachePerPermissions();
   }
 
 }
diff --git a/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php b/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php
index cd6cb9f..9116aaf 100644
--- a/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php
+++ b/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\toolbar\Tests;
 
-use Drupal\Component\Serialization\Json;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Url;
@@ -212,10 +211,97 @@ function testUserRoleUpdateSubtreesHashCacheClear() {
   }
 
   /**
+   * Tests that all toolbar cache entries for a user are cleared with a cache
+   * tag for that user, i.e. cache entries for all languages for that user.
+   */
+  function testCacheClearByCacheTag() {
+    // Test that the toolbar admin menu subtrees cache is invalidated for a user
+    // across multiple languages.
+    $this->drupalLogin($this->adminUser);
+    $toolbarCache = $this->container->get('cache.toolbar');
+    $admin_user_id = $this->adminUser->id();
+    $admin_user_2_id = $this->adminUser2->id();
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for adminUser against the language "en".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_id . ':' . 'en');
+    $this->assertEqual(in_array('user:' . $admin_user_id, $cache->tags), 'A cache tag in the toolbar cache under the key "user" exists for admin_user against the language "en".');
+
+    // Assert that no toolbar cache exists for adminUser against the
+    // language "fr".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_id . ':' . 'fr');
+    $this->assertFalse($cache, 'No toolbar cache exists for admin_user against the language "fr".');
+
+    // Install a second language.
+    $edit = array(
+      'predefined_langcode' => 'fr',
+    );
+    $this->drupalPostForm('admin/config/regional/language/add', $edit, 'Add language');
+
+    // Request a page in 'fr' to update the cache.
+    $this->drupalGet('fr/test-page');
+    $this->assertResponse(200);
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for adminUser against the language "fr".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_id . ':' . 'fr');
+    $this->assertEqual(in_array('user:' . $admin_user_id, $cache->tags), 'A cache tag in the toolbar cache under the key "user" exists for admin_user against the language "fr".');
+
+    // Log in the adminUser2 user. We will use this user as a control to
+    // verify that clearing a cache tag for adminUser does not clear the cache
+    // for adminUser2.
+    $this->drupalLogin($this->adminUser2);
+
+    // Request a page in 'en' to create the cache.
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for adminUser2 against the language "en".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_2_id . ':' . 'en');
+    $this->assertEqual(in_array('user:' . $admin_user_2_id, $cache->tags), 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "en".');
+
+    // Request a page in 'fr' to create the cache.
+    $this->drupalGet('fr/test-page');
+    $this->assertResponse(200);
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for adminUser against the language "fr".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_2_id . ':' . 'fr');
+    $this->assertEqual(in_array('user:' . $admin_user_2_id, $cache->tags), 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "fr".');
+
+    // Log in the admin user and clear the caches for this user using a tag.
+    $this->drupalLogin($this->adminUser);
+    Cache::invalidateTags(array('user:' . $admin_user_id));
+
+    // Assert that no toolbar cache exists for adminUser against the
+    // language "en".
+    $cache = $toolbarCache->get($admin_user_id . ':' . 'en');
+    $this->assertFalse($cache, 'No toolbar cache exists for admin_user against the language "en".');
+
+    // Assert that no toolbar cache exists for adminUser against the
+    // language "fr".
+    $cache = $toolbarCache->get($admin_user_id . ':' . 'fr');
+    $this->assertFalse($cache, 'No toolbar cache exists for admin_user against the language "fr".');
+
+    // Log in adminUser2 and verify that this user's caches still exist.
+    $this->drupalLogin($this->adminUser2);
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for adminUser2 against the language "en".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_2_id . ':' . 'en');
+    $this->assertEqual(in_array('user:' . $admin_user_2_id, $cache->tags), 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "en".');
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for adminUser2 against the language "fr".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_2_id . ':' . 'fr');
+    $this->assertEqual(in_array('user:' . $admin_user_2_id, $cache->tags), 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "fr".');
+  }
+
+  /**
    * Tests that changes to a user account by another user clears the changed
    * account's toolbar cached, not the user's who took the action.
    */
   function testNonCurrentUserAccountUpdates() {
+    $toolbarCache = $this->container->get('cache.toolbar');
     $admin_user_id = $this->adminUser->id();
     $admin_user_2_id = $this->adminUser2->id();
     $this->hash = $this->getSubtreesHash();
@@ -252,7 +338,9 @@ function testNonCurrentUserAccountUpdates() {
    * Tests that toolbar cache is cleared when string translations are made.
    */
   function testLocaleTranslationSubtreesHashCacheClear() {
+    $toolbarCache = $this->container->get('cache.toolbar');
     $admin_user = $this->adminUser;
+    $admin_user_id = $this->adminUser->id();
     // User to translate and delete string.
     $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
 
@@ -286,6 +374,11 @@ function testLocaleTranslationSubtreesHashCacheClear() {
     $this->drupalGet($langcode . '/test-page');
     $this->assertResponse(200);
 
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for adminUser against the language "xx".
+    $cache = $toolbarCache->get('toolbar_' . $admin_user_id . ':' . $langcode);
+    $this->assertEqual(in_array('user:' . $admin_user_id, $cache->tags), 'A cache tag in the toolbar cache under the key "user" exists for admin_user against the language "xx".');
+
     // Get a baseline hash for the admin menu subtrees before translating one
     // of the menu link items.
     $original_subtree_hash = $this->getSubtreesHash();
@@ -333,7 +426,7 @@ function testLocaleTranslationSubtreesHashCacheClear() {
   }
 
   /**
-   * Tests that the 'toolbar/subtrees/{hash}' is reachable and correct.
+   * Tests that the 'toolbar/subtrees/{hash}' is reachable.
    */
   function testSubtreesJsonRequest() {
     $admin_user = $this->adminUser;
@@ -343,12 +436,28 @@ function testSubtreesJsonRequest() {
 
     $this->drupalGetJSON('toolbar/subtrees/' . $subtrees_hash);
     $this->assertResponse('200');
-    $json_callback_start = substr($this->getRawContent(), 0, 39);
-    $json_callback_end = substr($this->getRawContent(), -2, 2);
-    $json = substr($this->getRawContent(), 39, strlen($this->getRawContent()) - 41);
-    $this->assertTrue($json_callback_start === '/**/Drupal.toolbar.setSubtrees.resolve(' && $json_callback_end === ');', 'Subtrees response is wrapped in callback.');
-    $subtrees = Json::decode($json);
-    $this->assertEqual(array_keys($subtrees), ['system-admin_content', 'system-admin_structure', 'system-themes_page', 'system-modules_list', 'system-admin_config', 'entity-user-collection', 'front'], 'Correct subtrees JSON returned.');
+
+    // Test that the subtrees hash changes with a different language code and
+    // that JSON is returned when a language code is specified.
+    // Create a new language with the langcode 'xx'.
+    $langcode = 'xx';
+    // The English name for the language. This will be translated.
+    $name = $this->randomMachineName(16);
+    $edit = array(
+      'predefined_langcode' => 'custom',
+      'langcode' => $langcode,
+      'label' => $name,
+      'direction' => LanguageInterface::DIRECTION_LTR,
+    );
+    $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
+
+    // Get a page with the new language langcode in the URL.
+    $this->drupalGet('xx/test-page');
+    // Request a new page to refresh the drupalSettings object.
+    $subtrees_hash = $this->getSubtreesHash();
+
+    $this->drupalGetJSON('toolbar/subtrees/' . $subtrees_hash . '/' . $langcode);
+    $this->assertResponse('200');
   }
 
   /**
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index d837b17..d0b4059 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -6,7 +6,6 @@
  */
 
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Menu\MenuTreeParameters;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -14,6 +13,7 @@
 use Drupal\Component\Datetime\DateTimePlus;
 use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\user\Entity\Role;
 use Drupal\Core\Url;
 
 /**
@@ -158,9 +158,10 @@ function toolbar_toolbar() {
   // toolbar_subtrees route. We provide the JavaScript requesting that JSONP
   // script here with the hash parameter that is needed for that route.
   // @see toolbar_subtrees_jsonp()
-  list($hash, $hash_cacheability) = _toolbar_get_subtrees_hash();
+  $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
   $subtrees_attached['drupalSettings']['toolbar'] = [
-    'subtreesHash' => $hash,
+    'subtreesHash' => _toolbar_get_subtrees_hash($langcode),
+    'langcode' => $langcode,
   ];
 
   // The administration element has a link that is themed to correspond to
@@ -197,7 +198,6 @@ function toolbar_toolbar() {
     ),
     '#weight' => -15,
   );
-  $hash_cacheability->applyTo($items['administration']);
 
   return $items;
 }
@@ -272,30 +272,8 @@ function toolbar_menu_navigation_links(array $tree) {
 
 /**
  * Returns the rendered subtree of each top-level toolbar link.
- *
- * @return array
- *   An array with the following key-value pairs:
- *   - 'subtrees': the rendered subtrees
- *   - 'cacheability: the associated cacheability.
  */
 function toolbar_get_rendered_subtrees() {
-  $data = [
-    '#pre_render' => ['_toolbar_do_get_rendered_subtrees'],
-    '#cache' => [
-      'keys' => [
-        'toolbar_rendered_subtrees',
-      ],
-    ],
-    '#cache_properties' => ['#subtrees'],
-  ];
-  \Drupal::service('renderer')->renderPlain($data);
-  return [$data['#subtrees'], CacheableMetadata::createFromRenderArray($data)];
-}
-
-/**
- * #pre_render callback for toolbar_get_rendered_subtrees().
- */
-function _toolbar_do_get_rendered_subtrees(array $data) {
   $menu_tree = \Drupal::service('toolbar.menu_tree');
   // Load the administration menu. The first level is the "Administration" link.
   // In order to load the children of that link and the subsequent two levels,
@@ -311,15 +289,12 @@ function _toolbar_do_get_rendered_subtrees(array $data) {
   );
   $tree = $menu_tree->transform($tree, $manipulators);
   $subtrees = array();
-  // Calculated the combined cacheability of all subtrees.
-  $cacheability = new CacheableMetadata();
   foreach ($tree as $element) {
     /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
     $link = $element->link;
     if ($element->subtree) {
       $subtree = $menu_tree->build($element->subtree);
       $output = \Drupal::service('renderer')->renderPlain($subtree);
-      $cacheability = $cacheability->merge(CacheableMetadata::createFromRenderArray($subtree));
     }
     else {
       $output = '';
@@ -331,22 +306,49 @@ function _toolbar_do_get_rendered_subtrees(array $data) {
 
     $subtrees[$id] = $output;
   }
-
-  // Store the subtrees, along with the cacheability metadata.
-  $cacheability->applyTo($data);
-  $data['#subtrees'] = $subtrees;
-
-  return $data;
+  return $subtrees;
 }
 
 /**
  * Returns the hash of the per-user rendered toolbar subtrees.
  *
+ * @param string $langcode
+ *   The langcode of the current request.
+ *
  * @return string
  *   The hash of the admin_menu subtrees.
  */
-function _toolbar_get_subtrees_hash() {
-  list($subtrees, $cacheability) = toolbar_get_rendered_subtrees();
-  $hash = Crypt::hashBase64(serialize($subtrees));
-  return [$hash, $cacheability];
+function _toolbar_get_subtrees_hash($langcode) {
+  $uid = \Drupal::currentUser()->id();
+  $cid = _toolbar_get_user_cid($uid, $langcode);
+  if ($cache = \Drupal::cache('toolbar')->get($cid)) {
+    $hash = $cache->data;
+  }
+  else {
+    $subtrees = toolbar_get_rendered_subtrees();
+    $hash = Crypt::hashBase64(serialize($subtrees));
+    // Cache using a tag 'user' so that we can invalidate all user-specific
+    // caches later, based on the user's ID regardless of language.
+    // Clear the cache when the 'locale' tag is deleted. This ensures a fresh
+    // subtrees rendering when string translations are made.
+    $role_list_cache_tags = \Drupal::entityManager()->getDefinition('user_role')->getListCacheTags();
+    \Drupal::cache('toolbar')->set($cid, $hash, Cache::PERMANENT, Cache::mergeTags(array('user:' . $uid, 'locale', 'config:system.menu.admin'), $role_list_cache_tags));
+  }
+  return $hash;
 }
+
+/**
+ * Returns a cache ID from the user and language IDs.
+ *
+ * @param int $uid
+ *   A user ID.
+ * @param string $langcode
+ *   The langcode of the current request.
+ *
+ * @return string
+ *   A unique cache ID for the user.
+ */
+function _toolbar_get_user_cid($uid, $langcode) {
+  return 'toolbar_' . $uid . ':' . $langcode;
+}
+
diff --git a/core/modules/toolbar/toolbar.routing.yml b/core/modules/toolbar/toolbar.routing.yml
index c1b8581..4cb014e 100644
--- a/core/modules/toolbar/toolbar.routing.yml
+++ b/core/modules/toolbar/toolbar.routing.yml
@@ -1,5 +1,5 @@
 toolbar.subtrees:
-  path: '/toolbar/subtrees/{hash}'
+  path: '/toolbar/subtrees/{hash}/{langcode}'
   defaults:
     _controller: '\Drupal\toolbar\Controller\ToolbarController::subtreesJsonp'
     langcode: null
