diff --git a/src/Filters/Block.php b/src/Filters/Block.php index 1d2bf5ae2..e2523c151 100644 --- a/src/Filters/Block.php +++ b/src/Filters/Block.php @@ -906,6 +906,8 @@ private function getFeaturesBlock($filter, $selectedFilters, $idLang) */ private function sortFeatureBlock($featureBlock) { + $collator = $this->getValuesCollator(); + //Natural sort foreach ($featureBlock as $key => $value) { $temp = []; @@ -913,7 +915,14 @@ private function sortFeatureBlock($featureBlock) $temp[$idFeatureValue] = $featureValueInfos['name']; } - natcasesort($temp); + if ($collator !== null) { + // Locale aware sort so accented characters (e.g. Czech č, š, ř) are ordered + // according to the current language rules instead of by their raw byte value, + // which pushed them after every plain ASCII letter. + $collator->asort($temp); + } else { + natcasesort($temp); + } $temp2 = []; foreach ($temp as $keytemp => $valuetemp) { @@ -926,6 +935,37 @@ private function sortFeatureBlock($featureBlock) return $featureBlock; } + /** + * Builds a locale aware collator for sorting filter values, or null when the intl + * extension is not available (the caller then falls back to natcasesort()). + * + * @return \Collator|null + */ + private function getValuesCollator() + { + if (!class_exists('Collator')) { + return null; + } + + $locale = isset($this->context->language->locale) ? $this->context->language->locale : ''; + if ($locale === '') { + // No usable locale (e.g. a language without one configured); fall back to natcasesort. + return null; + } + + $collator = collator_create($locale); + if ($collator === null) { + return null; + } + + // Case insensitive (as natcasesort was) but accent and locale aware. + $collator->setStrength(\Collator::SECONDARY); + // Keep the "natural" numeric ordering natcasesort provided (e.g. 2 before 10). + $collator->setAttribute(\Collator::NUMERIC_COLLATION, \Collator::ON); + + return $collator; + } + /** * Add the categories filter condition based on the parent and config variables * diff --git a/tests/php/FacetedSearch/Filters/BlockTest.php b/tests/php/FacetedSearch/Filters/BlockTest.php index c272a9aec..9c87fa8bb 100644 --- a/tests/php/FacetedSearch/Filters/BlockTest.php +++ b/tests/php/FacetedSearch/Filters/BlockTest.php @@ -1189,6 +1189,48 @@ private function mockLayeredCategory($result) ->andReturn($result); } + /** + * Feature filter values must be sorted in a locale aware way: accented characters such as the + * Czech "Č" have to be ordered next to their base letter instead of after every ASCII letter. + * + * @see https://github.com/PrestaShop/ps_facetedsearch/issues/1201 + */ + public function testSortFeatureBlockSortsValuesLocaleAware() + { + if (!class_exists('Collator')) { + $this->markTestSkipped('The intl extension is required for locale aware sorting.'); + } + + $this->contextMock->language->locale = 'cs-CZ'; + + $featureBlock = [ + 10 => [ + 'values' => [ + 1 => ['name' => 'Value'], + 2 => ['name' => 'Else'], + 3 => ['name' => 'Beta'], + 4 => ['name' => 'Černá'], + 5 => ['name' => 'Czech'], + 6 => ['name' => 'Other'], + 7 => ['name' => 'Alpha'], + ], + ], + ]; + + $method = new \ReflectionMethod(Block::class, 'sortFeatureBlock'); + $method->setAccessible(true); + $sorted = $method->invoke($this->block, $featureBlock); + + $names = array_values(array_map(function ($value) { + return $value['name']; + }, $sorted[10]['values'])); + + $this->assertSame( + ['Alpha', 'Beta', 'Czech', 'Černá', 'Else', 'Other', 'Value'], + $names + ); + } + private function getPriceSpecificationMock() { // Mock symbols