diff --git a/core/lib/Drupal/Component/Utility/FormattableString.php b/core/lib/Drupal/Component/Utility/FormattableString.php
new file mode 100644
index 0000000..571b4b8
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/FormattableString.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Component\Utility\FormattableString.
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Formats a string for HTML display by replacing variable placeholders.
+ *
+ * When cast to a string it replaces variable placeholders in the string with
+ * the arguments passed in during construction and escapes the values so they
+ * can be safely displayed as HTML. It should be used on any unknown text that
+ * is intended to be printed to an HTML page (especially text that may have come
+ * from untrusted users, since in that case it prevents cross-site scripting and
+ * other security problems).
+ *
+ * This class is not intended for passing arbitrary user input into any HTML
+ * attribute value, as only URL attributes such as "src" and "href" are
+ * supported (using ":variable"). Never use this method on unsafe HTML
+ * attributes such as "on*" and "style" and take care when using this with
+ * unsupported attributes such as "title" or "alt" as this can lead to
+ * unexpected output.
+ *
+ * In most cases, you should use TranslatableString or PluralTranslatableString
+ * rather than this object, since it will translate the text (on
+ * non-English-only sites) in addition to formatting it.
+ *
+ * @ingroup sanitization
+ *
+ * @see \Drupal\Core\StringTranslation\TranslatableString
+ * @see \Drupal\Core\StringTranslation\PluralTranslatableString
+ */
+class FormattableString implements SafeStringInterface {
+
+  use PlaceholderTrait;
+
+  /**
+   * The arguments to replace placeholders with.
+   *
+   * @var array
+   */
+  protected $arguments = [];
+
+  /**
+   * Creates a FormattableString object if necessary.
+   *
+   * If $string is equal to a blank string then it is not necessary to create a
+   * SafeString object.
+   *
+   * @param string $string
+   *   A string containing placeholders. The string itself will not be escaped,
+   *   any unsafe content must be in $args and inserted via placeholders.
+   * @param array $args
+   *   An array with placeholder replacements, keyed by placeholder. See
+   *   \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
+   *   additional information about placeholders.
+   *
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   A formattable string.
+   *
+   * @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
+   */
+  public function __construct($string, array $arguments) {
+    $this->string = (string) $string;
+    $this->arguments = $arguments;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    return static::placeholderFormat($this->string, $this->arguments);
+  }
+
+  /**
+   * Returns the string length.
+   *
+   * @return int
+   *   The length of the string.
+   */
+  public function count() {
+    return Unicode::strlen($this->string);
+  }
+
+  /**
+   * Returns a representation of the object for use in JSON serialization.
+   *
+   * @return string
+   *   The safe string content.
+   */
+  public function jsonSerialize() {
+    return $this->__toString();
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Utility/PlaceholderTrait.php b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
index 5922b3f..4fe1ba8 100644
--- a/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
+++ b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
@@ -15,18 +15,62 @@
   /**
    * Formats a string by replacing variable placeholders.
    *
+   * This trait is not intended for passing arbitrary user input into any HTML
+   * attribute value, as only URL attributes such as "src" and "href" are
+   * supported (using ":variable"). Never use this method on unsafe HTML
+   * attributes such as "on*" and "style" and take care when using this with
+   * unsupported attributes such as "title" or "alt" as this can lead to
+   * unexpected output.
+   *
    * @param string $string
    *   A string containing placeholders.
    * @param array $args
-   *   An associative array of replacements to make.
+   *   An associative array of replacements to make. Occurrences in $string of
+   *   any key in $args are replaced with the corresponding value, after
+   *   optional sanitization and formatting. The type of sanitization and
+   *   formatting depends on the first character of the key:
+   *   - @variable: Escaped to HTML using Html::escape() unless the value is
+   *     already HTML-safe. Use this as the default choice for anything
+   *     displayed on a page on the site, but not within HTML attributes.
+   *   - %variable: Escaped to HTML just like @variable, but also wrapped in
+   *     <em> tags, which makes the following HTML code:
+   *     @code
+   *       <em class="placeholder">text output here.</em>
+   *     @endcode
+   *     As with @variable, do not use this within HTML attributes.
+   *   - :variable: Escaped to HTML using Html::escape() and filtered for
+   *     dangerous protocols using UrlHelper::stripDangerousProtocols(). Use
+   *     this when passing in a URL, such as when using the "src" or "href"
+   *     attributes, ensuring the value is always wrapped in quotes:
+   *     - Secure: <a href=":variable">@variable</a>
+   *     - Insecure: <a href=:variable>@variable</a>
+   *     When ":variable" comes from arbitrary user input, the result is secure,
+   *     but not guaranteed to be a valid URL (which means the resulting output
+   *     could fail HTML validation). To guarantee a valid URL, use
+   *     Url::fromUri($user_input)->toString() (which either throws an exception
+   *     or returns a well-formed URL) before passing the result into a
+   *     ":variable" placeholder.
+   *   - !variable: Inserted as is, with no sanitization or formatting. Only
+   *     use this when the resulting string is being generated for one of:
+   *     - Non-HTML usage, such as a plain-text email.
+   *     - Non-direct HTML output, such as a plain-text variable that will be
+   *       printed as an HTML attribute value and therefore formatted with
+   *       self::checkPlain() as part of that.
+   *     - Some other special reason for suppressing sanitization.
    * @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\TranslatableString::render()
+   * @ingroup sanitization
+   *
+   * @see \Drupal\Component\Utility\FormattableString
+   * @see \Drupal\Core\StringTranslation\TranslatableString
+   * @see \Drupal\Core\StringTranslation\PluralTranslatableString
+   * @see \Drupal\Component\Utility\Html::escape()
+   * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
+   * @see \Drupal\Core\Url::fromUri()
    */
   protected static function placeholderFormat($string, array $args, &$safe = TRUE) {
     // Transform arguments before inserting them.
diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index c1c86f5..a8c9576 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -162,79 +162,41 @@ public static function checkPlain($text) {
   /**
    * Formats a string for HTML display by replacing variable placeholders.
    *
-   * This method replaces variable placeholders in a string with the requested
-   * values and escapes the values so they can be safely displayed as HTML. It
-   * should be used on any unknown text that is intended to be printed to an
-   * HTML page (especially text that may have come from untrusted users, since
-   * in that case it prevents cross-site scripting and other security problems).
-   *
-   * This method is not intended for passing arbitrary user input into any
-   * HTML attribute value, as only URL attributes such as "src" and "href" are
-   * supported (using ":variable"). Never use this method on unsafe HTML
-   * attributes such as "on*" and "style" and take care when using this with
-   * unsupported attributes such as "title" or "alt" as this can lead to
-   * unexpected output.
-   *
-   * In most cases, you should use t() rather than calling this function
-   * directly, since it will translate the text (on non-English-only sites) in
-   * addition to formatting it.
-   *
    * @param string $string
-   *   A string containing placeholders. The string itself is not escaped, any
-   *   unsafe content must be in $args and inserted via placeholders.
+   *   A string containing placeholders. The string itself will not be escaped,
+   *   any unsafe content must be in $args and inserted via placeholders.
    * @param array $args
-   *   An associative array of replacements to make. Occurrences in $string of
-   *   any key in $args are replaced with the corresponding value, after
-   *   optional sanitization and formatting. The type of sanitization and
-   *   formatting depends on the first character of the key:
-   *   - @variable: Escaped to HTML using Html::escape() unless the value is
-   *     already HTML-safe. Use this as the default choice for anything
-   *     displayed on a page on the site, but not within HTML attributes.
-   *   - %variable: Escaped to HTML just like @variable, but also wrapped in
-   *     <em> tags, which makes the following HTML code:
-   *     @code
-   *       <em class="placeholder">text output here.</em>
-   *     @endcode
-   *     As with @variable, do not use this within HTML attributes.
-   *   - :variable: Escaped to HTML using Html::escape() and filtered for
-   *     dangerous protocols using UrlHelper::stripDangerousProtocols(). Use
-   *     this when passing in a URL, such as when using the "src" or "href"
-   *     attributes, ensuring the value is always wrapped in quotes:
-   *     - Secure: <a href=":variable">@variable</a>
-   *     - Insecure: <a href=:variable>@variable</a>
-   *     When ":variable" comes from arbitrary user input, the result is secure,
-   *     but not guaranteed to be a valid URL (which means the resulting output
-   *     could fail HTML validation). To guarantee a valid URL, use
-   *     Url::fromUri($user_input)->toString() (which either throws an exception
-   *     or returns a well-formed URL) before passing the result into a
-   *     ":variable" placeholder.
-   *   - !variable: Inserted as is, with no sanitization or formatting. Only
-   *     use this when the resulting string is being generated for one of:
-   *     - Non-HTML usage, such as a plain-text email.
-   *     - Non-direct HTML output, such as a plain-text variable that will be
-   *       printed as an HTML attribute value and therefore formatted with
-   *       self::checkPlain() as part of that.
-   *     - Some other special reason for suppressing sanitization.
+   *   An array with placeholder replacements, keyed by placeholder. See
+   *   \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
+   *   additional information about placeholders.
    *
-   * @return string
-   *   The formatted string, which is marked as safe unless sanitization of an
-   *   unsafe argument was suppressed (see above).
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   The formatted string, which is an instance of SafeStringInterface unless
+   *   sanitization of an unsafe argument was suppressed (see above).
    *
    * @ingroup sanitization
    *
-   * @see t()
-   * @see \Drupal\Component\Utility\Html::escape()
-   * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
-   * @see \Drupal\Core\Url::fromUri()
+   * @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
+   * @see \Drupal\Component\Utility\FormattableString
+   *
+   * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
+   *   Use \Drupal\Component\Utility\FormattableString.
    */
   public static function format($string, array $args) {
+    // If the string has arguments that start with '!' we consider it unsafe
+    // and return a string instead of an object for backward compatibility
+    // purposes.
+    // @todo https://www.drupal.org/node/2571695 remove this temporary
+    //   workaround.
     $safe = TRUE;
-    $output = static::placeholderFormat($string, $args, $safe);
-    if ($safe) {
-      static::$safeStrings[$output]['html'] = TRUE;
+    foreach ($args as $key => $value) {
+      if ($key[0] == '!' && !static::isSafe($value)) {
+        $safe = FALSE;
+      }
     }
-    return $output;
+    $safe_string = new FormattableString($string, $args);
 
+    return $safe ? $safe_string : (string) $safe_string;
   }
 
 }
diff --git a/core/lib/Drupal/Core/StringTranslation/PluralTranslatableString.php b/core/lib/Drupal/Core/StringTranslation/PluralTranslatableString.php
index f29d6e7..b9e79e5 100644
--- a/core/lib/Drupal/Core/StringTranslation/PluralTranslatableString.php
+++ b/core/lib/Drupal/Core/StringTranslation/PluralTranslatableString.php
@@ -63,18 +63,18 @@ class PluralTranslatableString extends TranslatableString {
    *   ease translation. Use @count in place of the item count, as in
    *   "@count new comments".
    * @param array $args
-   *   (optional) An associative array of replacements to make after
-   *   translation. Instances of any key in this array are replaced with the
-   *   corresponding value. Based on the first character of the key, the value
-   *   is escaped and/or themed. See
-   *   \Drupal\Component\Utility\SafeMarkup::format(). Note that you do not need
-   *   to include @count in this array; this replacement is done automatically
+   *   (optional) An array with placeholder replacements, keyed by placeholder.
+   *   See \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
+   *   additional information about placeholders. Note that you do not need to
+   *   include @count in this array; this replacement is done automatically
    *   for the plural cases.
    * @param array $options
    *   (optional) An associative array of additional options. See t() for
    *   allowed keys.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    *   (optional) The string translation service.
+   *
+   * @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
    */
   public function __construct($count, $singular, $plural, array $args = [], array $options = [], TranslationInterface $string_translation = NULL) {
     $this->count = $count;
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslatableString.php b/core/lib/Drupal/Core/StringTranslation/TranslatableString.php
index 92345b7..0dbb3a6 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslatableString.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslatableString.php
@@ -73,10 +73,14 @@ class TranslatableString implements SafeStringInterface {
    *   The string that is to be translated.
    * @param array $arguments
    *   (optional) An array with placeholder replacements, keyed by placeholder.
+   *   See \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
+   *   additional information about placeholders.
    * @param array $options
    *   (optional) An array of additional options.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    *   (optional) The string translation service.
+   *
+   * @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
    */
   public function __construct($string, array $arguments = array(), array $options = array(), TranslationInterface $string_translation = NULL) {
     $this->string = $string;
diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php
index 3629cda..a2f324d 100644
--- a/core/lib/Drupal/Core/Template/Attribute.php
+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -111,11 +111,15 @@ protected function createAttributeValue($name, $value) {
     }
     // An array value or 'class' attribute name are forced to always be an
     // AttributeArray value for consistency.
-    if (is_array($value) || $name == 'class') {
+    if ($name == 'class' && !is_array($value)) {
+      // Cast the value to string in case it implements SafeStringInterface.
+      $value = [(string) $value];
+    }
+    if (is_array($value)) {
       // Cast the value to an array if the value was passed in as a string.
       // @todo Decide to fix all the broken instances of class as a string
       // in core or cast them.
-      $value = new AttributeArray($name, (array) $value);
+      $value = new AttributeArray($name, $value);
     }
     elseif (is_bool($value)) {
       $value = new AttributeBoolean($name, $value);
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index 641ac89..afbffcc 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -135,7 +135,7 @@ public function challengeException(Request $request, \Exception $previous) {
     $challenge = SafeMarkup::format('Basic realm="@realm"', array(
       '@realm' => !empty($site_name) ? $site_name : 'Access restricted',
     ));
-    return new UnauthorizedHttpException($challenge, 'No authentication credentials provided.', $previous);
+    return new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Component/Utility/FormattableStringTest.php b/core/tests/Drupal/Tests/Component/Utility/FormattableStringTest.php
new file mode 100644
index 0000000..670b6e5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Utility/FormattableStringTest.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Component\Utility\FormattableStringTest.
+ */
+
+namespace Drupal\Tests\Component\Utility;
+
+use Drupal\Component\Utility\FormattableString;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the TranslatableString class.
+ *
+ * @coversDefaultClass \Drupal\Component\Utility\FormattableString
+ * @group utility
+ */
+class FormattableStringTest extends UnitTestCase {
+
+  /**
+   * @covers ::__toString
+   * @covers ::jsonSerialize
+   */
+  public function testToString() {
+    $string = 'Can I please have a @replacement';
+    $formattable_string = new FormattableString($string, ['@replacement' => 'kitten']);
+    $text = (string) $formattable_string;
+    $this->assertEquals('Can I please have a kitten', $text);
+    $text = $formattable_string->jsonSerialize();
+    $this->assertEquals('Can I please have a kitten', $text);
+  }
+
+  /**
+   * @covers ::count
+   */
+  public function testCount() {
+    $string = 'Can I please have a @replacement';
+    $formattable_string = new FormattableString($string, ['@replacement' => 'kitten']);
+    $this->assertEquals(strlen($string), $formattable_string->count());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php b/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php
index 1cef375..156a125 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php
@@ -427,9 +427,9 @@ public function testSetCacheAuthUser() {
    * @covers ::setCache
    */
   public function testSetCacheWithSafeStrings() {
-    // A call to SafeMarkup::format() is appropriate in this test as a way to
-    // add a string to the safe list in the simplest way possible.
-    SafeMarkup::format('@value', ['@value' => 'a_safe_string']);
+    SafeMarkup::setMultiple([
+      'a_safe_string' => ['html' => TRUE],
+    ]);
     $form_build_id = 'the_form_build_id';
     $form = [
       '#form_id' => 'the_form_id'
diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
index b4a192d..9ad792e 100644
--- a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
+++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Template\AttributeArray;
 use Drupal\Core\Template\AttributeString;
 use Drupal\Tests\UnitTestCase;
+use Drupal\Component\Utility\SafeStringInterface;
 
 /**
  * @coversDefaultClass \Drupal\Core\Template\Attribute
@@ -30,6 +31,18 @@ public function testConstructor() {
     $attribute = new Attribute(['selected' => TRUE, 'checked' => FALSE]);
     $this->assertTrue($attribute['selected']->value());
     $this->assertFalse($attribute['checked']->value());
+
+    // Test that non-array values with name "class" are cast to array.
+    $attribute = new Attribute(array('class' => 'example-class'));
+    $this->assertTrue(isset($attribute['class']));
+    $this->assertEquals(new AttributeArray('class', array('example-class')), $attribute['class']);
+
+    // Test that safe string objects work correctly.
+    $safe_string = $this->prophesize(SafeStringInterface::class);
+    $safe_string->__toString()->willReturn('example-class');
+    $attribute = new Attribute(array('class' => $safe_string->reveal()));
+    $this->assertTrue(isset($attribute['class']));
+    $this->assertEquals(new AttributeArray('class', array('example-class')), $attribute['class']);
   }
 
   /**
