diff --git a/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php b/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php
index ce592d27fb34ff77a7eab5f947e6ac7bfa2f9957..346b280ca08e18ceb1174ef4a3e1d9536bcbf581 100644
--- a/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php
+++ b/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php
@@ -4,7 +4,7 @@
 
 use Drupal\migrate\Row;
 use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
-use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
+use Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait;
 
 /**
  * Drupal 7 i18n content block translations source from database.
diff --git a/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php b/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php
index 29a64920b721d8bacec7bc8ee8393158d4a82e07..f6ea52e84aba71a19914174621dd0dae10d07de6 100644
--- a/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php
+++ b/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php
@@ -2,6 +2,8 @@
 
 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;
@@ -10,6 +12,11 @@
 
 /**
  * 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 {
 
diff --git a/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php b/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php
index 3003310e6d8329305aff694c6d6b900a4dec1870..0aede4ac4a95844cf6d92b901675f5af5b0cbe87 100644
--- a/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php
+++ b/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\menu_link_content\Plugin\migrate\source\d6;
 
-use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
-use Drupal\migrate\Row;
 use Drupal\menu_link_content\Plugin\migrate\source\MenuLink;
+use Drupal\migrate\Row;
+use Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait;
 
 // cspell:ignore mlid
 
diff --git a/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php b/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php
index 85a2a4f61505369d178acd447f0cba2da8c1fc15..fd7228fea9f2b89f734a3c77a7df63a485075b9b 100644
--- a/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php
+++ b/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\menu_link_content\Plugin\migrate\source\d7;
 
-use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
-use Drupal\migrate\Row;
 use Drupal\menu_link_content\Plugin\migrate\source\MenuLink;
+use Drupal\migrate\Row;
+use Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait;
 
 // cspell:ignore mlid objectid textgroup
 
diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/I18nQueryTrait.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/I18nQueryTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2baab04d4228bacd17e4afde1646d3ae5a050c2
--- /dev/null
+++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/I18nQueryTrait.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\migrate_drupal\Plugin\migrate\source;
+
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Row;
+
+// cspell:ignore objectid
+
+/**
+ * Gets an i18n translation from the source database.
+ */
+trait I18nQueryTrait {
+
+  /**
+   * The i18n string table name.
+   *
+   * @var string
+   */
+  protected string $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, string $property_not_in_row, string $object_id_name, MigrateIdMapInterface $id_map): bool {
+    $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();
+    $row->setSourceProperty($property_not_in_row . '_translated', $results['translation'] ?? NULL);
+    return TRUE;
+  }
+
+}
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/I18nQueryTraitTest.php b/core/modules/migrate_drupal/tests/src/Kernel/I18nQueryTraitTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..11668be1c7f8705efdb5036c3984b19d81a3e89f
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/src/Kernel/I18nQueryTraitTest.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\migrate_drupal\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
+use Drupal\migrate\Plugin\MigrationInterface;
+
+/**
+ * Tests instantiating migrate source plugins using I18nQueryTrait.
+ *
+ * @group migrate_drupal
+ */
+class I18nQueryTraitTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'block_content',
+    'menu_link_content',
+    'migrate',
+    'migrate_drupal',
+    'taxonomy',
+  ];
+
+  /**
+   * Tests instantiating migrate source plugins using I18nQueryTrait.
+   *
+   * I18nQueryTrait was originally in the content_translation module, which
+   * could lead to fatal errors instantiating the source plugins that use it
+   * when the content_translation module was not installed.
+   *
+   * @param string $plugin_id
+   *   The ID of a Migrate source plugin that uses I18nQueryTrait.
+   *
+   * @dataProvider providerI18nQueryTraitPlugins
+   */
+  public function testMigrateSourcePluginUsingI18nQueryTraitDiscovery(string $plugin_id): void {
+    // Namespace for uninstalled module content_translation needs to be removed
+    // for this test.
+    $this->disablePsr4ForUninstalledModules(['content_translation']);
+
+    $migration = $this->createMock(MigrationInterface::class);
+    $this->assertInstanceOf(SourcePluginBase::class, \Drupal::service('plugin.manager.migrate.source')->createInstance($plugin_id, [], $migration));
+  }
+
+  /**
+   * Removes PSR-4 namespaces from class loader for uninstalled modules.
+   *
+   * TestRunnerKernel registers namespaces for all modules, including
+   * uninstalled modules. This method removes the PSR-4 namespace for the list
+   * of modules passed in after confirming they are all uninstalled.
+   *
+   * @param string[] $remove_psr4_modules
+   *   List of machine names of modules that are uninstalled and whose PSR-4
+   *   namespaces should be removed from the class loader.
+   */
+  protected function disablePsr4ForUninstalledModules(array $remove_psr4_modules): void {
+    /** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */
+    $module_list = \Drupal::service('extension.list.module');
+    $available_modules = $module_list->getAllAvailableInfo();
+    $installed_modules = $module_list->getAllInstalledInfo();
+    $prefixes = $this->classLoader->getPrefixesPsr4();
+    foreach ($remove_psr4_modules as $module) {
+      $this->assertArrayHasKey($module, $available_modules);
+      $this->assertArrayNotHasKey($module, $installed_modules);
+      if (isset($prefixes["Drupal\\$module\\"])) {
+        // Cannot actually remove the PSR4 prefix from the class loader, so set
+        // the path to a wrong location.
+        $this->classLoader->setPsr4("Drupal\\$module\\", '');
+      }
+    }
+  }
+
+  /**
+   * Provides data for testMigrateSourcePluginUsingI18nQueryTraitDiscovery().
+   */
+  public static function providerI18nQueryTraitPlugins(): array {
+    return [
+      ['d6_box_translation'],
+      ['d7_block_custom_translation'],
+      ['d6_menu_link_translation'],
+      ['d7_menu_link_translation'],
+      ['d7_term_localized_translation'],
+    ];
+  }
+
+}
diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php b/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php
index 5ca0dfe7d503c3a6ec90d09426f3361ddcdb194b..d5adfdc3f3ba495886d3cfb0c1741f0888805d11 100644
--- a/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php
+++ b/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\taxonomy\Plugin\migrate\source\d7;
 
-use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
 use Drupal\migrate\Row;
+use Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait;
 
 // cspell:ignore ltlanguage objectid