diff --git a/includes/common.inc b/includes/common.inc
index eb68850..fa61580 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2313,12 +2313,6 @@ function l($text, $path, array $options = array()) {
     $options['attributes']['class'][] = 'active';
   }
 
-  // Remove all HTML and PHP tags from a tooltip. For best performance, we act only
-  // if a quick strpos() pre-check gave a suspicion (because strip_tags() is expensive).
-  if (isset($options['attributes']['title']) && strpos($options['attributes']['title'], '<') !== FALSE) {
-    $options['attributes']['title'] = strip_tags($options['attributes']['title']);
-  }
-
   // Determine if rendering of the link is to be done with a theme function
   // or the inline default. Inline is faster, but if the theme system has been
   // loaded and a module or theme implements a preprocess or process function
diff --git a/includes/menu.inc b/includes/menu.inc
index e7abda7..b8d9ce1 100644
--- a/includes/menu.inc
+++ b/includes/menu.inc
@@ -694,13 +694,29 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
   elseif ($link_translate) {
     $item['title'] = $item['link_title'];
   }
+  if ($link_translate) {
+    $item['description'] = $item['link_description'];
+  }
 
   // Translate description, see the motivation above.
   if (!empty($item['description'])) {
     $original_description = $item['description'];
     $item['description'] = t($item['description']);
-    if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
-      $item['localized_options']['attributes']['title'] = $item['description'];
+    if ($link_translate && isset($item['options']['attributes']['title'])) {
+      // For performance reasons, only run strip_tags() and decode_entities()
+      // when needed.
+      if ($item['options']['attributes']['title'] == $original_description) {
+        // The original description does not contain tags or entities, so we
+        // assume that the translated description does not either.
+        $item['localized_options']['attributes']['title'] = $item['description'];
+      }
+      elseif (strcspn($original_description, '<&') < strlen($original_description) && $item['options']['attributes']['title'] == decode_entities(strip_tags($original_description))) {
+        // The description contains entities and/or tags, but the link title
+        // attribute matches the original description after tags have been
+        // stripped and entities converted, so strip tags and convert entities
+        // in the translated description.
+        $item['localized_options']['attributes']['title'] = decode_entities(strip_tags($item['description']));
+      }
     }
   }
 }
@@ -2668,9 +2684,10 @@ function _menu_link_build($item) {
   $item += array(
     'menu_name' => 'navigation',
     'link_title' => $item['title'],
+    'link_description' => $item['description'],
     'link_path' => $item['path'],
     'hidden' => 0,
-    'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
+    'options' => empty($item['description']) ? array() : array('attributes' => array('title' => decode_entities(strip_tags($item['description'])))),
   );
   return $item;
 }
