diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 0c18a60..dac97ce 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -654,12 +654,13 @@ function _menu_check_access(&$item, $map) {
  *   $item['title'] is localized according to $item['title_callback'].
  *   If an item's callback is check_plain(), $item['options']['html'] becomes
  *   TRUE.
- *   $item['description'] is translated using t().
+ *   $item['description'] is computed using $item['description_callback'] if
+ *   specified; otherwise it is translated using t().
  *   When doing link translation and the $item['options']['attributes']['title']
  *   (link title attribute) matches the description, it is translated as well.
  */
 function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
-  $callback = $item['title_callback'];
+  $title_callback = $item['title_callback'];
   $item['localized_options'] = $item['options'];
   // All 'class' attributes are assumed to be an array during rendering, but
   // links stored in the database may use an old string value.
@@ -678,7 +679,7 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
   if (!$link_translate || ($item['title'] == $item['link_title'])) {
     // t() is a special case. Since it is used very close to all the time,
     // we handle it directly instead of using indirect, slower methods.
-    if ($callback == 't') {
+    if ($title_callback == 't') {
       if (empty($item['title_arguments'])) {
         $item['title'] = t($item['title']);
       }
@@ -686,15 +687,15 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
         $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
       }
     }
-    elseif ($callback) {
+    elseif ($title_callback) {
       if (empty($item['title_arguments'])) {
-        $item['title'] = $callback($item['title']);
+        $item['title'] = $title_callback($item['title']);
       }
       else {
-        $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
+        $item['title'] = call_user_func_array($title_callback, menu_unserialize($item['title_arguments'], $map));
       }
       // Avoid calling check_plain again on l() function.
-      if ($callback == 'check_plain') {
+      if ($title_callback == 'check_plain') {
         $item['localized_options']['html'] = TRUE;
       }
     }
@@ -706,11 +707,34 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
   // 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 (!empty($item['description_arguments']) || !empty($item['description'])) {
+    $description_callback = $item['description_callback'];
+    // If the description callback is t(), call it directly.
+    if ($description_callback == 't') {
+       if (empty($item['description_arguments'])) {
+         $item['description'] = t($item['description']);
+       }
+       else {
+         $item['description'] = t($item['description'], menu_unserialize($item['description_arguments'], $map));
+       }
+    }
+    elseif ($description_callback) {
+       // If there are no arguments, call the description callback directly.
+       if (empty($item['description_arguments'])) {
+         $item['description'] = $description_callback($item['description']);
+       }
+       // Otherwise, use call_user_func_array() to pass the arguments.
+       else {
+         $item['description'] = call_user_func_array($description_callback, menu_unserialize($item['description_arguments'], $map));
+       }
     }
   }
+  // If the title and description are the same, use the translated description
+  // as a localized title.
+  if ($link_translate && isset($original_description) && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
+    $item['localized_options']['attributes']['title'] = $item['description'];
+  }
 }
 
 /**
@@ -1406,6 +1430,8 @@ function _menu_build_tree($menu_name, array $parameters = array()) {
       'theme_arguments',
       'type',
       'description',
+      'description_callback',
+      'description_arguments',
     ));
     for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
       $query->orderBy('p' . $i, 'ASC');
@@ -3686,6 +3712,8 @@ function _menu_router_build($callbacks) {
       'theme arguments' => array(),
       'theme callback' => '',
       'description' => '',
+      'description arguments' => array(),
+      'description callback' => 't',
       'position' => '',
       'context' => 0,
       'tab_parent' => '',
@@ -3740,6 +3768,8 @@ function _menu_router_save($menu, $masks) {
       'theme_arguments',
       'type',
       'description',
+      'description_callback',
+      'description_arguments',
       'position',
       'weight',
       'include_file',
@@ -3770,6 +3800,8 @@ function _menu_router_save($menu, $masks) {
       'theme_arguments' => serialize($item['theme arguments']),
       'type' => $item['type'],
       'description' => $item['description'],
+      'description_callback' => $item['description callback'],
+      'description_arguments' => ($item['description arguments'] ? serialize($item['description arguments']) : ''),
       'position' => $item['position'],
       'weight' => $item['weight'],
       'include_file' => $item['include file'],
diff --git a/core/modules/menu/menu.admin.inc b/core/modules/menu/menu.admin.inc
index 2e1725d..4751fcd 100644
--- a/core/modules/menu/menu.admin.inc
+++ b/core/modules/menu/menu.admin.inc
@@ -50,7 +50,7 @@ function menu_overview_form($form, &$form_state, $menu) {
   global $menu_admin;
   $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/menu.admin.css');
   $sql = "
-    SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.delivery_callback, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.*
+    SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.delivery_callback, m.title, m.title_callback, m.title_arguments, m.type, m.description, m.description_callback, m.description_arguments, ml.*
     FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
     WHERE ml.menu_name = :menu
     ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 2fb21c7..a419f95 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -693,6 +693,10 @@ function hook_menu_get_item_alter(&$router_item, $path, $original_map) {
  *   - "title arguments": Arguments to send to t() or your custom callback,
  *     with path component substitution as described above.
  *   - "description": The untranslated description of the menu item.
+ *   - description callback: Function to generate the description; defaults to
+ *     t(). If you require only the raw string to be output, set this to FALSE.
+ *   - description arguments: Arguments to send to t() or your custom callback,
+ *     with path component substitution as described above.
  *   - "page callback": The function to call to display a web page when the user
  *     visits the path. If omitted, the parent menu item's callback will be used
  *     instead.
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index f967bb6..bea6fd4 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1160,6 +1160,20 @@ function system_schema() {
         'type' => 'text',
         'not null' => TRUE,
       ),
+      'description_callback' => array(
+        'description' => 'A function which will alter the description. Defaults to t().',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'description_arguments' => array(
+        'description' => 'A serialized array of arguments for the description callback. If empty, the description will be used as the sole argument for the description callback.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
       'position' => array(
         'description' => 'The position of the block (left or right) on the system administration page for this item.',
         'type' => 'varchar',
@@ -1887,6 +1901,32 @@ function system_update_8010() {
 }
 
 /**
+ * Add description_callback and description_arguments fields to {menu_router}.
+ */
+function system_update_8011() {
+  if (!db_field_exists('menu_router', 'description_callback')) {
+    $description_callback = array(
+      'description' => 'A function which will alter the description. Defaults to t().',
+      'type' => 'varchar',
+      'length' => 255,
+      'not null' => TRUE,
+      'default' => '',
+    );
+    db_add_field('menu_router', 'description_callback', $description_callback);
+  }
+  if (!db_field_exists('menu_router', 'description_arguments')) {
+    $description_arguments = array(
+      'description' => 'A serialized array of arguments for the description callback. If empty, the description will be used as the sole argument for the description callback.',
+      'type' => 'varchar',
+      'length' => 255,
+      'not null' => TRUE,
+      'default' => '',
+    );
+    db_add_field('menu_router', 'description_arguments', $description_arguments);
+  }
+}
+
+/**
  * @} End of "defgroup updates-7.x-to-8.x".
  * The next series of updates should start at 9000.
  */
diff --git a/core/modules/system/tests/menu.test b/core/modules/system/tests/menu.test
index dbd94a1..627ab54 100644
--- a/core/modules/system/tests/menu.test
+++ b/core/modules/system/tests/menu.test
@@ -184,6 +184,16 @@ class MenuRouterTestCase extends WebTestBase {
   }
 
   /**
+   * Tests menu item descriptions.
+   */
+  function testDescriptionMenuItems() {
+    // Verify that the menu router item title is output as page title.
+    $this->drupalGet('menu_callback_description');
+    $this->assertText(t('Menu item description text'));
+    $this->assertRaw(check_plain('<strong>Menu item description arguments</strong>'));
+  }
+
+  /**
    * Test the theme callback when it is set to use an administrative theme.
    */
   function testThemeCallbackAdministrative() {
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index 6e6bb4d..8f24a4f 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -22,6 +22,28 @@ function menu_test_menu() {
     'type' => MENU_CALLBACK,
     'access arguments' => array('access content'),
   );
+  // This item uses system_admin_menu_block_page() to list child items.
+  $items['menu_callback_description'] = array(
+    'title' => 'Menu item title',
+    'page callback' => 'system_admin_menu_block_page',
+    'description' => 'Menu item description parent',
+    'access arguments' => array('access content'),
+    'file' => 'system.admin.inc',
+    'file path' => drupal_get_path('module', 'system'),
+  );
+  $items['menu_callback_description/description-plain'] = array(
+    'title' => 'Menu item with a regular description',
+    'page callback' => 'menu_test_callback',
+    'description' => 'Menu item description text',
+    'access arguments' => array('access content'),
+  );
+  $items['menu_callback_description/description-callback'] = array(
+    'title' => 'Menu item with a description set with a callback',
+    'page callback' => 'menu_test_callback',
+    'description callback' => 'check_plain',
+    'description arguments' => array('<strong>Menu item description arguments</strong>'),
+    'access arguments' => array('access content'),
+  );
   // Use FALSE as 'title callback' to bypass t().
   $items['menu_no_title_callback'] = array(
     'title' => 'A title with @placeholder',
