diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index ccf703e..d68214a 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -2215,6 +2215,19 @@ function menu_secondary_local_tasks() {
  */
 function menu_local_actions() {
   $links = menu_local_tasks();
+  $router_item = menu_get_item();
+  foreach (Drupal::moduleHandler()->invokeAll('local_actions_info') as $route_name => $route_info) {
+    if (in_array($router_item['route_name'], $route_info['appears_on'])) {
+      $route_path = _menu_router_translate_route($route_name);
+      $links['actions'][$route_path] = array(
+        '#theme' => 'menu_local_action',
+        '#link' => array(
+          'title' => $route_info['title'],
+          'href' => $route_path,
+        ),
+      );
+    }
+  }
   return $links['actions'];
 }
 
@@ -2677,8 +2690,9 @@ function menu_router_build($save = FALSE) {
         // so the path of the route takes precedence.
         if (isset($router_items[$path]['route_name'])) {
           $router_item = $router_items[$path];
-          $router_item = _menu_router_merge_route($router_item, $path);
-          $new_path = $router_item['path'];
+          $router_item['page callback'] = 'USES_ROUTE';
+          $router_item['access callback'] = TRUE;
+          $new_path = _menu_router_translate_route($router_item['route_name']);
           unset($router_items[$path]);
           $router_items[$new_path] = $router_item;
           $path = $new_path;
@@ -2697,32 +2711,20 @@ function menu_router_build($save = FALSE) {
 }
 
 /**
- * Merges a legacy menu router item with its referenced route.
+ * Translates a route name to its router item path.
  *
- * @param array $router_item
- *   The router item to modify.
- * @param string $path
- *   The path this router item is for.
+ * @param string $route_name
+ *   The route name to translate.
  *
- * @return array
- *   The modified router item, including the path pattern from the route in the
- *   'path' key.
+ * @return string
+ *   The translated path pattern from the route.
  */
-function _menu_router_merge_route(array $router_item, $path) {
-  $router_item['path'] = $path;
-
-  $route_provider = drupal_container()->get('router.route_provider');
-
-  $route = $route_provider->getRouteByName($router_item['route_name']);
-  $router_item['path'] = trim($route->getPattern(), '/');
-
-  $router_item['page callback'] = 'USES_ROUTE';
-  $router_item['access callback'] = TRUE;
+function _menu_router_translate_route($route_name) {
+  $route = Drupal::service('router.route_provider')->getRouteByName($route_name);
+  $path = trim($route->getPattern(), '/');
 
   // Translate placeholders, e.g. {foo} -> %.
-  $router_item['path'] = preg_replace('/{' . DRUPAL_PHP_FUNCTION_PATTERN . '}/', '%', $router_item['path']);
-
-  return $router_item;
+  return preg_replace('/{' . DRUPAL_PHP_FUNCTION_PATTERN . '}/', '%', $path);
 }
 
 /**
diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module
index 1a5ed47..859b55f 100644
--- a/core/modules/aggregator/aggregator.module
+++ b/core/modules/aggregator/aggregator.module
@@ -95,11 +95,6 @@ function aggregator_menu() {
     'route_name' => 'aggregator_admin_overview',
     'weight' => 10,
   );
-  $items['admin/config/services/aggregator/add/feed'] = array(
-    'title' => 'Add feed',
-    'route_name' => 'aggregator_feed_add',
-    'type' => MENU_LOCAL_ACTION,
-  );
   $items['admin/config/services/aggregator/add/category'] = array(
     'title' => 'Add category',
     'page callback' => 'drupal_get_form',
@@ -108,11 +103,6 @@ function aggregator_menu() {
     'type' => MENU_LOCAL_ACTION,
     'file' => 'aggregator.admin.inc',
   );
-  $items['admin/config/services/aggregator/add/opml'] = array(
-    'title' => 'Import OPML',
-    'type' => MENU_LOCAL_ACTION,
-    'route_name' => 'aggregator_opml_add',
-  );
   $items['admin/config/services/aggregator/remove/%aggregator_feed'] = array(
     'title' => 'Remove items',
     'route_name' => 'aggregator_feed_items_delete',
@@ -249,6 +239,26 @@ function aggregator_menu() {
 }
 
 /**
+ * Implements hook_local_actions_info()
+ */
+function aggregator_local_actions_info() {
+  return array(
+    'aggregator_opml_add' => array(
+      'title' => 'Import OPML',
+      'appears_on' => array(
+        'aggregator_admin_overview',
+      ),
+    ),
+    'aggregator_feed_add' => array(
+      'title' => 'Add feed',
+      'appears_on' => array(
+        'aggregator_admin_overview',
+      ),
+    ),
+  );
+}
+
+/**
  * Title callback: Returns a title for aggregator category pages.
  *
  * @param $category
diff --git a/core/modules/picture/picture.module b/core/modules/picture/picture.module
index 60d0f5b..d0cbb4d 100644
--- a/core/modules/picture/picture.module
+++ b/core/modules/picture/picture.module
@@ -48,6 +48,20 @@ function picture_permission() {
 }
 
 /**
+ * Implements hook_local_actions_info()
+ */
+function picture_local_actions_info() {
+  return array(
+    'picture_mapping_page_add' => array(
+      'title' => 'Add picture mapping',
+      'appears_on' => array(
+        'picture_mapping_page',
+      ),
+    ),
+  );
+}
+
+/**
  * Implements hook_menu().
  */
 function picture_menu() {
@@ -59,11 +73,6 @@ function picture_menu() {
     'weight' => 10,
     'route_name' => 'picture_mapping_page',
   );
-  $items['admin/config/media/picturemapping/add'] = array(
-    'title' => 'Add picture mapping',
-    'route_name' => 'picture_mapping_page_add',
-    'type' => MENU_LOCAL_ACTION,
-  );
   $items['admin/config/media/picturemapping/%picture_mapping/edit'] = array(
     'title' => 'Edit picture mapping',
     'route_name' => 'picture_mapping_page_edit',
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/LocalActionTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/LocalActionTest.php
new file mode 100644
index 0000000..d5ba5b1
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/LocalActionTest.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Menu\LocalActionTest.
+ */
+
+namespace Drupal\system\Tests\Menu;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests local actions.
+ */
+class LocalActionTest extends WebTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array('menu_test');
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Local actions',
+      'description' => 'Tests local actions derived from router and added/altered via hooks.',
+      'group' => 'Menu',
+    );
+  }
+
+  /**
+   * Tests appearance of local actions.
+   */
+  function testLocalAction() {
+    $this->drupalGet('menu-test-local-action');
+    // Ensure that both menu and route based actions are shown.
+    $this->assertLocalAction(array(
+      'menu-test-local-action/hook_menu' => 'My hook_menu action',
+      'menu-test-local-action/routing' => 'My routing action',
+    ));
+  }
+
+  /**
+   * Asserts local actions in the page output.
+   *
+   * @param array $actions
+   *   A list of expected action link titles, keyed by the hrefs.
+   */
+  protected function assertLocalAction(array $actions) {
+    $elements = $this->xpath('//a[contains(@class, :class)]', array(
+      ':class' => 'button-action',
+    ));
+    $index = 0;
+    foreach ($actions as $href => $title) {
+      $this->assertEqual((string) $elements[$index], $title);
+      $this->assertEqual($elements[$index]['href'], url($href));
+      $index++;
+    }
+  }
+
+}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 5360a83..4e434a4 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -865,6 +865,29 @@ function hook_menu() {
 }
 
 /**
+ * Define route-based local actions.
+ *
+ * Instead of using MENU_LOCAL_ACTION in hook_menu(), implement
+ * hook_local_actions_info().
+ *
+ * @return array
+ *   An associative array keyed by the local action's route name, containing:
+ *   - title: The title of the local action.
+ *   - routes: An array of route names for this action to be display on.
+ */
+function hook_local_actions_info() {
+  return array(
+    'mymodule.route.action' => array(
+      'title' => 'Perform local action',
+      'appears_on' => array(
+        'mymodule.other_route',
+        'mymodule.other_other_route',
+      ),
+    ),
+  );
+}
+
+/**
  * Alter the data being saved to the {menu_router} table after hook_menu is invoked.
  *
  * This hook is invoked by menu_router_build(). The menu definitions are passed
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 695c216..93c47f8 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -416,10 +416,36 @@ function menu_test_menu() {
     'type' => MENU_LOCAL_TASK,
   );
 
+  $items['menu-test-local-action'] = array(
+    'title' => 'Local action parent',
+    'route_name' => 'menu_test_local_action1',
+  );
+
+  $items['menu-test-local-action/hook_menu'] = array(
+    'title' => 'My hook_menu action',
+    'route_name' => 'menu_test_local_action2',
+    'weight' => -10,
+    'type' => MENU_LOCAL_ACTION,
+  );
+
   return $items;
 }
 
 /**
+ * Implements hook_local_actions_info().
+ */
+function menu_test_local_actions_info() {
+  return array(
+    'menu_test_local_action3' => array(
+      'title' => 'My routing action',
+      'appears_on' => array(
+        'menu_test_local_action1',
+      ),
+    ),
+  );
+}
+
+/**
  * Implements hook_menu_local_tasks().
  */
 function menu_test_menu_local_tasks(&$data, $router_item, $root_path) {
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index 12c5ad1..09b42ba 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
@@ -16,3 +16,24 @@ menu_router_test3:
     _content: '\Drupal\menu_test\TestControllers::test2'
   requirements:
     _access: 'FALSE'
+
+menu_test_local_action1:
+  pattern: '/menu-test-local-action'
+  defaults:
+    _content: '\Drupal\menu_test\TestControllers::test1'
+  requirements:
+    _access: 'TRUE'
+
+menu_test_local_action2:
+  pattern: '/menu-test-local-action/hook_menu'
+  defaults:
+    _content: '\Drupal\menu_test\TestControllers::test2'
+  requirements:
+    _access: 'TRUE'
+
+menu_test_local_action3:
+  pattern: '/menu-test-local-action/routing'
+  defaults:
+    _content: '\Drupal\menu_test\TestControllers::test2'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module
index f169fd0..3e72e92 100644
--- a/core/modules/views_ui/views_ui.module
+++ b/core/modules/views_ui/views_ui.module
@@ -30,12 +30,6 @@ function views_ui_menu() {
     'type' => MENU_DEFAULT_LOCAL_TASK,
   );
 
-  $items['admin/structure/views/add'] = array(
-    'title' => 'Add new view',
-    'route_name' => 'views_ui.add',
-    'type' => MENU_LOCAL_ACTION,
-  );
-
   $items['admin/structure/views/settings'] = array(
     'title' => 'Settings',
     'route_name' => 'views_ui.settings.basic',
@@ -114,6 +108,20 @@ function views_ui_entity_info(&$entity_info) {
 }
 
 /**
+ * Implements hook_local_actions_info().
+ */
+function views_ui_local_actions_info() {
+  return array(
+    'views_ui.add' => array(
+      'title' => 'Add new view',
+      'appears_on' => array(
+        'views_ui.list',
+      ),
+    ),
+  );
+}
+
+/**
  * Implements hook_theme().
  */
 function views_ui_theme() {
