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..5e633fc 100644
--- a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php
+++ b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php
@@ -118,7 +118,7 @@ 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 YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories(), ['title' => ['title_arguments', 'title_context']]);
       $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->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..129998e 100644
--- a/core/lib/Drupal/Core/Menu/LocalActionManager.php
+++ b/core/lib/Drupal/Core/Menu/LocalActionManager.php
@@ -142,7 +142,7 @@ 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 YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories(), ['title' => ['title_arguments', 'title_context']]);
       $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->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..7f26a4a 100644
--- a/core/lib/Drupal/Core/Menu/LocalTaskManager.php
+++ b/core/lib/Drupal/Core/Menu/LocalTaskManager.php
@@ -142,7 +142,7 @@ 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 YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories(), ['title' => ['title_arguments', 'title_context']]);
       $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->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..d4ed05e 100644
--- a/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php
+++ b/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php
@@ -10,9 +10,16 @@
 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 not coming from some other source of
+ * plugin definition.
  */
 class YamlDiscovery implements DiscoveryInterface {
 
@@ -26,6 +33,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
@@ -33,9 +49,21 @@ class YamlDiscovery implements DiscoveryInterface {
    *   'MODULE.test.yml'.
    * @param array $directories
    *   An array of directories to scan.
+   * @param array $translatable_properties
+   *   An array of properties whose values should be treated. For example, if
+   *   the values for the 'title' and 'description' keys in the YAML need
+   *   translation, and if the corresponding translation arguments and context
+   *   can be found in the 'title_arguments', 'title_context',
+   *   'description_arguments', and 'description_context' keys, then
+   *   $translation_properties should be set to:
+   *   [
+   *     'title' => ['title_arguments', 'title_context'],
+   *     'description' => ['description_arguments', 'description_context'],
+   *   ].
    */
-  function __construct($name, array $directories) {
+  function __construct($name, array $directories, array $translatable_properties = []) {
     $this->discovery = new ComponentYamlDiscovery($name, $directories);
+    $this->translatableProperties = $translatable_properties;
   }
 
   /**
@@ -48,6 +76,27 @@ public function getDefinitions() {
     $definitions = array();
     foreach ($plugins as $provider => $list) {
       foreach ($list as $id => $definition) {
+        // Add translation wrappers.
+        foreach ($this->translatableProperties as $property => list($arguments_property, $context_property)) {
+          if (isset($definition[$property])) {
+            $arguments = [];
+            $options = [];
+            // Move the t() arguments from the definition to the translation
+            // wrapper.
+            if (isset($definition[$arguments_property])) {
+              $arguments = $definition[$arguments_property];
+              unset($definition[$arguments_property]);
+            }
+            // Move the t() context from the definition to the translation
+            // wrapper.
+            if (isset($definition[$context_property])) {
+              $options['context'] = $definition[$context_property];
+              unset($definition[$context_property]);
+            }
+            $definition[$property] = new TranslationWrapper($definition[$property], $arguments, $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 e2c1a98..916b422 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
@@ -70,6 +70,16 @@ public function getUntranslatedString() {
   }
 
   /**
+   * Gets all arguments from this translation wrapper.
+   *
+   * @return string[]
+   *   The array of arguments.
+   */
+  public function getArguments() {
+    return $this->arguments;
+  }
+
+  /**
    * Gets a specific option from this translation wrapper.
    *
    * @param $name
@@ -83,6 +93,16 @@ public function getOption($name) {
   }
 
   /**
+   * Gets all options from this translation wrapper.
+   *
+   * @return array
+   *   The array of options.
+   */
+  public function getOptions() {
+    return $this->options;
+  }
+
+  /**
    * Implements the magic __toString() method.
    */
   public function __toString() {
@@ -90,6 +110,21 @@ public function __toString() {
   }
 
   /**
+   * Creates a clone of the object with updated arguments and options.
+   *
+   * @param string[] $arguments
+   *   (optional) An array with placeholder replacements, keyed by placeholder.
+   *   If NULL, the current arguments will be used.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslationWrapper
+   *   The new object.
+   */
+  public function withArguments(array $arguments = NULL) {
+    $arguments = isset($arguments) ? $arguments : $this->arguments;
+    return new static($this->string, $arguments, $this->options);
+  }
+
+  /**
    * Renders the object as a string.
    *
    * @return string
diff --git a/core/modules/comment/comment.links.task.yml b/core/modules/comment/comment.links.task.yml
index a976171..efa3b08 100644
--- a/core/modules/comment/comment.links.task.yml
+++ b/core/modules/comment/comment.links.task.yml
@@ -24,7 +24,7 @@ comment.admin_new:
   parent_id: comment.admin
 
 comment.admin_approval:
-  title: 'Unapproved comments'
+  title: 'Unapproved comments (@count)'
   route_name: comment.admin_approval
   class: Drupal\comment\Plugin\Menu\LocalTask\UnapprovedComments
   parent_id: comment.admin
diff --git a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php
index 0b8f6da..b2e7a64 100644
--- a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php
+++ b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php
@@ -10,6 +10,7 @@
 use Drupal\comment\CommentStorageInterface;
 use Drupal\Core\Menu\LocalTaskDefault;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -57,7 +58,11 @@ public static function create(ContainerInterface $container, array $configuratio
    * {@inheritdoc}
    */
   public function getTitle() {
-    return t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount()));
+    $title = $this->pluginDefinition['title'];
+    if ($title instanceof TranslationWrapper) {
+      $title = $title->withArguments(['@count' => $this->commentStorage->getUnapprovedCount()]);
+    }
+    return (string) $title;
   }
 
 }
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..75b82ed 100644
--- a/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php
+++ b/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Menu\ContextualLinkDefault;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 
 /**
  * Defines a contextual link plugin with a dynamic title.
@@ -26,17 +27,16 @@ class ConfigTranslationContextualLink extends ContextualLinkDefault {
    * {@inheritdoc}
    */
   public function getTitle() {
-    $options = array();
-    if (!empty($this->pluginDefinition['title_context'])) {
-      $options['context'] = $this->pluginDefinition['title_context'];
+    $title = $this->pluginDefinition['title'];
+    if ($title instanceof TranslationWrapper) {
+      // 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());
+      $title = $title->withArguments(array('@type_name' => $type_name));
     }
-
-    // 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 (string) $title;
   }
 
   /**
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..5fa071b 100644
--- a/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php
+++ b/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Menu\LocalTaskDefault;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 
 /**
  * Defines a local task plugin with a dynamic title.
@@ -26,17 +27,16 @@ 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);
+    $title = $this->pluginDefinition['title'];
+    if ($title instanceof TranslationWrapper) {
+      $title =  $title->withArguments(array('@type_name' => $type_name));
+    }
+    return (string) $title;
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Menu/MenuRouterTest.php b/core/modules/system/src/Tests/Menu/MenuRouterTest.php
index ef78c7b..5bf8fef 100644
--- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php
+++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php
@@ -72,6 +72,9 @@ 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->assertNoRaw("<script>alert('Welcome to the jungle!')</script>");
+    $this->assertRaw(htmlspecialchars("<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.task.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml
index c8cd284..081fed2 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
@@ -82,3 +82,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.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index 33a337b..62704ca 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:
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/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/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..9f51367 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,46 @@ public function testGetDefinitions() {
   }
 
   /**
+   * @covers ::getDefinitions
+   */
+  public function testGetDefinitionsWithTranslatableDefinitions() {
+    $root = vfsStream::setup('root');
+
+    $file_1 = <<<'EOS'
+test_plugin:
+  title: test title
+EOS;
+    $file_2 = <<<'EOS'
+test_plugin2:
+  title: test @count
+  title_arguments: { @count: 12 }
+  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')], ['title' => ['title_arguments', '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']->getArguments());
+    $this->assertEquals([], $plugin_1['title']->getOptions());
+    $this->assertInstanceOf(TranslationWrapper::class, $plugin_2['title']);
+    $this->assertEquals(['@count' => 12], $plugin_2['title']->getArguments());
+    $this->assertEquals(['context' => 'test-context'], $plugin_2['title']->getOptions());
+  }
+
+  /**
    * Tests the getDefinition() method.
    */
   public function testGetDefinition() {
