Repository: sitepark/atoolo-search-bundle
Pull request #36: Feature: Use the correct indexer depending on the page language, even if there are english content in the (german) default files
Author: login: sitepark-hying
Created: 2025-04-09T14:26:16Z | Merged: 2025-04-11T05:30:11Z



Diff:
diff --git a/config/indexer.yaml b/config/indexer.yaml
index 006e45c..cc6b53e 100644
--- a/config/indexer.yaml
+++ b/config/indexer.yaml
@@ -52,9 +52,6 @@ services:
     tags:
       - { name: 'atoolo_search.indexer.document_enricher.schema2x', priority: 100 }
 
-  atoolo_search.indexer.translation_splitter:
-    class: Atoolo\Search\Service\Indexer\SiteKit\SubDirTranslationSplitter
-
   atoolo_search.indexer.aborter:
     class: Atoolo\Search\Service\Indexer\IndexingAborter
     arguments:
@@ -97,7 +94,6 @@ services:
       - '@atoolo_search.indexer.internal_resource_progress_state'
       - '@atoolo_search.indexer.location_finder'
       - '@atoolo_resource.resource_loader'
-      - '@atoolo_search.indexer.translation_splitter'
       - '@atoolo_search.indexer.solr_index_service'
       - '@atoolo_search.indexer.aborter'
       - '@atoolo_search.indexer.configuration_loader'
diff --git a/src/Service/Indexer/InternalResourceIndexer.php b/src/Service/Indexer/InternalResourceIndexer.php
index 19cb491..34ea3e5 100644
--- a/src/Service/Indexer/InternalResourceIndexer.php
+++ b/src/Service/Indexer/InternalResourceIndexer.php
@@ -48,6 +48,16 @@ class InternalResourceIndexer implements Indexer
 
     private bool $skipCleanup = false;
 
+    /**
+     * @var array<string>|null
+     */
+    private ?array $managedIndices = null;
+
+    /**
+     * @var array<string, string|false>
+     */
+    private array $validIndexnameByLanguage = [];
+
     /**
      * @param iterable<DocumentEnricher<IndexDocument>> $documentEnricherList
      */
@@ -57,7 +67,6 @@ public function __construct(
         private IndexerProgressHandler $progressHandler,
         private readonly LocationFinder $finder,
         private readonly ResourceLoader $resourceLoader,
-        private readonly TranslationSplitter $translationSplitter,
         private readonly SolrIndexService $indexService,
         private readonly IndexingAborter $aborter,
         private readonly IndexerConfigurationLoader $configLoader,
@@ -212,6 +221,14 @@ private function getIndexerParameter(): IndexerParameter
         return $this->parameter ??= ($this->loadIndexerParameter());
     }
 
