Сортировка с учетом локали

Сортировка подсказок в Awesomplete по умолчанию основана на сравнении строк без учета языковых особенностей, что приводит к заметным искажениям при работе с кириллицей, диакритикой и различными алфавитами. Базовая логика сравнения ориентирована на ASCII-подобные строки, поэтому порядок элементов часто выглядит случайным с точки зрения пользователя, особенно в многоязычных интерфейсах.

Внутри Awesomplete процесс формирования списка проходит через два ключевых этапа: фильтрацию и сортировку. После того как набор данных проходит через filter, применяется функция sort, которая по умолчанию выглядит как простое лексикографическое сравнение:

sort: function(a, b) {
    return (a.label || a).localeCompare(b.label || b);
}

На практике это уже лучше, чем обычное сравнение оператором >, но поведение localeCompare без параметров не гарантирует корректной локализации. В частности:

  • регистр может влиять на порядок;
  • диакритические знаки учитываются неочевидным образом;
  • кириллица сортируется по Unicode-порядку, а не по алфавиту языка;
  • смешанные наборы символов (латиница + кириллица) дают нестабильный результат.

Проблема языковой неоднородности

В реальных интерфейсах список автодополнения почти всегда содержит:

  • имена пользователей;
  • географические названия;
  • товары с локализованными названиями;
  • транслитерации и смешанные строки.

Без учета локали сортировка превращается в технический порядок кодовых точек Unicode, что нарушает ожидаемую когнитивную модель пользователя.

Например, для русского языка корректный порядок:

Аарон
Алексей
Борис
Виктор
Глеб

Но при стандартной сортировке возможны смещения, особенно при наличии латинских символов или вариативных регистров.

Использование localeCompare с локалью

Первый уровень исправления — явное указание локали:

const compareRu = (a, b) =>
    (a.label || a).localeCompare(b.label || b, 'ru');

Это уже включает правила русского языка, включая:

  • правильный алфавитный порядок;
  • учет специфики букв ё, е;
  • корректную обработку регистра в рамках локали.

Однако даже этого недостаточно для сложных интерфейсов.

Нормализация регистра и чувствительности

Для унификации сортировки обычно добавляют параметры чувствительности:

const compareRu = (a, b) =>
    (a.label || a).localeCompare((b.label || b), 'ru', {
        sensitivity: 'base'
    });

Параметр sensitivity: 'base' отключает различие между:

  • а и А;
  • е и ё (частично, зависит от движка);
  • буквами с диакритикой и без них.

Это делает сортировку более предсказуемой в пользовательских интерфейсах.

Использование Intl.Collator

Более производительный и гибкий подход — использование Intl.Collator. Он создается один раз и затем переиспользуется, что критично при больших списках.

const collator = new Intl.Collator('ru', {
    sensitivity: 'base',
    numeric: true
});

Параметр numeric: true добавляет естественную сортировку чисел внутри строк:

item 2
item 10
item 20

без превращения в:

item 10
item 2
item 20

Интеграция с Awesomplete

Awesomplete позволяет переопределять поведение сортировки через опцию sort. Это основной механизм подключения локализованной логики.

const collator = new Intl.Collator('ru', {
    sensitivity: 'base',
    numeric: true
});

new Awesomplete(input, {
    list: data,
    sort: function(a, b) {
        return collator.compare(a.label || a, b.label || b);
    }
});

В этом варианте сортировка становится:

  • стабильной;
  • предсказуемой;
  • независимой от браузерной реализации localeCompare.

Сортировка объектов с метаданными

В реальных сценариях элементы списка редко являются строками. Чаще используются структуры:

[
    { label: "Москва", value: "MSK" },
    { label: "Магадан", value: "MAG" },
    { label: "Минск", value: "MSQ" }
]

В этом случае важно явно выделить поле сортировки:

const collator = new Intl.Collator('ru', {
    sensitivity: 'base'
});

function sortCities(a, b) {
    return collator.compare(a.label, b.label);
}

И подключение:

new Awesomplete(input, {
    list: cities,
    sort: sortCities
});

Учет смешанных языков

При наличии мультиязычных данных (например, русский + английский) появляется дополнительная проблема: выбор базовой локали.

Типичный подход:

const collator = new Intl.Collator(undefined, {
    sensitivity: 'base'
});

Передача undefined заставляет браузер использовать системную локаль пользователя. Это дает более естественный порядок для конечного пользователя, но снижает предсказуемость на серверной стороне.

Альтернативный подход — фиксированная локаль интерфейса:

const collator = new Intl.Collator('en', {
    sensitivity: 'base'
});

или

const collator = new Intl.Collator('ru', {
    sensitivity: 'base'
});

Стабильность сортировки и побочные эффекты

Важный аспект — стабильность алгоритма. Awesomplete не гарантирует стабильную сортировку при равных значениях. Если collator.compare(a, b) === 0, порядок элементов может зависеть от исходного массива.

Для стабилизации добавляют вторичный ключ:

function stableSort(a, b) {
    const primary = collator.compare(a.label, b.label);
    if (primary !== 0) return primary;

    return a.label.localeCompare(b.label);
}

Или более универсально:

function stableSort(a, b, indexMap) {
    const primary = collator.compare(a.label, b.label);
    return primary !== 0 ? primary : indexMap[a.id] - indexMap[b.id];
}

Производительность при больших списках

При тысячах элементов вызов localeCompare внутри сортировки становится узким местом. Intl.Collator решает проблему за счет предварительной компиляции правил сравнения.

Оптимизированный вариант:

const collator = new Intl.Collator('ru', {
    sensitivity: 'base'
});

const sortFn = collator.compare.bind(collator);

И использование:

new Awesomplete(input, {
    list: largeDataset,
    sort: sortFn
});

Связь с фильтрацией

Сортировка тесно связана с фильтрацией. Awesomplete сначала отбрасывает неподходящие элементы, затем сортирует оставшиеся. Это означает, что оптимизация сортировки напрямую влияет на UX автодополнения.

Комбинация фильтра и сортировки:

const collator = new Intl.Collator('ru', {
    sensitivity: 'base'
});

new Awesomplete(input, {
    list: data,

    filter: function(text, input) {
        return collator
            .compare(text.slice(0, input.length), input) === 0;
    },

    sort: function(a, b) {
        return collator.compare(a.label, b.label);
    }
});

Такой подход делает поведение списка ближе к языковым ожиданиям пользователя, а не к технической сортировке строк.