diff --git a/search_api_clir.info b/search_api_clir.info
deleted file mode 100644
index ec03729..0000000
--- a/search_api_clir.info
+++ /dev/null
@@ -1 +0,0 @@
-name = Search API Cross-language information retrieval
diff --git a/search_api_clir.info.yml b/search_api_clir.info.yml
new file mode 100644
index 0000000..6c3af5b
--- /dev/null
+++ b/search_api_clir.info.yml
@@ -0,0 +1,8 @@
+type: module
+name: 'Search API Cross-Language Information Retrieval'
+description: 'Offers language fallbacks for multilingual searches.'
+package: Search
+core: 8.x
+dependencies:
+  - search_api:search_api
+
diff --git a/src/Plugin/search_api/processor/Clir.php b/src/Plugin/search_api/processor/Clir.php
new file mode 100644
index 0000000..55af536
--- /dev/null
+++ b/src/Plugin/search_api/processor/Clir.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Drupal\search_api_clir\Plugin\search_api\processor;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api\Processor\ProcessorPluginBase;
+use Drupal\search_api\Processor\ProcessorProperty;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\SearchApiException;
+
+/**
+ * Provides cross-language information retrieval (CLIR).
+ *
+ * @SearchApiProcessor(
+ *   id = "search_api_clir",
+ *   label = @Translation("Cross-language information retrieval"),
+ *   description = @Translation("Adds language fallbacks for search queries."),
+ *   stages = {
+ *     "add_properties" = 0,
+ *     "pre_index_save" = 0,
+ *     "preprocess_query" = 0,
+ *   }
+ * )
+ */
+class Clir extends ProcessorPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function supportsIndex(IndexInterface $index) {
+    foreach ($index->getDatasources() as $datasource) {
+      if ($datasource->getEntityTypeId()) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+    $properties = [];
+
+    if (!$datasource) {
+      $definition = [
+        'label' => $this->t('Missing translations'),
+        'description' => $this->t("Languages for which this entity doesn't have a translation."),
+        'type' => 'string',
+        'processor_id' => $this->getPluginId(),
+        'hidden' => TRUE,
+        'is_list' => TRUE,
+      ];
+      $properties['search_api_clir_missing_translations'] = new ProcessorProperty($definition);
+    }
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addFieldValues(ItemInterface $item) {
+    try {
+      $entity = $item->getOriginalObject()->getValue();
+    }
+    catch (SearchApiException $e) {
+      return;
+    }
+    if (!($entity instanceof ContentEntityInterface)) {
+      return;
+    }
+    $all_languages = \Drupal::languageManager()->getLanguages();
+    $translations = $entity->getTranslationLanguages();
+    $missing = array_keys(array_diff_key($all_languages, $translations));
+
+    $fields = $item->getFields();
+    $fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($fields, NULL, 'search_api_clir_missing_translations');
+    foreach ($fields as $field) {
+      foreach ($missing as $langcode) {
+        $field->addValue($langcode);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preIndexSave() {
+    $field = $this->ensureField(NULL, 'search_api_clir_missing_translations', 'string');
+    $field->setHidden();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preprocessSearchQuery(QueryInterface $query) {
+    // @todo $languages should be an array of langcodes, from highest to lowest
+    //   priority.
+    $languages = [];
+
+    // If there is only a single language, we don't need any special CLIR
+    // treatment.
+    if (count($languages) < 2) {
+      return;
+    }
+
+    // Set query to use all languages. We then set our own conditions to
+    // restrict that to proper fallbacks.
+    $query->setLanguages($languages);
+
+    // Fallback condition looks like this:
+    // language = $primary OR
+    // (language = $fallback AND $primary IN missing_translations)
+    $field = $this->findField(NULL, 'search_api_clir_missing_translations', 'string')
+      ->getFieldIdentifier();
+    $language_filter = $query->createConditionGroup('OR');
+    $query->addConditionGroup($language_filter);
+    $higher_priority = [];
+    foreach ($languages as $langcode) {
+      if (!$higher_priority) {
+        $language_filter->addCondition('search_api_language', $langcode);
+      }
+      else {
+        $nested_filter = $query->createConditionGroup('AND');
+        $language_filter->addConditionGroup($nested_filter);
+        $nested_filter->addCondition('search_api_language', $langcode);
+        foreach ($higher_priority as $other_langcode) {
+          $nested_filter->addCondition($field, $other_langcode);
+        }
+      }
+
+      $higher_priority[] = $langcode;
+    }
+  }
+
+}
