diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index d578499..56566e1 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -40,10 +40,6 @@ function template_preprocess_menu_local_task(&$variables) {
     $active = SafeMarkup::format('<span class="visually-hidden">@label</span>', array('@label' => t('(active tab)')));
     $link_text = t('@local-task-title@active', array('@local-task-title' => $link_text, '@active' => $active));
   }
-  else {
-    // @todo Remove this once https://www.drupal.org/node/2338081 is fixed.
-    $link_text = SafeMarkup::checkPlain($link_text);
-  }
 
   $link['localized_options']['set_active_class'] = TRUE;
 
diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php
index 4fe7416..19671fe 100644
--- a/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php
+++ b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\Core\Menu;
 
-use Drupal\Core\Plugin\PluginBase;
+use Drupal\Component\Plugin\PluginBase;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -17,21 +17,10 @@ class ContextualLinkDefault extends PluginBase implements ContextualLinkInterfac
 
   /**
    * {@inheritdoc}
-   *
-   * @todo: It might be helpful at some point to move this getTitle logic into
-   *   a trait.
    */
   public function getTitle(Request $request = NULL) {
-    $options = array();
-    if (!empty($this->pluginDefinition['title_context'])) {
-      $options['context'] = $this->pluginDefinition['title_context'];
-    }
-    $args = array();
-    if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) {
-      $args = (array) $title_arguments;
-    }
-
-    return $this->t($this->pluginDefinition['title'], $args, $options);
+    // The title from YAML file discovery may be a TranslationWrapper object.
+    return (string) $this->pluginDefinition['title'];
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php
index 3482adc..25421d7 100644
--- a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php
+++ b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php
@@ -118,8 +118,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Mo
    */
   protected function getDiscovery() {
     if (!isset($this->discovery)) {
-      $this->discovery = new YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories());
-      $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
+      $yaml_discovery = new YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories());
+      $yaml_discovery->addTranslatableProperty('title', 'title_context');
+      $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
     }
     return $this->discovery;
   }
diff --git a/core/lib/Drupal/Core/Menu/LocalActionDefault.php b/core/lib/Drupal/Core/Menu/LocalActionDefault.php
index b830a2f..b0109a9 100644
--- a/core/lib/Drupal/Core/Menu/LocalActionDefault.php
+++ b/core/lib/Drupal/Core/Menu/LocalActionDefault.php
@@ -7,8 +7,9 @@
 
 namespace Drupal\Core\Menu;
 
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Routing\RouteProviderInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -19,6 +20,8 @@
  */
 class LocalActionDefault extends PluginBase implements LocalActionInterface, ContainerFactoryPluginInterface {
 
+  use DependencySerializationTrait;
+
   /**
    * The route provider to load routes by name.
    *
@@ -68,15 +71,8 @@ public function getRouteName() {
    */
   public function getTitle(Request $request = NULL) {
     // Subclasses may pull in the request or specific attributes as parameters.
-    $options = array();
-    if (!empty($this->pluginDefinition['title_context'])) {
-      $options['context'] = $this->pluginDefinition['title_context'];
-    }
-    $args = array();
-    if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) {
-      $args = (array) $title_arguments;
-    }
-    return $this->t($this->pluginDefinition['title'], $args, $options);
+    // The title from YAML file discovery may be a TranslationWrapper object.
+    return (string) $this->pluginDefinition['title'];
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/LocalActionManager.php b/core/lib/Drupal/Core/Menu/LocalActionManager.php
index db6acef..c387b15 100644
--- a/core/lib/Drupal/Core/Menu/LocalActionManager.php
+++ b/core/lib/Drupal/Core/Menu/LocalActionManager.php
@@ -142,8 +142,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Re
    */
   protected function getDiscovery() {
     if (!isset($this->discovery)) {
-      $this->discovery = new YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories());
-      $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
+      $yaml_discovery = new YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories());
+      $yaml_discovery->addTranslatableProperty('title', 'title_context');
+      $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
     }
     return $this->discovery;
   }
diff --git a/core/lib/Drupal/Core/Menu/LocalTaskDefault.php b/core/lib/Drupal/Core/Menu/LocalTaskDefault.php
index ecef19c..6a09ffa 100644
--- a/core/lib/Drupal/Core/Menu/LocalTaskDefault.php
+++ b/core/lib/Drupal/Core/Menu/LocalTaskDefault.php
@@ -7,7 +7,8 @@
 
 namespace Drupal\Core\Menu;
 
