Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion src/Filters/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -906,14 +906,23 @@ private function getFeaturesBlock($filter, $selectedFilters, $idLang)
*/
private function sortFeatureBlock($featureBlock)
{
$collator = $this->getValuesCollator();

//Natural sort
foreach ($featureBlock as $key => $value) {
$temp = [];
foreach ($featureBlock[$key]['values'] as $idFeatureValue => $featureValueInfos) {
$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) {
Expand All @@ -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
*
Expand Down
42 changes: 42 additions & 0 deletions tests/php/FacetedSearch/Filters/BlockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading