From 47cb398a471b23795c7351cf2ea9b1ab9ae979b5 Mon Sep 17 00:00:00 2001 From: Audrius Date: Sat, 27 Jun 2026 04:11:07 +0200 Subject: [PATCH 1/2] Sort filter values in a locale aware way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature filter values were ordered with natcasesort(), which compares strings byte by byte. Multi-byte accented characters (Czech č, š, ř, etc.) therefore sorted after every plain ASCII letter instead of next to their base letter, so a value like "Černá" ended up at the bottom of the list rather than near "Czech". Use a Collator built from the current language locale when the intl extension is available, with a secondary strength (case insensitive, like before) and numeric collation (to keep the natural 2-before-10 ordering). Fall back to natcasesort() when intl is not present. --- src/Filters/Block.php | 36 +++++++++++++++- tests/php/FacetedSearch/Filters/BlockTest.php | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/Filters/Block.php b/src/Filters/Block.php index 1d2bf5ae2..0e503009b 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,31 @@ 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; + } + + $collator = collator_create($this->context->language->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 From f51ef4d0137ad0b28b8cdbf39828c893fb19ddd0 Mon Sep 17 00:00:00 2001 From: Audrius Date: Tue, 7 Jul 2026 21:11:45 +0300 Subject: [PATCH 2/2] Guard the values collator against a missing language locale getValuesCollator() read $this->context->language->locale unconditionally, so any call path where the language carries no locale raised an Undefined property notice (surfaced by the existing BlockTest fixtures, whose context language has no locale). Guard the access and fall back to natcasesort when no locale is set, which also protects a real store whose language has an empty locale configured. --- src/Filters/Block.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Filters/Block.php b/src/Filters/Block.php index 0e503009b..e2523c151 100644 --- a/src/Filters/Block.php +++ b/src/Filters/Block.php @@ -947,7 +947,13 @@ private function getValuesCollator() return null; } - $collator = collator_create($this->context->language->locale); + $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; }