diff --git a/core/lib/Drupal/Component/Discovery/StubTrait.php b/core/lib/Drupal/Component/Discovery/StubTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..b727d7a8339a82d18a510940683d080e89dea888
--- /dev/null
+++ b/core/lib/Drupal/Component/Discovery/StubTrait.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Component\Discovery;
+
+/**
+ * Defines an empty trait that can stand in for a missing trait.
+ */
+trait StubTrait {}
diff --git a/core/lib/Drupal/Component/Discovery/TraitSafeClassLoader.php b/core/lib/Drupal/Component/Discovery/TraitSafeClassLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..c137aad07b8252d1ac917969b28fb7dcd2499fb2
--- /dev/null
+++ b/core/lib/Drupal/Component/Discovery/TraitSafeClassLoader.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Component\Discovery;
+
+/**
+ * Defines a classloader that handles missing traits.
+ *
+ * This does not really load classes, but exists to work around a PHP limitation
+ * when it attempts to load a class that relies on a trait that does not exist.
+ * This is a common situation with Drupal plugins, which may be intended to be
+ * dormant unless certain other modules are installed.
+ */
+class TraitSafeClassLoader {
+
+  /**
+   * Flag indicating whether there was an attempt to load a missing trait.
+   */
+  protected bool $missingTrait = FALSE;
+
+  /**
+   * Aliases trait to a stub trait and sets the missing trait flag.
+   *
+   * This method is registered as a class loader during attribute discovery and
+   * runs last. Any call to this method means that $class is missing, and if
+   * $class is a trait, the flag is set.
+   *
+   * @param string $class
+   *   The classname to load.
+   */
+  public function loadClass(string $class): void {
+    if (str_ends_with($class, 'Trait')) {
+      $this->missingTrait = TRUE;
+      class_alias(StubTrait::class, $class, TRUE);
+    }
+  }
+
+  /**
+   * Returns whether there was an attempt to load a missing trait.
+   *
+   * @return bool
+   *   TRUE if there was an attempt to load a missing trait, otherwise FALSE.
+   */
+  public function hasMissingTrait(): bool {
+    return $this->missingTrait;
+  }
+
+  /**
+   * Resets the missing trait flag to FALSE.
+   */
+  public function reset(): void {
+    $this->missingTrait = FALSE;
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php b/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php
index cc7cb5f168481cf2ce2d6aa88dd68e0e30189955..ad2b3eb31b13b08d948a9f678f32a4d1de75e681 100644
--- a/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php
+++ b/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Component\Plugin\Discovery;
 
+use Drupal\Component\Discovery\TraitSafeClassLoader;
 use Drupal\Component\Plugin\Attribute\AttributeInterface;
 use Drupal\Component\Plugin\Attribute\Plugin;
 use Drupal\Component\FileCache\FileCacheFactory;
@@ -19,6 +20,11 @@ class AttributeClassDiscovery implements DiscoveryInterface {
    */
   protected FileCacheInterface $fileCache;
 
+  /**
+   * An array of classes to skip.
+   */
+  protected static array $skipClasses = [];
+
   /**
    * Constructs a new instance.
    *
@@ -59,6 +65,9 @@ protected function getFileCacheSuffix(string $default_suffix): string {
   public function getDefinitions() {
     $definitions = [];
 
+    $autoloader = new TraitSafeClassLoader();
+    spl_autoload_register([$autoloader, 'loadClass']);
+
     // Search for classes within all PSR-4 namespace locations.
     foreach ($this->getPluginNamespaces() as $namespace => $dirs) {
       foreach ($dirs as $dir) {
@@ -81,18 +90,6 @@ public function getDefinitions() {
               $sub_path = $iterator->getSubIterator()->getSubPath();
               $sub_path = $sub_path ? str_replace(DIRECTORY_SEPARATOR, '\\', $sub_path) . '\\' : '';
               $class = $namespace . '\\' . $sub_path . $fileinfo->getBasename('.php');
-              try {
-                ['id' => $id, 'content' => $content] = $this->parseClass($class, $fileinfo);
-                if ($id) {
-                  $definitions[$id] = $content;
-                  // Explicitly serialize this to create a new object instance.
-                  $this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]);
-                }
-                else {
-                  // Store a NULL object, so that the file is not parsed again.
-                  $this->fileCache->set($fileinfo->getPathName(), [NULL]);
-                }
-              }
               // Plugins may rely on Attribute classes defined by modules that
               // are not installed. In such a case, a 'class not found' error
               // may be thrown from reflection. However, this is an unavoidable
@@ -101,16 +98,48 @@ public function getDefinitions() {
               // so that it is scanned each time. This ensures that the plugin
               // definition will be found if the module it requires is
               // enabled.
+              // Additionally, PHP handles missing traits as an unrecoverable
+              // error. Register a special classloader that prevents a missing
+              // trait from causing an error, but stores that it was unable to
+              // find something. Because the classloader will result in the
+              // class being successfully autoloaded, store an array of classes
+              // to skip if this method is called again.
+              if (array_key_exists($class, self::$skipClasses)) {
+                continue;
+              }
+              try {
+                $class_exists = \class_exists($class, TRUE);
+                if (!$class_exists || $autoloader->hasMissingTrait()) {
+                  self::$skipClasses[$class] = TRUE;
+                  $autoloader->reset();
+                  continue;
+                }
+              }
               catch (\Error $e) {
+                self::$skipClasses[$class] = TRUE;
+                $autoloader->reset();
                 if (!preg_match('/(Class|Interface) .* not found$/', $e->getMessage())) {
+                  spl_autoload_unregister([$autoloader, 'loadClass']);
                   throw $e;
                 }
+                continue;
+              }
+              ['id' => $id, 'content' => $content] = $this->parseClass($class, $fileinfo);
+              if ($id) {
+                $definitions[$id] = $content;
+                // Explicitly serialize this to create a new object instance.
+                $this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]);
+              }
+              else {
+                // Store a NULL object, so that the file is not parsed again.
+                $this->fileCache->set($fileinfo->getPathName(), [NULL]);
               }
             }
           }
         }
       }
     }
+    spl_autoload_unregister([$autoloader, 'loadClass']);
 
     // Plugin discovery is a memory expensive process due to reflection and the
     // number of files involved. Collect cycles at the end of discovery to be as
diff --git a/core/modules/content_translation/content_translation.services.yml b/core/modules/content_translation/content_translation.services.yml
index f2a40968fc07d0e2fdcb34ca75dddacfe7590186..df356de471d674457e695118dd31c64ba5731d83 100644
--- a/core/modules/content_translation/content_translation.services.yml
+++ b/core/modules/content_translation/content_translation.services.yml
@@ -1,3 +1,10 @@
+parameters:
+  content_translation.moved_classes:
+    'Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait':
+      class: 'Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait'
+      deprecation_version: drupal:11.2.0
+      removed_version: drupal:12.0.0
+      change_record: https://www.drupal.org/node/3439256
 services:
   _defaults:
     autoconfigure: true
diff --git a/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php b/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php
deleted file mode 100644
index f6ea52e84aba71a19914174621dd0dae10d07de6..0000000000000000000000000000000000000000
--- a/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-
-namespace Drupal\content_translation\Plugin\migrate\source;
-
-@trigger_error('The ' . __NAMESPACE__ . '\I18nQueryTrait is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use \Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait instead. See https://www.drupal.org/node/3439256', E_USER_DEPRECATED);
-
-use Drupal\migrate\Plugin\MigrateIdMapInterface;
-use Drupal\migrate\MigrateException;
-use Drupal\migrate\Row;
-
-// cspell:ignore objectid
-
-/**
- * Gets an i18n translation from the source database.
- *
- * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use
- * \Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait instead.
- *
- * @see https://www.drupal.org/node/3439256
- */
-trait I18nQueryTrait {
-
-  /**
-   * The i18n string table name.
-   *
-   * @var string
-   */
-  protected $i18nStringTable;
-
-  /**
-   * Gets the translation for the property not already in the row.
-   *
-   * For some i18n migrations there are two translation values, such as a
-   * translated title and a translated description, that need to be retrieved.
-   * Since these values are stored in separate rows of the i18nStringTable
-   * table we get them individually, one in the source plugin query() and the
-   * other in prepareRow(). The names of the properties varies, for example,
-   * in BoxTranslation they are 'body' and 'title' whereas in
-   * MenuLinkTranslation they are 'title' and 'description'. This will save both
-   * translations to the row.
-   *
-   * @param \Drupal\migrate\Row $row
-   *   The current migration row which must include both a 'language' property
-   *   and an 'objectid' property. The 'objectid' is the value for the
-   *   'objectid' field in the i18n_string table.
-   * @param string $property_not_in_row
-   *   The name of the property to get the translation for.
-   * @param string $object_id_name
-   *   The value of the objectid in the i18n table.
-   * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
-   *   The ID map.
-   *
-   * @return bool
-   *   FALSE if the property has already been migrated.
-   *
-   * @throws \Drupal\migrate\MigrateException
-   */
-  protected function getPropertyNotInRowTranslation(Row $row, $property_not_in_row, $object_id_name, MigrateIdMapInterface $id_map) {
-    $language = $row->getSourceProperty('language');
-    if (!$language) {
-      throw new MigrateException('No language found.');
-    }
-    $object_id = $row->getSourceProperty($object_id_name);
-    if (!$object_id) {
-      throw new MigrateException('No objectid found.');
-    }
-
-    // If this row has been migrated it is a duplicate so skip it.
-    if ($id_map->lookupDestinationIds([$object_id_name => $object_id, 'language' => $language])) {
-      return FALSE;
-    }
-
-    // Save the translation for the property already in the row.
-    $property_in_row = $row->getSourceProperty('property');
-    $row->setSourceProperty($property_in_row . '_translated', $row->getSourceProperty('translation'));
-
-    // Get the translation, if one exists, for the property not already in the
-    // row.
-    $query = $this->select($this->i18nStringTable, 'i18n')
-      ->fields('i18n', ['lid'])
-      ->condition('i18n.property', $property_not_in_row)
-      ->condition('i18n.objectid', $object_id);
-    $query->leftJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]');
-    $query->condition('lt.language', $language);
-    $query->addField('lt', 'translation');
-    $results = $query->execute()->fetchAssoc();
-    if (!$results) {
-      $row->setSourceProperty($property_not_in_row . '_translated', NULL);
-    }
-    else {
-      $row->setSourceProperty($property_not_in_row . '_translated', $results['translation']);
-    }
-    return TRUE;
-  }
-
-}
diff --git a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php
index cfd853f6e9a6700d9c06ba0733760ebbaae88b02..2c66d43b8200d17326fcaca071b0bf899d2661ce 100644
--- a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php
+++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php
@@ -6,6 +6,7 @@
 use Drupal\block_content\Access\RefinableDependentAccessTrait;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
@@ -14,21 +15,22 @@
 use Drupal\Core\Form\SubformStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\layout_builder\Plugin\Derivative\InlineBlockDeriver;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines an inline block plugin type.
  *
- * @Block(
- *  id = "inline_block",
- *  admin_label = @Translation("Inline block"),
- *  category = @Translation("Inline blocks"),
- *  deriver = "Drupal\layout_builder\Plugin\Derivative\InlineBlockDeriver",
- * )
- *
  * @internal
  *   Plugin classes are internal.
  */
+#[Block(
+   id: 'inline_block',
+   admin_label: new TranslatableMarkup('Inline block'),
+   category: new TranslatableMarkup('Inline blocks'),
+   deriver: InlineBlockDeriver::class,
+)]
 class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface, RefinableDependentAccessInterface {
 
   use RefinableDependentAccessTrait;
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php
index b3d11614cce5337f4493f569424e128b949fc17b..b069dca082817d03a84fe29b3bc2dffd83b94bc4 100644
--- a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php
@@ -46,8 +46,12 @@ public function testGetDefinitions(): void {
     $discovery_path = __DIR__ . "/../../../../../fixtures/plugins/Plugin";
     // File path that should be discovered within that directory.
     $file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest1.php';
-    // Define a file path within the directory that should not be discovered.
-    $non_discoverable_file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest2.php';
+    // Define file paths within the directory that should not be discovered.
+    $non_discoverable_file_paths = [
+      $discovery_path . '/PluginNamespace/AttributeDiscoveryTest2.php',
+      $discovery_path . '/PluginNamespace/AttributeDiscoveryTestMissingInterface.php',
+      $discovery_path . '/PluginNamespace/AttributeDiscoveryTestMissingTrait.php',
+    ];
 
     $discovery = new AttributeClassDiscovery(['com\example' => [$discovery_path]]);
     $this->assertEquals([
@@ -69,8 +73,12 @@ public function testGetDefinitions(): void {
       'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
     ], unserialize($file_cache->get($file_path)['content']));
 
-    // The plugin that extends a missing class should not be cached.
-    $this->assertNull($file_cache->get($non_discoverable_file_path));
+    // The plugins that extend a missing class, implement a missing interface,
+    // and use a missing trait should not be cached.
+    foreach ($non_discoverable_file_paths as $non_discoverable_file_path) {
+      $this->assertTrue(file_exists($non_discoverable_file_path));
+      $this->assertNull($file_cache->get($non_discoverable_file_path));
+    }
 
     // Change the file cache entry.
     // The file cache is keyed by the file path, and we'll add some known
diff --git a/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTestMissingInterface.php b/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTestMissingInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..3c9c2d2684d5db2ea621c4f10babc4f097a1de4e
--- /dev/null
+++ b/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTestMissingInterface.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace com\example\PluginNamespace;
+
+use Drupal\a_module_that_does_not_exist\Plugin\CustomInterface;
+
+/**
+ * Provides a custom test plugin that implements a missing interface.
+ */
+#[CustomPlugin(
+  id: "discovery_test_missing_interface",
+  title: "Discovery test plugin missing interface"
+)]
+class AttributeDiscoveryTestMissingInterface implements CustomInterface {}
diff --git a/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTestMissingTrait.php b/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTestMissingTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..e9d041c6b852fa48db3d9fee1784a0f362acc56d
--- /dev/null
+++ b/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTestMissingTrait.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace com\example\PluginNamespace;
+
+use Drupal\a_module_that_does_not_exist\Plugin\CustomTrait;
+
+/**
+ * Provides a custom test plugin that uses a missing trait.
+ */
+#[CustomPlugin(
+  id: "discovery_test_missing_trait",
+  title: "Discovery test plugin missing trait"
+)]
+class AttributeDiscoveryTestMissingTrait {
+  use CustomTrait;
+
+}