 core/assets/vendor/classList/LICENSE.md            |    2 +
 core/assets/vendor/classList/README.md             |    7 +
 core/assets/vendor/classList/classList.js          |  179 ++++++++++++++++++++
 core/assets/vendor/classList/classList.min.js      |    2 +
 core/core.services.yml                             |    2 +-
 core/includes/common.inc                           |   65 +++----
 core/includes/menu.inc                             |    4 +
 core/includes/theme.inc                            |   94 ++++++----
 core/lib/Drupal/Core/Utility/LinkGenerator.php     |   66 +++++---
 .../Drupal/Core/Utility/LinkGeneratorInterface.php |    9 +-
 core/misc/active-link.js                           |   63 +++++++
 core/misc/ajax.js                                  |   38 +++--
 core/misc/drupal.js                                |    2 +-
 core/misc/tabledrag.js                             |   10 +-
 .../Drupal/image/Tests/ImageFieldDisplayTest.php   |    2 +-
 core/modules/language/language.negotiation.inc     |    1 +
 .../Drupal/language/Plugin/Block/LanguageBlock.php |    1 +
 .../language/Tests/LanguageSwitchingTest.php       |  144 +++++++++++++++-
 .../Tests/LanguageUILanguageNegotiationTest.php    |    7 +-
 .../Controller/LanguageTestController.php          |    3 +
 .../Drupal/system/Controller/SystemController.php  |   68 ++++++++
 .../Drupal/system/Tests/Batch/ProcessingTest.php   |   12 +-
 .../Drupal/system/Tests/Common/JavaScriptTest.php  |    2 +-
 .../Drupal/system/Tests/Theme/FunctionsTest.php    |   28 ++-
 core/modules/system/system.module                  |   51 ++++++
 .../modules/batch_test/batch_test.callbacks.inc    |    2 +-
 .../Controller/CommonTestController.php            |    5 +
 .../Tests/Core/Utility/LinkGeneratorTest.php       |  111 +++++++++---
 28 files changed, 822 insertions(+), 158 deletions(-)