+    /**
+     * @return array<string>
+     */
+    public function getManagedIndices(): array
+    {
+        return $this->managedIndices ??= $this->indexService->getManagedIndices();
+    }
+
     private function loadIndexerParameter(): IndexerParameter
     {
         $config = $this->configLoader->load($this->source);
@@ -250,112 +267,93 @@ private function indexResources(
         if (count($pathList) === 0) {
             return;
         }
-
-        $splitterResult = $this->translationSplitter->split($pathList);
-        $this->indexTranslationSplittedResources(
-            $parameter,
-            $splitterResult,
-        );
-    }
-
-    /**
-     * There is a separate Solr index for each language. This allows
-     * language-specific tokenizers and other language-relevant configurations
-     * to be used. Via the `$splitterResult` all paths are separated according
-     * to their languages and can be indexed separately. Each language is
-     * indexed separately here.
-     */
-    private function indexTranslationSplittedResources(
-        IndexerParameter $parameter,
-        TranslationSplitterResult $splitterResult,
-    ): void {
-
+        $offset = 0;
+        $successCount = 0;
+        $updatedIndexLocales = [];
         $processId = uniqid('', true);
+        $sortedPathlist = $this->sortPathlistByLocale($pathList);
 
-        $index = $this->indexService->getIndex(ResourceLanguage::default());
-
-        $this->indexResourcesPerLanguageIndex(
-            $processId,
-            $parameter,
-            ResourceLanguage::default(),
-            $index,
-            $splitterResult->getBases(),
-        );
+        while (true) {
+            $resourceLists = $this->loadResources(
+                $sortedPathlist,
+                $offset,
+                $parameter->chunkSize,
+            );
+            if ($resourceLists === false) {
+                break;
+            }
 
-        foreach ($splitterResult->getLanguages() as $lang) {
-            try {
-                $langIndex = $this->indexService->getIndex($lang);
-                $this->indexResourcesPerLanguageIndex(
-                    $processId,
-                    $parameter,
+            foreach ($resourceLists as $resourceLang => $resourceList) {
+                $lang = ResourceLanguage::of($resourceLang);
+                $indexedCount = $this->indexResourcesForLanguage(
                     $lang,
-                    $langIndex,
-                    $splitterResult->getTranslations($lang),
+                    $resourceList,
+                    $processId,
                 );
-            } catch (UnsupportedIndexLanguageException $e) {
-                $this->handleError($e->getMessage());
-                continue;
+                if ($indexedCount === false) {
+                    continue;
+                }
+                $successCount += $indexedCount;
+
+                if (!in_array($lang, $updatedIndexLocales)) {
+                    $updatedIndexLocales[] = $lang;
+                }
             }
+            $offset += $parameter->chunkSize;
+        }
+
+        $cleanupThreshold = $parameter->cleanupThreshold;
+        if ($cleanupThreshold > 0 && $successCount >= $cleanupThreshold) {
+            $this->commitLocaleIndices($updatedIndexLocales, $processId);
         }
     }
 
     /**
-     * The resources for a language are indexed here.
-     *
-     * @param string[] $locations
+     * @param ResourceLanguage $resourceLang
+     * @param array<Resource> $resourceList
+     * @param string $processId
+     * @return int|false
      */
-    private function indexResourcesPerLanguageIndex(
+    private function indexResourcesForLanguage(
+        ResourceLanguage $resourceLang,
+        array $resourceList,
         string $processId,
-        IndexerParameter $parameter,
-        ResourceLanguage $lang,
-        string $index,
-        array $locations,
-    ): void {
-
-        if (empty($locations)) {
-            return;
-        }
-
-        $offset = 0;
-        $successCount = 0;
+    ): int|false {
 
-        $managedIndices = $this->indexService->getManagedIndices();
-        if (!in_array($index, $managedIndices)) {
-            $this->handleError('Index "' . $index . '" not found');
-            return;
+        $index = $this->getIndexByLanguage($resourceLang);
+        if ($index === false) {
+            return false;
         }
+        $indexedCount = $this->indexChunks(
+            $processId,
+            $resourceLang,
+            $index,
+            $resourceList,
+        );
+        gc_collect_cycles();
+        return  $indexedCount;
+    }
 
-        while (true) {
-            $indexedCount = $this->indexChunks(
-                $processId,
-                $lang,
-                $index,
-                $locations,
-                $offset,
-                $parameter->chunkSize,
-            );
-
-            gc_collect_cycles();
+    /**
+     * @param array<ResourceLanguage> $indexLocales
+     * @param string $processId
+     * @return void
+     */
+    private function commitLocaleIndices(
+        array $indexLocales,
+        string $processId,
+    ): void {
 
-            if ($indexedCount === false) {
-                break;
+        if (!$this->skipCleanup) {
+            foreach ($indexLocales as $indexLocale) {
+                $this->indexService->deleteExcludingProcessId(
+                    $indexLocale,
+                    $this->source,
+                    $processId,
+                );
+                $this->indexService->commit($indexLocale);
             }
-            $successCount += $indexedCount;
-            $offset += $parameter->chunkSize;
         }
-
-        if (
-            !$this->skipCleanup &&
-            $parameter->cleanupThreshold > 0 &&
-            $successCount >= $parameter->cleanupThreshold
-        ) {
-            $this->indexService->deleteExcludingProcessId(
-                $lang,
-                $this->source,
-                $processId,
-            );
-        }
-        $this->indexService->commit($lang);
     }
 
     /**
@@ -365,33 +363,24 @@ private function indexResourcesPerLanguageIndex(
      * methods accept a chunk with all paths that are to be indexed via a
      * request.
      *
-     * @param string[] $locations
+     * @param string $processId
+     * @param ResourceLanguage $lang
+     * @param string $index
+     * @param array<Resource> $resourceList
      */
     private function indexChunks(
         string $processId,
         ResourceLanguage $lang,
         string $index,
-        array $locations,
-        int $offset,
-        int $length,
+        array $resourceList,
     ): int|false {
-        $resourceList = $this->loadResources(
-            $lang,
-            $locations,
-            $offset,
-            $length,
-        );
-        if ($resourceList === false) {
-            return false;
-        }
+
         if ($this->aborter->isAbortionRequested($index)) {
             $this->aborter->resetAbortionRequest($index);
             $this->progressHandler->abort();
             return false;
         }
-        if (empty($resourceList)) {
-            return 0;
-        }
+
         $this->progressHandler->advance(count($resourceList));
         $result = $this->add($lang, $processId, $resourceList);
 
@@ -404,11 +393,16 @@ private function indexChunks(
     }
 
     /**
+     * Load the <Resource> objects for all $locations and return them in a
+     * associative array with all resources for each found locale
+     * within all the resources.
+     *
      * @param string[] $locations
-     * @return Resource[]|false
+     * @poram int $offset
+     * @poram int $length
+     * @return  array<string, array<Resource>>|false
      */
     private function loadResources(
-        ResourceLanguage $lang,
         array $locations,
         int $offset,
         int $length,
@@ -421,20 +415,24 @@ private function loadResources(
 
         $end = min($length, $maxLength) + $offset;
 
-        $resourceList = [];
+        $resourceLists = [];
         for ($i = $offset; $i < $end; $i++) {
-            $location = ResourceLocation::of(
-                $locations[$i],
-                $lang,
-            );
+            $resourceLocation = $this->toResourceLocation($locations[$i]);
+            if ($resourceLocation === null) {
+                continue;
+            }
+
             try {
-                $resource = $this->resourceLoader->load($location);
-                $resourceList[] = $resource;
+                $resource = $this->resourceLoader->load($resourceLocation);
+                if (array_key_exists($resource->lang->code, $resourceLists) === false) {
+                    $resourceLists[$resource->lang->code] = [];
+                }
+                $resourceLists[$resource->lang->code][] = $resource;
             } catch (Throwable $e) {
                 $this->handleError($e);
             }
         }
-        return $resourceList;
+        return $resourceLists;
     }
 
     /**
@@ -498,4 +496,107 @@ private function deleteErrorProtocol(): void
             'crawl_status:error OR crawl_status:warning',
         );
     }
+
+    /**
+     * Sorts the pathList by the translated locals within the path:
+     * `/dir/file.php` is lower than
+     * `/dir/anotherFile.php.translations/de_DE.php` is lower than
+     * `/dir/file.php.translations/it_IT.php` is equal to
+     * `/dir/nextFile.php.translations/it_IT.php`
+     *
+     * @param array<string> $pathList
+     * @return array<string> $sortedList
+     */
+    private function sortPathlistByLocale(array $pathList): array
+    {
+        usort($pathList, function (string $pathA, string $pathB) {
+            $posA = strrpos($pathA, '.php.translations');
+            $posB = strrpos($pathB, '.php.translations');
+            if ($posA === false || $posB === false) {
+                $eq = $posA <=> $posB;
+                return $eq;
+            }
+            $localeA = substr($pathA, strrpos($pathA, '/') + 1);
+            $localeB = substr($pathB, strrpos($pathB, '/') + 1);
+            return $localeA <=> $localeB;
+        });
+        return $pathList;
+    }
+
+    /**
+     * Converts a path like '/dir/file_a.php' or
+     * '/dir/file_b.php.translations/de_DE.php' to corresponding
+     * ResourceLocation object with the location and ResourceLanguage of th path
+     *
+     * @param string $path
+     * @return ResourceLocation|null
+     */
+    private function toResourceLocation(string $path): ?ResourceLocation
+    {
+        $normalizedPath = $this->normalizePath($path);
+        if (empty($normalizedPath)) {
+            return null;
+        }
+
+        $pos = strrpos($normalizedPath, '.php.translations');
+        if ($pos === false) {
+            return ResourceLocation::of($normalizedPath);
+        }
+
+        $localeFilename = basename($normalizedPath);
+        $locale = basename($localeFilename, '.php');
+
+        return ResourceLocation::of(
+            substr($normalizedPath, 0, $pos + 4),
+            ResourceLanguage::of($locale),
+        );
+    }
+
+    /**
+     * A path can signal to be translated into another language via
+     * the URL parameter loc. For example,
+     * `/dir/file.php?loc=it_IT` defines that the path
+     * `/dir/file.php.translations/it_IT.php` is to be used.
+     * This method translates the URL parameter into the correct path.
+     */
+    private function normalizePath(string $path): string
+    {
+        $queryString = parse_url($path, PHP_URL_QUERY);
+        if (!is_string($queryString)) {
+            return $path;
+        }
+        $urlPath = parse_url($path, PHP_URL_PATH);
+        if (!is_string($urlPath)) {
+            return '';
+        }
+        parse_str($queryString, $params);
+        if (!isset($params['loc']) || !is_string($params['loc'])) {
+            return $urlPath;
+        }
+        $loc = $params['loc'];
+        return $urlPath . '.translations/' . $loc . ".php";
+    }
+
+    /**
+     * @param ResourceLanguage $lang
+     * @return false|string
+     */
+    private function getIndexByLanguage(ResourceLanguage $lang): false|string
+    {
+        $index = $this->validIndexnameByLanguage[$lang->code] ?? null;
+        if ($index === null) {
+            try {
+                $index = $this->indexService->getIndex($lang);
+                if (!in_array($index, $this->getManagedIndices())) {
+                    $this->handleError('Unmanaged Index: ' . $index);
+                    $index = false;
+                }
+            } catch (UnsupportedIndexLanguageException $e) {
+                $this->handleError($e->getMessage());
+                $index = false;
+            }
+            $this->validIndexnameByLanguage[$lang->code] = $index;
+        }
+        return $index;
+    }
 }
diff --git a/src/Service/Indexer/SiteKit/SubDirTranslationSplitter.php b/src/Service/Indexer/SiteKit/SubDirTranslationSplitter.php
deleted file mode 100644
index 393c170..0000000
--- a/src/Service/Indexer/SiteKit/SubDirTranslationSplitter.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Atoolo\Search\Service\Indexer\SiteKit;
-
-use Atoolo\Resource\ResourceLanguage;
-use Atoolo\Resource\ResourceLocation;
-use Atoolo\Search\Service\Indexer\TranslationSplitter;
-use Atoolo\Search\Service\Indexer\TranslationSplitterResult;
-
-/**
- * The task of the SubDirTranslationSplitter is to receive a list of resource
- * paths and use the file path to check for which language the resource
- * must be indexed, as each language is stored in a separate index.
- */
-class SubDirTranslationSplitter implements TranslationSplitter
-{
-    /**
-     * @param string[] $pathList
-     */
-    public function split(array $pathList): TranslationSplitterResult
-    {
-        $bases = [];
-        $translations = [];
-        foreach ($pathList as $path) {
-            $location = $this->toResourceLocation($path);
-            if ($location === null) {
-                continue;
-            }
-            if ($location->lang === ResourceLanguage::default()) {
-                $bases[] = $location->location;
-                continue;
-            }
-            if (!isset($translations[$location->lang->code])) {
-                $translations[$location->lang->code] = [];
-            }
-            $translations[$location->lang->code][] = $location->location;
-        }
-        gc_collect_cycles();
-
-        return new TranslationSplitterResult($bases, $translations);
-    }
-
-    private function toResourceLocation(string $path): ?ResourceLocation
-    {
-        $normalizedPath = $this->normalizePath($path);
-        if (empty($normalizedPath)) {
-            return null;
-        }
-
-        $pos = strrpos($normalizedPath, '.php.translations');
-        if ($pos === false) {
-            return ResourceLocation::of($normalizedPath);
-        }
-
-        $localeFilename = basename($normalizedPath);
-        $locale = basename($localeFilename, '.php');
-
-        return ResourceLocation::of(
-            substr($normalizedPath, 0, $pos + 4),
-            ResourceLanguage::of($locale),
-        );
-    }
-
-    /**
-     * A path can signal to be translated into another language via
-     * the URL parameter loc. For example,
-     * `/dir/file.php?loc=it_IT` defines that the path
-     * `/dir/file.php.translations/it_IT.php` is to be used.
-     * This method translates the URL parameter into the correct path.
-     */
-    private function normalizePath(string $path): string
-    {
-        $queryString = parse_url($path, PHP_URL_QUERY);
-        if (!is_string($queryString)) {
-            return $path;
-        }
-        $urlPath = parse_url($path, PHP_URL_PATH);
-        if (!is_string($urlPath)) {
-            return '';
-        }
-        parse_str($queryString, $params);
-        if (!isset($params['loc']) || !is_string($params['loc'])) {
-            return $urlPath;
-        }
-        $loc = $params['loc'];
-        return $urlPath . '.translations/' . $loc . ".php";
-    }
-}
diff --git a/src/Service/Indexer/TranslationSplitter.php b/src/Service/Indexer/TranslationSplitter.php
deleted file mode 100644
index ab88453..0000000
--- a/src/Service/Indexer/TranslationSplitter.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Atoolo\Search\Service\Indexer;
-
-/**
- * The `TranslationSplitter` interface is implemented in order to
- * group a list of paths in the respective languages.
- */
-interface TranslationSplitter
-{
-    /**
-     * @param string[] $pathList
-     */
-    public function split(array $pathList): TranslationSplitterResult;
-}
diff --git a/src/Service/Indexer/TranslationSplitterResult.php b/src/Service/Indexer/TranslationSplitterResult.php
deleted file mode 100644
index 1359b03..0000000
--- a/src/Service/Indexer/TranslationSplitterResult.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Atoolo\Search\Service\Indexer;
-
-use Atoolo\Resource\ResourceLanguage;
-
-class TranslationSplitterResult
-{
-    /**
-     * @param string[] $bases
-     * @param array<string,array<string>> $translations
-     */
-    public function __construct(
-        private readonly array $bases,
-        private readonly array $translations,
-    ) {}
-
-    /**
-     * @return ResourceLanguage[]
-     */
-    public function getLanguages(): array
-    {
-        $languages = array_keys($this->translations);
-        sort($languages);
-        return array_map(
-            static fn($lang)
-                => ResourceLanguage::of($lang),
-            $languages,
-        );
-    }
-
-    /**
-     * @return string[]
-     */
-    public function getBases(): array
-    {
-        return $this->bases;
-    }
-
-    /**
-    * @return string[]
-    */
-    public function getTranslations(ResourceLanguage $lang): array
-    {
-        return $this->translations[$lang->code] ?? [];
-    }
-}
diff --git a/test/Service/Indexer/InternalResourceIndexerTest.php b/test/Service/Indexer/InternalResourceIndexerTest.php
index aa4ea86..d51e594 100644
--- a/test/Service/Indexer/InternalResourceIndexerTest.php
+++ b/test/Service/Indexer/InternalResourceIndexerTest.php
@@ -18,10 +18,8 @@
 use Atoolo\Search\Service\Indexer\InternalResourceIndexer;
 use Atoolo\Search\Service\Indexer\LocationFinder;
 use Atoolo\Search\Service\Indexer\ResourceFilter;
-use Atoolo\Search\Service\Indexer\SiteKit\SubDirTranslationSplitter;
 use Atoolo\Search\Service\Indexer\SolrIndexService;
 use Atoolo\Search\Service\Indexer\SolrIndexUpdater;
-use Atoolo\Search\Service\Indexer\TranslationSplitter;
 use Exception;
 use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\MockObject\MockObject;
@@ -61,8 +59,6 @@ class InternalResourceIndexerTest extends TestCase
 
     private DocumentEnricher&MockObject $documentEnricher;
 
-    private TranslationSplitter $translationSplitter;
-
     private IndexerConfigurationLoader&MockObject $indexerConfigurationLoader;
 
     private IndexerConfiguration $indexerConfiguration;
@@ -90,16 +86,19 @@ public function setUp(): void
             ->willReturnCallback(function ($resource, $doc) {
                 return $doc;
             });
-        $this->translationSplitter = new SubDirTranslationSplitter();
         $this->resourceLoader = $this->createStub(ResourceLoader::class);
         $this->resourceLoader->method('load')
             ->willReturnCallback(function ($location) {
+                $resourceLang = $location->lang;
+                if ($location->location === '/a/en.php') {
+                    $resourceLang = ResourceLanguage::of('en_EN');
+                }
                 return new Resource(
                     $location->location,
                     '',
                     '',
                     '',
-                    $location->lang,
+                    $resourceLang,
                     new DataBag([]),
                 );
             });
@@ -154,7 +153,6 @@ public function setUp(): void
             $this->indexerProgressHandler,
             $this->finder,
             $this->resourceLoader,
-            $this->translationSplitter,
             $this->solrIndexService,
             $this->aborter,
             $this->indexerConfigurationLoader,
@@ -495,4 +493,41 @@ public function testLock(): void
             $lock->release();
         }
     }
+
+    public function testWithDifferentLocaleInResource(): void
+    {
+        $this->finder->method('findAll')
+            ->willReturn([
+                '/a/a.php',
+                '/a/b.php',
+                '/a/en.php',// will return a 'en_EN' Resource
+            ]);
+        $this->indexerFilter->method('accept')
+            ->willReturn(true);
+
+        // one for 'default' lang and one for resource internal 'en' locale
+        $this->indexerProgressHandler->expects($this->exactly(2))
+            ->method('advance');
+        $this->updater->expects($this->exactly(3))
+            ->method('addDocument');
+
+        $this->indexer->index();
+    }
+
+    public function testIndexWithoutLanguages(): void
+    {
+        $this->finder->method('findAll')
+            ->willReturn([
+                '?key=value',
+                '/a/b.php?key=value',
+                '/a/b.php?loc=',
+            ]);
+        $this->indexerFilter->method('accept')
+            ->willReturn(true);
+
+        $this->updater->expects($this->exactly(2))
+            ->method('addDocument');
+
+        $this->indexer->index();
+    }
 }
diff --git a/test/Service/Indexer/SiteKit/SubDirTranslationSplitterTest.php b/test/Service/Indexer/SiteKit/SubDirTranslationSplitterTest.php
deleted file mode 100644
index c336e1f..0000000
--- a/test/Service/Indexer/SiteKit/SubDirTranslationSplitterTest.php
+++ /dev/null
@@ -1,119 +0,0 @@
-<?php
-
-namespace Atoolo\Search\Test\Service\Indexer\SiteKit;
-
-use Atoolo\Resource\ResourceLanguage;
-use Atoolo\Search\Service\Indexer\SiteKit\SubDirTranslationSplitter;
-use PHPUnit\Framework\Attributes\CoversClass;
-use PHPUnit\Framework\TestCase;
-
-#[CoversClass(SubDirTranslationSplitter::class)]
-class SubDirTranslationSplitterTest extends TestCase
-{
-    public function testBases(): void
-    {
-        $splitter = new SubDirTranslationSplitter();
-        $result = $splitter->split(['/a/b.php']);
-
-        $this->assertEquals(
-            ['/a/b.php'],
-            $result->getBases(),
-            'unexpected bases',
-        );
-    }
-
-    public function testLanguages(): void
-    {
-        $splitter = new SubDirTranslationSplitter();
-        $result = $splitter->split([
-            '/a/b.php',
-            '/a/b.php.translations/it_IT.php',
-            '/a/b.php.translations/en_US.php',
-        ]);
-
-        $this->assertEquals(
-            [
-                ResourceLanguage::of('en_US'),
-                ResourceLanguage::of('it_IT'),
-            ],
-            $result->getLanguages(),
-            'unexpected locales',
-        );
-    }
-
-    public function testGetTranlsations(): void
-    {
-        $splitter = new SubDirTranslationSplitter();
-        $result = $splitter->split([
-            '/a/b.php',
-            '/a/b.php.translations/it_IT.php',
-            '/a/b.php.translations/en_US.php',
-            '/c/d.php',
-            '/c/d.php.translations/it_IT.php',
-            '/c/d.php.translations/en_US.php',
-        ]);
-
-        $lang = ResourceLanguage::of('it_IT');
-        $translations = $result->getTranslations($lang);
-
-        $expected = [
-            '/a/b.php',
-            '/c/d.php',
-        ];
-
-        $this->assertEquals(
-            $expected,
-            $translations,
-            'unexpected translations',
-        );
-    }
-
-    public function testSplitWithLocParameter(): void
-    {
-        $splitter = new SubDirTranslationSplitter();
-        $result = $splitter->split([
-            '/a/b.php?loc=en_US',
-        ]);
-
-        $lang = ResourceLanguage::of('en_US');
-        $translations = $result->getTranslations($lang);
-
-        $expected = [
-            '/a/b.php',
-        ];
-
-        $this->assertEquals(
-            $expected,
-            $translations,
-            'unexpected translations',
-        );
-    }
-
-    public function testSplitWithOutPath(): void
-    {
-        $splitter = new SubDirTranslationSplitter();
-        $result = $splitter->split([
-            '?a=b',
-        ]);
-
-        $this->assertEquals(
-            [],
-            $result->getBases(),
-            'bases should be empty',
-        );
-    }
-
-    public function testSplitWithUnsupportedParameter(): void
-    {
-        $splitter = new SubDirTranslationSplitter();
-        $result = $splitter->split([
-            '/test.php?a=b',
-        ]);
-
-        $this->assertEquals(
-            ['/test.php'],
-            $result->getBases(),
-            'unexpected bases',
-        );
-    }
-}
diff --git a/test/Service/Indexer/TranslationSplitterResultTest.php b/test/Service/Indexer/TranslationSplitterResultTest.php
deleted file mode 100644
index 1c9b663..0000000
--- a/test/Service/Indexer/TranslationSplitterResultTest.php
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-namespace Atoolo\Search\Test\Service\Indexer;
-
-use Atoolo\Resource\ResourceLanguage;
-use Atoolo\Resource\ResourceLocation;
-use Atoolo\Search\Service\Indexer\TranslationSplitterResult;
-use PHPUnit\Framework\Attributes\CoversClass;
-use PHPUnit\Framework\TestCase;
-
-#[CoversClass(TranslationSplitterResult::class)]
-class TranslationSplitterResultTest extends TestCase
-{
-    public function testGetBases(): void
-    {
-        $result = new TranslationSplitterResult(
-            [
-                ResourceLocation::of('/a/b.php'),
-            ],
-            [],
-        );
-        $this->assertEquals(
-            [
-                ResourceLocation::of('/a/b.php'),
-            ],
-            $result->getBases(),
-            'unexpected bases',
-        );
-    }
-
-    public function testLanguages(): void
-    {
-        $it = ResourceLanguage::of('it_IT');
-        $en = ResourceLanguage::of('en_US');
-
-        $result = new TranslationSplitterResult(
-            [],
-            [
-                $it->code => [ResourceLocation::of('/a/b.php', $it)],
-                $en->code => [ResourceLocation::of('/a/b.php', $en)],
-            ],
-        );
-
-        $this->assertEquals(
-            [$en, $it],
-            $result->getLanguages(),
-            'unexpected languages',
-        );
-    }
-
-    public function testGetTranslations(): void
-    {
-        $it = ResourceLanguage::of('it_IT');
-        $en = ResourceLanguage::of('en_US');
-        $result = new TranslationSplitterResult(
-            [],
-            [
-                $it->code => [
-                    ResourceLocation::of('/a/b.php', $it),
-                    ResourceLocation::of('/c/d.php', $it),
-                ],
-                $en->code => [
-                    ResourceLocation::of('/a/b.php', $en),
-                    ResourceLocation::of('/a/b.php', $en),
-                ],
-            ],
-        );
-
-        $translations = $result->getTranslations($it);
-
-        $expected = [
-            ResourceLocation::of('/a/b.php', $it),
-            ResourceLocation::of('/c/d.php', $it),
-        ];
-
-        $this->assertEquals(
-            $expected,
-            $translations,
-            'unexpected translations',
-        );
-    }
-    public function testGetMissingTranslations(): void
-    {
-        $result = new TranslationSplitterResult([], []);
-        $lang = ResourceLanguage::of('en_US');
-
-        $this->assertEquals(
-            [],
-            $result->getTranslations($lang),
-            'empty array expected',
-        );
-    }
-}
diff --git a/var/cache/.gitkeep b/var/cache/.gitkeep
deleted file mode 100644
index e69de29..0000000


Reviews:
user: login: sitepark-veltrup
body: Bring code coverage to 100%
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T14:48:03Z
user: login: sitepark-veltrup
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T09:08:53Z

Review comments:
diff_hunk: @@ -498,4 +490,82 @@ private function deleteErrorProtocol(): void
             'crawl_status:error OR crawl_status:warning',
         );
     }
+
+    /**
+     * Sorts the pathList by the translated locals within the path:
+     * `/dir/file.php` is lover tan
+     * `/dir/anotherFile.php.translations/de_DE.php` is lover tan
path: src/Service/Indexer/InternalResourceIndexer.php
user: login: sitepark-veltrup
body: ```suggestion
     * `/dir/file.php` is lower than
     * `/dir/anotherFile.php.translations/de_DE.php` is lower than
```
created_at: 2025-04-09T14:35:19Z
author_association: MEMBER
diff_hunk: @@ -498,4 +490,82 @@ private function deleteErrorProtocol(): void
             'crawl_status:error OR crawl_status:warning',
         );
     }
+
+    /**
+     * Sorts the pathList by the translated locals within the path:
+     * `/dir/file.php` is lover tan
+     * `/dir/anotherFile.php.translations/de_DE.php` is lover tan
+     * `/dir/file.php.translations/it_IT.php` is equal to
+     * `/dir/nextFile.php.translations/it_IT.php`
+     *
+     * @param array<string> $pathList
+     */
+    private function sortPathlistByLocale(array &$pathList): void
path: src/Service/Indexer/InternalResourceIndexer.php
user: login: sitepark-veltrup
body: I find it cleaner if `sortPathlistByLocale()` returns a modified list:
```php
private function sortPathlistByLocale(array $pathList): array
```
created_at: 2025-04-09T14:36:38Z
author_association: MEMBER
diff_hunk: @@ -251,52 +250,15 @@ private function indexResources(
             return;
         }
 
-        $splitterResult = $this->translationSplitter->split($pathList);
-        $this->indexTranslationSplittedResources(
-            $parameter,
-            $splitterResult,
-        );
-    }
-
-    /**
-     * There is a separate Solr index for each language. This allows
-     * language-specific tokenizers and other language-relevant configurations
-     * to be used. Via the `$splitterResult` all paths are separated according
-     * to their languages and can be indexed separately. Each language is
-     * indexed separately here.
-     */
-    private function indexTranslationSplittedResources(
-        IndexerParameter $parameter,
-        TranslationSplitterResult $splitterResult,
-    ): void {
+        $this->sortPathlistByLocale($pathList);
 
         $processId = uniqid('', true);
 
-        $index = $this->indexService->getIndex(ResourceLanguage::default());
-
         $this->indexResourcesPerLanguageIndex(
path: src/Service/Indexer/InternalResourceIndexer.php
user: login: sitepark-veltrup
body: I don't think the method name `indexResourcesPerLanguageIndex()` fits anymore.
created_at: 2025-04-09T14:38:26Z
author_association: MEMBER
diff_hunk: @@ -318,29 +278,57 @@ private function indexResourcesPerLanguageIndex(
 
         $offset = 0;
         $successCount = 0;
-
+        $alreadyTreatedMissingIndices = [];
+        $updatedIndexLocales = [];
         $managedIndices = $this->indexService->getManagedIndices();
-        if (!in_array($index, $managedIndices)) {
-            $this->handleError('Index "' . $index . '" not found');
-            return;
-        }
 
         while (true) {
-            $indexedCount = $this->indexChunks(
-                $processId,
-                $lang,
-                $index,
+
+            $resourceLists = $this->loadResources(
                 $locations,
                 $offset,
                 $parameter->chunkSize,
             );
+            if ($resourceLists === false) {
+                break;
+            }
 
-            gc_collect_cycles();
+            foreach ($resourceLists as $resourceLang => $resourceList) {
+                $lang = ResourceLanguage::of($resourceLang);
+                $index = null;
+                try {
+                    $index = $this->indexService->getIndex($lang);
+                    if (!in_array($index, $managedIndices)) {
+                        if (!in_array($lang, $alreadyTreatedMissingIndices)) {
+                            $this->handleError('Unmanaged Index: ' . $index);
+                            $alreadyTreatedMissingIndices[] = $lang;
+                        }
+                        continue;
+                    }
+                } catch (UnsupportedIndexLanguageException $e) {
+                    if (!in_array($lang, $alreadyTreatedMissingIndices)) {
+                        $this->handleError($e->getMessage());
+                        $alreadyTreatedMissingIndices[] = $lang;
+                    }
+                    continue;
+                }
 
-            if ($indexedCount === false) {
-                break;
+                $indexedCount = $this->indexChunks(
+                    $processId,
+                    $lang,
+                    $index,
+                    $resourceList,
+                );
+                gc_collect_cycles();
+
+                if ($indexedCount === false) {
+                    continue;
+                }
+                $successCount += $indexedCount;
+                if (!in_array($lang, $updatedIndexLocales)) {
+                    $updatedIndexLocales[] = $lang;
+                }
path: src/Service/Indexer/InternalResourceIndexer.php
user: login: sitepark-veltrup
body: Better to use a separate method to reduce complexity.
created_at: 2025-04-09T14:43:03Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-10T07:30:21Z
body: ## [Codecov](https://app.codecov.io/gh/sitepark/atoolo-search-bundle/pull/36?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sitepark) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`2b34e23`)](https://app.codecov.io/gh/sitepark/atoolo-search-bundle/commit/2b34e23f55634d54f3c5838ae51ef07e9ccdfdfa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sitepark) to head [(`13a7dfc`)](https://app.codecov.io/gh/sitepark/atoolo-search-bundle/commit/13a7dfcc4b2c310a5bc85dd36dc83bc926e4948f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sitepark).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##                main       #36   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
  Complexity       736       736           
===========================================
  Files             61        59    -2     
  Lines           2759      2738   -21     
===========================================
- Hits            2759      2738   -21     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sitepark/atoolo-search-bundle/pull/36?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sitepark).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sitepark).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: sitepark-hying
created_at: 2025-04-10T07:33:58Z
body: ok, all points are now implemented.
author_association: CONTRIBUTOR
user: login: sitepark-veltrup
created_at: 2025-04-10T09:10:43Z
body: Can you build an E2E test for it after the merge?
https://github.com/sitepark/atoolo-e2e-test
author_association: MEMBER