diff --git a/core/lib/Drupal/Core/Controller/TitleResolverInterface.php b/core/lib/Drupal/Core/Controller/TitleResolverInterface.php
index 810309f..3eab0a0 100644
--- a/core/lib/Drupal/Core/Controller/TitleResolverInterface.php
+++ b/core/lib/Drupal/Core/Controller/TitleResolverInterface.php
@@ -28,8 +28,9 @@
    * @param \Symfony\Component\Routing\Route $route
    *   The route information of the route to fetch the title.
    *
-   * @return string|null
-   *   The title for the route.
+   * @return string|array|null
+   *   The title for the route. May be a string or a render array, or NULL if
+   *   there is no title.
    */
   public function getTitle(Request $request, Route $route);
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php b/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php
index f7dbc50..29cd7be 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php
@@ -26,6 +26,13 @@ public function getBaseFormId() {
   /**
    * {@inheritdoc}
    */
+  public function getTitle() {
+    return $this->getQuestion();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getDescription() {
     return $this->t('This action cannot be undone.');
   }
@@ -57,8 +64,6 @@ public function getFormName() {
   public function buildForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildForm($form, $form_state);
 
-    $form['#title'] = $this->getQuestion();
-
     $form['#attributes']['class'][] = 'confirmation';
     $form['description'] = array('#markup' => $this->getDescription());
     $form[$this->getFormName()] = array('#type' => 'hidden', '#value' => 1);
diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
index 156e749..0bc52be 100644
--- a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Entity\Controller;
 
+use Drupal\Component\Utility\String;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
@@ -74,10 +75,47 @@ public static function create(ContainerInterface $container) {
    *   A render array as expected by drupal_render().
    */
   public function view(EntityInterface $_entity, $view_mode = 'full', $langcode = NULL) {
-    $page = $this->entityManager
+    $build = $this->entityManager
       ->getViewBuilder($_entity->getEntityTypeId())
       ->view($_entity, $view_mode, $langcode);
 
+    foreach ($_entity->uriRelationships() as $rel) {
+      // Set the node path as the canonical URL to prevent duplicate content.
+      $build['#attached']['html_head_link'][] = array(
+        array(
+          'rel' => $rel,
+          'href' => $_entity->url($rel),
+        ),
+        TRUE,
+      );
+
+      if ($rel == 'canonical') {
+        // Set the non-aliased canonical path as a default shortlink.
+        $build['#attached']['html_head_link'][] = array(
+          array(
+            'rel' => 'shortlink',
+            'href' => $_entity->url($rel, array('alias' => TRUE)),
+          ),
+          TRUE,
+        );
+      }
+    }
+
+    return $build;
+  }
+
+  /**
+   * The _title_callback for the page that renders a single entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $_entity
+   *   The current entity.
+   * @param string $view_mode
+   *   The view mode that should be used to display the entity.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function title(EntityInterface $_entity, $view_mode = 'full') {
     // If the entity's label is rendered using a field formatter, set the
     // rendered title field formatter as the page title instead of the default
     // plain text title. This allows attributes set on the field to propagate
@@ -91,11 +129,11 @@ public function view(EntityInterface $_entity, $view_mode = 'full', $langcode =
         $build = $this->entityManager->getTranslationFromContext($_entity)
           ->get($label_field)
           ->view($view_mode);
-        $page['#title'] = $this->renderer->render($build);
+        return $build;
       }
     }
 
-    return $page;
+    return String::checkPlain($this->entityManager->getTranslationFromContext($_entity)->label());
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php
index c330d3a..e46c425 100644
--- a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php
+++ b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php
@@ -21,21 +21,27 @@ class EntityRouteEnhancer implements RouteEnhancerInterface {
    * {@inheritdoc}
    */
   public function enhance(array $defaults, Request $request) {
+    $enhanced_route_defaults = [];
     if (empty($defaults['_controller'])) {
       if (!empty($defaults['_entity_form'])) {
-        $defaults['_controller'] = 'controller.entity_form:getContentResult';
+        $enhanced_route_defaults['_controller'] = 'controller.entity_form:getContentResult';
+        // If no _title or _title_callback are specified, opt in to the default.
+        if (empty($defaults['_title']) && empty($defaults['_title_callback'])) {
+          $enhanced_route_defaults['_title_callback'] = 'controller.entity_form:getTitle';
+        }
       }
       elseif (!empty($defaults['_entity_list'])) {
-        $defaults['_controller'] = '\Drupal\Core\Entity\Controller\EntityListController::listing';
-        $defaults['entity_type'] = $defaults['_entity_list'];
-        unset($defaults['_entity_list']);
+        $enhanced_route_defaults['_controller'] = '\Drupal\Core\Entity\Controller\EntityListController::listing';
+        $enhanced_route_defaults['entity_type'] = $defaults['_entity_list'];
+        unset($enhanced_route_defaults['_entity_list']);
       }
       elseif (!empty($defaults['_entity_view'])) {
-        $defaults['_controller'] = '\Drupal\Core\Entity\Controller\EntityViewController::view';
+        $enhanced_route_defaults['_controller'] = '\Drupal\Core\Entity\Controller\EntityViewController::view';
+        $enhanced_route_defaults['_title_callback'] = '\Drupal\Core\Entity\Controller\EntityViewController::title';
         if (strpos($defaults['_entity_view'], '.') !== FALSE) {
           // The _entity_view entry is of the form entity_type.view_mode.
           list($entity_type, $view_mode) = explode('.', $defaults['_entity_view']);
-          $defaults['view_mode'] = $view_mode;
+          $enhanced_route_defaults['view_mode'] = $view_mode;
         }
         else {
           // Only the entity type is nominated, the view mode will use the
@@ -44,7 +50,7 @@ public function enhance(array $defaults, Request $request) {
         }
         // Set by reference so that we get the upcast value.
         if (!empty($defaults[$entity_type])) {
-          $defaults['_entity'] = &$defaults[$entity_type];
+          $enhanced_route_defaults['_entity'] = &$defaults[$entity_type];
         }
         else {
           // The entity is not keyed by its entity_type. Attempt to find it
@@ -62,7 +68,7 @@ public function enhance(array $defaults, Request $request) {
                     // We have the matching entity type. Set the '_entity' key
                     // to point to this named placeholder. The entity in this
                     // position is the one being rendered.
-                    $defaults['_entity'] = &$defaults[$name];
+                    $enhanced_route_defaults['_entity'] = &$defaults[$name];
                   }
                 }
               }
@@ -78,6 +84,15 @@ public function enhance(array $defaults, Request $request) {
         unset($defaults['_entity_view']);
       }
     }
+
+    if (count($enhanced_route_defaults)) {
+      // Update the defaults on the request.
+      $defaults += $enhanced_route_defaults;
+      // Also update the defaults on the route object. Otherwise e.g. title
+      // callbacks will not be able to use the enhanced defaults.
+      $defaults[RouteObjectInterface::ROUTE_OBJECT]->addDefaults($enhanced_route_defaults);
+    }
+
     return $defaults;
   }
 
diff --git a/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php b/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php
index a11e27f..95ac590 100644
--- a/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php
@@ -29,6 +29,13 @@ public function getBaseFormId() {
   /**
    * {@inheritdoc}
    */
+  public function getTitle() {
+    return $this->getQuestion();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getDescription() {
     return $this->t('This action cannot be undone.');
   }
@@ -60,8 +67,6 @@ public function getFormName() {
   public function buildForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildForm($form, $form_state);
 
-    $form['#title'] = $this->getQuestion();
-
     $form['#attributes']['class'][] = 'confirmation';
     $form['description'] = array('#markup' => $this->getDescription());
     $form[$this->getFormName()] = array('#type' => 'hidden', '#value' => 1);
diff --git a/core/lib/Drupal/Core/Entity/EntityForm.php b/core/lib/Drupal/Core/Entity/EntityForm.php
index a0b18fa..026464d 100644
--- a/core/lib/Drupal/Core/Entity/EntityForm.php
+++ b/core/lib/Drupal/Core/Entity/EntityForm.php
@@ -54,6 +54,38 @@ class EntityForm extends FormBase implements EntityFormInterface {
   /**
    * {@inheritdoc}
    */
+  public function getTitle() {
+    // If this entity is of a certain bundle, use the bundle label, otherwise
+    // use the entity type label.
+    $bundle_entity_type = $this->entity->getEntityType()->getBundleEntityType();
+    if ($bundle_entity_type === 'bundle') {
+      $type = $this->entity->getEntityType()->getLowercaseLabel();
+    }
+    else {
+      $type = entity_load($this->entity->getEntityType()->getBundleEntityType(), $this->entity->bundle())->label();
+    }
+
+    switch ($this->getOperation()) {
+      case 'add':
+        return $this->t('Add <em>@type</em>', ['@type' => $type]);
+
+      case 'edit':
+        return $this->t('<em>Edit @type</em> @title', ['@type' => $type, '@title' => $this->entity->label()]);
+
+      case 'delete':
+        return $this->t('<em>Delete @type</em> @title', ['@type' => $type, '@title' => $this->entity->label()]);
+
+      case 'configure':
+        return $this->t('<em>Configure @type</em> @title', ['@type' => $type, '@title' => $this->entity->label()]);
+
+      case 'duplicate':
+        return $this->t('<em>Duplicate @type</em> @title', ['@type' => $type, '@title' => $this->entity->label()]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function setOperation($operation) {
     // If NULL is passed, do not overwrite the operation.
     if ($operation) {
diff --git a/core/lib/Drupal/Core/Entity/EntityFormInterface.php b/core/lib/Drupal/Core/Entity/EntityFormInterface.php
index a8fe39e..24b6f40 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormInterface.php
@@ -19,6 +19,14 @@
 interface EntityFormInterface extends BaseFormIdInterface {
 
   /**
+   * Gets the page title to use when this form is the main content on the page.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function getTitle();
+
+  /**
    * Sets the operation for this form.
    *
    * @param string $operation
diff --git a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php
index 093cb34..82e7078 100644
--- a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php
+++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Controller\FormController;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Wrapping controller for entity forms that serve as the main page body.
@@ -81,4 +82,19 @@ protected function getFormObject(RouteMatchInterface $route_match, $form_arg) {
     return $form_object;
   }
 
+  /**
+   * Invokes the form and returns the resulting title.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function getTitle(Request $request) {
+    /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
+    $form_object = $this->entityManager->getFormObject($request, $this->formBuilder);
+    return $form_object->getTitle();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Form/ConfirmFormBase.php b/core/lib/Drupal/Core/Form/ConfirmFormBase.php
index 2896690..fbc489f 100644
--- a/core/lib/Drupal/Core/Form/ConfirmFormBase.php
+++ b/core/lib/Drupal/Core/Form/ConfirmFormBase.php
@@ -44,8 +44,6 @@ public function getFormName() {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
-    $form['#title'] = $this->getQuestion();
-
     $form['#attributes']['class'][] = 'confirmation';
     $form['description'] = array('#markup' => $this->getDescription());
     $form[$this->getFormName()] = array('#type' => 'hidden', '#value' => 1);
diff --git a/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php b/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php
index bf86365..1fc6fe5 100644
--- a/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php
+++ b/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php
@@ -103,8 +103,6 @@ public function setMenuLinkInstance(MenuLinkInterface $menu_link) {
    * {@inheritdoc}
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
-    $form['#title'] = $this->t('Edit menu link %title', array('%title' => $this->menuLink->getTitle()));
-
     $provider = $this->menuLink->getProvider();
     $form['info'] = array(
       '#type' => 'item',
diff --git a/core/modules/action/action.routing.yml b/core/modules/action/action.routing.yml
index acb533a..0155288 100644
--- a/core/modules/action/action.routing.yml
+++ b/core/modules/action/action.routing.yml
@@ -10,7 +10,6 @@ action.admin_add:
   path: '/admin/config/system/actions/add/{action_id}'
   defaults:
     _entity_form: 'action.add'
-    _title: 'Add'
   requirements:
     _permission: 'administer actions'
 
@@ -18,7 +17,6 @@ entity.action.edit_form:
   path: '/admin/config/system/actions/configure/{action}'
   defaults:
     _entity_form: 'action.edit'
-    _title: 'Edit'
   requirements:
     _permission: 'administer actions'
 
@@ -26,7 +24,6 @@ entity.action.delete_form:
   path: '/admin/config/system/actions/configure/{action}/delete'
   defaults:
     _entity_form: 'action.delete'
-    _title: 'Delete'
   requirements:
     _permission: 'administer actions'
 
diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml
index 3109e9e..2000388 100644
--- a/core/modules/aggregator/aggregator.routing.yml
+++ b/core/modules/aggregator/aggregator.routing.yml
@@ -18,7 +18,6 @@ aggregator.feed_items_delete:
   path: '/admin/config/services/aggregator/delete/{aggregator_feed}'
   defaults:
     _entity_form: 'aggregator_feed.delete_items'
-    _title: 'Delete items'
   requirements:
     _permission: 'administer news feeds'
 
@@ -26,7 +25,6 @@ aggregator.feed_refresh:
   path: '/admin/config/services/aggregator/update/{aggregator_feed}'
   defaults:
     _controller: '\Drupal\aggregator\Controller\AggregatorController::feedRefresh'
-    _title: 'Update items'
   requirements:
     _permission: 'administer news feeds'
     _csrf_token: 'TRUE'
@@ -53,15 +51,13 @@ entity.aggregator_feed.canonical:
   path: '/aggregator/sources/{aggregator_feed}'
   defaults:
     _entity_view: 'aggregator_feed'
-    _title_callback: '\Drupal\aggregator\Controller\AggregatorController::feedTitle'
   requirements:
     _permission: 'access news feeds'
 
 entity.aggregator_feed.edit_form:
   path: '/aggregator/sources/{aggregator_feed}/configure'
   defaults:
-    _entity_form: 'aggregator_feed.default'
-    _title: 'Configure'
+    _entity_form: 'aggregator_feed.configure'
   requirements:
     _permission: 'administer news feeds'
   options:
@@ -71,7 +67,6 @@ entity.aggregator_feed.delete_form:
   path: '/aggregator/sources/{aggregator_feed}/delete'
   defaults:
     _entity_form: 'aggregator_feed.delete'
-    _title: 'Delete feed'
   requirements:
     _permission: 'administer news feeds'
   options:
diff --git a/core/modules/aggregator/src/Controller/AggregatorController.php b/core/modules/aggregator/src/Controller/AggregatorController.php
index b49f3f5..9cd23e7 100644
--- a/core/modules/aggregator/src/Controller/AggregatorController.php
+++ b/core/modules/aggregator/src/Controller/AggregatorController.php
@@ -177,17 +177,4 @@ public function pageLast() {
     return $build;
   }
 
-  /**
-   * Route title callback.
-   *
-   * @param \Drupal\aggregator\FeedInterface $aggregator_feed
-   *   The aggregator feed.
-   *
-   * @return string
-   *   The feed label.
-   */
-  public function feedTitle(FeedInterface $aggregator_feed) {
-    return Xss::filter($aggregator_feed->label());
-  }
-
 }
diff --git a/core/modules/aggregator/src/Entity/Feed.php b/core/modules/aggregator/src/Entity/Feed.php
index 6f3d2e2..d0cac31 100644
--- a/core/modules/aggregator/src/Entity/Feed.php
+++ b/core/modules/aggregator/src/Entity/Feed.php
@@ -28,6 +28,7 @@
  *     "views_data" = "Drupal\aggregator\AggregatorFeedViewsData",
  *     "form" = {
  *       "default" = "Drupal\aggregator\FeedForm",
+ *       "configure" = "Drupal\aggregator\FeedForm",
  *       "delete" = "Drupal\aggregator\Form\FeedDeleteForm",
  *       "delete_items" = "Drupal\aggregator\Form\FeedItemsDeleteForm",
  *     }
diff --git a/core/modules/block/block.routing.yml b/core/modules/block/block.routing.yml
index 2f26a38..cd6c78d 100644
--- a/core/modules/block/block.routing.yml
+++ b/core/modules/block/block.routing.yml
@@ -13,15 +13,13 @@ entity.block.delete_form:
   path: '/admin/structure/block/manage/{block}/delete'
   defaults:
     _entity_form: 'block.delete'
-    _title: 'Delete block'
   requirements:
     _permission: 'administer blocks'
 
 entity.block.edit_form:
   path: '/admin/structure/block/manage/{block}'
   defaults:
-    _entity_form: 'block.default'
-    _title: 'Configure block'
+    _entity_form: 'block.configure'
   requirements:
     _entity_access: 'block.update'
 
diff --git a/core/modules/block/src/Controller/BlockController.php b/core/modules/block/src/Controller/BlockController.php
index 5ed1ce0..c5b55e3 100644
--- a/core/modules/block/src/Controller/BlockController.php
+++ b/core/modules/block/src/Controller/BlockController.php
@@ -56,7 +56,6 @@ public static function create(ContainerInterface $container) {
    */
   public function demo($theme) {
     $page = [
-      '#title' => $this->themeHandler->getName($theme),
       '#type' => 'page',
       '#attached' => array(
         'drupalSettings' => [
diff --git a/core/modules/block/src/Entity/Block.php b/core/modules/block/src/Entity/Block.php
index 4af2f80..128f0ba 100644
--- a/core/modules/block/src/Entity/Block.php
+++ b/core/modules/block/src/Entity/Block.php
@@ -28,6 +28,7 @@
  *     "list_builder" = "Drupal\block\BlockListBuilder",
  *     "form" = {
  *       "default" = "Drupal\block\BlockForm",
+ *       "configure" = "Drupal\block\BlockForm",
  *       "delete" = "Drupal\block\Form\BlockDeleteForm"
  *     }
  *   },
diff --git a/core/modules/block_content/block_content.routing.yml b/core/modules/block_content/block_content.routing.yml
index 1c78f0c..dc927cf 100644
--- a/core/modules/block_content/block_content.routing.yml
+++ b/core/modules/block_content/block_content.routing.yml
@@ -30,7 +30,6 @@ entity.block_content_type.delete_form:
   path: '/admin/structure/block/block-content/manage/{block_content_type}/delete'
   defaults:
     _entity_form: 'block_content_type.delete'
-    _title: 'Delete'
   requirements:
     _entity_access: 'block_content_type.delete'
   options:
@@ -58,7 +57,6 @@ entity.block_content.delete_form:
   path: '/block/{block_content}/delete'
   defaults:
     _entity_form: 'block_content.delete'
-    _title: 'Delete'
   options:
     _admin_route: TRUE
   requirements:
@@ -68,7 +66,6 @@ block_content.type_add:
   path: '/admin/structure/block/block-content/types/add'
   defaults:
     _entity_form: 'block_content_type.add'
-    _title: 'Add'
   requirements:
     _permission: 'administer blocks'
 
@@ -76,7 +73,6 @@ entity.block_content_type.edit_form:
   path: '/admin/structure/block/block-content/manage/{block_content_type}'
   defaults:
     _entity_form: 'block_content_type.edit'
-    _title: 'Edit'
   requirements:
     _entity_access: 'block_content_type.update'
 
diff --git a/core/modules/block_content/src/BlockContentForm.php b/core/modules/block_content/src/BlockContentForm.php
index 1b81be2..85fadd8 100644
--- a/core/modules/block_content/src/BlockContentForm.php
+++ b/core/modules/block_content/src/BlockContentForm.php
@@ -95,9 +95,6 @@ public function form(array $form, FormStateInterface $form_state) {
     $block = $this->entity;
     $account = $this->currentUser();
 
-    if ($this->operation == 'edit') {
-      $form['#title'] = $this->t('Edit custom block %label', array('%label' => $block->label()));
-    }
     // Override the default CSS class name, since the user-defined custom block
     // type name in 'TYPE-block-form' potentially clashes with third-party class
     // names.
diff --git a/core/modules/book/src/Form/BookAdminEditForm.php b/core/modules/book/src/Form/BookAdminEditForm.php
index 2cf7a18..415e362 100644
--- a/core/modules/book/src/Form/BookAdminEditForm.php
+++ b/core/modules/book/src/Form/BookAdminEditForm.php
@@ -70,7 +70,6 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state, NodeInterface $node = NULL) {
-    $form['#title'] = $node->label();
     $form['#node'] = $node;
     $this->bookAdminTable($node, $form);
     $form['save'] = array(
diff --git a/core/modules/book/src/Form/BookOutlineForm.php b/core/modules/book/src/Form/BookOutlineForm.php
index f343933..e103a05 100644
--- a/core/modules/book/src/Form/BookOutlineForm.php
+++ b/core/modules/book/src/Form/BookOutlineForm.php
@@ -66,8 +66,6 @@ public function getBaseFormId() {
    * {@inheritdoc}
    */
   public function form(array $form, FormStateInterface $form_state) {
-    $form['#title'] = $this->entity->label();
-
     if (!isset($this->entity->book)) {
       // The node is not part of any book yet - set default options.
       $this->entity->book = $this->bookManager->getLinkDefaults($this->entity->id());
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 163b13a..f742e68 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -314,10 +314,6 @@ function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL)
  * Implements hook_form_FORM_ID_alter() for field_ui_field_storage_add_form.
  */
 function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state) {
-  $route_match = \Drupal::routeMatch();
-  if ($form_state->get('entity_type_id') == 'comment' && $route_match->getParameter('commented_entity_type')) {
-    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
-  }
   if (!_comment_entity_uses_integer_id($form_state->get('entity_type_id'))) {
     // You cannot use comment fields on entity types with non-integer IDs.
     unset($form['add']['new_storage_type']['#options']['comment']);
@@ -327,26 +323,6 @@ function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInt
 /**
  * Implements hook_form_FORM_ID_alter().
  */
-function comment_form_field_ui_form_display_overview_form_alter(&$form, FormStateInterface $form_state) {
-  $route_match = \Drupal::routeMatch();
-  if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
-    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function comment_form_field_ui_display_overview_form_alter(&$form, FormStateInterface $form_state) {
-  $route_match = \Drupal::routeMatch();
-  if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
-    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
 function comment_form_field_ui_field_storage_edit_form_alter(&$form, FormStateInterface $form_state) {
   if ($form['#field']->getType() == 'comment') {
     // We only support posting one comment at the time so it doesn't make sense
diff --git a/core/modules/comment/comment.routing.yml b/core/modules/comment/comment.routing.yml
index 4799400..b3b2b82 100644
--- a/core/modules/comment/comment.routing.yml
+++ b/core/modules/comment/comment.routing.yml
@@ -19,8 +19,7 @@ comment.admin_approval:
 entity.comment.edit_form:
   path: '/comment/{comment}/edit'
   defaults:
-    _title: 'Edit'
-    _entity_form: 'comment.default'
+    _entity_form: 'comment.edit'
   requirements:
     _entity_access: 'comment.update'
 
@@ -45,7 +44,6 @@ entity.comment.canonical:
 entity.comment.delete_form:
   path: '/comment/{comment}/delete'
   defaults:
-    _title: 'Delete'
     _entity_form: 'comment.delete'
   requirements:
     _entity_access: 'comment.delete'
@@ -54,7 +52,7 @@ comment.reply:
   path: '/comment/reply/{entity_type}/{entity}/{field_name}/{pid}'
   defaults:
     _controller: '\Drupal\comment\Controller\CommentController::getReplyForm'
-    _title: 'Add new comment'
+    _title_callback: '\Drupal\comment\Controller\CommentController::getReplyTitle'
     pid: ~
   requirements:
     _access: 'TRUE'
@@ -92,7 +90,6 @@ entity.comment_type.delete_form:
   path: '/admin/structure/comment/manage/{comment_type}/delete'
   defaults:
     _entity_form: 'comment_type.delete'
-    _title: 'Delete'
   requirements:
     _entity_access: 'comment_type.delete'
   options:
@@ -112,7 +109,6 @@ entity.comment_type.edit_form:
   path: '/admin/structure/comment/manage/{comment_type}'
   defaults:
     _entity_form: 'comment_type.edit'
-    _title: 'Edit'
   requirements:
     _entity_access: 'comment_type.update'
   options:
diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php
index a9d742b..d571957 100644
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php
@@ -70,6 +70,23 @@ protected function init(FormStateInterface $form_state) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    // Always use the entity type ('comment'), not the bundle (e.g. 'Default
+    // comments').
+    $type = $this->entity->getEntityType()->getLowercaseLabel();
+
+    switch ($this->getOperation()) {
+      case 'add':
+        return $this->t('Add <em>@type</em>', ['@type' => $type]);
+
+      case 'edit':
+        return $this->t('<em>Edit @type</em> @title', ['@type' => $type, '@title' => $this->entity->label()]);
+    }
+  }
+
+  /**
    * Overrides Drupal\Core\Entity\EntityForm::form().
    */
   public function form(array $form, FormStateInterface $form_state) {
@@ -115,11 +132,6 @@ public function form(array $form, FormStateInterface $form_state) {
     if ($is_admin) {
       $author = $comment->getAuthorName();
       $status = $comment->getStatus();
-      if (empty($comment_preview)) {
-        $form['#title'] = $this->t('Edit comment %title', array(
-          '%title' => $comment->getSubject(),
-        ));
-      }
     }
     else {
       if ($this->currentUser->isAuthenticated()) {
diff --git a/core/modules/comment/src/Controller/CommentController.php b/core/modules/comment/src/Controller/CommentController.php
index 84fbf72..c10c283 100644
--- a/core/modules/comment/src/Controller/CommentController.php
+++ b/core/modules/comment/src/Controller/CommentController.php
@@ -255,9 +255,6 @@ public function getReplyForm(Request $request, EntityInterface $entity, $field_n
         unset($build['commented_entity']['#cache']);
       }
     }
-    else {
-      $build['#title'] = $this->t('Preview comment');
-    }
 
     // Show the actual reply box.
     $comment = $this->entityManager()->getStorage('comment')->create(array(
@@ -272,6 +269,23 @@ public function getReplyForm(Request $request, EntityInterface $entity, $field_n
   }
 
   /**
+   * The _title_callback for the comment reply form.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request object.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function getReplyTitle(Request $request) {
+    $title = $this->t('Add new comment');
+    if ($request->request->get('op') == $this->t('Preview')) {
+      $title = $this->t('Preview comment');
+    }
+    return $title;
+  }
+
+  /**
    * Returns a set of nodes' last read timestamps.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
diff --git a/core/modules/comment/src/Entity/Comment.php b/core/modules/comment/src/Entity/Comment.php
index 8b24aba..e6e0cf3 100644
--- a/core/modules/comment/src/Entity/Comment.php
+++ b/core/modules/comment/src/Entity/Comment.php
@@ -31,6 +31,7 @@
  *     "views_data" = "Drupal\comment\CommentViewsData",
  *     "form" = {
  *       "default" = "Drupal\comment\CommentForm",
+ *       "edit" = "Drupal\comment\CommentForm",
  *       "delete" = "Drupal\comment\Form\DeleteForm"
  *     },
  *     "translation" = "Drupal\comment\CommentTranslationHandler"
diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml
index 8eebac3..d81402b 100644
--- a/core/modules/config/config.routing.yml
+++ b/core/modules/config/config.routing.yml
@@ -10,6 +10,7 @@ config.diff:
   path: '/admin/config/development/configuration/sync/diff/{source_name}/{target_name}'
   defaults:
     _controller: '\Drupal\config\Controller\ConfigController::diff'
+    _title_callback: '\Drupal\config\Controller\ConfigController::diffTitle'
     target_name: NULL
   requirements:
     _permission: 'synchronize configuration'
@@ -18,6 +19,7 @@ config.diff_collection:
   path: '/admin/config/development/configuration/sync/diff_collection/{collection}/{source_name}/{target_name}'
   defaults:
     _controller: '\Drupal\config\Controller\ConfigController::diff'
+    _title_callback: '\Drupal\config\Controller\ConfigController::diffTitle'
     target_name: NULL
   requirements:
     _permission: 'synchronize configuration'
diff --git a/core/modules/config/src/Controller/ConfigController.php b/core/modules/config/src/Controller/ConfigController.php
index dc56910..30cf791 100644
--- a/core/modules/config/src/Controller/ConfigController.php
+++ b/core/modules/config/src/Controller/ConfigController.php
@@ -136,7 +136,6 @@ public function diff($source_name, $target_name = NULL, $collection = NULL) {
 
     $build = array();
 
-    $build['#title'] = t('View changes of @config_file', array('@config_file' => $source_name));
     // Add the CSS for the inline diff.
     $build['#attached']['library'][] = 'system/diff';
 
@@ -162,4 +161,18 @@ public function diff($source_name, $target_name = NULL, $collection = NULL) {
 
     return $build;
   }
+
+  /**
+   * The _title_callback for the diff page.
+   *
+   * @param string $source_name
+   *   The name of the configuration file.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function diffTitle($source_name) {
+    return t('View changes of @config_file', array('@config_file' => $source_name));;
+  }
+
 }
diff --git a/core/modules/config_translation/config_translation.routing.yml b/core/modules/config_translation/config_translation.routing.yml
index 0826cbe..386f1a8 100644
--- a/core/modules/config_translation/config_translation.routing.yml
+++ b/core/modules/config_translation/config_translation.routing.yml
@@ -10,5 +10,6 @@ config_translation.entity_list:
   path: '/admin/config/regional/config-translation/{mapper_id}'
   defaults:
     _controller: '\Drupal\config_translation\Controller\ConfigTranslationListController::listing'
+    _title_callback: '\Drupal\config_translation\Controller\ConfigTranslationListController::listingTitle'
   requirements:
     _permission: 'translate configuration'
diff --git a/core/modules/config_translation/src/ConfigNamesMapper.php b/core/modules/config_translation/src/ConfigNamesMapper.php
index b681b33..b03d8e9 100644
--- a/core/modules/config_translation/src/ConfigNamesMapper.php
+++ b/core/modules/config_translation/src/ConfigNamesMapper.php
@@ -238,6 +238,7 @@ public function getOverviewRoute() {
       $this->getBaseRoute()->getPath() . '/translate',
       array(
         '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
+        '_title_callback' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPageTitle',
         'plugin_id' => $this->getPluginId(),
       ),
       array('_config_translation_overview_access' => 'TRUE')
diff --git a/core/modules/config_translation/src/Controller/ConfigTranslationController.php b/core/modules/config_translation/src/Controller/ConfigTranslationController.php
index 72fe9cc..a62179e 100644
--- a/core/modules/config_translation/src/Controller/ConfigTranslationController.php
+++ b/core/modules/config_translation/src/Controller/ConfigTranslationController.php
@@ -107,6 +107,24 @@ public static function create(ContainerInterface $container) {
   }
 
   /**
+   * The _title_callback for the language translations overview page.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   Page request object.
+   * @param string $plugin_id
+   *   The plugin ID of the mapper.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function itemPageTitle(Request $request, $plugin_id) {
+    /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
+    $mapper = $this->configMapperManager->createInstance($plugin_id);
+    $mapper->populateFromRequest($request);
+    return $this->t('Translations for %label', array('%label' => $mapper->getTitle()));
+  }
+
+  /**
    * Language translations overview page for a configuration name.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
@@ -125,7 +143,6 @@ public function itemPage(Request $request, RouteMatchInterface $route_match, $pl
     $mapper->populateFromRequest($request);
 
     $page = array();
-    $page['#title'] = $this->t('Translations for %label', array('%label' => $mapper->getTitle()));
 
     // It is possible the original language this configuration was saved with is
     // not on the system. For example, the configuration shipped in English but
diff --git a/core/modules/config_translation/src/Controller/ConfigTranslationListController.php b/core/modules/config_translation/src/Controller/ConfigTranslationListController.php
index dd4d38e..d488593 100644
--- a/core/modules/config_translation/src/Controller/ConfigTranslationListController.php
+++ b/core/modules/config_translation/src/Controller/ConfigTranslationListController.php
@@ -67,12 +67,32 @@ public function listing($mapper_id) {
     // controller defined, use it. Other mappers, for examples the ones for
     // node_type and block, fallback to the generic configuration translation
     // list controller.
-    $build = $this->entityManager()
+    return $this->entityManager()
       ->getHandler($entity_type, 'config_translation_list')
       ->setMapperDefinition($mapper_definition)
       ->render();
-    $build['#title'] = $mapper->getTypeLabel();
-    return $build;
+  }
+
+  /**
+   * The _title_callback for the listing page.
+   *
+   * @param string $mapper_id
+   *   The name of the mapper.
+   *
+   * @return string|array
+   *   The page title.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   *   Throws an exception if a mapper plugin could not be instantiated from the
+   *   mapper definition in the constructor.
+   */
+  public function listingTitle($mapper_id) {
+    $mapper_definition = $this->mapperManager->getDefinition($mapper_id);
+    $mapper = $this->mapperManager->createInstance($mapper_id, $mapper_definition);
+    if (!$mapper) {
+      throw new NotFoundHttpException();
+    }
+    return $mapper->getTypeLabel();;
   }
 
 }
diff --git a/core/modules/config_translation/src/Form/ConfigTranslationAddForm.php b/core/modules/config_translation/src/Form/ConfigTranslationAddForm.php
index 883574d..3f037c5 100644
--- a/core/modules/config_translation/src/Form/ConfigTranslationAddForm.php
+++ b/core/modules/config_translation/src/Form/ConfigTranslationAddForm.php
@@ -27,6 +27,7 @@ public function getFormId() {
    */
   public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL, $plugin_id = NULL, $langcode = NULL) {
     $form = parent::buildForm($form, $form_state, $request, $plugin_id, $langcode);
+    // @todo Remove #title
     $form['#title'] = $this->t('Add @language translation for %label', array(
       '%label' => $this->mapper->getTitle(),
       '@language' => $this->language->getName(),
diff --git a/core/modules/config_translation/src/Form/ConfigTranslationEditForm.php b/core/modules/config_translation/src/Form/ConfigTranslationEditForm.php
index 2c1fc3c..e2f0037 100644
--- a/core/modules/config_translation/src/Form/ConfigTranslationEditForm.php
+++ b/core/modules/config_translation/src/Form/ConfigTranslationEditForm.php
@@ -27,6 +27,7 @@ public function getFormId() {
    */
   public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL, $plugin_id = NULL, $langcode = NULL) {
     $form = parent::buildForm($form, $form_state, $request, $plugin_id, $langcode);
+    // @todo Remove #title
     $form['#title'] = $this->t('Edit @language translation for %label', array(
       '%label' => $this->mapper->getTitle(),
       '@language' => $this->language->getName(),
diff --git a/core/modules/contact/contact.routing.yml b/core/modules/contact/contact.routing.yml
index 2608a37..0e7e522 100644
--- a/core/modules/contact/contact.routing.yml
+++ b/core/modules/contact/contact.routing.yml
@@ -2,7 +2,6 @@ entity.contact_form.delete_form:
   path: '/admin/structure/contact/manage/{contact_form}/delete'
   defaults:
     _entity_form: 'contact_form.delete'
-    _title: 'Delete'
   requirements:
     _entity_access: 'contact_form.delete'
 
@@ -18,7 +17,6 @@ contact.form_add:
   path: '/admin/structure/contact/add'
   defaults:
     _entity_form: 'contact_form.add'
-    _title: 'Add contact form'
   requirements:
     _permission: 'administer contact forms'
 
@@ -26,14 +24,13 @@ entity.contact_form.edit_form:
   path: '/admin/structure/contact/manage/{contact_form}'
   defaults:
     _entity_form: 'contact_form.edit'
-    _title: 'Edit contact form'
   requirements:
     _entity_access: 'contact_form.update'
 
 contact.site_page:
   path: '/contact'
   defaults:
-    _title: 'Contact'
+    _title_callback: '\Drupal\contact\Controller\ContactController::contactSitePageTitle'
     _controller: '\Drupal\contact\Controller\ContactController::contactSitePage'
     contact_form: NULL
   requirements:
@@ -42,7 +39,7 @@ contact.site_page:
 contact.site_page_form:
   path: '/contact/{contact_form}'
   defaults:
-    _title: 'Contact'
+    _title_callback: '\Drupal\contact\Controller\ContactController::contactSitePageTitle'
     _controller: '\Drupal\contact\Controller\ContactController::contactSitePage'
   requirements:
     _entity_access: 'contact_form.view'
@@ -50,7 +47,7 @@ contact.site_page_form:
 entity.user.contact_form:
   path: '/user/{user}/contact'
   defaults:
-    _title: 'Contact'
+    _title_callback: '\Drupal\contact\Controller\ContactController::contactPersonalPageTitle'
     _controller: '\Drupal\contact\Controller\ContactController::contactPersonalPage'
   requirements:
     _access_contact_personal_tab: 'TRUE'
diff --git a/core/modules/contact/src/Controller/ContactController.php b/core/modules/contact/src/Controller/ContactController.php
index 78ee8e0..d71b5b8 100644
--- a/core/modules/contact/src/Controller/ContactController.php
+++ b/core/modules/contact/src/Controller/ContactController.php
@@ -60,6 +60,38 @@ public static function create(ContainerInterface $container) {
   }
 
   /**
+   * The _title_callback for the site-wide contact forms.
+   *
+   * @param \Drupal\contact\ContactFormInterface $contact_form
+   *   The contact form to use.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function contactSitePageTitle(ContactFormInterface $contact_form = NULL) {
+    // Use the default form if no form has been passed.
+    if (empty($contact_form)) {
+      $contact_form = $this->entityManager()
+        ->getStorage('contact_form')
+        ->load($this->config('contact.settings')->get('default_form'));
+    }
+    return String::checkPlain($contact_form->label());
+  }
+
+  /**
+   * The _title_callback for the personal contact forms.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The account for which a personal contact form should be generated.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function contactPersonalPageTitle(UserInterface $user) {
+    return $this->t('Contact @username', array('@username' => $user->getUsername()));
+  }
+
+  /**
    * Presents the site-wide contact form.
    *
    * @param \Drupal\contact\ContactFormInterface $contact_form
@@ -102,9 +134,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) {
         'contact_form' => $contact_form->id(),
       ));
 
-    $form = $this->entityFormBuilder()->getForm($message);
-    $form['#title'] = String::checkPlain($contact_form->label());
-    return $form;
+    return $this->entityFormBuilder()->getForm($message);
   }
 
   /**
@@ -136,9 +166,7 @@ public function contactPersonalPage(UserInterface $user) {
       'recipient' => $user->id(),
     ));
 
-    $form = $this->entityFormBuilder()->getForm($message);
-    $form['#title'] = $this->t('Contact @username', array('@username' => $user->getUsername()));
-    return $form;
+    return $this->entityFormBuilder()->getForm($message);
   }
 
   /**
diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php
index 2d6b19d..7f6875a 100644
--- a/core/modules/content_translation/src/ContentTranslationHandler.php
+++ b/core/modules/content_translation/src/ContentTranslationHandler.php
@@ -239,18 +239,7 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, En
     $is_translation = !$form_object->isDefaultFormLangcode($form_state);
     $has_translations = count($translations) > 1;
 
-    // Adjust page title to specify the current language being edited, if we
-    // have at least one translation.
     $languages = $this->languageManager->getLanguages();
-    if (isset($languages[$form_langcode]) && ($has_translations || $new_translation)) {
-      $title = $this->entityFormTitle($entity);
-      // When editing the original values display just the entity label.
-      if ($form_langcode != $entity_langcode) {
-        $t_args = array('%language' => $languages[$form_langcode]->getName(), '%title' => $entity->label());
-        $title = empty($source_langcode) ? $title . ' [' . t('%language translation', $t_args) . ']' : t('Create %language translation of %title', $t_args);
-      }
-      $form['#title'] = $title;
-    }
 
     // Display source language selector only if we are creating a new
     // translation and there are at least two translations available.
diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php
index d00dee2..f7fad28 100644
--- a/core/modules/content_translation/src/Controller/ContentTranslationController.php
+++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
@@ -256,8 +256,6 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
       );
     }
 
-    $build['#title'] = $this->t('Translations of %label', array('%label' => $entity->label()));
-
     // Add metadata to the build render array to let other modules know about
     // which entity this is.
     $build['#entity'] = $entity;
@@ -272,6 +270,22 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
   }
 
   /**
+   * The _title_callback for the translations overview page.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object from which to extract the entity type.
+   * @param string $entity_type_id
+   *   (optional) The entity type ID.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function overviewTitle(Request $request, $entity_type_id = NULL) {
+    $entity = $request->attributes->get($entity_type_id);
+    return $this->t('Translations of %label', array('%label' => $entity->label()));
+  }
+
+  /**
    * Builds an add translation page.
    *
    * @param \Drupal\Core\Language\LanguageInterface $source
@@ -310,6 +324,56 @@ public function add(LanguageInterface $source, LanguageInterface $target, RouteM
   }
 
   /**
+   * The _title_callback for the entity translation addition form.
+   *
+   * @param \Drupal\Core\Language\LanguageInterface $source
+   *   The language of the values being translated. Defaults to the entity
+   *   language.
+   * @param \Drupal\Core\Language\LanguageInterface $target
+   *   The language of the translated values. Defaults to the current content
+   *   language.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object from which to extract the entity type.
+   * @param string $entity_type_id
+   *   (optional) The entity type ID.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function addTitle(LanguageInterface $source, LanguageInterface $target, Request $request, $entity_type_id = NULL) {
+    $entity = $request->attributes->get($entity_type_id);
+
+    $entity_langcode = $entity->getUntranslated()->language()->getId();
+    $source_langcode = $source->getId();
+    $target_langcode = $target->getId();
+
+    $new_translation = !empty($source_langcode);
+    $translations = $entity->getTranslationLanguages();
+    if ($new_translation) {
+      // Make sure a new translation does not appear as existing yet.
+      unset($translations[$target_langcode]);
+    }
+    $has_translations = count($translations) > 1;
+
+    // Adjust page title to specify the current language being edited, if we
+    // have at least one translation.
+    $languages = language_list();
+    if (isset($languages[$target_langcode]) && ($has_translations || $new_translation)) {
+      $title = $entity->label();
+      // When editing the original values display just the entity label.
+      if ($target_langcode != $entity_langcode) {
+        $t_args = array('%language' => $languages[$target_langcode]->getName(), '%title' => $entity->label());
+        $title = empty($source_langcode) ? $title . ' [' . t('%language translation', $t_args) . ']' : t('Create %language translation of %title', $t_args);
+      }
+    }
+    else {
+      $title = 'Add';
+    }
+
+    return $title;
+  }
+
+  /**
    * Builds the edit translation page.
    *
    * @param \Drupal\Core\Language\LanguageInterface $language
diff --git a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
index f6c5bc0..e184616 100644
--- a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
+++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
@@ -66,6 +66,7 @@ protected function alterRoutes(RouteCollection $collection) {
         $path,
         array(
           '_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::overview',
+          '_title_callback' => '\Drupal\content_translation\Controller\ContentTranslationController::overviewTitle',
           'entity_type_id' => $entity_type_id,
         ),
         array(
@@ -89,7 +90,7 @@ protected function alterRoutes(RouteCollection $collection) {
           '_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::add',
           'source' => NULL,
           'target' => NULL,
-          '_title' => 'Add',
+          '_title_callback' => '\Drupal\content_translation\Controller\ContentTranslationController::addTitle',
           'entity_type_id' => $entity_type_id,
 
         ),
diff --git a/core/modules/dblog/dblog.routing.yml b/core/modules/dblog/dblog.routing.yml
index 4322f67..63e69ec 100644
--- a/core/modules/dblog/dblog.routing.yml
+++ b/core/modules/dblog/dblog.routing.yml
@@ -10,7 +10,7 @@ dblog.confirm:
   path: '/admin/reports/dblog/confirm'
   defaults:
     _form: '\Drupal\dblog\Form\DblogClearLogConfirmForm'
-    _title: 'Confirm delete recent log messages'
+    _title_callback: '\Drupal\dblog\Form\DblogClearLogConfirmForm::getQuestion'
   requirements:
     _permission: 'access site reports'
 
diff --git a/core/modules/field_ui/field_ui.routing.yml b/core/modules/field_ui/field_ui.routing.yml
index 22b8c7b..63c60c5 100644
--- a/core/modules/field_ui/field_ui.routing.yml
+++ b/core/modules/field_ui/field_ui.routing.yml
@@ -34,7 +34,6 @@ field_ui.entity_view_mode_add_type:
   path: '/admin/structure/display-modes/view/add/{entity_type_id}'
   defaults:
     _entity_form: 'entity_view_mode.add'
-    _title: 'Add view mode'
   requirements:
     _permission: 'administer display modes'
 
@@ -42,7 +41,6 @@ entity.entity_view_mode.edit_form:
   path: '/admin/structure/display-modes/view/manage/{entity_view_mode}'
   defaults:
     _entity_form: 'entity_view_mode.edit'
-    _title: 'Edit view mode'
   requirements:
     _entity_access: 'entity_view_mode.update'
 
@@ -50,7 +48,6 @@ entity.entity_view_mode.delete_form:
   path: '/admin/structure/display-modes/view/manage/{entity_view_mode}/delete'
   defaults:
     _entity_form: 'entity_view_mode.delete'
-    _title: 'Delete view mode'
   requirements:
     _entity_access: 'entity_view_mode.delete'
 
@@ -74,7 +71,6 @@ field_ui.entity_form_mode_add_type:
   path: '/admin/structure/display-modes/form/add/{entity_type_id}'
   defaults:
     _entity_form: 'entity_form_mode.add'
-    _title: 'Add form mode'
   requirements:
     _permission: 'administer display modes'
 
@@ -82,7 +78,6 @@ entity.entity_form_mode.edit_form:
   path: '/admin/structure/display-modes/form/manage/{entity_form_mode}'
   defaults:
     _entity_form: 'entity_form_mode.edit'
-    _title: 'Edit form mode'
   requirements:
     _entity_access: 'entity_form_mode.update'
 
@@ -90,6 +85,5 @@ entity.entity_form_mode.delete_form:
   path: '/admin/structure/display-modes/form/manage/{entity_form_mode}/delete'
   defaults:
     _entity_form: 'entity_form_mode.delete'
-    _title: 'Delete form mode'
   requirements:
     _entity_access: 'entity_form_mode.delete'
diff --git a/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php b/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php
index d6d801b..643ec14 100644
--- a/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php
+++ b/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php
@@ -30,8 +30,6 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
     $form = parent::buildForm($form, $form_state);
     // Change replace_pattern to avoid undesired dots.
     $form['id']['#machine_name']['replace_pattern'] = '[^a-z0-9_]+';
-    $definition = $this->entityManager->getDefinition($this->targetEntityTypeId);
-    $form['#title'] = $this->t('Add new %label @entity-type', array('%label' => $definition->getLabel(), '@entity-type' => $this->entityType->getLowercaseLabel()));
     return $form;
   }
 
diff --git a/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php
index 0bdce49..328c559 100644
--- a/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php
+++ b/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php
@@ -73,6 +73,18 @@ protected function init(FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
+  public function getTitle() {
+    if ($this->getOperation() === 'add') {
+      $definition = $this->entityManager->getDefinition($this->targetEntityTypeId);
+      return $this->t('Add new %label @entity-type', array('%label' => $definition->getLabel(), '@entity-type' => $this->entityType->getLowercaseLabel()));
+    }
+
+    return parent::getTitle();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function form(array $form, FormStateInterface $form_state) {
     $form['label'] = array(
       '#type' => 'textfield',
diff --git a/core/modules/field_ui/src/Form/FieldEditForm.php b/core/modules/field_ui/src/Form/FieldEditForm.php
index 95985e5..60f8b87 100644
--- a/core/modules/field_ui/src/Form/FieldEditForm.php
+++ b/core/modules/field_ui/src/Form/FieldEditForm.php
@@ -73,13 +73,6 @@ public function buildForm(array $form, FormStateInterface $form_state, FieldConf
     $bundle = $this->field->bundle;
     $entity_type = $this->field->entity_type;
     $field_storage = $this->field->getFieldStorageDefinition();
-    $bundles = entity_get_bundles();
-
-    $form_title = $this->t('%field settings for %bundle', array(
-      '%field' => $this->field->getLabel(),
-      '%bundle' => $bundles[$entity_type][$bundle]['label'],
-    ));
-    $form['#title'] = $form_title;
 
     $form['#field'] = $field_storage;
     // Create an arbitrary entity object (used by the 'default value' widget).
@@ -238,11 +231,17 @@ public function delete(array &$form, FormStateInterface $form_state) {
    * @param \Drupal\field\FieldConfigInterface $field_config
    *   The field.
    *
-   * @return string
-   *   The label of the field.
+   * @return string|array
+   *   The page title.
    */
   public function getTitle(FieldConfigInterface $field_config) {
-    return String::checkPlain($field_config->label());
+    $bundle = $field_config->bundle;
+    $entity_type = $field_config->entity_type;
+    $bundles = entity_get_bundles();
+    return $this->t('%field settings for %bundle', array(
+      '%field' => $field_config->getLabel(),
+      '%bundle' => $bundles[$entity_type][$bundle]['label'],
+    ));
   }
 
 }
diff --git a/core/modules/field_ui/src/Form/FieldStorageEditForm.php b/core/modules/field_ui/src/Form/FieldStorageEditForm.php
index 5cd57b7..4a36f10 100644
--- a/core/modules/field_ui/src/Form/FieldStorageEditForm.php
+++ b/core/modules/field_ui/src/Form/FieldStorageEditForm.php
@@ -78,7 +78,6 @@ public static function create(ContainerInterface $container) {
   public function buildForm(array $form, FormStateInterface $form_state, FieldConfigInterface $field_config = NULL) {
     $this->field = $field_config;
     $form_state->set('field', $field_config);
-    $form['#title'] = $this->field->label();
 
     $field_storage = $this->field->getFieldStorageDefinition();
     $form['#field'] = $field_storage;
@@ -215,4 +214,17 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     }
   }
 
+  /**
+   * The _title_callback for the field storage settings form.
+   *
+   * @param \Drupal\field\FieldConfigInterface $field_config
+   *   The field.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function getTitle(FieldConfigInterface $field_config) {
+    return $field_config->label();
+  }
+
 }
diff --git a/core/modules/field_ui/src/Routing/RouteSubscriber.php b/core/modules/field_ui/src/Routing/RouteSubscriber.php
index 7715b7d..c760000 100644
--- a/core/modules/field_ui/src/Routing/RouteSubscriber.php
+++ b/core/modules/field_ui/src/Routing/RouteSubscriber.php
@@ -71,7 +71,10 @@ protected function alterRoutes(RouteCollection $collection) {
 
         $route = new Route(
           "$path/fields/{field_config}/storage",
-          array('_form' => '\Drupal\field_ui\Form\FieldStorageEditForm'),
+          array(
+            '_form' => '\Drupal\field_ui\Form\FieldStorageEditForm',
+            '_title_callback' => '\Drupal\field_ui\Form\FieldStorageEditForm::getTitle',
+          ),
           array('_entity_access' => 'field_config.update'),
           $options
         );
diff --git a/core/modules/filter/filter.routing.yml b/core/modules/filter/filter.routing.yml
index 9fff1b1..8f5a27a 100644
--- a/core/modules/filter/filter.routing.yml
+++ b/core/modules/filter/filter.routing.yml
@@ -27,7 +27,6 @@ filter.format_add:
   path: '/admin/config/content/formats/add'
   defaults:
     _entity_form: filter_format.add
-    _title: 'Add text format'
   requirements:
     _entity_create_access: 'filter_format'
 
@@ -35,7 +34,6 @@ entity.filter_format.edit_form:
   path: '/admin/config/content/formats/manage/{filter_format}'
   defaults:
     _entity_form: filter_format.edit
-    _title_callback: '\Drupal\filter\Controller\FilterController::getLabel'
   requirements:
     _entity_access: 'filter_format.update'
 
diff --git a/core/modules/filter/src/Controller/FilterController.php b/core/modules/filter/src/Controller/FilterController.php
index a921cb0..4b2fd0b 100644
--- a/core/modules/filter/src/Controller/FilterController.php
+++ b/core/modules/filter/src/Controller/FilterController.php
@@ -37,17 +37,4 @@ function filterTips(FilterFormatInterface $filter_format = NULL) {
     return $build;
   }
 
-  /**
-   * Gets the label of a filter format.
-   *
-   * @param \Drupal\filter\FilterFormatInterface $filter_format
-   *   The filter format.
-   *
-   * @return string
-   *   The label of the filter format.
-   */
-  public function getLabel(FilterFormatInterface $filter_format) {
-    return $filter_format->label();
-  }
-
 }
diff --git a/core/modules/filter/src/FilterFormatEditForm.php b/core/modules/filter/src/FilterFormatEditForm.php
index 3ce6905..ed0efa7 100644
--- a/core/modules/filter/src/FilterFormatEditForm.php
+++ b/core/modules/filter/src/FilterFormatEditForm.php
@@ -23,7 +23,6 @@ public function form(array $form, FormStateInterface $form_state) {
       throw new NotFoundHttpException();
     }
 
-    $form['#title'] = $this->entity->label();
     $form = parent::form($form, $form_state);
     $form['roles']['#default_value'] = array_keys(filter_get_roles_by_format($this->entity));
     return $form;
diff --git a/core/modules/forum/forum.routing.yml b/core/modules/forum/forum.routing.yml
index 14e55e2..8631b20 100644
--- a/core/modules/forum/forum.routing.yml
+++ b/core/modules/forum/forum.routing.yml
@@ -18,7 +18,7 @@ forum.index:
   path: '/forum'
   defaults:
     _controller: '\Drupal\forum\Controller\ForumController::forumIndex'
-    _title: 'Forums'
+    _title_callback: '\Drupal\forum\Controller\ForumController::forumIndexTitle'
   requirements:
     _permission: 'access content'
 
diff --git a/core/modules/forum/src/Controller/ForumController.php b/core/modules/forum/src/Controller/ForumController.php
index 951e254..5ece46c 100644
--- a/core/modules/forum/src/Controller/ForumController.php
+++ b/core/modules/forum/src/Controller/ForumController.php
@@ -143,18 +143,27 @@ public function forumPage(TermInterface $taxonomy_term) {
    *   A render array.
    */
   public function forumIndex() {
-    $vocabulary = $this->vocabularyStorage->load($this->config('forum.settings')->get('vocabulary'));
     $index = $this->forumManager->getIndex();
     $build = $this->build($index->forums, $index);
+    return $build;
+  }
+
+  /**
+   * The _title_callback for the forum index page.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function forumIndexTitle() {
+    $index = $this->forumManager->getIndex();
     if (empty($index->forums)) {
       // Root of empty forum.
-      $build['#title'] = $this->t('No forums defined');
+      return $this->t('No forums defined');
     }
     else {
       // Set the page title to forum's vocabulary name.
-      $build['#title'] = $vocabulary->label();
+      return $this->vocabularyStorage->load($this->config('forum.settings')->get('vocabulary'))->label();
     }
-    return $build;
   }
 
   /**
diff --git a/core/modules/help/help.routing.yml b/core/modules/help/help.routing.yml
index 505251d..0cdab2e 100644
--- a/core/modules/help/help.routing.yml
+++ b/core/modules/help/help.routing.yml
@@ -10,6 +10,6 @@ help.page:
   path: '/admin/help/{name}'
   defaults:
     _controller: '\Drupal\help\Controller\HelpController::helpPage'
-    _title: 'Help'
+    _title_callback: '\Drupal\help\Controller\HelpController::title'
   requirements:
     _permission: 'access administration pages'
diff --git a/core/modules/help/src/Controller/HelpController.php b/core/modules/help/src/Controller/HelpController.php
index 216d2c7..144d3dc 100644
--- a/core/modules/help/src/Controller/HelpController.php
+++ b/core/modules/help/src/Controller/HelpController.php
@@ -119,7 +119,6 @@ public function helpPage($name) {
     $build = array();
     if ($this->moduleHandler()->implementsHook($name, 'help')) {
       $info = system_get_info('module');
-      $build['#title'] = String::checkPlain($info[$name]['name']);
 
       $temp = $this->moduleHandler()->invoke($name, 'help', array("help.page.$name", $this->routeMatch));
       if (empty($temp)) {
@@ -154,4 +153,25 @@ public function helpPage($name) {
     }
   }
 
+  /**
+   * The _title_callback for the general help page for a module.
+   *
+   * @param string $name
+   *   A module name to display a help page for.
+   *
+   * @return string|array
+   *   The page title.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   */
+  public function title($name) {
+    if ($this->moduleHandler()->implementsHook($name, 'help')) {
+      $info = system_get_info('module');
+      return String::checkPlain($info[$name]['name']);
+    }
+    else {
+      throw new NotFoundHttpException();
+    }
+  }
+
 }
diff --git a/core/modules/image/image.routing.yml b/core/modules/image/image.routing.yml
index ffeed86..8a87df0 100644
--- a/core/modules/image/image.routing.yml
+++ b/core/modules/image/image.routing.yml
@@ -2,7 +2,6 @@ image.style_add:
   path: '/admin/config/media/image-styles/add'
   defaults:
     _entity_form: image_style.add
-    _title: 'Add image style'
   requirements:
     _permission: 'administer image styles'
 
@@ -10,7 +9,6 @@ entity.image_style.edit_form:
   path: '/admin/config/media/image-styles/manage/{image_style}'
   defaults:
     _entity_form: image_style.edit
-    _title: 'Edit style'
   requirements:
     _permission: 'administer image styles'
 
@@ -18,7 +16,6 @@ entity.image_style.delete_form:
   path: '/admin/config/media/image-styles/manage/{image_style}/delete'
   defaults:
     _entity_form: 'image_style.delete'
-    _title: 'Delete'
   requirements:
     _permission: 'administer image styles'
 
diff --git a/core/modules/image/src/Form/ImageEffectAddForm.php b/core/modules/image/src/Form/ImageEffectAddForm.php
index e491d07..c2b7d43 100644
--- a/core/modules/image/src/Form/ImageEffectAddForm.php
+++ b/core/modules/image/src/Form/ImageEffectAddForm.php
@@ -50,7 +50,6 @@ public static function create(ContainerInterface $container) {
   public function buildForm(array $form, FormStateInterface $form_state, ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
     $form = parent::buildForm($form, $form_state, $image_style, $image_effect);
 
-    $form['#title'] = $this->t('Add %label effect', array('%label' => $this->imageEffect->label()));
     $form['actions']['submit']['#value'] = $this->t('Add effect');
 
     return $form;
diff --git a/core/modules/image/src/Form/ImageEffectEditForm.php b/core/modules/image/src/Form/ImageEffectEditForm.php
index 1e2ba85..540def8 100644
--- a/core/modules/image/src/Form/ImageEffectEditForm.php
+++ b/core/modules/image/src/Form/ImageEffectEditForm.php
@@ -21,7 +21,6 @@ class ImageEffectEditForm extends ImageEffectFormBase {
   public function buildForm(array $form, FormStateInterface $form_state, ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
     $form = parent::buildForm($form, $form_state, $image_style, $image_effect);
 
-    $form['#title'] = $this->t('Edit %label effect', array('%label' => $this->imageEffect->label()));
     $form['actions']['submit']['#value'] = $this->t('Update effect');
 
     return $form;
diff --git a/core/modules/image/src/Form/ImageStyleEditForm.php b/core/modules/image/src/Form/ImageStyleEditForm.php
index bc58404..558b178 100644
--- a/core/modules/image/src/Form/ImageStyleEditForm.php
+++ b/core/modules/image/src/Form/ImageStyleEditForm.php
@@ -55,7 +55,6 @@ public static function create(ContainerInterface $container) {
    */
   public function form(array $form, FormStateInterface $form_state) {
     $user_input = $form_state->getUserInput();
-    $form['#title'] = $this->t('Edit style %name', array('%name' => $this->entity->label()));
     $form['#tree'] = TRUE;
     $form['#attached']['library'][] = 'image/admin';
 
diff --git a/core/modules/language/language.routing.yml b/core/modules/language/language.routing.yml
index d21941d..e667a86 100644
--- a/core/modules/language/language.routing.yml
+++ b/core/modules/language/language.routing.yml
@@ -26,7 +26,6 @@ language.add:
   path: '/admin/config/regional/language/add'
   defaults:
     _entity_form: 'configurable_language.add'
-    _title: 'Add language'
   requirements:
     _entity_create_access: 'configurable_language'
 
@@ -34,7 +33,6 @@ entity.configurable_language.edit_form:
   path: '/admin/config/regional/language/edit/{configurable_language}'
   defaults:
     _entity_form: 'configurable_language.edit'
-    _title: 'Edit language'
   requirements:
     _entity_access: 'configurable_language.update'
 
@@ -50,7 +48,6 @@ entity.configurable_language.delete_form:
   path: '/admin/config/regional/language/delete/{configurable_language}'
   defaults:
     _entity_form: 'configurable_language.delete'
-    _title: 'Delete language'
   requirements:
     _entity_access: 'configurable_language.delete'
 
diff --git a/core/modules/language/src/Form/LanguageAddForm.php b/core/modules/language/src/Form/LanguageAddForm.php
index fc27626..335b406 100644
--- a/core/modules/language/src/Form/LanguageAddForm.php
+++ b/core/modules/language/src/Form/LanguageAddForm.php
@@ -31,8 +31,6 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function form(array $form, FormStateInterface $form_state) {
-    $form['#title'] = $this->t('Add language');
-
     $predefined_languages = $this->languageManager->getStandardLanguageListWithoutConfigured();
 
     $predefined_languages['custom'] = $this->t('Custom language...');
diff --git a/core/modules/menu_link_content/menu_link_content.routing.yml b/core/modules/menu_link_content/menu_link_content.routing.yml
index 8c00ee0..4ff4bf0 100644
--- a/core/modules/menu_link_content/menu_link_content.routing.yml
+++ b/core/modules/menu_link_content/menu_link_content.routing.yml
@@ -9,8 +9,7 @@ entity.menu.add_link_form:
 entity.menu_link_content.canonical:
   path: '/admin/structure/menu/item/{menu_link_content}/edit'
   defaults:
-    _entity_form: 'menu_link_content.default'
-    _title: 'Edit menu link'
+    _entity_form: 'menu_link_content.edit'
   requirements:
     _entity_access: 'menu_link_content.update'
 
@@ -26,6 +25,5 @@ entity.menu_link_content.delete_form:
   path: '/admin/structure/menu/item/{menu_link_content}/delete'
   defaults:
     _entity_form: 'menu_link_content.delete'
-    _title: 'Delete menu link'
   requirements:
     _entity_access: 'menu_link_content.delete'
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
index 152dc54..0e2ca6f 100644
--- a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -25,6 +25,7 @@
  *     "access" = "Drupal\menu_link_content\MenuLinkContentAccessControlHandler",
  *     "form" = {
  *       "default" = "Drupal\menu_link_content\Form\MenuLinkContentForm",
+ *       "edit" = "Drupal\menu_link_content\Form\MenuLinkContentForm",
  *       "delete" = "Drupal\menu_link_content\Form\MenuLinkContentDeleteForm"
  *     }
  *   },
diff --git a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
index 0882384..fa29a13 100644
--- a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
+++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
@@ -153,7 +153,6 @@ public function setMenuLinkInstance(MenuLinkInterface $menu_link) {
    * {@inheritdoc}
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
-    $this->setOperation('default');
     $this->init($form_state);
 
     return $this->form($form, $form_state);
diff --git a/core/modules/menu_ui/menu_ui.routing.yml b/core/modules/menu_ui/menu_ui.routing.yml
index 7f94a6d..a5b6cd3 100644
--- a/core/modules/menu_ui/menu_ui.routing.yml
+++ b/core/modules/menu_ui/menu_ui.routing.yml
@@ -42,7 +42,6 @@ entity.menu.add_form:
   path: '/admin/structure/menu/add'
   defaults:
     _entity_form: 'menu.add'
-    _title: 'Add menu'
   requirements:
     _entity_create_access: 'menu'
 
@@ -50,7 +49,6 @@ entity.menu.edit_form:
   path: '/admin/structure/menu/manage/{menu}'
   defaults:
     _entity_form: 'menu.edit'
-    _title_callback: '\Drupal\menu_ui\Controller\MenuController::menuTitle'
   requirements:
     _entity_access: 'menu.update'
 
@@ -58,6 +56,5 @@ entity.menu.delete_form:
   path: '/admin/structure/menu/manage/{menu}/delete'
   defaults:
     _entity_form: 'menu.delete'
-    _title: 'Delete menu'
   requirements:
     _entity_access: 'menu.delete'
diff --git a/core/modules/menu_ui/src/Controller/MenuController.php b/core/modules/menu_ui/src/Controller/MenuController.php
index 95788bd..89479d6 100644
--- a/core/modules/menu_ui/src/Controller/MenuController.php
+++ b/core/modules/menu_ui/src/Controller/MenuController.php
@@ -65,17 +65,4 @@ public function getParentOptions(Request $request) {
     return new JsonResponse($options);
   }
 
-  /**
-   * Route title callback.
-   *
-   * @param \Drupal\system\MenuInterface $menu
-   *   The menu entity.
-   *
-   * @return string
-   *   The menu label.
-   */
-  public function menuTitle(MenuInterface $menu) {
-    return Xss::filter($menu->label());
-  }
-
 }
diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php
index 1ad5271..3d2e354 100644
--- a/core/modules/menu_ui/src/MenuForm.php
+++ b/core/modules/menu_ui/src/MenuForm.php
@@ -99,10 +99,6 @@ public static function create(ContainerInterface $container) {
   public function form(array $form, FormStateInterface $form_state) {
     $menu = $this->entity;
 
-    if ($this->operation == 'edit') {
-      $form['#title'] = $this->t('Edit menu %label', array('%label' => $menu->label()));
-    }
-
     $form['label'] = array(
       '#type' => 'textfield',
       '#title' => $this->t('Title'),
diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml
index 8e3e59e..c580135 100644
--- a/core/modules/node/node.routing.yml
+++ b/core/modules/node/node.routing.yml
@@ -2,6 +2,7 @@ node.multiple_delete_confirm:
   path: '/admin/content/node/delete'
   defaults:
     _form: '\Drupal\node\Form\DeleteMultiple'
+    _title_callback: '\Drupal\node\Form\DeleteMultiple::getQuestion'
   requirements:
     _permission: 'administer nodes'
 
@@ -45,8 +46,8 @@ entity.node.preview:
 entity.node.version_history:
   path: '/node/{node}/revisions'
   defaults:
-    _title: 'Revisions'
     _controller: '\Drupal\node\Controller\NodeController::revisionOverview'
+    _title_callback: '\Drupal\node\Controller\NodeController::revisionOverviewTitle'
   requirements:
     _access_node_revision: 'view'
   options:
@@ -64,7 +65,7 @@ node.revision_revert_confirm:
   path: '/node/{node}/revisions/{node_revision}/revert'
   defaults:
     _form: '\Drupal\node\Form\NodeRevisionRevertForm'
-    _title: 'Revert to earlier revision'
+    _title_callback: '\Drupal\node\Form\NodeRevisionRevertForm::getQuestion'
   requirements:
     _access_node_revision: 'update'
   options:
@@ -74,7 +75,7 @@ node.revision_delete_confirm:
   path: '/node/{node}/revisions/{node_revision}/delete'
   defaults:
     _form: '\Drupal\node\Form\NodeRevisionDeleteForm'
-    _title: 'Delete earlier revision'
+    _title_callback: '\Drupal\node\Form\NodeRevisionDeleteForm::getQuestion'
   requirements:
     _access_node_revision: 'delete'
   options:
@@ -93,7 +94,6 @@ node.type_add:
   path: '/admin/structure/types/add'
   defaults:
     _entity_form: 'node_type.add'
-    _title: 'Add content type'
   requirements:
     _permission: 'administer content types'
 
@@ -108,7 +108,6 @@ entity.node_type.delete_form:
   path: '/admin/structure/types/manage/{node_type}/delete'
   defaults:
     _entity_form: 'node_type.delete'
-    _title: 'Delete'
   requirements:
     _entity_access: 'node_type.delete'
 
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index f4a30ee..3d2a160 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -147,6 +147,19 @@ public function revisionPageTitle($node_revision) {
   }
 
   /**
+   * The _title_callback for the revision overview page.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   A node object.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function revisionOverviewTitle(NodeInterface $node) {
+    return $this->t('Revisions for %title', array('%title' => $node->label()));
+  }
+
+  /**
    * Generates an overview table of older revisions of a node.
    *
    * @param \Drupal\node\NodeInterface $node
@@ -161,7 +174,6 @@ public function revisionOverview(NodeInterface $node) {
     $type = $node->getType();
 
     $build = array();
-    $build['#title'] = $this->t('Revisions for %title', array('%title' => $node->label()));
     $header = array($this->t('Revision'), $this->t('Operations'));
 
     $revert_permission = (($account->hasPermission("revert $type revisions") || $account->hasPermission('revert all revisions') || $account->hasPermission('administer nodes')) && $node->access('update'));
diff --git a/core/modules/node/src/Controller/NodePreviewController.php b/core/modules/node/src/Controller/NodePreviewController.php
index 2134abe..d9e9f75 100644
--- a/core/modules/node/src/Controller/NodePreviewController.php
+++ b/core/modules/node/src/Controller/NodePreviewController.php
@@ -21,35 +21,12 @@ class NodePreviewController extends EntityViewController {
    */
   public function view(EntityInterface $node_preview, $view_mode_id = 'full', $langcode = NULL) {
     $node_preview->preview_view_mode = $view_mode_id;
-    $build = array('nodes' => parent::view($node_preview, $view_mode_id));
+    $build = parent::view($node_preview, $view_mode_id);
 
     $build['#attached']['library'][] = 'node/drupal.node.preview';
 
-    $build['#title'] = $build['nodes']['#title'];
-    unset($build['nodes']['#title']);
-
     // Don't render cache previews.
-    unset($build['nodes']['#cache']);
-
-    foreach ($node_preview->uriRelationships() as $rel) {
-      // Set the node path as the canonical URL to prevent duplicate content.
-      $build['#attached']['html_head_link'][] = array(
-        array(
-        'rel' => $rel,
-        'href' => $node_preview->url($rel),
-        )
-        , TRUE);
-
-      if ($rel == 'canonical') {
-        // Set the non-aliased canonical path as a default shortlink.
-        $build['#attached']['html_head_link'][] = array(
-          array(
-            'rel' => 'shortlink',
-            'href' => $node_preview->url($rel, array('alias' => TRUE)),
-          )
-        , TRUE);
-      }
-    }
+    unset($build['#cache']);
 
     return $build;
   }
@@ -59,12 +36,14 @@ public function view(EntityInterface $node_preview, $view_mode_id = 'full', $lan
    *
    * @param \Drupal\Core\Entity\EntityInterface $node_preview
    *   The current node.
+   * @param string $view_mode_id
+   *   The view mode that should be used to preview the node.
    *
    * @return string
    *   The page title.
    */
-  public function title(EntityInterface $node_preview) {
-    return String::checkPlain($this->entityManager->getTranslationFromContext($node_preview)->label());
+  public function title(EntityInterface $node_preview, $view_mode_id = 'full') {
+    return parent::title($node_preview, $view_mode_id);
   }
 
 }
diff --git a/core/modules/node/src/Controller/NodeViewController.php b/core/modules/node/src/Controller/NodeViewController.php
deleted file mode 100644
index 2e68391..0000000
--- a/core/modules/node/src/Controller/NodeViewController.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\node\Controller\NodeViewController.
- */
-
-namespace Drupal\node\Controller;
-
-use Drupal\Component\Utility\String;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\Controller\EntityViewController;
-
-/**
- * Defines a controller to render a single node.
- */
-class NodeViewController extends EntityViewController {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function view(EntityInterface $node, $view_mode = 'full', $langcode = NULL) {
-    $build = array('nodes' => parent::view($node));
-
-    $build['#title'] = $build['nodes']['#title'];
-    unset($build['nodes']['#title']);
-
-    foreach ($node->uriRelationships() as $rel) {
-      // Set the node path as the canonical URL to prevent duplicate content.
-      $build['#attached']['html_head_link'][] = array(
-        array(
-          'rel' => $rel,
-          'href' => $node->url($rel),
-        ),
-        TRUE,
-      );
-
-      if ($rel == 'canonical') {
-        // Set the non-aliased canonical path as a default shortlink.
-        $build['#attached']['html_head_link'][] = array(
-          array(
-            'rel' => 'shortlink',
-            'href' => $node->url($rel, array('alias' => TRUE)),
-          ),
-          TRUE,
-        );
-      }
-    }
-
-    return $build;
-  }
-
-  /**
-   * The _title_callback for the page that renders a single node.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $node
-   *   The current node.
-   *
-   * @return string
-   *   The page title.
-   */
-  public function title(EntityInterface $node) {
-    return String::checkPlain($this->entityManager->getTranslationFromContext($node)->label());
-  }
-
-}
diff --git a/core/modules/node/src/Entity/NodeRouteProvider.php b/core/modules/node/src/Entity/NodeRouteProvider.php
index fa1c02d..de41751 100644
--- a/core/modules/node/src/Entity/NodeRouteProvider.php
+++ b/core/modules/node/src/Entity/NodeRouteProvider.php
@@ -24,8 +24,7 @@ public function getRoutes( EntityTypeInterface $entity_type) {
     $route_collection = new RouteCollection();
     $route = (new Route('/node/{node}'))
       ->addDefaults([
-        '_controller' => '\Drupal\node\Controller\NodeViewController::view',
-        '_title_callback' => '\Drupal\node\Controller\NodeViewController::title',
+        '_entity_view' => 'node.full',
       ])
       ->setRequirement('_entity_access', 'node.view');
     $route_collection->add('entity.node.canonical', $route);
@@ -33,7 +32,6 @@ public function getRoutes( EntityTypeInterface $entity_type) {
     $route = (new Route('/node/{node}/delete'))
       ->addDefaults([
         '_entity_form' => 'node.delete',
-        '_title' => 'Delete',
       ])
       ->setRequirement('_entity_access', 'node.delete')
       ->setOption('_node_operation_route', TRUE);
diff --git a/core/modules/node/src/Form/NodeRevisionDeleteForm.php b/core/modules/node/src/Form/NodeRevisionDeleteForm.php
index 352cef0..2f45518 100644
--- a/core/modules/node/src/Form/NodeRevisionDeleteForm.php
+++ b/core/modules/node/src/Form/NodeRevisionDeleteForm.php
@@ -87,6 +87,8 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function getQuestion() {
+    $node_revision = NULL;
+    $this->revision = $this->nodeStorage->loadRevision($node_revision);
     return t('Are you sure you want to delete the revision from %revision-date?', array('%revision-date' => format_date($this->revision->getRevisionCreationTime())));
   }
 
diff --git a/core/modules/node/src/Form/NodeRevisionRevertForm.php b/core/modules/node/src/Form/NodeRevisionRevertForm.php
index d6e748e..13e8718 100644
--- a/core/modules/node/src/Form/NodeRevisionRevertForm.php
+++ b/core/modules/node/src/Form/NodeRevisionRevertForm.php
@@ -62,7 +62,8 @@ public function getFormId() {
   /**
    * {@inheritdoc}
    */
-  public function getQuestion() {
+  public function getQuestion($node_revision = NULL) {
+    $this->revision = $this->nodeStorage->loadRevision($node_revision);
     return t('Are you sure you want to revert to the revision from %revision-date?', array('%revision-date' => format_date($this->revision->getRevisionCreationTime())));
   }
 
diff --git a/core/modules/node/src/Form/NodeTypeDeleteConfirm.php b/core/modules/node/src/Form/NodeTypeDeleteConfirm.php
index 31c4461..0776f2b 100644
--- a/core/modules/node/src/Form/NodeTypeDeleteConfirm.php
+++ b/core/modules/node/src/Form/NodeTypeDeleteConfirm.php
@@ -74,7 +74,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ->execute();
     if ($num_nodes) {
       $caption = '<p>' . $this->formatPlural($num_nodes, '%type is used by 1 piece of content on your site. You can not remove this content type until you have removed all of the %type content.', '%type is used by @count pieces of content on your site. You may not remove %type until you have removed all of the %type content.', array('%type' => $this->entity->label())) . '</p>';
-      $form['#title'] = $this->getQuestion();
       $form['description'] = array('#markup' => $caption);
       return $form;
     }
diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php
index fb8daa9..5b001de 100644
--- a/core/modules/node/src/NodeForm.php
+++ b/core/modules/node/src/NodeForm.php
@@ -97,10 +97,6 @@ public function form(array $form, FormStateInterface $form_state) {
     /** @var \Drupal\node\NodeInterface $node */
     $node = $this->entity;
 
-    if ($this->operation == 'edit') {
-      $form['#title'] = $this->t('<em>Edit @type</em> @title', array('@type' => node_get_type_label($node), '@title' => $node->label()));
-    }
-
     $current_user = $this->currentUser();
 
     // Override the default CSS class name, since the user-defined node type
diff --git a/core/modules/node/src/NodeTypeForm.php b/core/modules/node/src/NodeTypeForm.php
index 2292cf0..2d87d25 100644
--- a/core/modules/node/src/NodeTypeForm.php
+++ b/core/modules/node/src/NodeTypeForm.php
@@ -55,7 +55,6 @@ public function form(array $form, FormStateInterface $form_state) {
 
     $type = $this->entity;
     if ($this->operation == 'add') {
-      $form['#title'] = String::checkPlain($this->t('Add content type'));
       $fields = $this->entityManager->getBaseFieldDefinitions('node');
       // Create a node with a fake bundle using the type's UUID so that we can
       // get the default values for workflow settings.
@@ -64,7 +63,6 @@ public function form(array $form, FormStateInterface $form_state) {
       $node = $this->entityManager->getStorage('node')->create(array('type' => $type->uuid()));
     }
     else {
-      $form['#title'] = $this->t('Edit %label content type', array('%label' => $type->label()));
       $fields = $this->entityManager->getFieldDefinitions('node', $type->id());
       // Create a node to get the current values for workflow settings fields.
       $node = $this->entityManager->getStorage('node')->create(array('type' => $type->id()));
diff --git a/core/modules/path/src/Form/EditForm.php b/core/modules/path/src/Form/EditForm.php
index 95d5e20..c90a1fb 100644
--- a/core/modules/path/src/Form/EditForm.php
+++ b/core/modules/path/src/Form/EditForm.php
@@ -36,7 +36,6 @@ protected function buildPath($pid) {
   public function buildForm(array $form, FormStateInterface $form_state, $pid = NULL) {
     $form = parent::buildForm($form, $form_state, $pid);
 
-    $form['#title'] = String::checkPlain($this->path['alias']);
     $form['pid'] = array(
       '#type' => 'hidden',
       '#value' => $this->path['pid'],
diff --git a/core/modules/responsive_image/responsive_image.routing.yml b/core/modules/responsive_image/responsive_image.routing.yml
index 2cf9926..7b8d171 100644
--- a/core/modules/responsive_image/responsive_image.routing.yml
+++ b/core/modules/responsive_image/responsive_image.routing.yml
@@ -10,7 +10,6 @@ responsive_image.mapping_page_add:
   path: '/admin/config/media/responsive-image-mapping/add'
   defaults:
     _entity_form: 'responsive_image_mapping.add'
-    _title: 'Add responsive image mapping'
   requirements:
     _permission: 'administer responsive images'
 
@@ -18,7 +17,6 @@ entity.responsive_image_mapping.edit_form:
   path: '/admin/config/media/responsive-image-mapping/{responsive_image_mapping}'
   defaults:
     _entity_form: 'responsive_image_mapping.edit'
-    _title: 'Edit responsive image mapping'
   requirements:
     _permission: 'administer responsive images'
 
@@ -26,7 +24,6 @@ entity.responsive_image_mapping.duplicate_form:
   path: '/admin/config/media/responsive-image-mapping/{responsive_image_mapping}/duplicate'
   defaults:
     _entity_form: 'responsive_image_mapping.duplicate'
-    _title: 'Duplicate responsive image mapping'
   requirements:
     _permission: 'administer responsive images'
 
@@ -34,6 +31,5 @@ responsive_image.mapping_action_confirm:
   path: '/admin/config/media/responsive-image-mapping/{responsive_image_mapping}/delete'
   defaults:
     _entity_form: 'responsive_image_mapping.delete'
-    _title: 'Delete'
   requirements:
     _permission: 'administer responsive images'
diff --git a/core/modules/responsive_image/src/ResponsiveImageMappingForm.php b/core/modules/responsive_image/src/ResponsiveImageMappingForm.php
index 1f47865..09c37f1 100644
--- a/core/modules/responsive_image/src/ResponsiveImageMappingForm.php
+++ b/core/modules/responsive_image/src/ResponsiveImageMappingForm.php
@@ -56,13 +56,8 @@ public function __construct(BreakpointManagerInterface $breakpoint_manager) {
    */
   public function form(array $form, FormStateInterface $form_state) {
     if ($this->operation == 'duplicate') {
-      $form['#title'] = $this->t('<em>Duplicate responsive image mapping</em> @label', array('@label' => $this->entity->label()));
       $this->entity = $this->entity->createDuplicate();
     }
-    if ($this->operation == 'edit') {
-      $form['#title'] = $this->t('<em>Edit responsive image mapping</em> @label', array('@label' => $this->entity->label()));
-    }
-
     /** @var \Drupal\responsive_image\ResponsiveImageMappingInterface $responsive_image_mapping */
     $responsive_image_mapping = $this->entity;
     $form['label'] = array(
diff --git a/core/modules/search/search.routing.yml b/core/modules/search/search.routing.yml
index ec273f1..b161815 100644
--- a/core/modules/search/search.routing.yml
+++ b/core/modules/search/search.routing.yml
@@ -10,7 +10,7 @@ search.reindex_confirm:
   path: '/admin/config/search/pages/reindex'
   defaults:
     _form: '\Drupal\search\Form\ReindexConfirm'
-    _title: 'Clear index'
+    _title_callback: '\Drupal\search\Form\ReindexConfirm::getQuestion'
   requirements:
     _permission: 'administer search'
 
@@ -18,7 +18,6 @@ search.add_type:
   path: '/admin/config/search/pages/add/{search_plugin_id}'
   defaults:
     _entity_form: 'search_page.add'
-    _title: 'Add new search page'
   requirements:
     _entity_create_access: 'search_page'
 
@@ -26,7 +25,6 @@ entity.search_page.edit_form:
   path: '/admin/config/search/pages/manage/{search_page}'
   defaults:
     _entity_form: 'search_page.edit'
-    _title_callback: '\Drupal\search\Controller\SearchController::editTitle'
   requirements:
     _entity_access: 'search_page.update'
 
@@ -57,7 +55,6 @@ entity.search_page.delete_form:
   path: '/admin/config/search/pages/manage/{search_page}/delete'
   defaults:
     _entity_form: 'search_page.delete'
-    _title: 'Delete'
   requirements:
     _entity_access: 'search_page.delete'
 
diff --git a/core/modules/search/src/Controller/SearchController.php b/core/modules/search/src/Controller/SearchController.php
index f3e0e06..5ec0f50 100644
--- a/core/modules/search/src/Controller/SearchController.php
+++ b/core/modules/search/src/Controller/SearchController.php
@@ -58,6 +58,21 @@ public static function create(ContainerInterface $container) {
   }
 
   /**
+   * The _title_callback for the search page.
+   *
+   * @param \Drupal\search\SearchPageInterface $entity
+   *   The search page entity.
+   *
+   * @return string|array
+   *   The page title.
+   *
+   * @see \Drupal\search\Routing\SearchPageRoutes
+   */
+  public function title(SearchPageInterface $entity) {
+    return $entity->getPlugin()->suggestedTitle();
+  }
+
+  /**
    * Creates a render array for the search page.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
@@ -79,7 +94,6 @@ public function view(Request $request, SearchPageInterface $entity) {
       $plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
     }
 
-    $build['#title'] = $plugin->suggestedTitle();
     $build['search_form'] = $this->entityFormBuilder()->getForm($entity, 'search');
 
     // Build search results, if keywords or other search parameters are in the
@@ -158,19 +172,6 @@ public function redirectSearchPage(SearchPageInterface $entity) {
   }
 
   /**
-   * Route title callback.
-   *
-   * @param \Drupal\search\SearchPageInterface $search_page
-   *   The search page entity.
-   *
-   * @return string
-   *   The title for the search page edit form.
-   */
-  public function editTitle(SearchPageInterface $search_page) {
-    return $this->t('Edit %label search page', array('%label' => $search_page->label()));
-  }
-
-  /**
    * Performs an operation on the search page entity.
    *
    * @param \Drupal\search\SearchPageInterface $search_page
diff --git a/core/modules/search/src/Routing/SearchPageRoutes.php b/core/modules/search/src/Routing/SearchPageRoutes.php
index c8ff06e..0fbce77 100644
--- a/core/modules/search/src/Routing/SearchPageRoutes.php
+++ b/core/modules/search/src/Routing/SearchPageRoutes.php
@@ -58,7 +58,7 @@ public function routes() {
         '/search',
         array(
           '_controller' => 'Drupal\search\Controller\SearchController::redirectSearchPage',
-          '_title' => 'Search',
+          '_title_callback' => 'Drupal\search\Controller\SearchController::title',
           'entity' => $default_page,
         ),
         array(
diff --git a/core/modules/shortcut/shortcut.routing.yml b/core/modules/shortcut/shortcut.routing.yml
index d6c0e1a..825b2c8 100644
--- a/core/modules/shortcut/shortcut.routing.yml
+++ b/core/modules/shortcut/shortcut.routing.yml
@@ -2,7 +2,6 @@ entity.shortcut_set.delete_form:
   path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}/delete'
   defaults:
     _entity_form: 'shortcut_set.delete'
-    _title: 'Delete shortcut set'
   requirements:
     _entity_access: 'shortcut_set.delete'
 
@@ -18,7 +17,6 @@ shortcut.set_add:
   path: '/admin/config/user-interface/shortcut/add-set'
   defaults:
     _entity_form: 'shortcut_set.add'
-    _title: 'Add shortcut set'
   requirements:
     _entity_create_access: 'shortcut_set'
 
@@ -26,7 +24,6 @@ entity.shortcut_set.edit_form:
   path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}'
   defaults:
     _entity_form: 'shortcut_set.edit'
-    _title: 'Edit shortcut set'
   requirements:
     _entity_access: 'shortcut_set.update'
 
@@ -82,7 +79,6 @@ entity.shortcut.delete_form:
   path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete'
   defaults:
     _entity_form: 'shortcut.delete'
-    _title: 'Delete'
   requirements:
     _entity_access: 'shortcut.delete'
 
diff --git a/core/modules/system/src/PathBasedBreadcrumbBuilder.php b/core/modules/system/src/PathBasedBreadcrumbBuilder.php
index 6080d6b..ed2bcea 100644
--- a/core/modules/system/src/PathBasedBreadcrumbBuilder.php
+++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php
@@ -139,6 +139,13 @@ public function build(RouteMatchInterface $route_match) {
       array_pop($path_elements);
       // Copy the path elements for up-casting.
       $route_request = $this->getRequestForPath(implode('/', $path_elements), $exclude);
+      // @todo this does not yet take into account paths with required slugs,
+      //   such as '/nl/node/1/translations/add/en/nl', where you cannot just
+      //   chop off the individual parts and expect it to work. The only reason
+      //   that "worked" so far, is because there was a static _title attribute
+      //   on the route, which then resulted in "node title > Translations > Add
+      //   > Add", which is utterly nonsensical. A way to deal with this must be
+      //   figured out.
       if ($route_request) {
         $route_match = RouteMatch::createFromRequest($route_request);
         $access = $this->accessManager->check($route_match, $this->currentUser);
diff --git a/core/modules/system/src/Tests/System/PageTitleTest.php b/core/modules/system/src/Tests/System/PageTitleTest.php
index 204d265..b84a169 100644
--- a/core/modules/system/src/Tests/System/PageTitleTest.php
+++ b/core/modules/system/src/Tests/System/PageTitleTest.php
@@ -104,19 +104,21 @@ function testTitleXSS() {
    * @see \Drupal\test_page_test\Controller\Test
    */
   public function testRoutingTitle() {
-    // Test the '#title' render array attribute.
+    // Test the '#title' render array attribute: it should NOT become the page
+    // title. This used to be the case, but not anymore in Drupal 8.
     $this->drupalGet('test-render-title');
 
-    $this->assertTitle('Foo | Drupal');
+    $this->assertNoTitle('Foo | Drupal');
     $result = $this->xpath('//h1');
-    $this->assertEqual('Foo', (string) $result[0]);
+    $this->assertNotEqual('Foo', (string) $result[0]);
 
-    // Test forms
+    // Test the '#title' render array attribute in a form. Just like before, it
+    // should NOT become the page title.
     $this->drupalGet('form-test/object-builder');
 
-    $this->assertTitle('Test dynamic title | Drupal');
+    $this->assertNoTitle('Test dynamic title | Drupal');
     $result = $this->xpath('//h1');
-    $this->assertEqual('Test dynamic title', (string) $result[0]);
+    $this->assertNotEqual('Test dynamic title', (string) $result[0]);
 
     // Set some custom translated strings.
     $this->addCustomTranslations('en', array('' => array(
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 798cc89..dc825ef 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -232,7 +232,6 @@ system.date_format_add:
   path: '/admin/config/regional/date-time/formats/add'
   defaults:
     _entity_form: 'date_format.add'
-    _title: 'Add date format'
   requirements:
     _permission: 'administer site configuration'
 
@@ -240,7 +239,6 @@ entity.date_format.edit_form:
   path: '/admin/config/regional/date-time/formats/manage/{date_format}'
   defaults:
     _entity_form: 'date_format.edit'
-    _title: 'Edit date format'
   requirements:
     _entity_access: 'date_format.update'
 
@@ -248,7 +246,6 @@ entity.date_format.delete_form:
   path: '/admin/config/regional/date-time/formats/manage/{date_format}/delete'
   defaults:
     _entity_form: 'date_format.delete'
-    _title: 'Delete date format'
   requirements:
     _entity_access: 'date_format.delete'
 
@@ -265,7 +262,7 @@ system.modules_list_confirm:
   path: '/admin/modules/list/confirm'
   defaults:
     _form: 'Drupal\system\Form\ModulesListConfirmForm'
-    _title: 'Some required modules must be enabled'
+    _title_callback: 'Drupal\system\Form\ModulesListConfirmForm::getQuestion'
   requirements:
     _permission: 'administer modules'
 
@@ -399,7 +396,7 @@ system.modules_uninstall_confirm:
   path: '/admin/modules/uninstall/confirm'
   defaults:
     _form: 'Drupal\system\Form\ModulesUninstallConfirmForm'
-    _title: 'Confirm uninstall'
+    _title_callback: 'Drupal\system\Form\ModulesUninstallConfirmForm::getQuestion'
   requirements:
     _permission: 'administer modules'
 
diff --git a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
index bf5fa29..9a132fe 100644
--- a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
+++ b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Entity\Query\QueryFactory;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Controller routines for entity_test routes.
@@ -58,11 +59,25 @@ public static function create(ContainerInterface $container) {
   public function testAdd($entity_type_id) {
     $entity = entity_create($entity_type_id, array());
     $form = $this->entityFormBuilder()->getForm($entity);
-    $form['#title'] = $this->t('Create an @type', array('@type' => $entity_type_id));
     return $form;
   }
 
   /**
+   * The _title_callback for the 'Add new entity_test' form.
+   *
+   * @param string $entity_type_id
+   *   Name of the entity type for which a create form should be displayed.
+   *
+   * @return string|array
+   *   The page title.
+   *
+   * @see \Drupal\entity_test\Routing\EntityTestRoutes::routes()
+   */
+  public function titleAdd($entity_type_id) {
+    return $this->t('Create an @type', array('@type' => $entity_type_id));
+  }
+
+  /**
    * Displays the 'Edit existing entity_test' form.
    *
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
@@ -78,11 +93,27 @@ public function testAdd($entity_type_id) {
   public function testEdit(RouteMatchInterface $route_match, $entity_type_id) {
     $entity = $route_match->getParameter($entity_type_id);
     $form = $this->entityFormBuilder()->getForm($entity);
-    $form['#title'] = $entity->label();
     return $form;
   }
 
   /**
+   * The _title_callback for the 'Edit existing entity_test' form.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object to get entity type from.
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return string|array
+   *   The page title.
+   *
+   * @see \Drupal\entity_test\Routing\EntityTestRoutes::routes()
+   */
+  public function titleEdit(Request $request, $entity_type_id) {
+    return $request->attributes->get($entity_type_id)->label();
+  }
+
+  /**
    * Returns an empty page.
    *
    * @see \Drupal\entity_test\Routing\EntityTestRoutes::routes()
@@ -160,7 +191,6 @@ public function listEntitiesAlphabetically($entity_type_id) {
     return [
       '#theme' => 'item_list',
       '#items' => $labels,
-      '#title' => $entity_type_id . ' entities',
       '#cache' => [
         'tags' => $cache_tags,
       ],
diff --git a/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php b/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php
index 53c1978..e4d0bb4 100644
--- a/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php
+++ b/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php
@@ -30,6 +30,7 @@ public function routes() {
       $routes["entity.$entity_type_id.add_form"] = new Route(
         "$entity_type_id/add",
         array('_controller' => '\Drupal\entity_test\Controller\EntityTestController::testAdd', 'entity_type_id' => $entity_type_id),
+        array('_title_callback' => '\Drupal\entity_test\Controller\EntityTestController::testTitle', 'entity_type_id' => $entity_type_id),
         array('_permission' => 'administer entity_test content')
       );
 
diff --git a/core/modules/taxonomy/src/Controller/TaxonomyController.php b/core/modules/taxonomy/src/Controller/TaxonomyController.php
index e24eb6f..c7398c2 100644
--- a/core/modules/taxonomy/src/Controller/TaxonomyController.php
+++ b/core/modules/taxonomy/src/Controller/TaxonomyController.php
@@ -18,19 +18,6 @@
 class TaxonomyController extends ControllerBase {
 
   /**
-   * Title callback for term pages.
-   *
-   * @param \Drupal\taxonomy\TermInterface $term
-   *   A taxonomy term entity.
-   *
-   * @return
-   *   The term name to be used as the page title.
-   */
-  public function getTitle(TermInterface $term) {
-    return $term->label();
-  }
-
-  /**
    * Returns a rendered edit form to create a new term associated to the given vocabulary.
    *
    * @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
@@ -57,17 +44,4 @@ public function vocabularyTitle(VocabularyInterface $taxonomy_vocabulary) {
     return Xss::filter($taxonomy_vocabulary->label());
   }
 
-  /**
-   * Route title callback.
-   *
-   * @param \Drupal\taxonomy\TermInterface $taxonomy_term
-   *   The taxonomy term.
-   *
-   * @return string
-   *   The term label.
-   */
-  public function termTitle(TermInterface $taxonomy_term) {
-    return Xss::filter($taxonomy_term->getName());
-  }
-
 }
diff --git a/core/modules/taxonomy/src/VocabularyForm.php b/core/modules/taxonomy/src/VocabularyForm.php
index ffedfda..71bbafe 100644
--- a/core/modules/taxonomy/src/VocabularyForm.php
+++ b/core/modules/taxonomy/src/VocabularyForm.php
@@ -51,12 +51,6 @@ public static function create(ContainerInterface $container) {
    */
   public function form(array $form, FormStateInterface $form_state) {
     $vocabulary = $this->entity;
-    if ($vocabulary->isNew()) {
-      $form['#title'] = $this->t('Add vocabulary');
-    }
-    else {
-      $form['#title'] = $this->t('Edit vocabulary');
-    }
 
     $form['name'] = array(
       '#type' => 'textfield',
diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml
index af86c9e..9d5585a 100644
--- a/core/modules/taxonomy/taxonomy.routing.yml
+++ b/core/modules/taxonomy/taxonomy.routing.yml
@@ -28,7 +28,6 @@ entity.taxonomy_term.delete_form:
   path: '/taxonomy/term/{taxonomy_term}/delete'
   defaults:
     _entity_form: 'taxonomy_term.delete'
-    _title: 'Delete term'
   options:
     _admin_route: TRUE
   requirements:
@@ -54,7 +53,6 @@ entity.taxonomy_vocabulary.delete_form:
   path: '/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/delete'
   defaults:
     _entity_form: 'taxonomy_vocabulary.delete'
-    _title: 'Delete vocabulary'
   requirements:
     _entity_access: 'taxonomy_vocabulary.delete'
 
@@ -92,7 +90,5 @@ entity.taxonomy_term.canonical:
   path: '/taxonomy/term/{taxonomy_term}'
   defaults:
     _entity_view: 'taxonomy_term.full'
-    _title: 'Taxonomy term'
-    _title_callback: '\Drupal\taxonomy\Controller\TaxonomyController::termTitle'
   requirements:
     _entity_access: 'taxonomy_term.view'
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 4f6a263..b50e2ce 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -79,7 +79,7 @@ user.multiple_cancel_confirm:
   path: '/admin/people/cancel'
   defaults:
     _form: '\Drupal\user\Form\UserMultipleCancelConfirm'
-    _title: 'Cancel user'
+    _title_callback: '\Drupal\user\Form\UserMultipleCancelConfirm::getQuestion'
   requirements:
     _permission: 'administer users'
 
diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php
index e42058e..1097ecb 100644
--- a/core/modules/views/src/Entity/View.php
+++ b/core/modules/views/src/Entity/View.php
@@ -443,4 +443,40 @@ public function mergeDefaultDisplaysOptions() {
     }
     $this->set('display', $displays);
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle($display_id) {
+    $title = NULL;
+
+    // Start with the default (master) display's title.
+    if (isset($this->display['default']['display_options']['title'])) {
+      $title = $this->display['default']['display_options']['title'];
+    }
+
+    // Then look at the specified display, it might override the title.
+    $display_options = $this->display[$display_id]['display_options'];
+
+    // Static title override.
+    if (isset($display_options['title'])) {
+      $title = $display_options['title'];
+    }
+
+    // Potentially dynamic title override.
+    if (isset($display_options['arguments'])) {
+      foreach ($display_options['arguments'] as $argument) {
+        if ($argument['title_enable']) {
+          $title = $argument['title'];
+          if (strstr($title, '%') !== FALSE) {
+            $title = FALSE;
+          }
+          break;
+        }
+      }
+    }
+
+    return $title;
+  }
+
 }
diff --git a/core/modules/views/src/Plugin/views/display/Page.php b/core/modules/views/src/Plugin/views/display/Page.php
index c422ac8..f5b33de 100644
--- a/core/modules/views/src/Plugin/views/display/Page.php
+++ b/core/modules/views/src/Plugin/views/display/Page.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\views\Plugin\views\display;
 
-use Drupal\Component\Utility\Xss;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\State\StateInterface;
@@ -113,6 +112,30 @@ protected function defineOptions() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  protected function getRoute($view_id, $display_id) {
+    $route = parent::getRoute($view_id, $display_id);
+
+    // Set the route title at route generation time if possible. The retrieved
+    // title may be one of 3 different kinds of values:
+    // - string or render array: static title
+    // - FALSE: a dynamic title that be handled in a title callback
+    // - NULL: no title.
+    $defaults = $route->getDefaults();
+    $title = $this->view->storage->getTitle($display_id);
+    if (is_string($title) || is_array($title)) {
+      $defaults['_title'] = $title;
+    }
+    else if ($title === FALSE) {
+      $defaults['_title_callback'] = 'Drupal\views\Routing\ViewPageController::title';
+    }
+    $route->setDefaults($defaults);
+
+    return $route;
+  }
+
+  /**
    * Overrides \Drupal\views\Plugin\views\display\PathPluginBase::execute().
    */
   public function execute() {
@@ -122,18 +145,7 @@ public function execute() {
     views_set_page_view($this->view);
 
     // And now render the view.
-    $render = $this->view->render();
-
-    // First execute the view so it's possible to get tokens for the title.
-    // And the title, which is much easier.
-    // @todo Figure out how to support custom response objects. Maybe for pages
-    //   it should be dropped.
-    if (is_array($render)) {
-      $render += array(
-        '#title' => Xss::filterAdmin($this->view->getTitle()),
-      );
-    }
-    return $render;
+    return $this->view->render();
   }
 
   /**
diff --git a/core/modules/views/src/Routing/ViewPageController.php b/core/modules/views/src/Routing/ViewPageController.php
index fd3ac25..3372b17 100644
--- a/core/modules/views/src/Routing/ViewPageController.php
+++ b/core/modules/views/src/Routing/ViewPageController.php
@@ -8,9 +8,11 @@
 namespace Drupal\views\Routing;
 
 use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\views\ViewExecutable;
 use Drupal\views\ViewExecutableFactory;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -69,7 +71,10 @@ public static function create(ContainerInterface $container) {
    *   The request.
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   The route match.
+   *
    * @return null|void
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
    */
   public function handle($view_id, $display_id, Request $request, RouteMatchInterface $route_match) {
     $entity = $this->storage->load($view_id);
@@ -81,6 +86,29 @@ public function handle($view_id, $display_id, Request $request, RouteMatchInterf
     $view->setDisplay($display_id);
     $view->initHandlers();
 
+    $args = $this->getArguments($view, $route_match);
+
+    $plugin_definition = $view->display_handler->getPluginDefinition();
+    if (!empty($plugin_definition['returns_response'])) {
+      return $view->executeDisplay($display_id, $args);
+    }
+    else {
+      return $view->buildRenderable($display_id, $args);
+    }
+  }
+
+  /**
+   * Gets arguments for the current display of the passed in view.
+   *
+   * @param ViewExecutable $view
+   *   The view whose arguments to get, with a specified current display.
+   * @param RouteMatchInterface $route_match
+   *   The current route, from which to get arguments.
+   *
+   * @return array
+   *   The arguments.
+   */
+  protected function getArguments(ViewExecutable $view, RouteMatchInterface $route_match) {
     $args = array();
     $map = $route_match->getRouteObject()->getOption('_view_argument_map', array());
     $arguments_length = count($view->argument);
@@ -103,14 +131,38 @@ public function handle($view_id, $display_id, Request $request, RouteMatchInterf
         $args[] = $arg;
       }
     }
+    return $args;
+  }
 
-    $plugin_definition = $view->display_handler->getPluginDefinition();
-    if (!empty($plugin_definition['returns_response'])) {
-      return $view->executeDisplay($display_id, $args);
-    }
-    else {
-      return $view->buildRenderable($display_id, $args);
+  /**
+   * The _title_callback for the view.
+   *
+   * @param string $view_id
+   *   The ID of the view
+   * @param string $display_id
+   *   The ID of the display.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match.
+   *
+   * @return string|array
+   *   The page title.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   */
+  public function title($view_id, $display_id, Request $request, RouteMatchInterface $route_match) {
+    $entity = $this->storage->load($view_id);
+    if (empty($entity)) {
+      throw new NotFoundHttpException(String::format('Page controller for view %id requested, but view was not found.', array('%id' => $view_id)));
     }
+    $view = $this->executableFactory->get($entity);
+    $view->setRequest($request);
+    $view->setDisplay($display_id);
+    $view->initHandlers();
+    $view->setArguments($this->getArguments($view, $route_match));
+    $view->buildTitle();
+    return Xss::filterAdmin($view->getTitle());
   }
 
 }
diff --git a/core/modules/views/src/ViewEntityInterface.php b/core/modules/views/src/ViewEntityInterface.php
index 1734552..e2cd6b9 100644
--- a/core/modules/views/src/ViewEntityInterface.php
+++ b/core/modules/views/src/ViewEntityInterface.php
@@ -71,5 +71,16 @@ public function duplicateDisplayAsType($old_display_id, $new_display_type);
    */
   public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL);
 
+  /**
+   * Get the title of a specific display.
+   *
+   * @param string $display_id
+   *   The display ID whose title to get, e.g., 'default', 'page_1', 'block_2'.
+   *
+   * @return string|array|bool|null
+   *   When the display has a static title: the corresponding string or render
+   *   array. When it has a dynamic title: FALSE. When it has no title: NULL.
+   */
+  public function getTitle($display_id);
 
 }
diff --git a/core/modules/views_ui/src/Controller/ViewsUIController.php b/core/modules/views_ui/src/Controller/ViewsUIController.php
index b797619..4da7277 100644
--- a/core/modules/views_ui/src/Controller/ViewsUIController.php
+++ b/core/modules/views_ui/src/Controller/ViewsUIController.php
@@ -206,17 +206,29 @@ public function autocompleteTag(Request $request) {
    *   An array containing the Views edit and preview forms.
    */
   public function edit(ViewUI $view, $display_id = NULL) {
+    $build['edit'] = $this->entityFormBuilder()->getForm($view, 'edit', array('display_id' => $display_id));
+    $build['preview'] = $this->entityFormBuilder()->getForm($view, 'preview', array('display_id' => $display_id));
+    return $build;
+  }
+
+  /**
+   * The _title_callback for the view edit form.
+   *
+   * @param \Drupal\views_ui\ViewUI $view
+   *   The view being deleted.
+   *
+   * @return string|array
+   *   The page title.
+   */
+  public function title(ViewUI $view) {
     $name = $view->label();
     $data = $this->viewsData->get($view->get('base_table'));
 
     if (isset($data['table']['base']['title'])) {
       $name .= ' (' . $data['table']['base']['title'] . ')';
     }
-    $build['#title'] = $name;
 
-    $build['edit'] = $this->entityFormBuilder()->getForm($view, 'edit', array('display_id' => $display_id));
-    $build['preview'] = $this->entityFormBuilder()->getForm($view, 'preview', array('display_id' => $display_id));
-    return $build;
+    return $name;
   }
 
 }
diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php
index e8464fa..b134b4f 100644
--- a/core/modules/views_ui/src/ViewUI.php
+++ b/core/modules/views_ui/src/ViewUI.php
@@ -1192,4 +1192,12 @@ public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) {
   public function getViewExecutable() {
     return $this->storage->getViewExecutable();
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle($display_id) {
+    return $this->storage->getTitle($display_id);
+  }
+
 }
diff --git a/core/modules/views_ui/views_ui.routing.yml b/core/modules/views_ui/views_ui.routing.yml
index 0b95812..acfb99f 100644
--- a/core/modules/views_ui/views_ui.routing.yml
+++ b/core/modules/views_ui/views_ui.routing.yml
@@ -10,7 +10,6 @@ views_ui.add:
   path: '/admin/structure/views/add'
   defaults:
     _entity_form: 'view.add'
-    _title: 'Add new view'
   requirements:
     _entity_create_access: view
 
@@ -68,7 +67,6 @@ entity.view.duplicate_form:
   path: '/admin/structure/views/view/{view}/duplicate'
   defaults:
     _entity_form: 'view.duplicate'
-    _title: 'Duplicate view'
   requirements:
     _entity_access: view.duplicate
 
@@ -76,7 +74,6 @@ entity.view.delete_form:
   path: '/admin/structure/views/view/{view}/delete'
   defaults:
     _entity_form: 'view.delete'
-    _title: 'Delete view'
   requirements:
     _entity_access: view.delete
 
@@ -96,6 +93,7 @@ entity.view.edit_form:
         type: entity:view
   defaults:
     _controller: '\Drupal\views_ui\Controller\ViewsUIController::edit'
+    _title_callback: '\Drupal\views_ui\Controller\ViewsUIController::title'
   requirements:
     _entity_access: view.update
 
@@ -108,6 +106,7 @@ entity.view.edit_display_form:
         type: entity:view
   defaults:
     _controller: '\Drupal\views_ui\Controller\ViewsUIController::edit'
+    _title_callback: '\Drupal\views_ui\Controller\ViewsUIController::title'
     display_id: NULL
   requirements:
     _entity_access: view.update
