diff --git a/search_api.module b/search_api.module
index 52e740ea..d619f1ce 100644
--- a/search_api.module
+++ b/search_api.module
@@ -191,6 +191,13 @@ function search_api_config_import_steps_alter(&$sync_steps, ConfigImporter $conf
  * By setting the $entity->search_api_skip_tracking property to a true-like
  * value before this hook is invoked, you can prevent this behavior and make the
  * Search API ignore this new entity.
+ *
+ * Note that this function implements tracking only on behalf of the "Content
+ * Entity" datasource defined in this module, not for entity-based datasources
+ * in general. Datasources defined by other modules still have to implement
+ * their own mechanism for tracking new/updated/deleted entities.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
  */
 function search_api_entity_insert(EntityInterface $entity) {
   // Check if the entity is a content entity.
@@ -210,7 +217,8 @@ function search_api_entity_insert(EntityInterface $entity) {
   }
   $datasource_id = 'entity:' . $entity->getEntityTypeId();
   foreach ($indexes as $index) {
-    $index->trackItemsInserted($datasource_id, $item_ids);
+    $filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $item_ids);
+    $index->trackItemsInserted($datasource_id, $filtered_item_ids);
   }
 }
 
@@ -225,6 +233,13 @@ function search_api_entity_insert(EntityInterface $entity) {
  * By setting the $entity->search_api_skip_tracking property to a true-like
  * value before this hook is invoked, you can prevent this behavior and make the
  * Search API ignore this update.
+ *
+ * Note that this function implements tracking only on behalf of the "Content
+ * Entity" datasource defined in this module, not for entity-based datasources
+ * in general. Datasources defined by other modules still have to implement
+ * their own mechanism for tracking new/updated/deleted entities.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
  */
 function search_api_entity_update(EntityInterface $entity) {
   // Check if the entity is a content entity.
@@ -264,7 +279,8 @@ function search_api_entity_update(EntityInterface $entity) {
   $deleted_item_ids = array_map($combine_id, $deleted_item_ids);
   foreach ($indexes as $index) {
     if ($inserted_item_ids) {
-      $index->trackItemsInserted($datasource_id, $inserted_item_ids);
+      $filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $inserted_item_ids);
+      $index->trackItemsInserted($datasource_id, $filtered_item_ids);
     }
     if ($updated_item_ids) {
       $index->trackItemsUpdated($datasource_id, $updated_item_ids);
@@ -286,6 +302,13 @@ function search_api_entity_update(EntityInterface $entity) {
  * Search API ignore this deletion. (Note that this might lead to stale data in
  * the tracking table or on the server, since the item will not removed from
  * there (if it has been added before).)
+ *
+ * Note that this function implements tracking only on behalf of the "Content
+ * Entity" datasource defined in this module, not for entity-based datasources
+ * in general. Datasources defined by other modules still have to implement
+ * their own mechanism for tracking new/updated/deleted entities.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
  */
 function search_api_entity_delete(EntityInterface $entity) {
   // Check if the entity is a content entity.
diff --git a/src/Plugin/search_api/datasource/ContentEntity.php b/src/Plugin/search_api/datasource/ContentEntity.php
index 95b190ce..dcfee6c2 100644
--- a/src/Plugin/search_api/datasource/ContentEntity.php
+++ b/src/Plugin/search_api/datasource/ContentEntity.php
@@ -952,8 +952,7 @@ protected function getPropertyPathDependencies($property_path, array $properties
    * {@inheritdoc}
    */
   public static function getIndexesForEntity(ContentEntityInterface $entity) {
-    $entity_type = $entity->getEntityTypeId();
-    $datasource_id = 'entity:' . $entity_type;
+    $datasource_id = 'entity:' . $entity->getEntityTypeId();
     $entity_bundle = $entity->bundle();
     $has_bundles = $entity->getEntityType()->hasKey('bundle');
 
@@ -969,15 +968,10 @@ public static function getIndexesForEntity(ContentEntityInterface $entity) {
       elseif ($has_bundles) {
         // If the entity type supports bundles, we also have to filter out
         // indexes that exclude the entity's bundle.
-        try {
-          $config = $index->getDatasource($datasource_id)->getConfiguration();
-          $default = !empty($config['bundles']['default']);
-          $bundle_set = in_array($entity_bundle, $config['bundles']['selected']);
-          if ($default == $bundle_set) {
-            unset($indexes[$index_id]);
-          }
-        }
-        catch (SearchApiException $e) {
+        $config = $index->getDatasource($datasource_id)->getConfiguration();
+        $default = !empty($config['bundles']['default']);
+        $bundle_set = in_array($entity_bundle, $config['bundles']['selected']);
+        if ($default == $bundle_set) {
           unset($indexes[$index_id]);
         }
       }
@@ -986,4 +980,44 @@ public static function getIndexesForEntity(ContentEntityInterface $entity) {
     return $indexes;
   }
 
+  /**
+   * Filters a set of datasource-specific item IDs.
+   *
+   * Returns only those item IDs that are valid for the given datasource and
+   * index. This method only checks the item language, though – whether an
+   * entity with that ID actually exists, or whether it has a bundle included
+   * for that datasource, is not verified.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index for which to validate.
+   * @param string $datasource_id
+   *   The ID of the datasource on the index for which to validate.
+   * @param string[] $item_ids
+   *   The item IDs to be validated.
+   *
+   * @return string[]
+   *   All given item IDs that are valid for that index and datasource.
+   */
+  public static function filterValidItemIds(IndexInterface $index, $datasource_id, array $item_ids) {
+    if (!$index->isValidDatasource($datasource_id)) {
+      return $item_ids;
+    }
+    $config = $index->getDatasource($datasource_id)->getConfiguration();
+    $default = !empty($config['languages']['default']);
+    $selected = $config['languages']['selected'];
+    $valid_ids = [];
+    foreach ($item_ids as $item_id) {
+      $pos = strrpos($item_id, ':');
+      // Item IDs without colons are always invalid.
+      if ($pos === FALSE) {
+        continue;
+      }
+      $langcode = substr($item_id, $pos + 1);
+      if ($default != in_array($langcode, $selected)) {
+        $valid_ids[] = $item_id;
+      }
+    }
+    return $valid_ids;
+  }
+
 }
diff --git a/tests/src/Kernel/LanguageKernelTest.php b/tests/src/Kernel/LanguageKernelTest.php
index ee3bdc2e..2d315a54 100644
--- a/tests/src/Kernel/LanguageKernelTest.php
+++ b/tests/src/Kernel/LanguageKernelTest.php
@@ -10,6 +10,7 @@
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\search_api\Entity\Index;
 use Drupal\search_api\Entity\Server;
+use Drupal\search_api\Utility\Utility;
 
 /**
  * Tests translation handling of the content entity datasource.
@@ -288,4 +289,75 @@ public function testItemTranslations() {
     $this->assertEquals(3, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'Three items are indexed.');
   }
 
+  /**
+   * Tests tracking when the datasource has only specific languages enabled.
+   */
+  public function testDatasourceLanguagesSetting() {
+    $datasource_id = 'entity:' . $this->testEntityTypeId;
+    $datasource = $this->index->getDatasource($datasource_id);
+    $datasource->setConfiguration([
+      'languages' => [
+        'default' => FALSE,
+        'selected' => [$this->langcodes[0], $this->langcodes[2]],
+      ],
+    ]);
+    $this->index->save();
+
+    // Create new entities with our three custom languages and one with the
+    // default language (English).
+    /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged[] $entities */
+    $entities = [];
+    $uid = $this->container->get('current_user')->id();
+    foreach ([0, 1, 2, 3] as $i) {
+      $entity = EntityTestMulRevChanged::create([
+        'id' => $i + 1,
+        'name' => 'test 1',
+        'user_id' => $uid,
+      ]);
+      if (!empty($this->langcodes[$i])) {
+        $entity->set('langcode', $this->langcodes[$i]);
+      }
+      $entity->save();
+      $entities[$i] = $entity;
+    }
+
+    // Only the ones with languages "l0" and "l2" should have been tracked.
+    $tracker = $this->index->getTrackerInstance();
+    $this->assertEquals(2, $tracker->getTotalItemsCount());
+    $expected = [
+      Utility::createCombinedId($datasource_id, "1:{$this->langcodes[0]}"),
+      Utility::createCombinedId($datasource_id, "3:{$this->langcodes[2]}"),
+    ];
+    $actual = $tracker->getRemainingItems();
+    sort($actual);
+    $this->assertEquals($expected, $actual);
+
+    // Add a translation for a disabled language. It should not be tracked.
+    $entities[0]->addTranslation($this->langcodes[1])->save();
+    $this->assertEquals(2, $tracker->getTotalItemsCount());
+    $actual = $tracker->getRemainingItems();
+    sort($actual);
+    $this->assertEquals($expected, $actual);
+
+    // Add a translation for an enabled language. It should be tracked.
+    $langcode = $this->langcodes[0];
+    $entities[1]->addTranslation($langcode)->save();
+    $this->assertEquals(3, $tracker->getTotalItemsCount());
+    $expected_1 = $expected;
+    $expected_1[] = Utility::createCombinedId($datasource_id, "2:$langcode");
+    sort($expected_1);
+    $actual = $tracker->getRemainingItems();
+    sort($actual);
+    $this->assertEquals($expected_1, $actual);
+
+    // Remove that translation again and verify that it's also deleted from the
+    // tracker.
+    $entities[1]->removeTranslation($langcode);
+    $entities[1]->save();
+    $this->assertEquals(2, $tracker->getTotalItemsCount());
+    $actual = $tracker->getRemainingItems();
+    sort($actual);
+    $this->assertEquals($expected, $actual);
+  }
+
 }