-use Drupal\Core\Plugin\PluginBase;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -16,6 +17,8 @@
  */
 class LocalTaskDefault extends PluginBase implements LocalTaskInterface {
 
+  use DependencySerializationTrait;
+
   /**
    * The route provider to load routes by name.
    *
@@ -75,16 +78,8 @@ public function getRouteParameters(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function getTitle(Request $request = NULL) {
-    // Subclasses may pull in the request or specific attributes as parameters.
-    $options = array();
-    if (!empty($this->pluginDefinition['title_context'])) {
-      $options['context'] = $this->pluginDefinition['title_context'];
-    }
-    $args = array();
-    if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) {
-      $args = (array) $title_arguments;
-    }
-    return $this->t($this->pluginDefinition['title'], $args, $options);
+    // The title from YAML file discovery may be a TranslationWrapper object.
+    return (string) $this->pluginDefinition['title'];
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/LocalTaskManager.php b/core/lib/Drupal/Core/Menu/LocalTaskManager.php
index 70b1102..5b12ac8 100644
--- a/core/lib/Drupal/Core/Menu/LocalTaskManager.php
+++ b/core/lib/Drupal/Core/Menu/LocalTaskManager.php
@@ -142,8 +142,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Re
    */
   protected function getDiscovery() {
     if (!isset($this->discovery)) {
-      $this->discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories());
-      $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
+      $yaml_discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories());
+      $yaml_discovery->addTranslatableProperty('title', 'title_context');
+      $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
     }
     return $this->discovery;
   }