@@ -2927,6 +2944,8 @@ function menu_link_save(&$item) {
     'menu_name' => 'navigation',
     'weight' => 0,
     'link_title' => '',
+    'link_description' => '',
+    'link_description' => '',
     'hidden' => 0,
     'has_children' => 0,
     'expanded' => 0,
@@ -2967,6 +2986,7 @@ function menu_link_save(&$item) {
         'weight' => $item['weight'],
         'module' => $item['module'],
         'link_title' => $item['link_title'],
+        'link_description' => $item['link_description'],
         'options' => serialize($item['options']),
         'customized' => $item['customized'],
         'updated' => $item['updated'],
@@ -3041,6 +3061,7 @@ function menu_link_save(&$item) {
         'p9' => $item['p9'],
         'module' => $item['module'],
         'link_title' => $item['link_title'],
+        'link_description' => $item['link_description'],
         'options' => serialize($item['options']),
         'customized' => $item['customized'],
       ))
diff --git a/modules/menu/menu.admin.inc b/modules/menu/menu.admin.inc
index 1f3c4f7..5a8d5b3 100644
--- a/modules/menu/menu.admin.inc
+++ b/modules/menu/menu.admin.inc
@@ -258,7 +258,20 @@ function theme_menu_overview_form($variables) {
 function menu_edit_item($form, &$form_state, $type, $item, $menu) {
   if ($type == 'add' || empty($item)) {
     // This is an add form, initialize the menu link.
-    $item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu['menu_name'], 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0);
+    $item = array(
+      'link_title' => '',
+      'mlid' => 0,
+      'plid' => 0,
+      'menu_name' => $menu['menu_name'],
+      'weight' => 0,
+      'link_path' => '',
+      'options' => array(),
+      'link_description' => '',
+      'module' => 'menu',
+      'expanded' => 0,
+      'hidden' => 0,
+      'has_children' => 0,
+    );
   }
   $form['actions'] = array('#type' => 'actions');
   $form['link_title'] = array(
@@ -305,10 +318,10 @@ function menu_edit_item($form, &$form_state, $type, $item, $menu) {
       '#description' => l($item['link_title'], $item['href'], $item['options']),
     );
   }
-  $form['description'] = array(
+  $form['link_description'] = array(
     '#type' => 'textarea',
     '#title' => t('Description'),
-    '#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '',
+    '#default_value' => $item['link_description'],
     '#rows' => 1,
     '#description' => t('Shown when hovering over the menu link.'),
   );
@@ -404,7 +417,7 @@ function menu_edit_item_submit($form, &$form_state) {
   $item['hidden'] = (int) !$item['enabled'];
   unset($item['enabled']);
 
-  $item['options']['attributes']['title'] = $item['description'];
+  $item['options']['attributes']['title'] = decode_entities(strip_tags($item['link_description']));
   list($item['menu_name'], $item['plid']) = explode(':', $item['parent']);
   if (!menu_link_save($item)) {
     drupal_set_message(t('There was an error saving the menu link.'), 'error');
diff --git a/modules/menu/menu.test b/modules/menu/menu.test
index 6c11da6..75d9fd7 100644
--- a/modules/menu/menu.test
+++ b/modules/menu/menu.test
@@ -292,7 +292,7 @@ class MenuTestCase extends DrupalWebTestCase {
     $edit = array(
       'link_path' => $link,
       'link_title' => $title,
-      'description' => '',
+      'link_description' => '',
       'enabled' => TRUE, // Use this to disable the menu and test.
       'expanded' => TRUE, // Setting this to true should test whether it works when we do the std_user tests.
       'parent' =>  $menu_name . ':' . $plid,
diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc
index 92c534e..6593fdb 100644
--- a/modules/system/system.admin.inc
+++ b/modules/system/system.admin.inc
@@ -25,10 +25,9 @@ function system_admin_config_page() {
       if (!$item['access']) {
         continue;
       }
-      // The link description, either derived from 'description' in hook_menu()
-      // or customized via menu module is used as title attribute.
       if (!empty($item['localized_options']['attributes']['title'])) {
-        $item['description'] = $item['localized_options']['attributes']['title'];
+        // The title attribute is unset to reduce redundancy in admin pages for
+        // screen readers.
         unset($item['localized_options']['attributes']['title']);
       }
       $block = $item;
diff --git a/modules/system/system.install b/modules/system/system.install
index 11967e4..e30f846 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -1147,13 +1147,18 @@ function system_schema() {
         'default' => '',
       ),
       'link_title' => array(
-      'description' => 'The text displayed for the link, which may be modified by a title callback stored in {menu_router}.',
+        'description' => 'The text displayed for the link, which may be modified by a title callback stored in {menu_router}.',
         'type' => 'varchar',
         'length' => 255,
         'not null' => TRUE,
         'default' => '',
         'translatable' => TRUE,
       ),
+      'link_description' => array(
+        'description' => 'A description of this item.',
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
       'options' => array(
         'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.',
         'type' => 'blob',
diff --git a/modules/system/system.module b/modules/system/system.module
index 21b23f4..a32ed0e 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -2110,10 +2110,9 @@ function system_admin_menu_block($item) {
   foreach ($query->execute() as $link) {
     _menu_link_translate($link);
     if ($link['access']) {
-      // The link description, either derived from 'description' in
-      // hook_menu() or customized via menu module is used as title attribute.
       if (!empty($link['localized_options']['attributes']['title'])) {
-        $link['description'] = $link['localized_options']['attributes']['title'];
+        // The title attribute is unset to reduce redundancy in admin pages for
+        // screen readers.
         unset($link['localized_options']['attributes']['title']);
       }
       // Prepare for sorting as in function _menu_tree_check_access().
