diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php
index 979dddd..e335b19 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php
@@ -30,8 +30,8 @@ public function load() {
   /**
    * {@inheritdoc}
    */
-  public function getOperations(EntityInterface $entity) {
-    $operations = parent::getOperations($entity);
+  public function getDefaultOperations(EntityInterface $entity) {
+    $operations = parent::getDefaultOperations($entity);
     $uri = $entity->uri();
 
     // Ensure the edit operation exists since it is access controlled.
diff --git a/core/lib/Drupal/Core/Entity/EntityListController.php b/core/lib/Drupal/Core/Entity/EntityListController.php
index fdbc6a9..1d3db14 100644
--- a/core/lib/Drupal/Core/Entity/EntityListController.php
+++ b/core/lib/Drupal/Core/Entity/EntityListController.php
@@ -112,6 +112,24 @@ protected function getLabel(EntityInterface $entity) {
    * {@inheritdoc}
    */
   public function getOperations(EntityInterface $entity) {
+    $operations = array_merge($this->moduleHandler->invokeAll('entity_operation', array($entity)), $this->getDefaultOperations($entity));
+    $this->moduleHandler->alter('entity_operation', $operations, $entity);
+    uasort($operations, '\Drupal\Component\Utility\SortArray::sortByWeightElement');
+
+    return $operations;
+  }
+
+  /**
+   * Gets this list's default operations.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity the operations are for.
+   *
+   * @return array
+   *   The array structure is identical to the return value of
+   *   self::getOperations().
+   */
+  protected function getDefaultOperations(EntityInterface $entity) {
     $uri = $entity->uri();
 
     $operations = array();
@@ -176,13 +194,9 @@ public function buildRow(EntityInterface $entity) {
    * @see \Drupal\Core\Entity\EntityListController::render()
    */
   public function buildOperations(EntityInterface $entity) {
-    // Retrieve and sort operations.
-    $operations = $this->getOperations($entity);
-    $this->moduleHandler->alter('entity_operation', $operations, $entity);
-    uasort($operations, 'drupal_sort_weight');
     $build = array(
       '#type' => 'operations',
-      '#links' => $operations,
+      '#links' => $this->getOperations($entity),
     );
     return $build;
   }
diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockListController.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockListController.php
index efa31b3..645bf6f 100644
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockListController.php
+++ b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockListController.php
@@ -34,8 +34,8 @@ public function buildRow(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  public function getOperations(EntityInterface $entity) {
-    $operations = parent::getOperations($entity);
+  public function getDefaultOperations(EntityInterface $entity) {
+    $operations = parent::getDefaultOperations($entity);
     // The custom block edit path does not contain '/edit'.
     if (isset($operations['edit'])) {
       $uri = $entity->uri();
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php
index 09a9de6..a81233a 100644
--- a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php
@@ -83,13 +83,13 @@ public function buildHeader() {
   /**
    * {@inheritdoc}
    */
-  public function buildOperations(EntityInterface $entity) {
-    $operations = parent::buildOperations($entity);
-    foreach (array_keys($operations['#links']) as $operation) {
+  public function getOperations(EntityInterface $entity) {
+    $operations = parent::getOperations($entity);
+    foreach (array_keys($operations) as $operation) {
       // This is a translation UI for translators. Show the translation
       // operation only.
       if (!($operation == 'translate')) {
-        unset($operations['#links'][$operation]);
+        unset($operations[$operation]);
       }
     }
     return $operations;
diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module
index ea29536..11d3449 100644
--- a/core/modules/field_ui/field_ui.module
+++ b/core/modules/field_ui/field_ui.module
@@ -163,9 +163,10 @@ function field_ui_form_node_type_form_alter(&$form, $form_state) {
 }
 
 /**
- * Implements hook_entity_operation_alter().
+ * Implements hook_entity_operation().
  */
-function field_ui_entity_operation_alter(array &$operations, EntityInterface $entity) {
+function field_ui_entity_operation(EntityInterface $entity) {
+  $operations = array();
   $info = $entity->entityInfo();
   // Add manage fields and display links if this entity type is the bundle
   // of another.
@@ -196,6 +197,8 @@ function field_ui_entity_operation_alter(array &$operations, EntityInterface $en
       );
     }
   }
+
+  return $operations;
 }
 
 /**
diff --git a/core/modules/node/lib/Drupal/node/NodeListController.php b/core/modules/node/lib/Drupal/node/NodeListController.php
index b9582ae..08789d7 100644
--- a/core/modules/node/lib/Drupal/node/NodeListController.php
+++ b/core/modules/node/lib/Drupal/node/NodeListController.php
@@ -125,8 +125,8 @@ public function buildRow(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  public function getOperations(EntityInterface $entity) {
-    $operations = parent::getOperations($entity);
+  protected function getDefaultOperations(EntityInterface $entity) {
+    $operations = parent::getDefaultOperations($entity);
 
     $destination = drupal_get_destination();
     foreach ($operations as $key => $operation) {
diff --git a/core/modules/system/entity.api.php b/core/modules/system/entity.api.php
index 3eb29dd..c476467 100644
--- a/core/modules/system/entity.api.php
+++ b/core/modules/system/entity.api.php
@@ -690,6 +690,27 @@ function hook_entity_field_info_alter(&$info, $entity_type) {
 }
 
 /**
+ * Declares entity operations.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ *   The entity on which the linked operations will be performed.
+ *
+ * @return array
+ *   Operations array as returned by
+ *   \Drupal\Core\Entity\EntityListControllerInterface::getOperations().
+ */
+function hook_entity_operation(\Drupal\Core\Entity\EntityInterface $entity) {
+  $operations = array();
+  $operations['translate'] = array(
+    'title' => t('Translate'),
+    'route_name' => 'foo_module.entity.translate',
+    'weight' => 50,
+  );
+
+  return $operations;
+}
+
+/**
  * Alter entity operations.
  *
  * @param array $operations
@@ -699,11 +720,12 @@ function hook_entity_field_info_alter(&$info, $entity_type) {
  *   The entity on which the linked operations will be performed.
  */
 function hook_entity_operation_alter(array &$operations, \Drupal\Core\Entity\EntityInterface $entity) {
-  $uri = $entity->uri();
+  // Alter the title and weight.
   $operations['translate'] = array(
-    'title' => t('Translate'),
-    'href' => $uri['path'] . '/translate',
-    'weight' => 50,
+    'title' => t('Translate @entity_type', array(
+      '@entity_type' => $entity->entityType(),
+    )),
+    'weight' => 99,
   );
 }
 
diff --git a/core/modules/user/lib/Drupal/user/Controller/UserListController.php b/core/modules/user/lib/Drupal/user/Controller/UserListController.php
index 4d2a406..1d1b1c4 100644
--- a/core/modules/user/lib/Drupal/user/Controller/UserListController.php
+++ b/core/modules/user/lib/Drupal/user/Controller/UserListController.php
@@ -142,8 +142,8 @@ public function buildRow(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  public function getOperations(EntityInterface $entity) {
-    $operations = parent::getOperations($entity);
+  public function getDefaultOperations(EntityInterface $entity) {
+    $operations = parent::getDefaultOperations($entity);
     if (isset($operations['edit'])) {
       $destination = drupal_get_destination();
       $operations['edit']['query'] = $destination;
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php
index 6db04b5..9814e4b 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Entity;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListController;
 use Drupal\entity_test\EntityTestListController;
 use Drupal\Tests\UnitTestCase;
 
@@ -34,6 +35,34 @@ class EntityListControllerTest extends UnitTestCase {
    */
   protected $entityListController;
 
+  /**
+   * The entity type used for testing.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $entityType;
+
+  /**
+   * The module handler used for testing.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The translation manager.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface
+   */
+  protected $translationManager;
+
+  /**
+   * The role storage used for testing.
+   *
+   * @var \Drupal\user\RoleStorageControllerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $roleStorage;
+
   public static function getInfo() {
     return array(
       'name' => 'Entity list controller test',
@@ -49,10 +78,49 @@ protected function setUp() {
     parent::setUp();
 
     $this->role = $this->getMock('Drupal\user\RoleInterface');
-    $role_storage_controller = $this->getMock('Drupal\user\RoleStorageControllerInterface');
-    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
-    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $this->entityListController = new TestEntityListController($entity_type, $role_storage_controller, $module_handler);
+    $this->roleStorage = $this->getMock('\Drupal\user\RoleStorageControllerInterface');
+    $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->entityType = $this->getMock('\Drupal\Core\Entity\EntityTypeInterface');
+    $this->translationManager = $this->getMock('\Drupal\Core\StringTranslation\TranslationInterface');
+    $this->entityListController = new TestEntityListController($this->entityType, $this->roleStorage, $this->moduleHandler);
+  }
+
+  /**
+   * @covers \Drupal\Core\Entity\EntityListController::getOperations
+   */
+  public function testGetOperations() {
+    $operations = array(
+      $this->randomName() => array(
+        'title' => $this->randomName(),
+      ),
+    );
+    $this->moduleHandler->expects($this->once())
+      ->method('invokeAll')
+      ->with('entity_operation', array($this->role))
+      ->will($this->returnValue($operations));
+    $this->moduleHandler->expects($this->once())
+      ->method('alter')
+      ->with('entity_operation');
+
+    $map = array(
+      array('update', TRUE),
+      array('delete', TRUE),
+    );
+    $this->role->expects($this->any())
+      ->method('access')
+      ->will($this->returnValue(TRUE));
+
+    $list = new EntityListController($this->entityType, $this->roleStorage, $this->moduleHandler);
+    $list->setTranslationManager($this->translationManager);
+
+    $operations = $list->getOperations($this->role);
+    $this->assertInternalType('array', $operations);
+    $this->assertArrayHasKey('edit', $operations);
+    $this->assertInternalType('array', $operations['edit']);
+    $this->assertArrayHasKey('title', $operations['edit']);
+    $this->assertArrayHasKey('delete', $operations);
+    $this->assertInternalType('array', $operations['delete']);
+    $this->assertArrayHasKey('title', $operations['delete']);
   }
 
   /**