diff --git a/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php
index 0b7d10f6..d0d35ba 100644
--- a/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php
+++ b/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php
@@ -10,9 +10,15 @@
 use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
 use Drupal\Component\Discovery\YamlDiscovery as ComponentYamlDiscovery;
 use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 
 /**
  * Allows YAML files to define plugin definitions.
+ *
+ * For translatable values of plugin defintions, like title, you can specify
+ * $translatable_properties as part of the constructor. This is used in order
+ * to use a translation wrapper to 100% indicate that something is safe, as it
+ * was written in the YAML file and did not come from plugin definition.
  */
 class YamlDiscovery implements DiscoveryInterface {
 
@@ -26,6 +32,15 @@ class YamlDiscovery implements DiscoveryInterface {
   protected $discovery;
 
   /**
+   * Contains an array of translatable properties passed along to t().
+   *
+   * @see \Drupal\Core\Plugin\Discovery\YamlDiscovery::__construct
+   *
+   * @var array
+   */
+  protected $translatableProperties = [];
+
+  /**
    * Construct a YamlDiscovery object.
    *
    * @param string $name
@@ -39,6 +54,23 @@ function __construct($name, array $directories) {
   }
 
   /**
+   * Set one or more of the YAML values as being translatable.
+   *
+   * @param $value_key
+   *   The key corresponding to the value in the YAML that contains a
+   *   translatable string.
+   * @param string $context_key
+   *   (Optional) the translation context for the value specified by the
+   *   $value_key.
+   *
+   * @return $this
+   */
+  public function addTranslatableProperty($value_key, $context_key = '') {
+    $this->translatableProperties[$value_key] = $context_key;
+    return $this;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getDefinitions() {
@@ -48,6 +80,20 @@ public function getDefinitions() {
     $definitions = array();
     foreach ($plugins as $provider => $list) {
       foreach ($list as $id => $definition) {
+        // Add translation wrappers.
+        foreach ($this->translatableProperties as $property => $context_key) {
+          if (isset($definition[$property])) {
+            $options = [];
+            // Move the t() context from the definition to the translation
+            // wrapper.
+            if ($context_key && isset($definition[$context_key])) {
+              $options['context'] = $definition[$context_key];
+              unset($definition[$context_key]);
+            }
+            $definition[$property] = new TranslationWrapper($definition[$property], [], $options);
+          }
+        }
+        // Add ID and provider.
         $definitions[$id] = $definition + array(
           'provider' => $provider,
           'id' => $id,
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
index 3a36db5..6bf591a 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
@@ -85,6 +85,16 @@ public function getOption($name) {
   }
 
   /**
+   * Gets all options from this translation wrapper.
+   *
+   * @return mixed[]
+   *   The array of options.
+   */
+  public function getOptions() {
+    return $this->options;
+  }
+
+  /**
    * Implements the magic __toString() method.
    */
   public function __toString() {
diff --git a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php
index 0b8f6da..bbbfa8e 100644
--- a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php
+++ b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php
@@ -10,12 +10,14 @@
 use Drupal\comment\CommentStorageInterface;
 use Drupal\Core\Menu\LocalTaskDefault;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a local task that shows the amount of unapproved comments.
  */
 class UnapprovedComments extends LocalTaskDefault implements ContainerFactoryPluginInterface {
+  use StringTranslationTrait;
 
   /**
    * The comment storage service.
@@ -57,7 +59,7 @@ public static function create(ContainerInterface $container, array $configuratio
    * {@inheritdoc}
    */
   public function getTitle() {
-    return t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount()));
+    return $this->t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount()));
   }
 
 }
diff --git a/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php b/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php
index 98ffb0e..c8989fc 100644
--- a/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php
+++ b/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php
@@ -9,11 +9,13 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Menu\ContextualLinkDefault;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 
 /**
  * Defines a contextual link plugin with a dynamic title.
  */
 class ConfigTranslationContextualLink extends ContextualLinkDefault {
+  use StringTranslationTrait;
 
   /**
    * The mapper plugin discovery service.
@@ -26,17 +28,12 @@ class ConfigTranslationContextualLink extends ContextualLinkDefault {
    * {@inheritdoc}
    */
   public function getTitle() {
-    $options = array();
-    if (!empty($this->pluginDefinition['title_context'])) {
-      $options['context'] = $this->pluginDefinition['title_context'];
-    }
-
-    // Take custom 'config_translation_plugin_id' plugin definition key to
-    // retrieve title. We need to retrieve a runtime title (as opposed to
-    // storing the title on the plugin definition for the link) because
-    // it contains translated parts that we need in the runtime language.
+    // Use the custom 'config_translation_plugin_id' plugin definition key to
+    // retrieve the title. We need to retrieve a runtime title (as opposed to
+    // storing the title on the plugin definition for the link) because it
+    // contains translated parts that we need in the runtime language.
     $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel());
-    return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options);
+    return $this->t('Translate @type_name', array('@type_name' => $type_name));
   }
 
   /**
diff --git a/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php b/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php
index 46a2ec2..496ed96 100644
--- a/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php
+++ b/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php
@@ -9,11 +9,13 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Menu\LocalTaskDefault;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 
 /**
  * Defines a local task plugin with a dynamic title.
  */
 class ConfigTranslationLocalTask extends LocalTaskDefault {
+  use StringTranslationTrait;
 
   /**
    * The mapper plugin discovery service.
@@ -26,17 +28,12 @@ class ConfigTranslationLocalTask extends LocalTaskDefault {
    * {@inheritdoc}
    */
   public function getTitle() {
-    $options = array();
-    if (!empty($this->pluginDefinition['title_context'])) {
-      $options['context'] = $this->pluginDefinition['title_context'];
-    }
-
     // Take custom 'config_translation_plugin_id' plugin definition key to
     // retrieve title. We need to retrieve a runtime title (as opposed to
     // storing the title on the plugin definition for the link) because
     // it contains translated parts that we need in the runtime language.
     $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel());
-    return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options);
+    return $this->t('Translate @type_name', array('@type_name' => $type_name));
   }
 
   /**
diff --git a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php
index 8846ab9..b8ed11c 100644
--- a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php
+++ b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\contextual\Tests;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\Url;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\simpletest\WebTestBase;
 use Drupal\Core\Template\Attribute;
@@ -46,7 +47,7 @@ class ContextualDynamicContextTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('contextual', 'node', 'views', 'views_ui', 'language');
+  public static $modules = array('contextual', 'node', 'views', 'views_ui', 'language', 'menu_test');
 
   protected function setUp() {
     parent::setUp();
@@ -137,6 +138,11 @@ function testDifferentPermissions() {
     $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it';
     $this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]);
     $this->assertContextualLinkPlaceHolder($id);
+
+    // Get a page where contextual links are directly rendered.
+    $this->drupalGet(Url::fromRoute('menu_test.contextual_test'));
+    $this->assertEscaped("<script>alert('Welcome to the jungle!')</script>");
+    $this->assertLink('Edit menu - contextual');
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Menu/LocalActionTest.php b/core/modules/system/src/Tests/Menu/LocalActionTest.php
index c049198..620aed5 100644
--- a/core/modules/system/src/Tests/Menu/LocalActionTest.php
+++ b/core/modules/system/src/Tests/Menu/LocalActionTest.php
@@ -30,6 +30,8 @@ public function testLocalAction() {
     // Ensure that both menu and route based actions are shown.
     $this->assertLocalAction([
       [Url::fromRoute('menu_test.local_action4'), 'My dynamic-title action'],
+      [Url::fromRoute('menu_test.local_action4'), htmlspecialchars("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')],
+      [Url::fromRoute('menu_test.local_action4'), htmlspecialchars("<script>alert('Welcome to the derived jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')],
       [Url::fromRoute('menu_test.local_action2'), 'My hook_menu action'],
       [Url::fromRoute('menu_test.local_action3'), 'My YAML discovery action'],
       [Url::fromRoute('menu_test.local_action5'), 'Title override'],
@@ -50,7 +52,9 @@ protected function assertLocalAction(array $actions) {
     foreach ($actions as $action) {
       /** @var \Drupal\Core\Url $url */
       list($url, $title) = $action;
-      $this->assertEqual((string) $elements[$index], $title);
+      // SimpleXML gives us the unescaped text, not the actual escaped markup,
+      // so use a pattern instead to check the raw content.
+      $this->assertPattern('@<a [^>]*class="[^"]*button-action[^"]*"[^>]*>' . preg_quote($title, '@') . '</@');
       $this->assertEqual($elements[$index]['href'], $url->toString());
       $index++;
     }
diff --git a/core/modules/system/src/Tests/Menu/LocalTasksTest.php b/core/modules/system/src/Tests/Menu/LocalTasksTest.php
index b7824ef..79dc5e4 100644
--- a/core/modules/system/src/Tests/Menu/LocalTasksTest.php
+++ b/core/modules/system/src/Tests/Menu/LocalTasksTest.php
@@ -48,17 +48,51 @@ protected function assertLocalTasks(array $routes, $level = 0) {
   }
 
   /**
+   * Ensures that some local task appears.
+   *
+   * @param string $title
+   *   The expected title.
+   *
+   * @return bool
+   *   TRUE if the local task exists on the page.
+   */
+  protected function assertLocalTaskAppers($title) {
+    // SimpleXML gives us the unescaped text, not the actual escaped markup,
+    // so use a pattern instead to check the raw content.
+    return $this->assertPattern('@<a [^>]*>' . preg_quote($title, '@') . '</a>@');
+  }
+
+  /**
    * Tests the plugin based local tasks.
    */
   public function testPluginLocalTask() {
+    // Verify local tasks defined in the hook.
+    $this->drupalGet(Url::fromRoute('menu_test.tasks_default'));
+    $this->assertLocalTasks([
+      ['menu_test.tasks_default', []],
+      ['menu_test.router_test1', ['bar' => 'unsafe']],
+      ['menu_test.router_test1', ['bar' => '1']],
+      ['menu_test.router_test2', ['bar' => '2']],
+    ]);
+
+    // Verify that script tags are escaped on output.
+    $title = htmlspecialchars("Task 1 <script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+    $this->assertLocalTaskAppers($title);
+    $title = htmlspecialchars("<script>alert('Welcome to the derived jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+    $this->assertLocalTaskAppers($title);
+
     // Verify that local tasks appear as defined in the router.
     $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_view'));
     $this->assertLocalTasks([
       ['menu_test.local_task_test_tasks_view', []],
       ['menu_test.local_task_test_tasks_edit', []],
       ['menu_test.local_task_test_tasks_settings', []],
+      ['menu_test.local_task_test_tasks_settings_dynamic', []],
     ]);
 
+    $title = htmlspecialchars("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+    $this->assertLocalTaskAppers($title);
+
     // Ensure the view tab is active.
     $result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a');
     $this->assertEqual(1, count($result), 'There is just a single active tab.');
diff --git a/core/modules/system/src/Tests/Menu/MenuRouterTest.php b/core/modules/system/src/Tests/Menu/MenuRouterTest.php
index ef78c7b..7fd809d 100644
--- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php
+++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php
@@ -72,6 +72,8 @@ protected function doTestHookMenuIntegration() {
     // Confirm local task links are displayed.
     $this->assertLink('Local task A');
     $this->assertLink('Local task B');
+    $this->assertNoLink('Local task C');
+    $this->assertEscaped("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES, 'UTF-8');
     // Confirm correct local task href.
     $this->assertLinkByHref(Url::fromRoute('menu_test.router_test1', ['bar' => $machine_name])->toString());
     $this->assertLinkByHref(Url::fromRoute('menu_test.router_test2', ['bar' => $machine_name])->toString());
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml
index 24c92a2..c752990 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml
@@ -26,6 +26,22 @@ menu_test.local_action4:
   appears_on:
     - menu_test.local_action1
 
+menu_test.local_action_derivative:
+  route_name: menu_test.local_action4
+  weight: -20
+  deriver: Drupal\menu_test\Plugin\Derivative\LocalActionTest
+  class: Drupal\Core\Menu\LocalActionDefault
+  appears_on:
+    - menu_test.local_action1
+
+menu_test.local_action6:
+  route_name: menu_test.local_action4
+  title: 'Dynamic local action with user input'
+  weight: -15
+  class: '\Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalAction5'
+  appears_on:
+    - menu_test.local_action1
+
 menu_test.hidden_menu_add:
   route_name: menu_test.hidden_menu_add
   title: 'Add menu'
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml
index b2d8e47..9a8d8ad 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml
@@ -1,12 +1,13 @@
-menu_test.hidden_manage:
-  title: 'List links'
+menu_test.contextual_hidden_manage:
+  title: 'List links - contextual'
   group: menu_test_menu
-  route_name: menu_test.hidden_manage
+  route_name: menu_test.contextual_hidden_manage
+  class: '\Drupal\menu_test\Plugin\Menu\ContextualLink\TestContextualLink'
 
-menu_test.hidden_manage_edit:
-  title: 'Edit menu'
+menu_test.contextual_hidden_manage_edit:
+  title: 'Edit menu - contextual'
   group: menu_test_menu
-  route_name: menu_test.hidden_manage_edit
+  route_name: menu_test.contextual_hidden_manage_edit
 
 menu_test.hidden_block_configure:
   title: 'Configure block'
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml
index c8cd284..cda0f5a 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml
@@ -10,6 +10,10 @@ menu_test.local_task_test_tasks_settings:
   route_name: menu_test.local_task_test_tasks_settings
   title: Settings
   base_route: menu_test.local_task_test_tasks_view
+menu_test.local_task_test_tasks_settings_dynamic:
+  route_name: menu_test.local_task_test_tasks_settings_dynamic
+  base_route: menu_test.local_task_test_tasks_view
+  class: \Drupal\menu_test\Plugin\Menu\LocalTask\TestTaskWithUserInput
 menu_test.local_task_test_tasks_settings_sub1:
   route_name: menu_test.local_task_test_tasks_settings_sub1
   title: sub1
@@ -53,6 +57,11 @@ menu_test.tasks_default_tab:
   route_name: menu_test.tasks_default
   title: 'View'
   base_route: menu_test.tasks_default
+menu_test.tasks_default_derived:
+  route_name: menu_test.router_test1
+  title: 'Derived'
+  base_route: menu_test.tasks_default
+  deriver: '\Drupal\menu_test\Plugin\Derivative\LocalTaskTestWithUnsafeTitle'
 
 menu_test.tasks_tasks_tab:
   route_name: menu_test.tasks_tasks
@@ -82,3 +91,8 @@ menu_test.router_test3:
   route_name: menu_test.router_test3
   title: 'Local task C'
   base_route: menu_test.router_test1
+
+menu_test.router_test4:
+  route_name: menu_test.router_test4
+  base_route: menu_test.router_test1
+  class: \Drupal\menu_test\Plugin\Menu\LocalTask\TestTaskWithUserInput
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index ec916cb..429b357 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -5,6 +5,8 @@
  * Module that implements various hooks for menu tests.
  */
 
+use Drupal\Core\Url;
+
 /**
  * Implements hook_menu_links_discovered_alter().
  */
@@ -26,20 +28,14 @@ function menu_test_menu_links_discovered_alter(&$links) {
 
 /**
  * Implements hook_menu_local_tasks().
- *
- * If the menu_test.settings configuration 'tasks.add' has been set, adds
- * several local tasks to menu-test/tasks.
  */
 function menu_test_menu_local_tasks(&$data, $route_name) {
-  if (!\Drupal::config('menu_test.settings')->get('tasks.add')) {
-    return;
-  }
-  if (in_array($route_name, array('menu_test.tasks_default', 'menu_test.tasks_empty', 'menu_test.tasks_tasks'))) {
+  if (in_array($route_name, array('menu_test.tasks_default'))) {
     $data['tabs'][0]['foo'] = array(
       '#theme' => 'menu_local_task',
       '#link' => array(
-        'title' => 'Task 1',
-        'href' => 'task/foo',
+        'title' => "Task 1 <script>alert('Welcome to the jungle!')</script>",
+        'url' => Url::fromRoute('menu_test.router_test1', array('bar' => '1')),
       ),
       '#weight' => 10,
     );
@@ -47,7 +43,7 @@ function menu_test_menu_local_tasks(&$data, $route_name) {
       '#theme' => 'menu_local_task',
       '#link' => array(
         'title' => 'Task 2',
-        'href' => 'task/bar',
+        'url' => Url::fromRoute('menu_test.router_test2', array('bar' => '2')),
       ),
       '#weight' => 20,
     );
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index 33a337b..b69d722 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
@@ -67,6 +67,13 @@ menu_test.router_test3:
   requirements:
     _access: 'FALSE'
 
+menu_test.router_test4:
+  path: '/foo/{bar}/d'
+  defaults:
+    _controller: '\Drupal\menu_test\TestControllers::test2'
+  requirements:
+    _access: 'TRUE'
+
 menu_test.local_action1:
   path: '/menu-test-local-action'
   defaults:
@@ -102,6 +109,27 @@ menu_test.local_action5:
   requirements:
     _access: 'TRUE'
 
+menu_test.contextual_test:
+  path: '/menu-test-contextual/default'
+  defaults:
+    _controller: '\Drupal\menu_test\TestControllers::testContextual'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.contextual_hidden_manage:
+  path: '/menu-test-contextual/{bar}'
+  defaults:
+    _controller: '\Drupal\menu_test\TestControllers::test1'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.contextual_hidden_manage_edit:
+  path: '/menu-test-contextual/{bar}/edit'
+  defaults:
+    _controller: '\Drupal\menu_test\TestControllers::test2'
+  requirements:
+    _access: 'TRUE'
+
 menu_test.local_task_test_tasks:
   path: '/menu-local-task-test/tasks'
   defaults:
@@ -109,6 +137,13 @@ menu_test.local_task_test_tasks:
   requirements:
     _access: 'TRUE'
 
+menu_test.tasks_default:
+  path: '/menu-local-task-test/default'
+  defaults:
+    _controller: '\Drupal\menu_test\TestControllers::test1'
+  requirements:
+    _access: 'TRUE'
+
 menu_test.local_task_test_tasks_tasks:
   path: '/menu-local-task-test/tasks/tasks'
   defaults:
@@ -137,6 +172,13 @@ menu_test.local_task_test_tasks_settings:
   requirements:
     _access: 'TRUE'
 
+menu_test.local_task_test_tasks_settings_dynamic:
+  path: '/menu-local-task-test/tasks/settings-dynamic'
+  defaults:
+    _controller: '\Drupal\menu_test\TestControllers::test1'
+  requirements:
+    _access: 'TRUE'
+
 menu_test.local_task_test_tasks_settings_sub1:
   path: '/menu-local-task-test/tasks/settings/sub1'
   defaults:
diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalActionTest.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalActionTest.php
new file mode 100644
index 0000000..729519a
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalActionTest.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_test\Plugin\Derivative\LocalActionTest.
+ */
+
+namespace Drupal\menu_test\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+
+/**
+ * Test derivative to check local action title escaping.
+ *
+ * @see \Drupal\system\Tests\Menu\LocalActionTest
+ */
+class LocalActionTest extends DeriverBase {
+
+  /**
+   * @inheritDoc
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    $this->derivatives['example'] = $base_plugin_definition + [
+      'title' => "<script>alert('Welcome to the derived jungle!')</script>",
+    ];
+
+    return $this->derivatives;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalTaskTestWithUnsafeTitle.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalTaskTestWithUnsafeTitle.php
new file mode 100644
index 0000000..af6ff91
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalTaskTestWithUnsafeTitle.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_test\Plugin\Derivative\LocalTaskTestWithUnsafeTitle.
+ */
+
+namespace Drupal\menu_test\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+
+/**
+ * Test derivative to check local task title escaping.
+ *
+ * @see \Drupal\system\Tests\Menu\LocalTasksTest
+ */
+class LocalTaskTestWithUnsafeTitle extends DeriverBase {
+
+  /**
+   * @inheritDoc
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    $this->derivatives['unsafe'] = [
+      'title' => "<script>alert('Welcome to the derived jungle!')</script>",
+      'route_parameters' => ['bar' => 'unsafe'],
+    ] + $base_plugin_definition;
+
+    return $this->derivatives;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php
new file mode 100644
index 0000000..fde09e2
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_test\Plugin\Menu\ContextualLink\TestContextualLink.
+ */
+
+namespace Drupal\menu_test\Plugin\Menu\ContextualLink;
+
+use Drupal\Core\Menu\ContextualLinkDefault;
+
+/**
+ * Defines a contextual link plugin with a dynamic title from user input.
+ */
+class TestContextualLink extends ContextualLinkDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return "<script>alert('Welcome to the jungle!')</script>";
+  }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php
index 3f9a44e..a40af0c 100644
--- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php
+++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php
@@ -8,12 +8,15 @@
 namespace Drupal\menu_test\Plugin\Menu\LocalAction;
 
 use Drupal\Core\Menu\LocalActionDefault;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 
 /**
  * Defines a local action plugin with a dynamic title.
  */
 class TestLocalAction4 extends LocalActionDefault {
 
+  use StringTranslationTrait;
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php
similarity index 57%
copy from core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php
copy to core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php
index 3f9a44e..c39f1d4 100644
--- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php
+++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Contains \Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalAction4.
+ * Contains \Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalAction5.
  */
 
 namespace Drupal\menu_test\Plugin\Menu\LocalAction;
@@ -10,15 +10,15 @@
 use Drupal\Core\Menu\LocalActionDefault;
 
 /**
- * Defines a local action plugin with a dynamic title.
+ * Defines a local action plugin with a dynamic title from user input.
  */
-class TestLocalAction4 extends LocalActionDefault {
+class TestLocalAction5 extends LocalActionDefault {
 
   /**
    * {@inheritdoc}
    */
   public function getTitle() {
-    return $this->t('My @arg action', array('@arg' => 'dynamic-title'));
+    return "<script>alert('Welcome to the jungle!')</script>";
   }
 
 }
diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTaskWithUserInput.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTaskWithUserInput.php
new file mode 100644
index 0000000..6ef6102
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTaskWithUserInput.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_test\Plugin\Menu\LocalTask\TestTaskWithUserInput.
+ */
+
+namespace Drupal\menu_test\Plugin\Menu\LocalTask;
+
+use Drupal\Core\Menu\LocalTaskDefault;
+use Symfony\Component\HttpFoundation\Request;
+
+class TestTaskWithUserInput extends LocalTaskDefault {
+
+  /**
+   * @inheritDoc
+   */
+  public function getTitle(Request $request = NULL) {
+    return "<script>alert('Welcome to the jungle!')</script>";
+  }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php
index ee5d594..67f0427 100644
--- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php
+++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php
@@ -8,9 +8,12 @@
 namespace Drupal\menu_test\Plugin\Menu\LocalTask;
 
 use Drupal\Core\Menu\LocalTaskDefault;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 
 class TestTasksSettingsSub1 extends LocalTaskDefault {
 
+  use StringTranslationTrait;
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/system/tests/modules/menu_test/src/TestControllers.php b/core/modules/system/tests/modules/menu_test/src/TestControllers.php
index 836415c..f3ff8d2 100644
--- a/core/modules/system/tests/modules/menu_test/src/TestControllers.php
+++ b/core/modules/system/tests/modules/menu_test/src/TestControllers.php
@@ -61,4 +61,20 @@ public function testDefaults($placeholder = NULL) {
     }
   }
 
+  /**
+   * Prints out test data with contextual links.
+   */
+  public function testContextual() {
+    return [
+      '#markup' => 'testContextual',
+      'stuff' => [
+        '#type' => 'contextual_links',
+        '#contextual_links' => [
+          'menu_test_menu' => [
+            'route_parameters' => ['bar' => 1],
+          ]
+        ]
+      ]
+    ];
+  }
 }
diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php
index e3825ca..ac92088 100644
--- a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Menu;
 
 use Drupal\Core\Menu\ContextualLinkDefault;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\ParameterBag;
 use Symfony\Component\HttpFoundation\Request;
@@ -63,17 +64,18 @@ protected function setUp() {
 
   protected function setupContextualLinkDefault() {
     $this->contextualLinkDefault = new ContextualLinkDefault($this->config, $this->pluginId, $this->pluginDefinition);
-    $this->contextualLinkDefault->setStringTranslation($this->stringTranslation);
   }
 
   /**
    * @covers ::getTitle
    */
-  public function testGetTitle($title = 'Example') {
-    $this->pluginDefinition['title'] = $title;
+  public function testGetTitle() {
+    $title = 'Example';
+    $this->pluginDefinition['title'] = (new TranslationWrapper($title))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], array(), array())
+      ->with($title, array(), array())
       ->will($this->returnValue('Example translated'));
 
     $this->setupContextualLinkDefault();
@@ -84,11 +86,12 @@ public function testGetTitle($title = 'Example') {
    * @covers ::getTitle
    */
   public function testGetTitleWithContext() {
-    $this->pluginDefinition['title'] = 'Example';
-    $this->pluginDefinition['title_context'] = 'context';
+    $title = 'Example';
+    $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context')))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], array(), array('context' => $this->pluginDefinition['title_context']))
+      ->with($title, array(), array('context' => 'context'))
       ->will($this->returnValue('Example translated with context'));
 
     $this->setupContextualLinkDefault();
@@ -99,11 +102,12 @@ public function testGetTitleWithContext() {
    * @covers ::getTitle
    */
   public function testGetTitleWithTitleArguments() {
-    $this->pluginDefinition['title'] = 'Example @test';
-    $this->pluginDefinition['title_arguments'] = array('@test' => 'value');
+    $title = 'Example @test';
+    $this->pluginDefinition['title'] = (new TranslationWrapper($title, array('@test' => 'value')))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array())
+      ->with($title, array('@test' => 'value'), array())
       ->will($this->returnValue('Example value'));
 
     $this->setupContextualLinkDefault();
diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php
index dade254..02d4578 100644
--- a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Menu;
 
 use Drupal\Core\Menu\LocalActionDefault;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\ParameterBag;
 use Symfony\Component\HttpFoundation\Request;
@@ -74,7 +75,6 @@ protected function setUp() {
    */
   protected function setupLocalActionDefault() {
     $this->localActionDefault = new LocalActionDefault($this->config, $this->pluginId, $this->pluginDefinition, $this->routeProvider);
-    $this->localActionDefault->setStringTranslation($this->stringTranslation);
   }
 
   /**
@@ -83,10 +83,11 @@ protected function setupLocalActionDefault() {
    * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle()
    */
   public function testGetTitle() {
-    $this->pluginDefinition['title'] = 'Example';
+    $this->pluginDefinition['title'] = (new TranslationWrapper('Example'))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], array(), array())
+      ->with('Example', array(), array())
       ->will($this->returnValue('Example translated'));
 
     $this->setupLocalActionDefault();
@@ -99,11 +100,11 @@ public function testGetTitle() {
    * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle()
    */
   public function testGetTitleWithContext() {
-    $this->pluginDefinition['title'] = 'Example';
-    $this->pluginDefinition['title_context'] = 'context';
+    $this->pluginDefinition['title'] = (new TranslationWrapper('Example', array(), array('context' => 'context')))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], array(), array('context' => 'context'))
+      ->with('Example', array(), array('context' => 'context'))
       ->will($this->returnValue('Example translated with context'));
 
     $this->setupLocalActionDefault();
@@ -114,11 +115,11 @@ public function testGetTitleWithContext() {
    * Tests the getTitle method with title arguments.
    */
   public function testGetTitleWithTitleArguments() {
-    $this->pluginDefinition['title'] = 'Example @test';
-    $this->pluginDefinition['title_arguments'] = array('@test' => 'value');
+    $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value')))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array())
+      ->with('Example @test', array('@test' => 'value'), array())
       ->will($this->returnValue('Example value'));
 
     $this->setupLocalActionDefault();
diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php
index 573444b..6e393a8 100644
--- a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php
@@ -10,8 +10,8 @@
 use Drupal\Core\Menu\LocalTaskDefault;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\Tests\UnitTestCase;
-use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Route;
 
 /**
@@ -77,9 +77,7 @@ protected function setUp() {
   protected function setupLocalTaskDefault() {
     $this->localTaskBase = new TestLocalTaskDefault($this->config, $this->pluginId, $this->pluginDefinition);
     $this->localTaskBase
-      ->setRouteProvider($this->routeProvider)
-      ->setStringTranslation($this->stringTranslation);
-
+      ->setRouteProvider($this->routeProvider);
   }
 
   /**
@@ -234,11 +232,12 @@ public function testActive() {
   /**
    * @covers ::getTitle
    */
-  public function testGetTitleWithoutContext() {
-    $this->pluginDefinition['title'] = 'Example';
+  public function testGetTitle() {
+    $this->pluginDefinition['title'] = (new TranslationWrapper('Example'))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], array(), array())
+      ->with('Example', array(), array())
       ->will($this->returnValue('Example translated'));
 
     $this->setupLocalTaskDefault();
@@ -249,11 +248,12 @@ public function testGetTitleWithoutContext() {
    * @covers ::getTitle
    */
   public function testGetTitleWithContext() {
-    $this->pluginDefinition['title'] = 'Example';
-    $this->pluginDefinition['title_context'] = 'context';
+    $title = 'Example';
+    $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context')))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], array(), array('context' => 'context'))
+      ->with($title, array(), array('context' => 'context'))
       ->will($this->returnValue('Example translated with context'));
 
     $this->setupLocalTaskDefault();
@@ -264,16 +264,16 @@ public function testGetTitleWithContext() {
    * @covers ::getTitle
    */
   public function testGetTitleWithTitleArguments() {
-    $this->pluginDefinition['title'] = 'Example @test';
-    $this->pluginDefinition['title_arguments'] = array('@test' => 'value');
+    $title = 'Example @test';
+    $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value')))
+      ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
       ->method('translate')
-      ->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array())
+      ->with($title, array('@test' => 'value'), array())
       ->will($this->returnValue('Example value'));
 
     $this->setupLocalTaskDefault();
-    $request = new Request();
-    $this->assertEquals('Example value', $this->localTaskBase->getTitle($request));
+    $this->assertEquals('Example value', $this->localTaskBase->getTitle());
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php
index 3aa99e4..22aacc4 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php
@@ -7,8 +7,10 @@
 
 namespace Drupal\Tests\Core\Plugin\Discovery;
 
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\Tests\UnitTestCase;
 use Drupal\Core\Plugin\Discovery\YamlDiscovery;
+use org\bovigo\vfs\vfsStream;
 
 /**
  * @coversDefaultClass \Drupal\Core\Plugin\Discovery\YamlDiscovery
@@ -71,6 +73,44 @@ public function testGetDefinitions() {
   }
 
   /**
+   * @covers ::getDefinitions
+   */
+  public function testGetDefinitionsWithTranslatableDefinitions() {
+    vfsStream::setup('root');
+
+    $file_1 = <<<'EOS'
+test_plugin:
+  title: test title
+EOS;
+    $file_2 = <<<'EOS'
+test_plugin2:
+  title: test title2
+  title_context: 'test-context'
+EOS;
+    vfsStream::create([
+      'test_1' => [
+        'test_1.test.yml' => $file_1,
+      ],
+      'test_2' => [
+        'test_2.test.yml' => $file_2,
+      ]]
+    );
+
+    $discovery = new YamlDiscovery('test', ['test_1' => vfsStream::url('root/test_1'), 'test_2' => vfsStream::url('root/test_2')]);
+    $discovery->addTranslatableProperty('title', 'title_context');
+    $definitions = $discovery->getDefinitions();
+
+    $this->assertCount(2, $definitions);
+    $plugin_1 = $definitions['test_plugin'];
+    $plugin_2 = $definitions['test_plugin2'];
+
+    $this->assertInstanceOf(TranslationWrapper::class, $plugin_1['title']);
+    $this->assertEquals([], $plugin_1['title']->getOptions());
+    $this->assertInstanceOf(TranslationWrapper::class, $plugin_2['title']);
+    $this->assertEquals(['context' => 'test-context'], $plugin_2['title']->getOptions());
+  }
+
+  /**
    * Tests the getDefinition() method.
    */
   public function testGetDefinition() {