diff --git a/core/assets/vendor/classList/LICENSE.md b/core/assets/vendor/classList/LICENSE.md
new file mode 100644
index 0000000..cb518af
--- /dev/null
+++ b/core/assets/vendor/classList/LICENSE.md
@@ -0,0 +1,2 @@
+This software is dedicated to the public domain. No warranty is expressed or implied.
+Use this software at your own risk.
diff --git a/core/assets/vendor/classList/README.md b/core/assets/vendor/classList/README.md
new file mode 100644
index 0000000..dd7359c
--- /dev/null
+++ b/core/assets/vendor/classList/README.md
@@ -0,0 +1,7 @@
+classList.js is a cross-browser JavaScript shim that fully implements `element.classList`. Refer to [the MDN page on `element.classList`][1] for more information.
+
+
+![Tracking image](https://in.getclicky.com/212712ns.gif)
+
+
+  [1]: https://developer.mozilla.org/en/DOM/element.classList "MDN / DOM / element.classList"
diff --git a/core/assets/vendor/classList/classList.js b/core/assets/vendor/classList/classList.js
new file mode 100644
index 0000000..1faaec8
--- /dev/null
+++ b/core/assets/vendor/classList/classList.js
@@ -0,0 +1,179 @@
+/*
+ * classList.js: Cross-browser full element.classList implementation.
+ * 2012-11-15
+ *
+ * By Eli Grey, http://eligrey.com
+ * Public Domain.
+ * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+ */
+
+/*global self, document, DOMException */
+
+/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
+
+if ("document" in self && !(
+		"classList" in document.createElement("_") &&
+		"classList" in document.createElementNS("http://www.w3.org/2000/svg", "svg")
+	)) {
+
+(function (view) {
+
+"use strict";
+
+if (!('Element' in view)) return;
+
+var
+	  classListProp = "classList"
+	, protoProp = "prototype"
+	, elemCtrProto = view.Element[protoProp]
+	, objCtr = Object
+	, strTrim = String[protoProp].trim || function () {
+		return this.replace(/^\s+|\s+$/g, "");
+	}
+	, arrIndexOf = Array[protoProp].indexOf || function (item) {
+		var
+			  i = 0
+			, len = this.length
+		;
+		for (; i < len; i++) {
+			if (i in this && this[i] === item) {
+				return i;
+			}
+		}
+		return -1;
+	}
+	// Vendors: please allow content code to instantiate DOMExceptions
+	, DOMEx = function (type, message) {
+		this.name = type;
+		this.code = DOMException[type];
+		this.message = message;
+	}
+	, checkTokenAndGetIndex = function (classList, token) {
+		if (token === "") {
+			throw new DOMEx(
+				  "SYNTAX_ERR"
+				, "An invalid or illegal string was specified"
+			);
+		}
+		if (/\s/.test(token)) {
+			throw new DOMEx(
+				  "INVALID_CHARACTER_ERR"
+				, "String contains an invalid character"
+			);
+		}
+		return arrIndexOf.call(classList, token);
+	}
+	, ClassList = function (elem) {
+		var
+			  trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
+			, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
+			, i = 0
+			, len = classes.length
+		;
+		for (; i < len; i++) {
+			this.push(classes[i]);
+		}
+		this._updateClassName = function () {
+			elem.setAttribute("class", this.toString());
+		};
+	}
+	, classListProto = ClassList[protoProp] = []
+	, classListGetter = function () {
+		return new ClassList(this);
+	}
+;
+// Most DOMException implementations don't allow calling DOMException's toString()
+// on non-DOMExceptions. Error's toString() is sufficient here.
+DOMEx[protoProp] = Error[protoProp];
+classListProto.item = function (i) {
+	return this[i] || null;
+};
+classListProto.contains = function (token) {
+	token += "";
+	return checkTokenAndGetIndex(this, token) !== -1;
+};
+classListProto.add = function () {
+	var
+		  tokens = arguments
+		, i = 0
+		, l = tokens.length
+		, token
+		, updated = false
+	;
+	do {
+		token = tokens[i] + "";
+		if (checkTokenAndGetIndex(this, token) === -1) {
+			this.push(token);
+			updated = true;
+		}
+	}
+	while (++i < l);
+
+	if (updated) {
+		this._updateClassName();
+	}
+};
+classListProto.remove = function () {
+	var
+		  tokens = arguments
+		, i = 0
+		, l = tokens.length
+		, token
+		, updated = false
+	;
+	do {
+		token = tokens[i] + "";
+		var index = checkTokenAndGetIndex(this, token);
+		if (index !== -1) {
+			this.splice(index, 1);
+			updated = true;
+		}
+	}
+	while (++i < l);
+
+	if (updated) {
+		this._updateClassName();
+	}
+};
+classListProto.toggle = function (token, forse) {
+	token += "";
+
+	var
+		  result = this.contains(token)
+		, method = result ?
+			forse !== true && "remove"
+		:
+			forse !== false && "add"
+	;
+
+	if (method) {
+		this[method](token);
+	}
+
+	return !result;
+};
+classListProto.toString = function () {
+	return this.join(" ");
+};
+
+if (objCtr.defineProperty) {
+	var classListPropDesc = {
+		  get: classListGetter
+		, enumerable: true
+		, configurable: true
+	};
+	try {
+		objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
+	} catch (ex) { // IE 8 doesn't support enumerable:true
+		if (ex.number === -0x7FF5EC54) {
+			classListPropDesc.enumerable = false;
+			objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
+		}
+	}
+} else if (objCtr[protoProp].__defineGetter__) {
+	elemCtrProto.__defineGetter__(classListProp, classListGetter);
+}
+
+}(self));
+
+}
diff --git a/core/assets/vendor/classList/classList.min.js b/core/assets/vendor/classList/classList.min.js
new file mode 100644
index 0000000..fa98825
--- /dev/null
+++ b/core/assets/vendor/classList/classList.min.js
@@ -0,0 +1,2 @@
+/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
+if("document" in self&&!("classList" in document.createElement("_")&&"classList" in document.createElementNS("http://www.w3.org/2000/svg","svg"))){(function(j){"use strict";if(!("Element" in j)){return}var a="classList",f="prototype",m=j.Element[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p<o;p++){if(p in this&&this[p]===q){return p}}return -1},n=function(o,p){this.name=o;this.code=DOMException[o];this.message=p},g=function(p,o){if(o===""){throw new n("SYNTAX_ERR","An invalid or illegal string was specified")}if(/\s/.test(o)){throw new n("INVALID_CHARACTER_ERR","String contains an invalid character")}return c.call(p,o)},d=function(s){var r=k.call(s.getAttribute("class")),q=r?r.split(/\s+/):[],p=0,o=q.length;for(;p<o;p++){this.push(q[p])}this._updateClassName=function(){s.setAttribute("class",this.toString())}},e=d[f]=[],i=function(){return new d(this)};n[f]=Error[f];e.item=function(o){return this[o]||null};e.contains=function(o){o+="";return g(this,o)!==-1};e.add=function(){var s=arguments,r=0,p=s.length,q,o=false;do{q=s[r]+"";if(g(this,q)===-1){this.push(q);o=true}}while(++r<p);if(o){this._updateClassName()}};e.remove=function(){var t=arguments,s=0,p=t.length,r,o=false;do{r=t[s]+"";var q=g(this,r);if(q!==-1){this.splice(q,1);o=true}}while(++s<p);if(o){this._updateClassName()}};e.toggle=function(p,q){p+="";var o=this.contains(p),r=o?q!==true&&"remove":q!==false&&"add";if(r){this[r](p)}return !o};e.toString=function(){return this.join(" ")};if(b.defineProperty){var l={get:i,enumerable:true,configurable:true};try{b.defineProperty(m,a,l)}catch(h){if(h.number===-2146823252){l.enumerable=false;b.defineProperty(m,a,l)}}}else{if(b[f].__defineGetter__){m.__defineGetter__(a,i)}}}(self))};
diff --git a/core/core.services.yml b/core/core.services.yml
index 6134359..6e0047d 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -276,7 +276,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 9d03679..0a77f0d 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 513849a..e52c0e6 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 ec208ef..6ddc8fe 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1188,6 +1188,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:
@@ -1203,6 +1215,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'];
@@ -1236,8 +1261,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++;
@@ -1249,16 +1273,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(
@@ -1271,30 +1295,34 @@ 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.
@@ -1309,7 +1337,7 @@ function theme_links($variables) {
         }
       }
 
