 core/core.services.yml                             |    2 +-
 core/includes/common.inc                           |   65 +++++-----
 core/includes/menu.inc                             |    4 +
 core/includes/theme.inc                            |   96 +++++++++-----
 core/lib/Drupal/Core/Utility/LinkGenerator.php     |   66 ++++++----
 .../Drupal/Core/Utility/LinkGeneratorInterface.php |    9 +-
 core/misc/active-link.js                           |   63 +++++++++
 core/misc/ajax.js                                  |    4 +-
 core/misc/drupal.js                                |    2 +-
 .../Drupal/image/Tests/ImageFieldDisplayTest.php   |    2 +-
 core/modules/language/language.negotiation.inc     |    1 +
 .../Drupal/language/Plugin/Block/LanguageBlock.php |    1 +
 .../language/Tests/LanguageSwitchingTest.php       |  136 +++++++++++++++++++-
 .../Tests/LanguageUILanguageNegotiationTest.php    |    7 +-
 .../Controller/LanguageTestController.php          |    3 +
 .../Drupal/system/Controller/SystemController.php  |   63 +++++++++
 .../Drupal/system/Tests/Common/JavaScriptTest.php  |    2 +-
 .../Drupal/system/Tests/Theme/FunctionsTest.php    |   28 ++--
 core/modules/system/system.module                  |   36 ++++++
 .../Controller/CommonTestController.php            |    5 +
 .../Tests/Core/Utility/LinkGeneratorTest.php       |  111 ++++++++++++----
 21 files changed, 575 insertions(+), 131 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index bffe0aa..919b112 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -271,7 +271,7 @@ services:
       - { name: persist }
   link_generator:
     class: Drupal\Core\Utility\LinkGenerator
-    arguments: ['@url_generator', '@module_handler', '@language_manager']
+    arguments: ['@url_generator', '@module_handler', '@language_manager', '@path.alias_manager.cached']
     calls:
       - [setRequest, ['@?request']]
   router.dynamic:
