diff --git a/core/includes/common.inc b/core/includes/common.inc
index 43c5384..667918e 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -25,6 +25,7 @@
 use Drupal\Core\Render\SafeString;
 use Drupal\Core\Render\Renderer;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\Request;
@@ -316,7 +317,7 @@ function check_url($uri) {
  *   Optional language code to translate to a language other than what is used
  *   to display the page.
  *
- * @return
+ * @return \Drupal\Core\StringTranslation\TranslationWrapper
  *   A translated string representation of the size.
  */
 function format_size($size, $langcode = NULL) {
@@ -325,16 +326,7 @@ function format_size($size, $langcode = NULL) {
   }
   else {
     $size = $size / Bytes::KILOBYTE; // Convert bytes to kilobytes.
-    $units = array(
-      t('@size KB', array(), array('langcode' => $langcode)),
-      t('@size MB', array(), array('langcode' => $langcode)),
-      t('@size GB', array(), array('langcode' => $langcode)),
-      t('@size TB', array(), array('langcode' => $langcode)),
-      t('@size PB', array(), array('langcode' => $langcode)),
-      t('@size EB', array(), array('langcode' => $langcode)),
-      t('@size ZB', array(), array('langcode' => $langcode)),
-      t('@size YB', array(), array('langcode' => $langcode)),
-    );
+    $units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
     foreach ($units as $unit) {
       if (round($size, 2) >= Bytes::KILOBYTE) {
         $size = $size / Bytes::KILOBYTE;
@@ -343,7 +335,26 @@ function format_size($size, $langcode = NULL) {
         break;
       }
     }
-    return str_replace('@size', round($size, 2), $unit);
+    $args = ['@size' => round($size, 2)];
+    $options = ['langcode' => $langcode];
+    switch ($unit) {
+      case 'KB':
+        return new TranslationWrapper('@size KB', $args, $options);
+      case 'MB':
+        return new TranslationWrapper('@size MB', $args, $options);
+      case 'GB':
+        return new TranslationWrapper('@size GB', $args, $options);
+      case 'TB':
+        return new TranslationWrapper('@size TB', $args, $options);
+      case 'PB':
+        return new TranslationWrapper('@size PB', $args, $options);
+      case 'EB':
+        return new TranslationWrapper('@size EB', $args, $options);
+      case 'ZB':
+        return new TranslationWrapper('@size ZB', $args, $options);
+      case 'YB':
+        return new TranslationWrapper('@size YB', $args, $options);
+    }
   }
 }
 
diff --git a/core/lib/Drupal/Component/Gettext/PoItem.php b/core/lib/Drupal/Component/Gettext/PoItem.php
index c845c4b..f673752 100644
--- a/core/lib/Drupal/Component/Gettext/PoItem.php
+++ b/core/lib/Drupal/Component/Gettext/PoItem.php
@@ -193,7 +193,7 @@ public function setFromArray(array $values = array()) {
         strpos($this->_source, LOCALE_PLURAL_DELIMITER) !== FALSE) {
       $this->setSource(explode(LOCALE_PLURAL_DELIMITER, $this->_source));
       $this->setTranslation(explode(LOCALE_PLURAL_DELIMITER, $this->_translation));
-      $this->setPlural(count($this->_translation) > 1);
+      $this->setPlural(count($this->_source) > 1);
     }
   }
 
diff --git a/core/lib/Drupal/Component/Utility/PlaceholderTrait.php b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
new file mode 100644
index 0000000..27f7839
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Utility\PlaceholderTrait.
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Offers functionality for formatting strings using placeholders.
+ */
+trait PlaceholderTrait {
+
+  /**
+   * Formats a string by replacing variable placeholders.
+   *
+   * Helper method for SafeMarkup::format() and
+   * TranslationManager::renderTranslatedString().
+   *
+   * @param string $string
+   *   A string containing placeholders.
+   * @param array $args
+   *   An associative array of replacements to make.
+   * @param bool &$safe
+   *   A boolean indicating whether the string is safe or not (optional).
+   *
+   * @return string
+   *   The string with the placeholders replaced.
+   *
+   * @see \Drupal\Component\Utility::SafeMarkup::format()
+   * @see \Drupal\Core\StringTranslation::renderTranslatedString()
+   */
+  protected static function placeholderFormat($string, array $args, &$safe = TRUE) {
+    // Transform arguments before inserting them.
+    foreach ($args as $key => $value) {
+      switch ($key[0]) {
+        case '@':
+          // Escaped only.
+          if (!SafeMarkup::isSafe($value)) {
+            $args[$key] = Html::escape($value);
+          }
+          break;
+
+        case '%':
+        default:
+          // Escaped and placeholder.
+          if (!SafeMarkup::isSafe($value)) {
+            $value = Html::escape($value);
+          }
+          $args[$key] = '<em class="placeholder">' . $value . '</em>';
+          break;
+
+        case '!':
+          // Pass-through.
+          if (!SafeMarkup::isSafe($value)) {
+            $safe = FALSE;
+          }
+      }
+    }
+    return strtr($string, $args);
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index 3020bbe..00b09f2 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -31,6 +31,7 @@
  * @see theme_render
  */
 class SafeMarkup {
+  use PlaceholderTrait;
 
   /**
    * The list of safe strings.
@@ -204,40 +205,12 @@ public static function checkPlain($text) {
    */
   public static function format($string, array $args) {
     $safe = TRUE;
-
-    // Transform arguments before inserting them.
-    foreach ($args as $key => $value) {
-      switch ($key[0]) {
-        case '@':
-          // Escaped only.
-          if (!SafeMarkup::isSafe($value)) {
-            $args[$key] = Html::escape($value);
-          }
-          break;
-
-        case '%':
-        default:
-          // Escaped and placeholder.
-          if (!SafeMarkup::isSafe($value)) {
-            $value = Html::escape($value);
-          }
-          $args[$key] = '<em class="placeholder">' . $value . '</em>';
-          break;
-
-        case '!':
-          // Pass-through.
-          if (!static::isSafe($value)) {
-            $safe = FALSE;
-          }
-      }
-    }
-
-    $output = strtr($string, $args);
+    $output = static::placeholderFormat($string, $args, $safe);
     if ($safe) {
       static::$safeStrings[$output]['html'] = TRUE;
     }
-
     return $output;
+
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index dd3c8f0..9d749fb 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -945,9 +945,6 @@ public function getEntityTypeLabels($group = FALSE) {
 
     foreach ($definitions as $entity_type_id => $definition) {
       if ($group) {
-        // We cast the optgroup label to string as array keys must not be
-        // objects and t() may return a TranslationWrapper once issue #2557113
-        // lands.
         $options[(string) $definition->getGroupLabel()][$entity_type_id] = $definition->getLabel();
       }
       else {
@@ -963,8 +960,6 @@ public function getEntityTypeLabels($group = FALSE) {
 
       // Make sure that the 'Content' group is situated at the top.
       $content = $this->t('Content', array(), array('context' => 'Entity type group'));
-      // We cast the optgroup label to string as array keys must not be objects
-      // and t() may return a TranslationWrapper once issue #2557113 lands.
       $options = array((string) $content => $options[(string) $content]) + $options;
     }
 
diff --git a/core/lib/Drupal/Core/OutputFormatter/DynamicStringWrapperInterface.php b/core/lib/Drupal/Core/OutputFormatter/DynamicStringWrapperInterface.php
new file mode 100644
index 0000000..f3c1b7b
--- /dev/null
+++ b/core/lib/Drupal/Core/OutputFormatter/DynamicStringWrapperInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\OutputFormatter\DynamicStringWrapperInterface.
+ */
+
+namespace Drupal\Core\OutputFormatter;
+
+/**
+ * TODO
+ */
+interface DynamicStringWrapperInterface {
+
+  /**
+   * Gets the raw string pattern stored in this string wrapper.
+   *
+   * @return string
+   *   The string pattern stored in this wrapper with the placeholder values not
+   *   replaced.
+   */
+  public function getPattern();
+
+  /**
+   * Gets all arguments from this string wrapper.
+   *
+   * @return mixed[]
+   *   The array of arguments.
+   */
+  public function getArguments();
+
+  /**
+   * Renders the object as a string.
+   *
+   * @param string $content_type
+   *   The output content type to be used to format the string.
+   *
+   * @return string
+   *   The translated string.
+   */
+  public function render($content_type = NULL);
+
+}
diff --git a/core/lib/Drupal/Core/OutputFormatter/HtmlAttributeOutputFormatter.php b/core/lib/Drupal/Core/OutputFormatter/HtmlAttributeOutputFormatter.php
new file mode 100644
index 0000000..59af023
--- /dev/null
+++ b/core/lib/Drupal/Core/OutputFormatter/HtmlAttributeOutputFormatter.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\OutputFormatter\HtmlAttributeOutputFormatter.
+ */
+
+namespace Drupal\Core\OutputFormatter;
+
+/**
+ * HTML markup output formatter.
+ */
+class HtmlAttributeOutputFormatter extends HtmlMarkupOutputFormatter {
+
+  // TODO figure this out
+
+}
diff --git a/core/lib/Drupal/Core/OutputFormatter/HtmlMarkupOutputFormatter.php b/core/lib/Drupal/Core/OutputFormatter/HtmlMarkupOutputFormatter.php
new file mode 100644
index 0000000..6c8cd19
--- /dev/null
+++ b/core/lib/Drupal/Core/OutputFormatter/HtmlMarkupOutputFormatter.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\OutputFormatter\HtmlMarkupOutputFormatter.
+ */
+
+namespace Drupal\Core\OutputFormatter;
+
+use Drupal\Component\Utility\PlaceholderTrait;
+
+/**
+ * HTML markup output formatter.
+ */
+class HtmlMarkupOutputFormatter implements OutputFormatterInterface {
+
+  use PlaceholderTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function format(DynamicStringWrapperInterface $string) {
+    $output = $string->getPattern();
+    $args = $string->getArguments();
+    if (!empty($args)) {
+      $output = static::placeholderFormat($output, $args);
+    }
+    return $output;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/OutputFormatter/OutputFormatterInterface.php b/core/lib/Drupal/Core/OutputFormatter/OutputFormatterInterface.php
new file mode 100644
index 0000000..9bc7324
--- /dev/null
+++ b/core/lib/Drupal/Core/OutputFormatter/OutputFormatterInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\OutputFormatter\OutputFormatterInterface.
+ */
+
+namespace Drupal\Core\OutputFormatter;
+
+/**
+ * TODO
+ */
+interface OutputFormatterInterface {
+
+  /**
+   * TODO
+   */
+  public function format(DynamicStringWrapperInterface $string);
+
+}
diff --git a/core/lib/Drupal/Core/OutputFormatter/PlainTextOutputFormatter.php b/core/lib/Drupal/Core/OutputFormatter/PlainTextOutputFormatter.php
new file mode 100644
index 0000000..843c175
--- /dev/null
+++ b/core/lib/Drupal/Core/OutputFormatter/PlainTextOutputFormatter.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\OutputFormatter\PlainTextOutputFormatter.
+ */
+
+namespace Drupal\Core\OutputFormatter;
+
+/**
+ * Plain text output formatter.
+ */
+class PlainTextOutputFormatter implements OutputFormatterInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function format(DynamicStringWrapperInterface $string) {
+    $args = $string->getArguments();
+    return $args ? strtr($string->getPattern(), $args) : $string->getPattern();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php
index c3e2f68..b340765 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php
@@ -33,7 +33,7 @@
    *      what is used to display the page.
    *   - 'context': The context the source string belongs to.
    *
-   * @return string
+   * @return string|\Drupal\Core\StringTranslation\TranslationWrapper
    *   The translated string.
    *
    * @see \Drupal\Component\Utility\SafeMarkup::format()
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php
index af6aa2b..6faac6a 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\StringTranslation;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\PlaceholderTrait;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\StringTranslation\Translator\TranslatorInterface;
@@ -17,6 +18,8 @@
  */
 class TranslationManager implements TranslationInterface, TranslatorInterface {
 
+  use PlaceholderTrait;
+
   /**
    * The language manager.
    *
@@ -140,21 +143,43 @@ public function getStringTranslation($langcode, $string, $context) {
    * {@inheritdoc}
    */
   public function translate($string, array $args = array(), array $options = array()) {
-    $string = $this->doTranslate($string, $options);
-    if (empty($args)) {
-      // We add the string to the safe list as opposed to making it an object
-      // implementing SafeStringInterface as we may need to call __toString()
-      // on the object before render time, at which point the string ceases to
-      // be safe, and working around this would require significant rework.
-      // Adding this string to the safe list is assumed to be safe because
-      // translate() should only be called with strings defined in code.
-      // @see \Drupal\Core\StringTranslation\TranslationInterface::translate()
-      SafeMarkup::setMultiple([$string => ['html' => TRUE]]);
-      return $string;
+    $safe = TRUE;
+    foreach (array_keys($args) as $arg_key) {
+      // If the string has arguments that start with '!' we consider it unsafe
+      // and return the translation as a string for backward compatibility
+      // purposes.
+      // @todo https://www.drupal.org/node/2570037 remove this temporary
+      // workaround.
+      if (0 === strpos($arg_key, '!') && !SafeMarkup::isSafe($args[$arg_key])) {
+        $safe = FALSE;
+        break;
+      }
     }
-    else {
-      return SafeMarkup::format($string, $args);
+    $wrapper = new TranslationWrapper($string, $args, $options);
+    return $safe ? $wrapper : $wrapper->render();
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * TODO deprecate/remove this
+   */
+  public function renderTranslatedString(TranslationWrapper $translated_string) {
+    $value = $this->getTranslatedPattern($translated_string);
+    $args = $translated_string->getArguments();
+    if (!empty($args)) {
+      $value = $this->placeholderFormat($value, $args);
     }
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * TODO
+   */
+  public function getTranslatedPattern(TranslationWrapper $translated_string) {
+    return $this->doTranslate($translated_string->getUntranslatedString(), $translated_string->getOptions());
   }
 
   /**
@@ -172,13 +197,11 @@ public function translate($string, array $args = array(), array $options = array
    *   The translated string.
    */
   protected function doTranslate($string, array $options = array()) {
-    // Merge in defaults.
-    if (empty($options['langcode'])) {
-      $options['langcode'] = $this->defaultLangcode;
-    }
-    if (empty($options['context'])) {
-      $options['context'] = '';
-    }
+    // Merge in options defaults.
+    $options = $options + [
+      'langcode' => $this->defaultLangcode,
+      'context' => '',
+    ];
     $translation = $this->getStringTranslation($options['langcode'], $string, $options['context']);
     return $translation === FALSE ? $string : $translation;
   }
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
index 10e9b12..53abbf2 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
@@ -9,17 +9,23 @@
 
 use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Component\Utility\ToStringTrait;
+use Drupal\Core\OutputFormatter\DynamicStringWrapperInterface;
+use Drupal\Core\OutputFormatter\HtmlMarkupOutputFormatter;
+use Drupal\Core\OutputFormatter\PlainTextOutputFormatter;
 
 /**
- * Provides a class to wrap a translatable string.
+ * Provides translatable string class.
  *
- * This class can be used to delay translating strings until the translation
- * system is ready. This is useful for using translation in very low level
- * subsystems like entity definition and stream wrappers.
+ * This class delays translating strings until rendering them.
  *
+ * This is useful for using translation in very low level subsystems like entity
+ * definition and stream wrappers.
+ *
+ * @see \Drupal\Core\StringTranslation\TranslationManager::translate()
+ * @see \Drupal\Core\StringTranslation\TranslationManager::renderTranslatedString()
  * @see \Drupal\Core\Annotation\Translation
  */
-class TranslationWrapper implements SafeStringInterface {
+class TranslationWrapper implements SafeStringInterface, DynamicStringWrapperInterface {
 
   use StringTranslationTrait;
   use ToStringTrait;
@@ -75,6 +81,13 @@ public function getUntranslatedString() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function getPattern() {
+    return $this->getStringTranslation()->getTranslatedPattern($this);
+  }
+
+  /**
    * Gets a specific option from this translation wrapper.
    *
    * @param $name
@@ -96,14 +109,39 @@ public function getOption($name) {
   public function getOptions() {
     return $this->options;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getArguments() {
+    return $this->arguments;
+  }
+
   /**
-   * Renders the object as a string.
+   * {@inheritdoc}
+   */
+  public function render($content_type = NULL) {
+    return $this->string === '' ? '' : $this->getOutputFormatter($content_type)->format($this);
+  }
+
+  /**
+   * TODO
    *
-   * @return string
-   *   The translated string.
+   * This should be an injected factory, likely a plugin manager.
+   *
+   * @return \Drupal\Core\OutputFormatter\OutputFormatterInterface
    */
-  public function render() {
-    return $this->t($this->string, $this->arguments, $this->options);
+  protected function getOutputFormatter($content_type) {
+    switch ($content_type) {
+      case 'text/plain':
+        return new PlainTextOutputFormatter();
+
+      case 'text/html; x-drupal-context: attribute':
+        return NULL;
+
+      default:
+        return new HtmlMarkupOutputFormatter();
+    }
   }
 
   /**
@@ -123,5 +161,4 @@ public function jsonSerialize() {
     return $this->__toString();
   }
 
-
 }
diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php
index cc0d591..76d7c6a 100644
--- a/core/lib/Drupal/Core/Template/Attribute.php
+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -109,7 +109,8 @@ protected function createAttributeValue($name, $value) {
     elseif (is_bool($value)) {
       $value = new AttributeBoolean($name, $value);
     }
-    elseif (!is_object($value)) {
+    // As a development aid, we allow the value to be a safe string object.
+    elseif (!is_object($value) || $value instanceof SafeStringInterface) {
       $value = new AttributeString($name, $value);
     }
     return $value;
diff --git a/core/lib/Drupal/Core/Validation/DrupalTranslator.php b/core/lib/Drupal/Core/Validation/DrupalTranslator.php
index a2bbf5b..ba1a1f9 100644
--- a/core/lib/Drupal/Core/Validation/DrupalTranslator.php
+++ b/core/lib/Drupal/Core/Validation/DrupalTranslator.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Validation;
 
+use Drupal\Component\Utility\SafeStringInterface;
+
 /**
  * Translates strings using Drupal's translation system.
  *
@@ -73,8 +75,14 @@ public function getLocale() {
   protected function processParameters(array $parameters) {
     $return = array();
     foreach ($parameters as $key => $value) {
+      // We allow the values in the parameters to be safe string objects. This
+      // can be useful when we want to use parameter values that are
+      // TranslationWrappers.
+      if ($value instanceof SafeStringInterface) {
+        $value = (string) $value;
+      }
       if (is_object($value)) {
-        // t() does not work will objects being passed as replacement strings.
+        // t() does not work with objects being passed as replacement strings.
       }
       // Check for symfony replacement patterns in the form "{{ name }}".
       elseif (strpos($key, '{{ ') === 0 && strrpos($key, ' }}') == strlen($key) - 3) {
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
index d873820..6fa8771 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
@@ -16,6 +16,7 @@
 use Drupal\Core\TypedData\Type\StringInterface;
 use Drupal\Core\TypedData\Type\UriInterface;
 use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
+use Drupal\Component\Utility\SafeStringInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 
@@ -49,7 +50,7 @@ public function validate($value, Constraint $constraint) {
     if ($typed_data instanceof IntegerInterface && filter_var($value, FILTER_VALIDATE_INT) === FALSE) {
       $valid = FALSE;
     }
-    if ($typed_data instanceof StringInterface && !is_scalar($value)) {
+    if ($typed_data instanceof StringInterface && !is_scalar($value) && !($value instanceof SafeStringInterface)) {
       $valid = FALSE;
     }
     // Ensure that URIs comply with http://tools.ietf.org/html/rfc3986, which
diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc
index d5bb690..9120763 100644
--- a/core/modules/ckeditor/ckeditor.admin.inc
+++ b/core/modules/ckeditor/ckeditor.admin.inc
@@ -117,8 +117,6 @@ function template_preprocess_ckeditor_settings_toolbar(&$variables) {
   $variables['active_buttons'] = array();
   foreach ($active_buttons as $row_number => $button_row) {
     foreach ($button_groups[$row_number] as $group_name) {
-      // We cast the group name to string as array keys must not be objects
-      // and t() may return a TranslationWrapper once issue #2557113 lands.
       $group_name = (string) $group_name;
       $variables['active_buttons'][$row_number][$group_name] = array(
         'group_name_class' => Html::getClass($group_name),
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index edd20a0..8bc499f 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -299,8 +299,6 @@ function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInt
     $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'))) {
-    // We cast the optgroup label to string as array keys must not be objects
-    // and t() will return a TranslationWrapper once issue #2557113 lands.
     $optgroup = (string) t('General');
     // You cannot use comment fields on entity types with non-integer IDs.
     unset($form['add']['new_storage_type']['#options'][$optgroup]['comment']);
diff --git a/core/modules/entity_reference/entity_reference.module b/core/modules/entity_reference/entity_reference.module
index 908e0e5..ae4cc03 100644
--- a/core/modules/entity_reference/entity_reference.module
+++ b/core/modules/entity_reference/entity_reference.module
@@ -126,8 +126,6 @@ function entity_reference_field_config_presave(FieldConfigInterface $field) {
  * Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
  */
 function entity_reference_form_field_ui_field_storage_add_form_alter(array &$form) {
-  // We cast the optgroup label to string as array keys must not be objects
-  // and t() may return a TranslationWrapper once issue #2557113 lands.
   $optgroup = (string) t('Reference');
   // Move the "Entity reference" option to the end of the list and rename it to
   // "Other".
diff --git a/core/modules/language/src/Form/NegotiationBrowserForm.php b/core/modules/language/src/Form/NegotiationBrowserForm.php
index 111f35a..5fa186a 100644
--- a/core/modules/language/src/Form/NegotiationBrowserForm.php
+++ b/core/modules/language/src/Form/NegotiationBrowserForm.php
@@ -82,8 +82,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     }
     else {
       $language_options = array(
-        // We cast the optgroup labels to string as array keys must not be objects
-        // and t() may return a TranslationWrapper once issue #2557113 lands.
         (string) $this->t('Existing languages') => $existing_languages,
         (string) $this->t('Languages not yet added') => $this->languageManager->getStandardLanguageListWithoutConfigured(),
       );
diff --git a/core/modules/locale/src/Form/ImportForm.php b/core/modules/locale/src/Form/ImportForm.php
index 86aecc3..f3007f6 100644
--- a/core/modules/locale/src/Form/ImportForm.php
+++ b/core/modules/locale/src/Form/ImportForm.php
@@ -94,8 +94,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     else {
       $default = key($existing_languages);
       $language_options = array(
-        // We cast the optgroup labels to string as array keys must not be objects
-        // and t() may return a TranslationWrapper once issue #2557113 lands.
         (string) $this->t('Existing languages') => $existing_languages,
         (string) $this->t('Languages not yet added') => $this->languageManager->getStandardLanguageListWithoutConfigured(),
       );
diff --git a/core/modules/locale/src/Tests/LocaleTranslationUiTest.php b/core/modules/locale/src/Tests/LocaleTranslationUiTest.php
index 6c35b18..a6999b8 100644
--- a/core/modules/locale/src/Tests/LocaleTranslationUiTest.php
+++ b/core/modules/locale/src/Tests/LocaleTranslationUiTest.php
@@ -64,7 +64,7 @@ public function testStringTranslation() {
     );
     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
     // Add string.
-    t($name, array(), array('langcode' => $langcode));
+    t($name, array(), array('langcode' => $langcode))->render();
     // Reset locale cache.
     $this->container->get('string_translation')->reset();
     $this->assertRaw('"edit-languages-' . $langcode . '-weight"', 'Language code found.');
@@ -237,9 +237,10 @@ public function testJavaScriptTranslation() {
 
     // Retrieve the source string of the first string available in the
     // {locales_source} table and translate it.
-    $source = db_select('locales_source', 'l')
-      ->fields('l', array('source'))
-      ->condition('l.source', '%.js%', 'LIKE')
+    $query = db_select('locales_source', 's');
+    $query->addJoin('INNER', 'locales_location', 'l', 's.lid = l.lid');
+    $source = $query->fields('s', array('source'))
+      ->condition('l.type', 'javascript')
       ->range(0, 1)
       ->execute()
       ->fetchField();
@@ -302,7 +303,7 @@ public function testStringValidation() {
     );
     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
     // Add string.
-    t($name, array(), array('langcode' => $langcode));
+    t($name, array(), array('langcode' => $langcode))->render();
     // Reset locale cache.
     $search = array(
       'string' => $name,
@@ -361,7 +362,7 @@ public function testStringSearch() {
     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
 
     // Add string.
-    t($name, array(), array('langcode' => $langcode));
+    t($name, array(), array('langcode' => $langcode))->render();
     // Reset locale cache.
     $this->container->get('string_translation')->reset();
     $this->drupalLogout();
diff --git a/core/modules/system/src/Tests/Form/FormTest.php b/core/modules/system/src/Tests/Form/FormTest.php
index 83b9129..389d23e 100644
--- a/core/modules/system/src/Tests/Form/FormTest.php
+++ b/core/modules/system/src/Tests/Form/FormTest.php
@@ -144,7 +144,7 @@ function testRequiredFields() {
               // Select elements are going to have validation errors with empty
               // input, since those are illegal choices. Just make sure the
               // error is not "field is required".
-              $this->assertTrue((empty($errors[$element]) || strpos('field is required', $errors[$element]) === FALSE), "Optional '$type' field '$element' is not treated as a required element");
+              $this->assertTrue((empty($errors[$element]) || strpos('field is required', (string) $errors[$element]) === FALSE), "Optional '$type' field '$element' is not treated as a required element");
             }
             else {
               // Make sure there is *no* form error for this element.
diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php
index e435c0a..f079785 100644
--- a/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php
+++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php
@@ -78,7 +78,7 @@ public function __construct() {
       'label' => t('User name'),
       'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserNameBlock',
       'context' => array(
-        'user' => new ContextDefinition('entity:user', t('User')),
+        'user' => $this->createContextDefinition('entity:user', t('User')),
       ),
     ));
 
@@ -87,7 +87,7 @@ public function __construct() {
       'label' => t('User name optional'),
       'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserNameBlock',
       'context' => array(
-        'user' => new ContextDefinition('entity:user', t('User'), FALSE),
+        'user' => $this->createContextDefinition('entity:user', t('User'), FALSE),
       ),
     ));
 
@@ -102,8 +102,8 @@ public function __construct() {
       'label' => t('Complex context'),
       'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockComplexContextBlock',
       'context' => array(
-        'user' => new ContextDefinition('entity:user', t('User')),
-        'node' => new ContextDefinition('entity:node', t('Node')),
+        'user' => $this->createContextDefinition('entity:user', t('User')),
+        'node' => $this->createContextDefinition('entity:node', t('Node')),
       ),
     ));
 
@@ -118,4 +118,24 @@ public function __construct() {
     // specified), so we provide it the discovery object.
     $this->factory = new ReflectionFactory($this->discovery);
   }
+
+  /**
+   * Creates a new context definition with a label that is cast to string.
+   *
+   * @param string $data_type
+   *   The required data type.
+   * @param mixed string|null $label
+   *   The label of this context definition for the UI.
+   * @param bool $required
+   *   Whether the context definition is required.
+   *
+   * @return \Drupal\Core\Plugin\Context\ContextDefinition
+   */
+  protected function createContextDefinition($data_type, $label, $required = TRUE) {
+    // We cast the label to string for testing purposes only, as it may be
+    // a TranslationWrapper and we will do assertEqual() checks on arrays that
+    // include ContextDefinition objects, and var_export() has problems
+    // printing TranslationWrapper objects.
+    return new ContextDefinition($data_type, (string) $label, $required);
+  }
 }
diff --git a/core/modules/user/src/Tests/UserCancelTest.php b/core/modules/user/src/Tests/UserCancelTest.php
index 5357013..648f56f 100644
--- a/core/modules/user/src/Tests/UserCancelTest.php
+++ b/core/modules/user/src/Tests/UserCancelTest.php
@@ -535,7 +535,7 @@ function testMassUserCancelByAdmin() {
     $this->drupalPostForm(NULL, NULL, t('Cancel accounts'));
     $status = TRUE;
     foreach ($users as $account) {
-      $status = $status && (strpos($this->content, t('%name has been deleted.', array('%name' => $account->getUsername()))) !== FALSE);
+      $status = $status && (strpos($this->content,  $account->getUsername() . '</em> has been deleted.') !== FALSE);
       $user_storage->resetCache(array($account->id()));
       $status = $status && !$user_storage->load($account->id());
     }
diff --git a/core/modules/views/src/Plugin/views/PluginBase.php b/core/modules/views/src/Plugin/views/PluginBase.php
index 4dff979..3e567a5 100644
--- a/core/modules/views/src/Plugin/views/PluginBase.php
+++ b/core/modules/views/src/Plugin/views/PluginBase.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Plugin\PluginBase as ComponentPluginBase;
 use Drupal\Core\Render\Element;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\ViewExecutable;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -562,7 +563,14 @@ protected function listLanguages($flags = LanguageInterface::STATE_ALL, array $c
     // Since this is not a real language, surround it by '***LANGUAGE_...***',
     // like the negotiated languages below.
     if ($flags & LanguageInterface::STATE_SITE_DEFAULT) {
-      $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $this->t($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName());
+      $name = $languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName();
+      // The language name may have already been translated, no need to
+      // translate it again.
+      // @see Drupal\Core\Language::filterLanguages().
+      if (!$name instanceof TranslationWrapper) {
+        $name = $this->t($name);
+      }
+      $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $name;
       // Remove site default language from $languages so it's not added
       // twice with the real languages below.
       unset($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]);
diff --git a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php
index 2e4564e..1a97ee5 100644
--- a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php
+++ b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php
@@ -52,8 +52,6 @@ public function tokenForm(&$form, FormStateInterface $form_state) {
 
     // Get a list of the available fields and arguments for token replacement.
     $options = array();
-    // We cast the optgroup labels to string as array keys must not be objects
-    // and t() may return a TranslationWrapper once issue #2557113 lands.
     $optgroup_arguments = (string) t('Arguments');
     $optgroup_fields = (string) t('Fields');
     foreach ($this->view->display_handler->getHandlers('field') as $field => $handler) {
diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
index 1bb3394..76ae608 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -1725,9 +1725,6 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
         );
 
         $options = array();
-        // We cast the optgroup label to string as array keys must not be
-        // objects and t() may return a TranslationWrapper once issue #2557113
-        // lands.
         $optgroup_arguments = (string) t('Arguments');
         foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
           $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', array('@argument' => $handler->adminLabel()));
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index 7ed1033..c1ea93d 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -862,8 +862,6 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
 
       // Setup the tokens for fields.
       $previous = $this->getPreviousFieldLabels();
-      // We cast the optgroup labels to string as array keys must not be objects
-      // and t() may return a TranslationWrapper once issue #2557113 lands.
       $optgroup_arguments = (string) t('Arguments');
       $optgroup_fields = (string) t('Fields');
       foreach ($previous as $id => $label) {
diff --git a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php
index be8cca3..f06254c 100644
--- a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php
+++ b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php
@@ -45,9 +45,6 @@ public function testGet(array $values, $expected) {
     $options = isset($values['context']) ? array(
       'context' => $values['context'],
     ) : array();
-    $this->translationManager->expects($this->once())
-      ->method('translate')
-      ->with($values['value'], $arguments, $options);
 
     $annotation = new Translation($values);
 
diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php
index ac92088..8aae7ff 100644
--- a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php
@@ -74,8 +74,8 @@ public function testGetTitle() {
     $this->pluginDefinition['title'] = (new TranslationWrapper($title))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with($title, array(), array())
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->will($this->returnValue('Example translated'));
 
     $this->setupContextualLinkDefault();
@@ -90,8 +90,8 @@ public function testGetTitleWithContext() {
     $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context')))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with($title, array(), array('context' => 'context'))
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->will($this->returnValue('Example translated with context'));
 
     $this->setupContextualLinkDefault();
@@ -106,8 +106,8 @@ public function testGetTitleWithTitleArguments() {
     $this->pluginDefinition['title'] = (new TranslationWrapper($title, array('@test' => 'value')))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with($title, array('@test' => 'value'), array())
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->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 02d4578..0f68d81 100644
--- a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php
@@ -86,8 +86,8 @@ public function testGetTitle() {
     $this->pluginDefinition['title'] = (new TranslationWrapper('Example'))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with('Example', array(), array())
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->will($this->returnValue('Example translated'));
 
     $this->setupLocalActionDefault();
@@ -103,8 +103,8 @@ public function testGetTitleWithContext() {
     $this->pluginDefinition['title'] = (new TranslationWrapper('Example', array(), array('context' => 'context')))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with('Example', array(), array('context' => 'context'))
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->will($this->returnValue('Example translated with context'));
 
     $this->setupLocalActionDefault();
@@ -118,8 +118,8 @@ public function testGetTitleWithTitleArguments() {
     $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value')))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with('Example @test', array('@test' => 'value'), array())
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->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 d4032ec..64d9b5b 100644
--- a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php
@@ -236,8 +236,8 @@ public function testGetTitle() {
     $this->pluginDefinition['title'] = (new TranslationWrapper('Example'))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with('Example', array(), array())
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->will($this->returnValue('Example translated'));
 
     $this->setupLocalTaskDefault();
@@ -252,8 +252,8 @@ public function testGetTitleWithContext() {
     $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context')))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with($title, array(), array('context' => 'context'))
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->will($this->returnValue('Example translated with context'));
 
     $this->setupLocalTaskDefault();
@@ -264,12 +264,11 @@ public function testGetTitleWithContext() {
    * @covers ::getTitle
    */
   public function testGetTitleWithTitleArguments() {
-    $title = 'Example @test';
     $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value')))
       ->setStringTranslation($this->stringTranslation);
     $this->stringTranslation->expects($this->once())
-      ->method('translate')
-      ->with($title, array('@test' => 'value'), array())
+      ->method('renderTranslatedString')
+      ->with($this->pluginDefinition['title'])
       ->will($this->returnValue('Example value'));
 
     $this->setupLocalTaskDefault();
diff --git a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php
index 53165c9..6041e74 100644
--- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\StringTranslation {
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\StringTranslation\TranslationManager;
 use Drupal\Tests\UnitTestCase;
 
@@ -59,6 +60,45 @@ public function testFormatPlural($count, $singular, $plural, array $args = array
     $this->assertEquals(SafeMarkup::isSafe($result), $safe);
   }
 
+  /**
+   * Tests translation using placeholders.
+   *
+   * @param string $string
+   *   A string containing the English string to translate.
+   * @param array $args
+   *   An associative array of replacements to make after translation.
+   * @param string $expected_string
+   *   The expected translated string value.
+   * @param bool $returns_translation_wrapper
+   *   Whether we are expecting a TranslationWrapper object to be returned.
+   *
+   * @dataProvider providerTestTranslatePlaceholder
+   */
+  public function testTranslatePlaceholder($string, array $args = array(), $expected_string, $returns_translation_wrapper) {
+    $actual = $this->translationManager->translate($string, $args);
+    if ($returns_translation_wrapper) {
+      $this->assertInstanceOf('Drupal\Component\Utility\SafeStringInterface', $actual);
+      $actual->setStringTranslation($this->translationManager);
+    }
+    else {
+      $this->assertTrue(is_string($actual));
+    }
+    $this->assertEquals($expected_string, $actual);
+  }
+
+  /**
+   * Provides test data for translate().
+   *
+   * @return array
+   */
+  public function providerTestTranslatePlaceholder() {
+    return [
+      ['foo @bar', ['@bar' => 'bar'], 'foo bar', TRUE],
+      ['bar !baz', ['!baz' => 'baz'], 'bar baz', FALSE],
+      ['bar @bar !baz', ['@bar' => 'bar', '!baz' => 'baz'], 'bar bar baz', FALSE],
+      ['bar !baz @bar', ['!baz' => 'baz', '@bar' => 'bar'], 'bar baz bar', FALSE],
+    ];
+  }
 }
 
 class TestTranslationManager extends TranslationManager {
diff --git a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php
index 3facf38..227330f 100644
--- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php
+++ b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\StringTranslation;
 
 use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -66,7 +67,7 @@ public function testToString() {
       ->willReturn('');
 
     $translation = $this->prophesize(TranslationInterface::class);
-    $translation->translate($string, [], [])->will(function () {
+    $translation->renderTranslatedString($text)->will(function () {
       throw new \Exception('Yes you may.');
     });
     $text->setStringTranslation($translation->reveal());
diff --git a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php
index 2bcb318..0707fa3 100644
--- a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php
+++ b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php
@@ -16,6 +16,7 @@
 use Drupal\Core\TypedData\PrimitiveInterface;
 use Drupal\Core\Validation\Plugin\Validation\Constraint\PrimitiveTypeConstraint;
 use Drupal\Core\Validation\Plugin\Validation\Constraint\PrimitiveTypeConstraintValidator;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -63,6 +64,7 @@ public function provideTestValidate() {
     $data[] = [new IntegerData(DataDefinition::create('integer')), 1.5, FALSE];
     $data[] = [new IntegerData(DataDefinition::create('integer')), 'test', FALSE];
     $data[] = [new StringData(DataDefinition::create('string')), 'test', TRUE];
+    $data[] = [new StringData(DataDefinition::create('string')), new TranslationWrapper('test'), TRUE];
     // It is odd that 1 is a valid string.
     // $data[] = [$this->getMock('Drupal\Core\TypedData\Type\StringInterface'), 1, FALSE];
     $data[] = [new StringData(DataDefinition::create('string')), [], FALSE];
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index 3433c94..2b6e94f 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -12,6 +12,8 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Component\Utility\PlaceholderTrait;
+use Drupal\Core\StringTranslation\TranslationWrapper;
 
 /**
  * Provides a base class and helpers for Drupal unit tests.
@@ -20,6 +22,8 @@
  */
 abstract class UnitTestCase extends \PHPUnit_Framework_TestCase {
 
+  use PlaceholderTrait;
+
   /**
    * The random generator.
    *
@@ -214,7 +218,18 @@ public function getStringTranslationStub() {
     $translation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface');
     $translation->expects($this->any())
       ->method('translate')
-      ->will($this->returnCallback('Drupal\Component\Utility\SafeMarkup::format'));
+      ->willReturnCallback(function ($string, array $args = array(), array $options = array()) use ($translation) {
+        $wrapper = new TranslationWrapper($string, $args, $options);
+        $wrapper->setStringTranslation($translation);
+        // Pretend everything is not safe.
+        // @todo https://www.drupal.org/node/2570037 return the wrapper instead.
+        return (string) $wrapper;
+      });
+    $translation->expects($this->any())
+      ->method('renderTranslatedString')
+      ->willReturnCallback(function (TranslationWrapper $wrapper) {
+        return SafeMarkup::format($wrapper->getUntranslatedString(), $wrapper->getArguments());
+      });
     $translation->expects($this->any())
       ->method('formatPlural')
       ->willReturnCallback(function ($count, $singular, $plural, array $args = [], array $options = []) {