-      $output .= '<li' . new Attribute(array('class' => $class)) . '>';
+      $output .= '<li' . new Attribute($li_attributes) . '>';
       $output .= $item;
       $output .= '</li>';
     }
@@ -2246,7 +2274,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'])) {
@@ -2256,7 +2285,8 @@ function template_preprocess_page(&$variables) {
       '#heading' => array(
         'text' => t('Secondary menu'),
         'class' => array('visually-hidden'),
-      )
+      ),
+      '#set_active_class' => TRUE,
     );
   }
 
@@ -2586,7 +2616,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..6054faa
--- /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.activeLinks = {
+  attach: function (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 6ecde95..3754af6 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -180,7 +180,7 @@ Drupal.ajax = function (base, element, element_settings) {
   // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
   // bind Ajax to links as well.
   if (this.element.form) {
-    this.form = $(this.element.form);
+    this.$form = $(this.element.form);
   }
 
   // If no Ajax callback URL was given, use the link href or form action.
@@ -189,7 +189,7 @@ Drupal.ajax = function (base, element, element_settings) {
       this.url = $(element).attr('href');
     }
     else if (element.form) {
-      this.url = this.form.attr('action');
+      this.url = this.$form.attr('action');
 
       // @todo If there's a file input on this form, then jQuery will submit the
       //   AJAX response with a hidden Iframe rather than the XHR object. If the
@@ -200,7 +200,7 @@ Drupal.ajax = function (base, element, element_settings) {
       //   elements that submit to the same URL as the form when there's a file
       //   input. For example, this means the Delete button on the edit form of
       //   an Article node doesn't open its confirmation form in a dialog.
-      if (this.form.find(':file').length) {
+      if (this.$form.find(':file').length) {
         return;
       }
     }
@@ -327,7 +327,7 @@ Drupal.ajax.prototype.eventResponse = function (element, event) {
   }
 
   try {
-    if (ajax.form) {
+    if (ajax.$form) {
       // If setClick is set, we must set this to ensure that the button's
       // value is passed.
       if (ajax.setClick) {
@@ -338,7 +338,7 @@ Drupal.ajax.prototype.eventResponse = function (element, event) {
         element.form.clk = element;
       }
 
-      ajax.form.ajaxSubmit(ajax.options);
+      ajax.$form.ajaxSubmit(ajax.options);
     }
     else {
       ajax.beforeSerialize(ajax.element, ajax.options);
@@ -362,12 +362,12 @@ Drupal.ajax.prototype.eventResponse = function (element, event) {
 Drupal.ajax.prototype.beforeSerialize = function (element, options) {
   // Allow detaching behaviors to update field values before collecting them.
   // This is only needed when field values are added to the POST data, so only
-  // when there is a form such that this.form.ajaxSubmit() is used instead of
+  // when there is a form such that this.$form.ajaxSubmit() is used instead of
   // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize()
-  // isn't called, but don't rely on that: explicitly check this.form.
-  if (this.form) {
+  // isn't called, but don't rely on that: explicitly check this.$form.
+  if (this.$form) {
     var settings = this.settings || drupalSettings;
-    Drupal.detachBehaviors(this.form, settings, 'serialize');
+    Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
   }
 
   // Prevent duplicate HTML ids in the returned markup.
@@ -421,7 +421,7 @@ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
   // to the form to submit the values in options.extraData. There is no simple
   // way to know which submission mechanism will be used, so we add to extraData
   // regardless, and allow it to be ignored in the former case.
-  if (this.form) {
+  if (this.$form) {
     options.extraData = options.extraData || {};
 
     // Let the server know when the IFRAME submission mechanism is used. The
@@ -491,9 +491,9 @@ Drupal.ajax.prototype.success = function (response, status) {
   // attachBehaviors() called on the new content from processing the response
   // commands is not sufficient, because behaviors from the entire form need
   // to be reattached.
-  if (this.form) {
+  if (this.$form) {
     var settings = this.settings || drupalSettings;
-    Drupal.attachBehaviors(this.form, settings);
+    Drupal.attachBehaviors(this.$form.get(0), settings);
   }
 
   // Remove any response-specific settings so they don't get used on the next
@@ -544,9 +544,9 @@ Drupal.ajax.prototype.error = function (response, uri) {
   // Re-enable the element.
   $(this.element).removeClass('progress-disabled').prop('disabled', false);
   // Reattach behaviors, if they were detached in beforeSerialize().
-  if (this.form) {
+  if (this.$form) {
     var settings = response.settings || this.settings || drupalSettings;
-    Drupal.attachBehaviors(this.form, settings);
+    Drupal.attachBehaviors(this.$form.get(0), settings);
   }
   throw new Drupal.AjaxError(response, uri);
 };
@@ -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);
     }
   },
 
@@ -634,8 +634,10 @@ Drupal.AjaxCommands.prototype = {
    */
   remove: function (ajax, response, status) {
     var settings = response.settings || ajax.settings || drupalSettings;
-    Drupal.detachBehaviors($(response.selector), settings);
-    $(response.selector).remove();
+    $(response.selector).each(function() {
+      Drupal.detachBehaviors(this, settings);
+    })
+    .remove();
   },
 
   /**
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/misc/tabledrag.js b/core/misc/tabledrag.js
index ae2dec0..e78d693 100644
--- a/core/misc/tabledrag.js
+++ b/core/misc/tabledrag.js
@@ -1069,9 +1069,15 @@ Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) {
  *   DOM element what will be swapped with the row group.
  */
 Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) {
-  Drupal.detachBehaviors(this.group, drupalSettings, 'move');
+  // Makes sure only DOM object are passed to Drupal.detachBehaviors().
+  this.group.forEach(function (row) {
+    Drupal.detachBehaviors(row, drupalSettings, 'move');
+  });
   $(row)[position](this.group);
-  Drupal.attachBehaviors(this.group, drupalSettings);
+  // Makes sure only DOM object are passed to Drupal.attachBehaviors()s.
+  this.group.forEach(function (row) {
+    Drupal.attachBehaviors(row, drupalSettings);
+  });
   this.changed = true;
   this.onSwap(row);
 };
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
index aa54d89..c8f205f 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php
@@ -113,7 +113,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 f53c81b..d97d49f 100644
--- a/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
+++ b/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php
@@ -47,6 +47,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..b4c4616 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php
@@ -42,8 +42,12 @@ function setUp() {
    * Functional tests for the language switcher block.
    */
   function testLanguageBlock() {
-    // Enable the language switching block.
-    $block = $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array('id' => 'test_language_block'));
+    // Enable the language switching block..
+    $block = $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array(
+      'id' => 'test_language_block',
+      // Ensure a 2-byte UTF-8 sequence is in the tested output.
+      'label' => $this->randomName(8) . '×',
+    ));
 
     // Add language.
     $edit = array(
@@ -55,9 +59,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 +169,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 fce6ba1..3d61174 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;
@@ -329,4 +330,71 @@ 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) {
+    // 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="&lt;front&gt;"') === 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']) . "'";
+    }
+    else {
+      $query .= " and not(@data-drupal-link-query)";
+    }
+    $query .= "]";
+
+    // Set the "active" class on all matching HTML elements.
+    $dom = new \DOMDocument();
+    @$dom->loadHTML('<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $element['#markup'] . '</body></html>');
+    $xpath = new \DOMXPath($dom);
+    foreach ($xpath->query($query) as $node) {
+      $class = $node->getAttribute('class');
+      if (strlen($class) > 0) {
+        $class .= ' ';
+      }
+      $class .= 'active';
+      $node->setAttribute('class', $class);
+    }
+
+    $body_dom_node = $dom->getElementsByTagName('body')->item(0);
+    $element['#markup'] = substr($dom->saveHTML($body_dom_node), 6, -7);
+
+    return $element;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php b/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php
index 11204fe..8450b9c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php
@@ -250,28 +250,28 @@ function _resultMessages($id) {
 
     switch ($id) {
       case 'batch_0':
-        $messages[] = 'results for batch 0<br />none';
+        $messages[] = 'results for batch 0<br>none';
         break;
 
       case 'batch_1':
-        $messages[] = 'results for batch 1<br />op 1: processed 10 elements';
+        $messages[] = 'results for batch 1<br>op 1: processed 10 elements';
         break;
 
       case 'batch_2':
-        $messages[] = 'results for batch 2<br />op 2: processed 10 elements';
+        $messages[] = 'results for batch 2<br>op 2: processed 10 elements';
         break;
 
       case 'batch_3':
-        $messages[] = 'results for batch 3<br />op 1: processed 10 elements<br />op 2: processed 10 elements';
+        $messages[] = 'results for batch 3<br>op 1: processed 10 elements<br>op 2: processed 10 elements';
         break;
 
       case 'batch_4':
-        $messages[] = 'results for batch 4<br />op 1: processed 10 elements';
+        $messages[] = 'results for batch 4<br>op 1: processed 10 elements';
         $messages = array_merge($messages, $this->_resultMessages('batch_2'));
         break;
 
       case 'batch_5':
-        $messages[] = 'results for batch 5<br />op 5: processed 10 elements';
+        $messages[] = 'results for batch 5<br>op 5: processed 10 elements';
         break;
 
       case 'chained':
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 360b153..e94cebf 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php
@@ -85,7 +85,7 @@ function testAddSetting() {
     drupal_render($attached);
     $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 1afa5e2..c29b14d 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -888,6 +888,20 @@ 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'),
+      array('system', 'classList'),
+    ),
+  );
+
   // Drupal's Ajax framework.
   $libraries['drupal.ajax'] = array(
     'title' => 'Drupal AJAX',
@@ -1134,6 +1148,20 @@ function system_library_info() {
     ),
   );
 
+  // IE9 classList polyfill.
+  $libraries['classList'] = array(
+    'title' => 'classList.js',
+    'website' => 'https://github.com/eligrey/classList.js',
+    'version' => 'master',
+    'js' => array(
+      'core/assets/vendor/classList/classList.min.js' => array(
+        'group' => JS_LIBRARY,
+        'weight' => -21,
+        'browsers' => array('IE' => 'lte IE 9', '!IE' => FALSE),
+      ),
+    ),
+  );
+
   // jQuery.
   $libraries['jquery'] = array(
     'title' => 'jQuery',
@@ -2101,6 +2129,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().
@@ -2120,6 +2149,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/batch_test/batch_test.callbacks.inc b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
index 6d9a9a6..ca1ea6f 100644
--- a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
+++ b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
@@ -94,7 +94,7 @@ function _batch_test_finished_helper($batch_id, $success, $results, $operations)
     $messages[] = t('An error occurred while processing @op with arguments:<br />@args', array('@op' => $error_operation[0], '@args' => print_r($error_operation[1], TRUE)));
   }
 
-  drupal_set_message(implode('<br />', $messages));
+  drupal_set_message(implode('<br>', $messages));
 }
 
 /**
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);
   }
 