diff --git a/core/includes/common.inc b/core/includes/common.inc
index c946eaf..be0eeef 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1214,12 +1214,24 @@ function drupal_http_header_attributes(array $attributes = array()) {
  *     internal to the site, $options['language'] is used to determine whether
  *     the link is "active", or pointing to the current page (the language as
  *     well as the path must match). This element is also used by url().
+ *   - 'set_active_class' (default FALSE): Whether l() should compare the $path,
+ *     language and query options to the current URL to determine whether the
+ *     link is "active". If so, an "active" class will be applied to the link.
+ *     It is important to use this sparingly since it is usually unnecessary and
+ *     requires extra processing.
+ *     For anonymous users, the "active" class will be calculated on the server,
+ *     because most sites serve each anonymous user the same cached page anyway.
+ *     For authenticated users, the "active" class will be calculated on the
+ *     client (through JavaScript), only data- attributes are added to links to
+ *     prevent breaking the render cache. The JavaScript is added in
+ *     system_page_build().
  *   - Additional $options elements used by the url() function.
  *
  * @return string
  *   An HTML string containing a link to the given path.
  *
  * @see url()
+ * @see system_page_build()
  */
 function l($text, $path, array $options = array()) {
   // Start building a structured representation of our link to be altered later.
@@ -1235,6 +1247,7 @@ function l($text, $path, array $options = array()) {
     'query' => array(),
     'html' => FALSE,
     'language' => NULL,
+    'set_active_class' => FALSE,
   );
 
   // Add a hreflang attribute if we know the language of this link's url and
@@ -1243,35 +1256,21 @@ function l($text, $path, array $options = array()) {
     $variables['options']['attributes']['hreflang'] = $variables['options']['language']->id;
   }
 
-  // Because l() is called very often we statically cache values that require an
-  // extra function call.
-  static $drupal_static_fast;
-  if (!isset($drupal_static_fast['active'])) {
-    $drupal_static_fast['active'] = &drupal_static(__FUNCTION__);
-  }
-  $active = &$drupal_static_fast['active'];
-  if (!isset($active)) {
-    $active = array(
-      'path' => current_path(),
-      'front_page' => drupal_is_front_page(),
-      'language' => language(Language::TYPE_URL)->id,
-      'query' => \Drupal::service('request')->query->all(),
-    );
-  }
-
-  // Determine whether this link is "active', meaning that it links to the
-  // current page. It is important that we stop checking "active" conditions if
-  // we know the link is not active. This helps ensure that l() remains fast.
-  // An active link's path is equal to the current path.
-  $variables['url_is_active'] = ($path == $active['path'] || ($path == '<front>' && $active['front_page']))
-  // The language of an active link is equal to the current language.
-  && (empty($variables['options']['language']) || $variables['options']['language']->id == $active['language'])
-  // The query parameters of an active link are equal to the current parameters.
-  && ($variables['options']['query'] == $active['query']);
+  // Set the "active" class if the 'set_active_class' option is not empty.
+  if (!empty($variables['options']['set_active_class'])) {
+    // Add a "data-drupal-link-query" attribute to let the drupal.active-link
+    // library know the query in a standardized manner.
+    if (!empty($variables['options']['query'])) {
+      $query = $variables['options']['query'];
+      ksort($query);
+      $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query);
+    }
 
-  // Add the "active" class if appropriate.
-  if ($variables['url_is_active']) {
-    $variables['options']['attributes']['class'][] = 'active';
+    // Add a "data-drupal-link-system-path" attribute to let the
+    // drupal.active-link library know the path in a standardized manner.
+    if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) {
+      $variables['options']['attributes']['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getSystemPath($path);
+    }
   }
 
   // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags()
@@ -2149,6 +2148,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
           // @todo Make this less hacky: http://drupal.org/node/1547376.
           $scriptPath = $GLOBALS['script_path'];
           $pathPrefix = '';
+          $current_query = \Drupal::service('request')->query->all();
           url('', array('script' => &$scriptPath, 'prefix' => &$pathPrefix));
           $current_path = current_path();
           $current_path_is_admin = FALSE;
@@ -2156,13 +2156,20 @@ function drupal_add_js($data = NULL, $options = NULL) {
           if (!(defined('MAINTENANCE_MODE') && MAINTENANCE_MODE === 'update')) {
             $current_path_is_admin = path_is_admin($current_path);
           }
-          $javascript['settings']['data'][] = array(
+          $path = array(
             'basePath' => base_path(),
             'scriptPath' => $scriptPath,
             'pathPrefix' => $pathPrefix,
             'currentPath' => $current_path,
             'currentPathIsAdmin' => $current_path_is_admin,
+            'isFront' => drupal_is_front_page(),
+            'currentLanguage' => \Drupal::languageManager()->getLanguage(Language::TYPE_URL)->id,
           );
+          if (!empty($current_query)) {
+            ksort($current_query);
+            $path['currentQuery'] = (object) $current_query;
+          }
+          $javascript['settings']['data'][] = array('path' => $path);
         }
         // All JavaScript settings are placed in the header of the page with
         // the library weight so that inline scripts appear afterwards.
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index b83c323..01fba6d 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -1705,6 +1705,7 @@ function theme_menu_link(array $variables) {
   if ($element['#below']) {
     $sub_menu = drupal_render($element['#below']);
   }
+  $element['#localized_options']['set_active_class'] = TRUE;
   $output = l($element['#title'], $element['#href'], $element['#localized_options']);
   return '<li' . new Attribute($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
 }
@@ -1740,6 +1741,8 @@ function theme_menu_local_task($variables) {
     $link['localized_options']['html'] = TRUE;
     $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active));
   }
+  $link['localized_options']['set_active_class'] = TRUE;
+
   if (!empty($link['href'])) {
     // @todo - remove this once all pages are converted to routes.
     $a_tag = l($link_text, $link['href'], $link['localized_options']);
@@ -1771,6 +1774,7 @@ function theme_menu_local_action($variables) {
   );
   $link['localized_options']['attributes']['class'][] = 'button';
   $link['localized_options']['attributes']['class'][] = 'button-action';
+  $link['localized_options']['set_active_class'] = TRUE;
 
   $output = '<li>';
   // @todo Remove this check and the call to l() when all pages are converted to
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index aaa3b74..b26c75a 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1189,6 +1189,18 @@ function template_preprocess_status_messages(&$variables) {
  *     l() as its $options parameter.
  *   - attributes: A keyed array of attributes for the UL containing the
  *     list of links.
+ *   - set_active_class: (optional) Whether theme_links() should compare the
+ *     route_name + route_parameters or href (path), language and query options
+ *     to the current URL for each of the links, to determine whether the link
+ *     is "active". If so, an "active" class will be applied to the list item
+ *     containing the link. It is important to use this sparingly since it is
+ *     usually unnecessary and requires extra processing.
+ *     For anonymous users, the "active" class will be calculated on the server,
+ *     because most sites serve each anonymous user the same cached page anyway.
+ *     For authenticated users, the "active" class will be calculated on the
+ *     client (through JavaScript), only data- attributes are added to list
+ *     items to prevent breaking the render cache. The JavaScript is added in
+ *     system_page_build().
  *   - heading: (optional) A heading to precede the links. May be an
  *     associative array or a string. If it's an array, it can have the
  *     following elements:
@@ -1204,6 +1216,19 @@ function template_preprocess_status_messages(&$variables) {
  *     navigate to or skip the links. See
  *     http://juicystudio.com/article/screen-readers-display-none.php and
  *     http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
+ *
+ * theme_links() unfortunately duplicates the "active" class handling of l() and
+ * LinkGenerator::generate() because it needs to be able to set the "active"
+ * class not on the links themselves ("a" tags), but on the list items ("li"
+ * tags) that contain the links. This is necessary for CSS to be able to style
+ * list items differently when the link is active, since CSS does not yet allow
+ * one to style list items only if it contains a certain element with a certain
+ * class. I.e. we cannot yet convert this jQuery selector to a CSS selector:
+ *   jQuery('li:has("a.active")')
+ *
+ * @see l()
+ * @see \Drupal\Core\Utility\LinkGenerator::generate()
+ * @see system_page_build()
  */
 function theme_links($variables) {
   $links = $variables['links'];
@@ -1237,8 +1262,7 @@ function theme_links($variables) {
 
     $num_links = count($links);
     $i = 0;
-    $active = \Drupal::linkGenerator()->getActive();
-    $language_url = \Drupal::languageManager()->getLanguage(Language::TYPE_URL);
+    $active_route = \Drupal::linkGenerator()->getActive();
 
     foreach ($links as $key => $link) {
       $i++;
@@ -1250,16 +1274,16 @@ function theme_links($variables) {
         'ajax' => NULL,
       );
 
-      $class = array();
+      $li_attributes = array('class' => array());
       // Use the array key as class name.
-      $class[] = drupal_html_class($key);
+      $li_attributes['class'][] = drupal_html_class($key);
       // Add odd/even, first, and last classes.
-      $class[] = ($i % 2 ? 'odd' : 'even');
+      $li_attributes['class'][] = ($i % 2 ? 'odd' : 'even');
       if ($i == 1) {
-        $class[] = 'first';
+        $li_attributes['class'][] = 'first';
       }
       if ($i == $num_links) {
-        $class[] = 'last';
+        $li_attributes['class'][] = 'last';
       }
 
       $link_element = array(
@@ -1272,30 +1296,36 @@ function theme_links($variables) {
         '#ajax' => $link['ajax'],
       );
 
-      // Handle links and ensure that the active class is added on the LIs.
-      if (isset($link['route_name'])) {
-        $variables = array(
-          'options' => array(),
-        );
-        if (!empty($link['language'])) {
-          $variables['options']['language'] = $link['language'];
-        }
+      // Handle links and ensure that the active class is added on the LIs, but
+      // only if the 'set_active_class' option is not empty.
+      if (isset($link['href']) || isset($link['route_name'])) {
+        if (!empty($variables['set_active_class'])) {
+          if (!empty($link['language'])) {
+            $li_attributes['hreflang'] = $link['language']->id;
+          }
 
-        if (($link['route_name'] == $active['route_name'])
-        // The language of an active link is equal to the current language.
-        && (empty($variables['options']['language']) || ($variables['options']['language']->id == $active['language']))
-        && ($link['route_parameters'] == $active['parameters'])) {
-          $class[] = 'active';
-        }
+          // Add a "data-drupal-link-query" attribute to let the
+          // drupal.active-link library know the query in a standardized
+          // manner.
+          if (!empty($link['query'])) {
+            $query = $link['query'];
+            ksort($query);
+            $li_attributes['data-drupal-link-query'] = Json::encode($query);
+          }
 
-        $item = drupal_render($link_element);
-      }
-      elseif (isset($link['href'])) {
-        $is_current_path = ($link['href'] == current_path() || ($link['href'] == '<front>' && drupal_is_front_page()));
-        $is_current_language = (empty($link['language']) || $link['language']->id == $language_url->id);
-        if ($is_current_path && $is_current_language) {
-          $class[] = 'active';
+          if (isset($link['route_name'])) {
+            $path = \Drupal::service('url_generator')->getPathFromRoute($link['route_name'], $link['route_parameters']);
+          }
+          else {
+            $path = $link['href'];
+          }
+
+          // Add a "data-drupal-link-system-path" attribute to let the
+          // drupal.active-link library know the path in a standardized
+          // manner.
+          $li_attributes['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getSystemPath($path);
         }
+
         $item = drupal_render($link_element);
       }
       // Handle title-only text items.
@@ -1310,7 +1340,7 @@ function theme_links($variables) {
         }
       }
 
-      $output .= '<li' . new Attribute(array('class' => $class)) . '>';
+      $output .= '<li' . new Attribute($li_attributes) . '>';
       $output .= $item;
       $output .= '</li>';
     }
@@ -2239,7 +2269,8 @@ function template_preprocess_page(&$variables) {
       '#heading' => array(
         'text' => t('Main menu'),
         'class' => array('visually-hidden'),
-      )
+      ),
+      '#set_active_class' => TRUE,
     );
   }
   if (!empty($variables['secondary_menu'])) {
@@ -2249,7 +2280,8 @@ function template_preprocess_page(&$variables) {
       '#heading' => array(
         'text' => t('Secondary menu'),
         'class' => array('visually-hidden'),
-      )
+      ),
+      '#set_active_class' => TRUE,
     );
   }
 
@@ -2579,7 +2611,7 @@ function drupal_common_theme() {
       'template' => 'status-messages',
     ),
     'links' => array(
-      'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array()),
+      'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array(), 'set_active_class' => FALSE),
     ),
     'dropbutton_wrapper' => array(
       'variables' => array('children' => NULL),
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index 3f63620..ba2b06e 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -7,12 +7,15 @@
 
 namespace Drupal\Core\Utility;
 
+use Drupal\Component\Utility\Json;
 use Drupal\Component\Utility\String;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Path\AliasManagerInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Session\AccountInterface;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -22,13 +25,6 @@
 class LinkGenerator implements LinkGeneratorInterface {
 
   /**
-   * Stores some information about the current request, like the language.
-   *
-   * @var array
-   */
-  protected $active;
-
-  /**
    * The url generator.
    *
    * @var \Drupal\Core\Routing\UrlGeneratorInterface
@@ -50,6 +46,13 @@ class LinkGenerator implements LinkGeneratorInterface {
   protected $languageManager;
 
   /**
+   * The path alias manager.
+   *
+   * @var \Drupal\Core\Path\AliasManagerInterface
+   */
+  protected $aliasManager;
+
+  /**
    * Constructs a LinkGenerator instance.
    *
    * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
@@ -58,11 +61,14 @@ class LinkGenerator implements LinkGeneratorInterface {
    *   The module handler.
    * @param \Drupal\Core\Language\LanguageManager $language_manager
    *   The language manager.
+   * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
+   *   The path alias manager.
    */
-  public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManager $language_manager) {
+  public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManager $language_manager, AliasManagerInterface $alias_manager) {
     $this->urlGenerator = $url_generator;
     $this->moduleHandler = $module_handler;
     $this->languageManager = $language_manager;
+    $this->aliasManager = $alias_manager;
   }
 
   /**
@@ -93,6 +99,15 @@ public function getActive() {
 
   /**
    * {@inheritdoc}
+   *
+   * For anonymous users, the "active" class will be calculated on the server,
+   * because most sites serve each anonymous user the same cached page anyway.
+   * For authenticated users, the "active" class will be calculated on the
+   * client (through JavaScript), only data- attributes are added to links to
+   * prevent breaking the render cache. The JavaScript is added in
+   * system_page_build().
+   *
+   * @see system_page_build()
    */
   public function generate($text, $route_name, array $parameters = array(), array $options = array()) {
     // Start building a structured representation of our link to be altered later.
@@ -110,30 +125,31 @@ public function generate($text, $route_name, array $parameters = array(), array
       'query' => array(),
       'html' => FALSE,
       'language' => NULL,
+      'set_active_class' => FALSE,
     );
+
     // Add a hreflang attribute if we know the language of this link's url and
     // hreflang has not already been set.
     if (!empty($variables['options']['language']) && !isset($variables['options']['attributes']['hreflang'])) {
       $variables['options']['attributes']['hreflang'] = $variables['options']['language']->id;
     }
 
-    // This is only needed for the active class. The generator also combines
-    // the parameters and $options['query'] and adds parameters that are not
-    // path slugs as query strings.
-    $full_parameters = $parameters + (array) $variables['options']['query'];
-
-    // Determine whether this link is "active", meaning that it has the same
-    // URL path and query string as the current page. Note that this may be
-    // removed from l() in https://drupal.org/node/1979468 and would be removed
-    // or altered here also.
-    $variables['url_is_active'] = $route_name == $this->active['route_name']
-      // The language of an active link is equal to the current language.
-      && (empty($variables['options']['language']) || $variables['options']['language']->id == $this->active['language'])
-      && $full_parameters == $this->active['parameters'];
-
-    // Add the "active" class if appropriate.
-    if ($variables['url_is_active']) {
-      $variables['options']['attributes']['class'][] = 'active';
+    // Set the "active" class if the 'set_active_class' option is not empty.
+    if (!empty($variables['options']['set_active_class'])) {
+      // Add a "data-drupal-link-query" attribute to let the
+      // drupal.active-link library know the query in a standardized manner.
+      if (!empty($variables['options']['query'])) {
+        $query = $variables['options']['query'];
+        ksort($query);
+        $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query);
+      }
+
+      // Add a "data-drupal-link-system-path" attribute to let the
+      // drupal.active-link library know the path in a standardized manner.
+      if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) {
+        $path = $this->urlGenerator->getPathFromRoute($route_name, $parameters);
+        $variables['options']['attributes']['data-drupal-link-system-path'] = $this->aliasManager->getSystemPath($path);
+      }
     }
 
     // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags()
diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
index 8bc7eb6..b832873 100644
--- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
@@ -36,8 +36,8 @@
    * @param array $options
    *   (optional) An associative array of additional options. Defaults to an
    *   empty array. It may contain the following elements:
-   *   - 'query': An array of query key/value-pairs (without any URL-encoding) to
-   *     append to the URL.
+   *   - 'query': An array of query key/value-pairs (without any URL-encoding)
+   *     to append to the URL.
    *   - absolute: Whether to force the output to be an absolute link (beginning
    *     with http:). Useful for links that will be displayed outside the site,
    *     such as in an RSS feed. Defaults to FALSE.
@@ -55,6 +55,11 @@
    *     internal to the site, $options['language'] is used to determine whether
    *     the link is "active", or pointing to the current page (the language as
    *     well as the path must match).
+   *   - 'set_active_class' (default FALSE): Whether this method should compare
+   *     the $route_name, $parameters, language and query options to the current
+   *     URL to determine whether the link is "active". If so, an "active" class
+   *     will be applied to the link. It is important to use this sparingly
+   *     since it is usually unnecessary and requires extra processing.
    *
    * @return string
    *   An HTML string containing a link to the given route and parameters.
diff --git a/core/misc/active-link.js b/core/misc/active-link.js
new file mode 100644
index 0000000..cb1caa6
--- /dev/null
+++ b/core/misc/active-link.js
@@ -0,0 +1,63 @@
+/**
+ * @file
+ * Attaches behaviors for Drupal's active link marking.
+ */
+
+(function (Drupal, drupalSettings) {
+
+"use strict";
+
+/**
+ * Append active class.
+ *
+ * The link is only active if its path corresponds to the current path, the
+ * language of the linked path is equal to the current language, and if the
+ * query parameters of the link equal those of the current request, since the
+ * same request with different query parameters may yield a different page
+ * (e.g. pagers, exposed View filters).
+ *
+ * Does not discriminate based on element type, so allows you to set the active
+ * class on any element: a, li…
+ */
+Drupal.behaviors.l = {
+  attach: function queryL (context) {
+    // Start by finding all potentially active links.
+    var path = drupalSettings.path;
+    var queryString = JSON.stringify(path.currentQuery);
+    var querySelector = path.currentQuery ? "[data-drupal-link-query='" + queryString + "']" : ':not([data-drupal-link-query])';
+    var originalSelectors = ['[data-drupal-link-system-path="' + path.currentPath + '"]'];
+    var selectors;
+
+    // If this is the front page, we have to check for the <front> path as well.
+    if (path.isFront) {
+      originalSelectors.push('[data-drupal-link-system-path="<front>"]');
+    }
+
+    // Add language filtering.
+    selectors = [].concat(
+      // Links without any hreflang attributes (most of them).
+      originalSelectors.map(function (selector) { return selector + ':not([hreflang])';}),
+      // Links with hreflang equals to the current language.
+      originalSelectors.map(function (selector) { return selector + '[hreflang="' + path.currentLanguage + '"]';})
+    );
+
+    // Add query string selector for pagers, exposed filters.
+    selectors = selectors.map(function (current) { return current + querySelector; });
+
+    // Query the DOM.
+    var activeLinks = context.querySelectorAll(selectors.join(','));
+    for (var i = 0, il = activeLinks.length; i < il; i += 1) {
+      activeLinks[i].classList.add('active');
+    }
+  },
+  detach: function (context, settings, trigger) {
+    if (trigger === 'unload') {
+      var activeLinks = context.querySelectorAll('[data-drupal-link-system-path].active');
+      for (var i = 0, il = activeLinks.length; i < il; i += 1) {
+        activeLinks[i].classList.remove('active');
+      }
+    }
+  }
+};
+
+})(Drupal, drupalSettings);
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index ad9dcae..63eb4a7 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -597,7 +597,7 @@ Drupal.AjaxCommands.prototype = {
       case 'empty':
       case 'remove':
         settings = response.settings || ajax.settings || drupalSettings;
-        Drupal.detachBehaviors(wrapper, settings);
+        Drupal.detachBehaviors(wrapper.get(0), settings);
     }
 
     // Add the new content to the page.
@@ -625,7 +625,7 @@ Drupal.AjaxCommands.prototype = {
     if (new_content.parents('html').length > 0) {
       // Apply any settings from the returned JSON if available.
       settings = response.settings || ajax.settings || drupalSettings;
-      Drupal.attachBehaviors(new_content, settings);
+      Drupal.attachBehaviors(new_content.get(0), settings);
     }
   },
 
diff --git a/core/misc/drupal.js b/core/misc/drupal.js
index 6c04130..e994ec6 100644
--- a/core/misc/drupal.js
+++ b/core/misc/drupal.js
@@ -267,7 +267,7 @@ Drupal.t = function (str, args, options) {
  * Returns the URL to a Drupal page.
  */
 Drupal.url = function (path) {
-  return drupalSettings.basePath + drupalSettings.scriptPath + path;
+  return drupalSettings.path.basePath + drupalSettings.path.scriptPath + path;
 };
 
 /**
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
index 0071179..88571c6 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
@@ -111,7 +111,7 @@ function _testImageFieldFormatters($scheme) {
       '#width' => 40,
       '#height' => 20,
     );
-    $default_output = l($image, 'node/' . $nid, array('html' => TRUE, 'attributes' => array('class' => 'active')));
+    $default_output = l($image, 'node/' . $nid, array('html' => TRUE));
     $this->drupalGet('node/' . $nid);
     $this->assertRaw($default_output, 'Image linked to content formatter displaying correctly on full node view.');
 
diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc
index f87284d..f59a94e 100644
--- a/core/modules/language/language.negotiation.inc
+++ b/core/modules/language/language.negotiation.inc
@@ -399,6 +399,7 @@ function language_switcher_url($type, $path) {
       'title'      => $language->name,
       'language'   => $language,
       'attributes' => array('class' => array('language-link')),
+      'set_active_class' => TRUE,
     );
   }
 
diff --git a/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php b/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
index fae6081..e347de5 100644
--- a/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
+++ b/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
@@ -49,6 +49,7 @@ public function build() {
             "language-switcher-{$links->method_id}",
           ),
         ),
+        '#set_active_class' => TRUE,
       );
     }
     return $build;
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
index 4613f43..826fcf9 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
@@ -55,9 +55,70 @@ function testLanguageBlock() {
     $edit = array('language_interface[enabled][language-url]' => '1');
     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
 
+    $this->doTestLanguageBlockAuthenticated($block->label());
+    $this->doTestLanguageBlockAnonymous($block->label());
+  }
+
+  /**
+   * For authenticated users, the "active" class is set by JavaScript.
+   *
+   * @param string $block_label
+   *   The label of the language switching block.
+   *
+   * @see testLanguageBlock()
+   */
+  protected function doTestLanguageBlockAuthenticated($block_label) {
+    // Assert that the language switching block is displayed on the frontpage.
+    $this->drupalGet('');
+    $this->assertText($block_label, 'Language switcher block found.');
+
+    // Assert that each list item and anchor element has the appropriate data-
+    // attributes.
+    list($language_switcher) = $this->xpath('//div[@id=:id]/div[contains(@class, "content")]', array(':id' => 'block-test-language-block'));
+    $list_items = array();
+    $anchors = array();
+    foreach ($language_switcher->ul->li as $list_item) {
+      $classes = explode(" ", (string) $list_item['class']);
+      list($langcode) = array_intersect($classes, array('en', 'fr'));
+      $list_items[] = array(
+        'langcode_class' => $langcode,
+        'data-drupal-link-system-path' => (string) $list_item['data-drupal-link-system-path'],
+      );
+      $anchors[] = array(
+        'hreflang' => (string) $list_item->a['hreflang'],
+        'data-drupal-link-system-path' => (string) $list_item->a['data-drupal-link-system-path'],
+      );
+    }
+    $expected_list_items = array(
+      0 => array('langcode_class' => 'en', 'data-drupal-link-system-path' => 'user/2'),
+      1 => array('langcode_class' => 'fr', 'data-drupal-link-system-path' => 'user/2'),
+    );
+    $this->assertIdentical($list_items, $expected_list_items, 'The list items have the correct attributes that will allow the drupal.active-link library to mark them as active.');
+    $expected_anchors = array(
+      0 => array('hreflang' => 'en', 'data-drupal-link-system-path' => 'user/2'),
+      1 => array('hreflang' => 'fr', 'data-drupal-link-system-path' => 'user/2'),
+    );
+    $this->assertIdentical($anchors, $expected_anchors, 'The anchors have the correct attributes that will allow the drupal.active-link library to mark them as active.');
+    $settings = $this->drupalGetSettings();
+    $this->assertIdentical($settings['path']['currentPath'], 'user/2', 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
+  }
+
+  /**
+   * For anonymous users, the "active" class is set by PHP.
+   *
+   * @param string $block_label
+   *   The label of the language switching block.
+   *
+   * @see testLanguageBlock()
+   */
+  protected function doTestLanguageBlockAnonymous($block_label) {
+    $this->drupalLogout();
+
     // Assert that the language switching block is displayed on the frontpage.
     $this->drupalGet('');
-    $this->assertText($block->label(), 'Language switcher block found.');
+    $this->assertText($block_label, 'Language switcher block found.');
 
     // Assert that only the current language is marked as active.
     list($language_switcher) = $this->xpath('//div[@id=:id]/div[contains(@class, "content")]', array(':id' => 'block-test-language-block'));
@@ -104,7 +165,80 @@ function testLanguageLinkActiveClass() {
     $edit = array('language_interface[enabled][language-url]' => '1');
     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
 
+    $this->doTestLanguageLinkActiveClassAuthenticated();
+    $this->doTestLanguageLinkActiveClassAnonymous();
+  }
+
+  /**
+   * For authenticated users, the "active" class is set by JavaScript.
+   *
+   * @see testLanguageLinkActiveClass()
+   */
+  protected function doTestLanguageLinkActiveClassAuthenticated() {
+    $function_name = '#type link';
+    $path = 'language_test/type-link-active-class';
+
+    // Test links generated by l() on an English page.
+    $current_language = 'English';
+    $this->drupalGet($path);
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', array(':id' => 'no_lang_link', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'en' link should be active.
+    $langcode = 'en';
+    $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'en_link', ':lang' => 'en', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'fr' link should not be active.
+    $langcode = 'fr';
+    $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'fr_link', ':lang' => 'fr', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Verify that drupalSettings contains the correct values.
+    $settings = $this->drupalGetSettings();
+    $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
+
+    // Test links generated by l() on a French page.
+    $current_language = 'French';
+    $this->drupalGet('fr/language_test/type-link-active-class');
+
+    // Language code 'none' link should be active.
+    $langcode = 'none';
+    $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', array(':id' => 'no_lang_link', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'en' link should not be active.
+    $langcode = 'en';
+    $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'en_link', ':lang' => 'en', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Language code 'fr' link should be active.
+    $langcode = 'fr';
+    $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'fr_link', ':lang' => 'fr', ':path' => $path));
+    $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode)));
+
+    // Verify that drupalSettings contains the correct values.
+    $settings = $this->drupalGetSettings();
+    $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
+    $this->assertIdentical($settings['path']['currentLanguage'], 'fr', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
+  }
+
+  /**
+   * For anonymous users, the "active" class is set by PHP.
+   *
+   * @see testLanguageLinkActiveClass()
+   */
+  protected function doTestLanguageLinkActiveClassAnonymous() {
     $function_name = '#type link';
+    $path = 'language_test/type-link-active-class';
+
+    $this->drupalLogout();
 
     // Test links generated by l() on an English page.
     $current_language = 'English';
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php
index 7c037b5..19744c2 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php
@@ -411,6 +411,11 @@ function testUrlLanguageFallback() {
     // Enable the language switcher block.
     $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array('id' => 'test_language_block'));
 
+    // Log out, because for anonymous users, the "active" class is set by PHP
+    // (which means we can easily test it here), whereas for authenticated users
+    // it is set by JavaScript.
+    $this->drupalLogout();
+
     // Access the front page without specifying any valid URL language prefix
     // and having as browser language preference a non-default language.
     $http_header = array("Accept-Language: $langcode_browser_fallback;q=1");
@@ -464,7 +469,7 @@ function testLanguageDomain() {
     $italian_url = url('admin', array('language' => $languages['it'], 'script' => ''));
     $url_scheme = $this->request->isSecure() ? 'https://' : 'http://';
     $correct_link = $url_scheme . $link;
-    $this->assertTrue($italian_url == $correct_link, format_string('The url() function returns the right URL (@url) in accordance with the chosen language', array('@url' => $italian_url)));
+    $this->assertEqual($italian_url, $correct_link, format_string('The url() function returns the right URL (@url) in accordance with the chosen language', array('@url' => $italian_url)));
 
     // Test HTTPS via options.
     $this->settingsSet('mixed_mode_sessions', TRUE);
diff --git a/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php b/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php
index 001fe39..781af06 100644
--- a/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php
+++ b/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php
@@ -58,6 +58,7 @@ public function typeLinkActiveClass() {
           'attributes' => array(
             'id' => 'no_lang_link',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
       'fr' => array(
@@ -69,6 +70,7 @@ public function typeLinkActiveClass() {
           'attributes' => array(
             'id' => 'fr_link',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
       'en' => array(
@@ -80,6 +82,7 @@ public function typeLinkActiveClass() {
           'attributes' => array(
             'id' => 'en_link',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
     );
diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php
index 6145a11..d25554dc 100644
--- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php
+++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Controller;
 
+use Drupal\Component\Utility\Json;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\Query\QueryFactory;
@@ -161,4 +162,66 @@ public function themeSetDefault() {
     return system_theme_default();
   }
 
+  /**
+   * #post_render_cache callback; sets the "active" class on relevant links.
+   *
+   * This is a PHP implementation of the drupal.active-link JavaScript library.
+   *
+   * @param array $element
+   *  A renderable array with the following keys:
+   *    - #markup
+   *    - #attached
+   * @param array $context
+   *   An array with the following keys:
+   *   - path: the system path of the currently active page
+   *   - front: whether the current page is the front page (which implies the
+   *     current path might also be <front>)
+   *   - language: the language code of the currently active page
+   *   - query: the query string for the currently active page
+   *
+   * @return array
+   *   The updated renderable array.
+   */
+  public static function setLinkActiveClass(array $element, array $context) {
+    // Ensure we only manipulate HTML markup.
+    if (substr($element['#markup'], 0, 9) !== '<!DOCTYPE') {
+      return $element;
+    }
+
+    // If none of the HTML in the current page contains even just the current
+    // page's attribute, return early.
+    if (strpos($element['#markup'], 'data-drupal-link-system-path="' . $context['path'] . '"') === FALSE && (!$context['front'] || strpos($element['#markup'], 'data-drupal-link-system-path="<front>"') === FALSE)) {
+      return $element;
+    }
+
+    // Build XPath query to find links that should get the "active" class.
+    $query = '//*[';
+    // An active link's path is equal to the current path.
+    $query .= '@data-drupal-link-system-path="' . $context['path'] . '"';
+    if ($context['front']) {
+      $query .= ' or @data-drupal-link-system-path="<front>"';
+    }
+    // The language of an active link is equal to the current language.
+    if ($context['language']) {
+      $query .= ' and (not(@hreflang) or @hreflang="' . $context['language'] . '")';
+    }
+    // The query parameters of an active link are equal to the current
+    // parameters.
+    if ($context['query']) {
+      $query .= ' and @data-drupal-link-query="' . Json::encode($context['query']) . '"';
+    }
+    $query .= ']';
+
+    // Set the "active" class on all matching HTML elements.
+    $dom = new \DOMDocument();
+    @$dom->loadHTML($element['#markup']);
+    $xpath = new \DOMXPath($dom);
+    foreach ($xpath->query($query) as $node) {
+      $node->setAttribute('class', $node->getAttribute('class') . ' active');
+    }
+    $element['#markup'] = $dom->saveHTML();
+
+    return $element;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
index 3a11ccc..52d5c1c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
@@ -80,7 +80,7 @@ function testAddSetting() {
     drupal_add_library('system', 'drupalSettings');
     $javascript = drupal_add_js();
     $last_settings = reset($javascript['settings']['data']);
-    $this->assertTrue(array_key_exists('currentPath', $last_settings), 'The current path JavaScript setting is set correctly.');
+    $this->assertTrue(array_key_exists('currentPath', $last_settings['path']), 'The current path JavaScript setting is set correctly.');
 
     $javascript = drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting');
     $last_settings = end($javascript['settings']['data']);
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php
index d1930c9..97335d2 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php
@@ -2,12 +2,15 @@
 
 /**
  * @file
- * Definition of Drupal\system\Tests\Theme\FunctionsTest.
+ * Contains \Drupal\system\Tests\Theme\FunctionsTest.
  */
 
 namespace Drupal\system\Tests\Theme;
 
+use Drupal\Core\Session\UserSession;
 use Drupal\simpletest\WebTestBase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Tests for common theme functions.
@@ -159,12 +162,6 @@ function testLinks() {
     $expected = '';
     $this->assertThemeOutput('links', $variables, $expected, 'Empty %callback with heading generates no output.');
 
-    // Set the current path to the front page path.
-    // Required to verify the "active" class in expected links below, and
-    // because the current path is different when running tests manually via
-    // simpletest.module ('batch') and via the testing framework ('').
-    _current_path(\Drupal::config('system.site')->get('page.front'));
-
     // Verify that a list of links is properly rendered.
     $variables = array();
     $variables['attributes'] = array('id' => 'somelinks');
@@ -191,7 +188,7 @@ function testLinks() {
     $expected_links .= '<ul id="somelinks">';
     $expected_links .= '<li class="a-link odd first"><a href="' . url('a/link') . '">' . check_plain('A <link>') . '</a></li>';
     $expected_links .= '<li class="plain-text even">' . check_plain('Plain "text"') . '</li>';
-    $expected_links .= '<li class="front-page odd active"><a href="' . url('<front>') . '" class="active">' . check_plain('Front page') . '</a></li>';
+    $expected_links .= '<li class="front-page odd"><a href="' . url('<front>') . '">' . check_plain('Front page') . '</a></li>';
     $expected_links .= '<li class="router-test even last"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . check_plain('Test route') . '</a></li>';
     $expected_links .= '</ul>';
 
@@ -224,11 +221,24 @@ function testLinks() {
     $expected_links .= '<ul id="somelinks">';
     $expected_links .= '<li class="a-link odd first"><a href="' . url('a/link') . '" class="a/class">' . check_plain('A <link>') . '</a></li>';
     $expected_links .= '<li class="plain-text even"><span class="a/class">' . check_plain('Plain "text"') . '</span></li>';
-    $expected_links .= '<li class="front-page odd active"><a href="' . url('<front>') . '" class="active">' . check_plain('Front page') . '</a></li>';
+    $expected_links .= '<li class="front-page odd"><a href="' . url('<front>') . '">' . check_plain('Front page') . '</a></li>';
     $expected_links .= '<li class="router-test even last"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . check_plain('Test route') . '</a></li>';
     $expected_links .= '</ul>';
     $expected = $expected_heading . $expected_links;
     $this->assertThemeOutput('links', $variables, $expected);
+
+    // Verify the data- attributes for setting the "active" class on links.
+    $this->container->set('current_user', new UserSession(array('uid' => 1)));
+    $variables['set_active_class'] = TRUE;
+    $expected_links = '';
+    $expected_links .= '<ul id="somelinks">';
+    $expected_links .= '<li class="a-link odd first" data-drupal-link-system-path="a/link"><a href="' . url('a/link') . '" class="a/class">' . check_plain('A <link>') . '</a></li>';
+    $expected_links .= '<li class="plain-text even"><span class="a/class">' . check_plain('Plain "text"') . '</span></li>';
+    $expected_links .= '<li class="front-page odd" data-drupal-link-system-path="&lt;front&gt;"><a href="' . url('<front>') . '">' . check_plain('Front page') . '</a></li>';
+    $expected_links .= '<li class="router-test even last" data-drupal-link-system-path="router_test/test1"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . check_plain('Test route') . '</a></li>';
+    $expected_links .= '</ul>';
+    $expected = $expected_heading . $expected_links;
+    $this->assertThemeOutput('links', $variables, $expected);
   }
 
   /**
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index c725d8a..9295d6a 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -906,6 +906,19 @@ function system_library_info() {
     ),
   );
 
+  // Drupal's active link marking.
+  $libraries['drupal.active-link'] = array(
+    'title' => 'Drupal active link marking',
+    'version' => \Drupal::VERSION,
+    'js' => array(
+      'core/misc/active-link.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'drupal'),
+      array('system', 'drupalSettings'),
+    ),
+  );
+
   // Drupal's Ajax framework.
   $libraries['drupal.ajax'] = array(
     'title' => 'Drupal AJAX',
@@ -2118,6 +2131,7 @@ function system_filetransfer_info() {
  * Implements hook_page_build().
  *
  * @see template_preprocess_maintenance_page()
+ * @see \Drupal\system\Controller\SystemController::setLinkActiveClass()
  */
 function system_page_build(&$page) {
   // Ensure the same CSS is loaded in template_preprocess_maintenance_page().
@@ -2137,6 +2151,28 @@ function system_page_build(&$page) {
       'weight' => CSS_COMPONENT - 10,
     );
   }
+
+  // Handle setting the "active" class on links by:
+  // - loading the active-link library if the current user is authenticated;
+  // - applying a post-render cache callback if the current user is anonymous.
+  // @see l()
+  // @see \Drupal\Core\Utility\LinkGenerator::generate()
+  // @see theme_links()
+  // @see \Drupal\system\Controller\SystemController::setLinkActiveClass
+  if (\Drupal::currentUser()->isAuthenticated()) {
+    $page['#attached']['library'][] = array('system', 'drupal.active-link');
+  }
+  else {
+    $page['#post_render_cache']['\Drupal\system\Controller\SystemController::setLinkActiveClass'] = array(
+      // Collect the current state that determines whether a link is active.
+      array(
+        'path' => current_path(),
+        'front' => drupal_is_front_page(),
+        'language' => language(\Drupal\Core\Language\Language::TYPE_URL)->id,
+        'query' => \Drupal::service('request')->query->all(),
+      )
+    );
+  }
 }
 
 /**
diff --git a/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php b/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php
index 12d287f..ffce7dc 100644
--- a/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php
+++ b/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php
@@ -35,6 +35,9 @@ public function typeLinkActiveClass() {
         '#type' => 'link',
         '#title' => t('Link with no query string'),
         '#href' => current_path(),
+        '#options' => array(
+          'set_active_class' => TRUE,
+        ),
       ),
       'with_query' => array(
         '#type' => 'link',
@@ -45,6 +48,7 @@ public function typeLinkActiveClass() {
             'foo' => 'bar',
             'one' => 'two',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
       'with_query_reversed' => array(
@@ -56,6 +60,7 @@ public function typeLinkActiveClass() {
             'one' => 'two',
             'foo' => 'bar',
           ),
+          'set_active_class' => TRUE,
         ),
       ),
     );
diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
index 6b7334f..eec43a6 100644
--- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
@@ -43,7 +43,6 @@ class LinkGeneratorTest extends UnitTestCase {
   protected $moduleHandler;
 
   /**
-   *
    * The mocked language manager.
    *
    * @var \PHPUnit_Framework_MockObject_MockObject
@@ -51,12 +50,20 @@ class LinkGeneratorTest extends UnitTestCase {
   protected $languageManager;
 
   /**
+   * The mocked path alias manager.
+   *
+   * @var \Drupal\Core\Path\AliasManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $aliasManager;
+
+  /**
    * Contains the LinkGenerator default options.
    */
   protected $defaultOptions = array(
     'query' => array(),
     'html' => FALSE,
     'language' => NULL,
+    'set_active_class' => FALSE,
   );
 
   /**
@@ -80,8 +87,9 @@ protected function setUp() {
     $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE);
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
     $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManager');
+    $this->aliasManager = $this->getMock('\Drupal\Core\Path\AliasManagerInterface');
 
-    $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->languageManager);
+    $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->languageManager, $this->aliasManager);
   }
 
   /**
@@ -312,19 +320,31 @@ public function testGenerateWithHtml() {
    *   service.
    */
   public function testGenerateActive() {
-    $this->urlGenerator->expects($this->exactly(7))
+    $this->urlGenerator->expects($this->exactly(8))
       ->method('generateFromRoute')
       ->will($this->returnValueMap(array(
         array('test_route_1', array(), FALSE, '/test-route-1'),
-        array('test_route_1', array(), FALSE, '/test-route-1'),
-        array('test_route_1', array(), FALSE, '/test-route-1'),
-        array('test_route_1', array(), FALSE, '/test-route-1'),
-        array('test_route_3', array(), FALSE, '/test-route-3'),
         array('test_route_3', array(), FALSE, '/test-route-3'),
         array('test_route_4', array('object' => '1'), FALSE, '/test-route-4/1'),
       )));
 
-    $this->moduleHandler->expects($this->exactly(7))
+    $this->urlGenerator->expects($this->exactly(7))
+      ->method('getPathFromRoute')
+      ->will($this->returnValueMap(array(
+        array('test_route_1', array(), 'test-route-1'),
+        array('test_route_3', array(), 'test-route-3'),
+        array('test_route_4', array('object' => '1'), 'test-route-4/1'),
+      )));
+
+    $this->aliasManager->expects($this->exactly(7))
+      ->method('getSystemPath')
+      ->will($this->returnValueMap(array(
+        array('test-route-1', NULL, 'test-route-1'),
+        array('test-route-3', NULL, 'test-route-3'),
+        array('test-route-4/1', NULL, 'test-route-4/1'),
+      )));
+
+    $this->moduleHandler->expects($this->exactly(8))
       ->method('alter');
 
     $this->setUpLanguageManager();
@@ -332,10 +352,10 @@ public function testGenerateActive() {
     // Render a link with a path different from the current path.
     $request = new Request(array(), array(), array('system_path' => 'test-route-2'));
     $this->linkGenerator->setRequest($request);
-    $result = $this->linkGenerator->generate('Test', 'test_route_1');
-    $this->assertNotTag(array(
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE));
+    $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
     ), $result);
 
     // Render a link with the same path as the current path.
@@ -345,17 +365,31 @@ public function testGenerateActive() {
     $raw_variables = new ParameterBag();
     $request->attributes->set('_raw_variables', $raw_variables);
     $this->linkGenerator->setRequest($request);
-    $result = $this->linkGenerator->generate('Test', 'test_route_1');
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE));
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
+    ), $result);
+
+    // Render a link with the same path as the current path, but with the
+    // set_active_class option disabled.
+    $request = new Request(array(), array(), array('system_path' => 'test-route-1', RouteObjectInterface::ROUTE_NAME => 'test_route_1'));
+    // This attribute is expected to be set in a Drupal request by
+    // \Drupal\Core\ParamConverter\ParamConverterManager
+    $raw_variables = new ParameterBag();
+    $request->attributes->set('_raw_variables', $raw_variables);
+    $this->linkGenerator->setRequest($request);
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => FALSE));
+    $this->assertNotTag(array(
+      'tag' => 'a',
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
     ), $result);
 
     // Render a link with the same path and language as the current path.
-    $result = $this->linkGenerator->generate('Test', 'test_route_1');
+    $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE));
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array('data-drupal-link-system-path' => 'test-route-1'),
     ), $result);
 
     // Render a link with the same path but a different language than the current
@@ -364,11 +398,17 @@ public function testGenerateActive() {
       'Test',
       'test_route_1',
       array(),
-      array('language' => new Language(array('id' => 'de')))
+      array(
+        'language' => new Language(array('id' => 'de')),
+        'set_active_class' => TRUE,
+      )
     );
-    $this->assertNotTag(array(
+    $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-1',
+        'hreflang' => 'de',
+      ),
     ), $result);
 
     // Render a link with the same path and query parameter as the current path.
@@ -380,11 +420,17 @@ public function testGenerateActive() {
       'Test',
       'test_route_3',
       array(),
-      array('query' => array('value' => 'example_1')
-    ));
+      array(
+        'query' => array('value' => 'example_1'),
+        'set_active_class' => TRUE,
+      )
+    );
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-3',
+        'data-drupal-link-query' => 'regexp:/.*value.*example_1.*/',
+      ),
     ), $result);
 
     // Render a link with the same path but a different query parameter than the
@@ -393,12 +439,19 @@ public function testGenerateActive() {
       'Test',
       'test_route_3',
       array(),
-      array('query' => array('value' => 'example_2'))
+      array(
+        'query' => array('value' => 'example_2'),
+        'set_active_class' => TRUE,
+      )
     );
-    $this->assertNotTag(array(
+    $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-3',
+        'data-drupal-link-query' => 'regexp:/.*value.*example_2.*/',
+      ),
     ), $result);
+
     // Render a link with the same path and query parameter as the current path.
     $request = new Request(array('value' => 'example_1'), array(), array('system_path' => 'test-route-4/1', RouteObjectInterface::ROUTE_NAME => 'test_route_4'));
     $raw_variables = new ParameterBag(array('object' => '1'));
@@ -408,11 +461,17 @@ public function testGenerateActive() {
       'Test',
       'test_route_4',
       array('object' => '1'),
-      array('query' => array('value' => 'example_1'))
+      array(
+        'query' => array('value' => 'example_1'),
+        'set_active_class' => TRUE,
+      )
     );
     $this->assertTag(array(
       'tag' => 'a',
-      'attributes' => array('class' => 'active'),
+      'attributes' => array(
+        'data-drupal-link-system-path' => 'test-route-4/1',
+        'data-drupal-link-query' => 'regexp:/.*value.*example_1.*/',
+      ),
     ), $result);
   }
 
