diff --git a/core/lib/Drupal/Component/Utility/FormattableString.php b/core/lib/Drupal/Component/Utility/FormattableString.php
new file mode 100644
index 0000000..971b5b5
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/FormattableString.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Component\Utility\FormattableString.
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Provides a formattable string class.
+ */
+class FormattableString implements SafeStringInterface {
+
+  use PlaceholderTrait;
+  use ToStringTrait;
+
+  /**
+   * The safe string.
+   *
+   * @var string
+   */
+  protected $string;
+
+  /**
+   * The arguments to replace placeholders with.
+   *
+   * @var array
+   */
+  protected $arguments = [];
+
+  /**
+   * Constructs a new class instance.
+   *
+   * @param string $string
+   *   The string that is to be translated.
+   * @param array $arguments
+   *   An array with placeholder replacements.
+   */
+  public function __construct($string, array $arguments) {
+    $this->string = $string;
+    $this->arguments = $arguments;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    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/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index c1c86f5..cc3c28b 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -216,9 +216,9 @@ public static function checkPlain($text) {
    *       self::checkPlain() as part of that.
    *     - Some other special reason for suppressing sanitization.
    *
-   * @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 a SafeMarkup object unless sanitization of
+   *   an unsafe argument was suppressed (see above).
    *
    * @ingroup sanitization
    *
@@ -228,13 +228,22 @@ public static function checkPlain($text) {
    * @see \Drupal\Core\Url::fromUri()
    */
   public static function format($string, array $args) {
+    assert(is_string($string), '$string ' . (string) $string . ' is not a string.');
+    assert($string != '', '$string is empty.');
+    // 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/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/modules/comment/comment.module b/core/modules/comment/comment.module
index 825cfc4..dae5696 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -568,7 +568,7 @@ function comment_preview(CommentInterface $comment, FormStateInterface $form_sta
     // Attach the user and time information.
     $author_name = $comment->getAuthorName();
     if (!empty($author_name)) {
-      $account = user_load_by_name($author_name);
+      $account = $comment->getOwner();
     }
     elseif (\Drupal::currentUser()->isAuthenticated() && empty($comment->is_anonymous)) {
       $account = \Drupal::currentUser();
diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php
index c96202b..93459e1 100644
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php
@@ -152,29 +152,23 @@ public function form(array $form, FormStateInterface $form_state) {
       '#default_value' => $author,
       '#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
       '#maxlength' => 60,
+      '#access' => $this->currentUser->isAnonymous() || (!$comment->getOwnerId() && $is_admin),
       '#size' => 30,
+      '#attributes'=> [
+        'data-drupal-default-value' => $config->get('anonymous')
+      ],
     );
-    if ($is_admin) {
-      $form['author']['name']['#type'] = 'entity_autocomplete';
-      $form['author']['name']['#target_type'] = 'user';
-      $form['author']['name']['#selection_settings'] = ['include_anonymous' => FALSE];
-      $form['author']['name']['#process_default_value'] = FALSE;
-      // The user name is validated and processed in static::buildEntity() and
-      // static::validate().
-      $form['author']['name']['#element_validate'] = array();
-      $form['author']['name']['#title'] = $this->t('Authored by');
-      $form['author']['name']['#description'] = $this->t('Leave blank for %anonymous.', array('%anonymous' => $config->get('anonymous')));
-    }
-    elseif ($this->currentUser->isAuthenticated()) {
-      $form['author']['name']['#type'] = 'item';
-      $form['author']['name']['#value'] = $form['author']['name']['#default_value'];
-      $form['author']['name']['#theme'] = 'username';
-      $form['author']['name']['#account'] = $this->currentUser;
-      $form['author']['name']['#cache']['contexts'][] = 'user';
-    }
-    elseif($this->currentUser->isAnonymous()) {
-      $form['author']['name']['#attributes']['data-drupal-default-value'] = $config->get('anonymous');
-    }
+
+    $owner = $comment->getOwner();
+    $form['author']['admin_select'] = [
+      '#type' => 'entity_autocomplete',
+      '#target_type' => 'user',
+      '#default_value' => $owner->isAnonymous() ? NULL : $owner,
+      '#selection_settings' => ['include_anonymous' => TRUE],
+      '#title' => $this->t('Authored by'),
+      '#description' => $this->t('Leave blank for %anonymous.', array('%anonymous' => $config->get('anonymous'))),
+      '#access' => $is_admin,
+    ];
 
     // Add author email and homepage fields depending on the current user.
     $form['author']['mail'] = array(
@@ -263,16 +257,27 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
     else {
       $comment->setCreatedTime(REQUEST_TIME);
     }
+    // Empty author-ID should revert to anonymous.
+    $author_id = $form_state->getValue('admin_select');
     $author_name = $form_state->getValue('name');
-
-    if (!$this->currentUser->isAnonymous()) {
-      // Assign the owner based on the given user name - none means anonymous.
-      $accounts = $this->entityManager->getStorage('user')
-        ->loadByProperties(array('name' => $author_name));
-      $account = reset($accounts);
-      $uid = $account ? $account->id() : 0;
-      $comment->setOwnerId($uid);
+    if ($comment->id() && $this->currentUser->hasPermission('administer comments')) {
+      // Admin can leave the author-ID blank to revert to anonymous.
+      $author_id = $author_id ?: 0;
+    }
+    if (!is_null($author_id)) {
+      $account = $this->entityManager->getStorage('user')->load($author_id);
+      if ($author_id || !$form['author']['name']['#access']) {
+        $comment->setAuthorName($account->getUserName());
+      }
+      else {
+        // Anonymous user - use the author name value.
+        $comment->setAuthorName($author_name);
+      }
+    }
+    else {
+      $author_id = $this->currentUser->id();
     }
+    $comment->setOwnerId($author_id);
 
     // If the comment was posted by an anonymous user and no author name was
     // required, use "Anonymous" by default.
diff --git a/core/modules/comment/src/Tests/CommentAnonymousTest.php b/core/modules/comment/src/Tests/CommentAnonymousTest.php
index fd813f7..96d01d8 100644
--- a/core/modules/comment/src/Tests/CommentAnonymousTest.php
+++ b/core/modules/comment/src/Tests/CommentAnonymousTest.php
@@ -100,7 +100,7 @@ function testAnonymous() {
     // Make sure the user data appears correctly when editing the comment.
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('comment/' . $anonymous_comment3->id() . '/edit');
-    $this->assertRaw($author_name, "The anonymous user's name is correct when editing the comment.");
+    $this->assertFieldByName('admin_select', '', "The author field is empty (i.e. anonymous) when editing the comment.");
     $this->assertRaw($author_mail, "The anonymous user's email address is correct when editing the comment.");
 
     // Unpublish comment.
diff --git a/core/modules/comment/src/Tests/CommentInterfaceTest.php b/core/modules/comment/src/Tests/CommentInterfaceTest.php
index 1136811..2795628 100644
--- a/core/modules/comment/src/Tests/CommentInterfaceTest.php
+++ b/core/modules/comment/src/Tests/CommentInterfaceTest.php
@@ -83,7 +83,7 @@ public function testCommentInterface() {
     )));
 
     // Test changing the comment author to "Anonymous".
-    $comment = $this->postComment(NULL, $comment->comment_body->value, $comment->getSubject(), array('name' => ''));
+    $comment = $this->postComment(NULL, $comment->comment_body->value, $comment->getSubject(), array('admin_select' => ''));
     $this->assertTrue($comment->getAuthorName() == t('Anonymous') && $comment->getOwnerId() == 0, 'Comment author successfully changed to anonymous.');
 
     // Test changing the comment author to an unverified user.
@@ -95,7 +95,7 @@ public function testCommentInterface() {
 
     // Test changing the comment author to a verified user.
     $this->drupalGet('comment/' . $comment->id() . '/edit');
-    $comment = $this->postComment(NULL, $comment->comment_body->value, $comment->getSubject(), array('name' => $this->webUser->getUsername()));
+    $comment = $this->postComment(NULL, $comment->comment_body->value, $comment->getSubject(), array('admin_select' => $this->webUser->getUsername() . ' (' . $this->webUser->id() . ')'));
     $this->assertTrue($comment->getAuthorName() == $this->webUser->getUsername() && $comment->getOwnerId() == $this->webUser->id(), 'Comment author successfully changed to a registered user.');
 
     $this->drupalLogout();
diff --git a/core/modules/comment/src/Tests/CommentPreviewTest.php b/core/modules/comment/src/Tests/CommentPreviewTest.php
index 644f89b..0b3d609 100644
--- a/core/modules/comment/src/Tests/CommentPreviewTest.php
+++ b/core/modules/comment/src/Tests/CommentPreviewTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\comment\Tests;
 
 use Drupal\comment\CommentManagerInterface;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\comment\Entity\Comment;
@@ -50,13 +51,13 @@ function testCommentPreview() {
     $edit['subject[0][value]'] = $this->randomMachineName(8);
     $edit['comment_body[0][value]'] = $this->randomMachineName(16);
     $this->drupalPostForm('node/' . $this->node->id(), $edit, t('Preview'));
-    $this->assertEscaped('<em>' . $this->webUser->id() . '</em>');
+    $this->assertEscaped('<em>' . $this->webUser-getUsername() . '</em>');
 
     \Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
     $this->drupalPostForm('node/' . $this->node->id(), $edit, t('Preview'));
     $this->assertTrue(SafeMarkup::isSafe($this->webUser->getDisplayName()), 'Username is marked safe');
     $this->assertNoEscaped('<em>' . $this->webUser->id() . '</em>');
-    $this->assertRaw('<em>' . $this->webUser->id() . '</em>');
+    $this->assertRaw('<em>' . $this->webUser->getUsername() . '</em>');
 
     // Add a user picture.
     $image = current($this->drupalGetTestFiles('image'));
@@ -140,7 +141,7 @@ function testCommentEditPreviewSave() {
     $date = new DrupalDateTime('2008-03-02 17:23');
     $edit['subject[0][value]'] = $this->randomMachineName(8);
     $edit['comment_body[0][value]'] = $this->randomMachineName(16);
-    $edit['name'] = $web_user->getUsername();
+    $edit['admin_select'] = $web_user->getAccountName() . ' (' . $web_user->id() . ')';
     $edit['date[date]'] = $date->format('Y-m-d');
     $edit['date[time]'] = $date->format('H:i:s');
     $raw_date = $date->getTimestamp();
@@ -154,13 +155,16 @@ function testCommentEditPreviewSave() {
     $this->assertTitle(t('Preview comment | Drupal'), 'Page title is "Preview comment".');
     $this->assertText($edit['subject[0][value]'], 'Subject displayed.');
     $this->assertText($edit['comment_body[0][value]'], 'Comment displayed.');
-    $this->assertText($edit['name'], 'Author displayed.');
+    $this->assertText($web_user->getAccountName(), 'Author displayed.');
     $this->assertText($expected_text_date, 'Date displayed.');
 
+    // Disable altering of usernames.
+    \Drupal::state()->set('user_hooks_test_user_format_name_alter', FALSE);
+
     // Check that the subject, comment, author and date fields are displayed with the correct values.
     $this->assertFieldByName('subject[0][value]', $edit['subject[0][value]'], 'Subject field displayed.');
     $this->assertFieldByName('comment_body[0][value]', $edit['comment_body[0][value]'], 'Comment field displayed.');
-    $this->assertFieldByName('name', $edit['name'], 'Author field displayed.');
+    $this->assertFieldByName('admin_select', $edit['admin_select'], 'Author field displayed.');
     $this->assertFieldByName('date[date]', $edit['date[date]'], 'Date field displayed.');
     $this->assertFieldByName('date[time]', $edit['date[time]'], 'Time field displayed.');
 
@@ -172,7 +176,7 @@ function testCommentEditPreviewSave() {
     $this->drupalGet('comment/' . $comment->id() . '/edit');
     $this->assertFieldByName('subject[0][value]', $edit['subject[0][value]'], 'Subject field displayed.');
     $this->assertFieldByName('comment_body[0][value]', $edit['comment_body[0][value]'], 'Comment field displayed.');
-    $this->assertFieldByName('name', $edit['name'], 'Author field displayed.');
+    $this->assertFieldByName('admin_select', $edit['admin_select'], 'Author field displayed.');
     $this->assertFieldByName('date[date]', $expected_form_date, 'Date field displayed.');
     $this->assertFieldByName('date[time]', $expected_form_time, 'Time field displayed.');
 
@@ -180,7 +184,6 @@ function testCommentEditPreviewSave() {
     $displayed = array();
     $displayed['subject[0][value]'] = (string) current($this->xpath("//input[@id='edit-subject-0-value']/@value"));
     $displayed['comment_body[0][value]'] = (string) current($this->xpath("//textarea[@id='edit-comment-body-0-value']"));
-    $displayed['name'] = (string) current($this->xpath("//input[@id='edit-name']/@value"));
     $displayed['date[date]'] = (string) current($this->xpath("//input[@id='edit-date-date']/@value"));
     $displayed['date[time]'] = (string) current($this->xpath("//input[@id='edit-date-time']/@value"));
     $this->drupalPostForm('comment/' . $comment->id() . '/edit', $displayed, t('Save'));
@@ -188,10 +191,11 @@ function testCommentEditPreviewSave() {
     // Check that the saved comment is still correct.
     $comment_storage = \Drupal::entityManager()->getStorage('comment');
     $comment_storage->resetCache(array($comment->id()));
+    /** @var \Drupal\comment\CommentInterface $comment_loaded */
     $comment_loaded = Comment::load($comment->id());
     $this->assertEqual($comment_loaded->getSubject(), $edit['subject[0][value]'], 'Subject loaded.');
     $this->assertEqual($comment_loaded->comment_body->value, $edit['comment_body[0][value]'], 'Comment body loaded.');
-    $this->assertEqual($comment_loaded->getAuthorName(), $edit['name'], 'Name loaded.');
+    $this->assertEqual($comment_loaded->getOwner()->id(), $web_user->id(), 'Name loaded.');
     $this->assertEqual($comment_loaded->getCreatedTime(), $raw_date, 'Date loaded.');
     $this->drupalLogout();
 
@@ -200,6 +204,8 @@ function testCommentEditPreviewSave() {
     $user_edit = array();
     $expected_created_time = $comment_loaded->getCreatedTime();
     $this->drupalLogin($web_user);
+    // Web-user cannot change the comment-author.
+    unset($edit['admin_select']);
     $this->drupalPostForm('comment/' . $comment->id() . '/edit', $user_edit, t('Save'));
     $comment_storage->resetCache(array($comment->id()));
     $comment_loaded = Comment::load($comment->id());
diff --git a/core/modules/comment/src/Tests/CommentTestBase.php b/core/modules/comment/src/Tests/CommentTestBase.php
index 12ea894..351f6da 100644
--- a/core/modules/comment/src/Tests/CommentTestBase.php
+++ b/core/modules/comment/src/Tests/CommentTestBase.php
@@ -70,6 +70,9 @@ protected function setUp() {
       'skip comment approval',
       'post comments',
       'access comments',
+      // Username's aren't shown in comment edit form autocomplete unless this
+      // permission is granted.
+      'access user profiles',
       'access content',
      ));
     $this->webUser = $this->drupalCreateUser(array(
diff --git a/core/modules/comment/src/Tests/CommentTranslationUITest.php b/core/modules/comment/src/Tests/CommentTranslationUITest.php
index 6397c69..efb6637 100644
--- a/core/modules/comment/src/Tests/CommentTranslationUITest.php
+++ b/core/modules/comment/src/Tests/CommentTranslationUITest.php
@@ -162,7 +162,7 @@ protected function doTestAuthoringInfo() {
         'created' => REQUEST_TIME - mt_rand(0, 1000),
       );
       $edit = array(
-        'name' => $user->getUsername(),
+        'admin_select' => $user->getUsername() . '(' . $user->id() . ')',
         'date[date]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d'),
         'date[time]' => format_date($values[$langcode]['created'], 'custom', 'H:i:s'),
       );
diff --git a/core/modules/user/tests/modules/user_hooks_test/user_hooks_test.module b/core/modules/user/tests/modules/user_hooks_test/user_hooks_test.module
index a036c65..98502ba 100644
--- a/core/modules/user/tests/modules/user_hooks_test/user_hooks_test.module
+++ b/core/modules/user/tests/modules/user_hooks_test/user_hooks_test.module
@@ -5,7 +5,7 @@
  * Support module for user hooks testing.
  */
 
-use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Render\SafeString;
 
 /**
  * Implements hook_user_format_name_alter().
@@ -13,7 +13,10 @@
 function user_hooks_test_user_format_name_alter(&$name, $account) {
   if (\Drupal::state()->get('user_hooks_test_user_format_name_alter', FALSE)) {
     if (\Drupal::state()->get('user_hooks_test_user_format_name_alter_safe', FALSE)) {
-      $name = SafeMarkup::format('<em>@uid</em>', array('@uid' => $account->id()));
+      // @todo change this to SafeMarkup::format() after
+      //  https://www.drupal.org/node/2559971 lands.
+      $build = ['#markup' => "<em>{$account->id()}</em>"];
+      $name = \Drupal::service('renderer')->renderPlain($build);
     }
     else {
       $name = '<em>' . $account->id() . '</em>';
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']);
   }
 
   /**
