Работа с разными алфавитами

При работе с автодополнением на основе Awesomplete в среде с многоалфавитными наборами данных основная сложность возникает не в рендеринге списка, а в нормализации и сравнении строк. Разные системы письма обладают различной степенью формальной и фонетической совместимости, а стандартное посимвольное сравнение JavaScript не учитывает ни диакритические знаки, ни особенности регистровой чувствительности, ни варианты представления символов Unicode.

Любая реализация поиска в Awesomplete, работающая с несколькими алфавитами, должна начинаться с приведения строк к единой нормальной форме Unicode. В противном случае визуально идентичные строки могут иметь разные бинарные представления.

На практике используется NFC или NFD, но для поисковых сценариев чаще применяется NFKD, так как он дополнительно разлагает совместимые символы:

function normalize(str) {
    return str
        .normalize('NFKD')
        .toLowerCase();
}

Такой слой позволяет корректно обрабатывать случаи с диакритикой: ée + ́, что упрощает последующее сравнение.

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

Переопределение фильтра Awesomplete для Unicode-данных

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

Расширение фильтра реализуется через опцию filter:

Awesomplete.prototype.filter = function (text, input) {
    const nText = normalize(text);
    const nInput = normalize(input);

    return nText.includes(nInput);
};

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

Транслитерация как слой межалфавитного сопоставления

При работе с данными, где одновременно присутствуют кириллица и латиница, возникает необходимость поиска по фонетическому эквиваленту. Например, ввод kazan должен находить Казань.

Решение заключается в предварительном преобразовании строки в транслитерированную форму:

const map = {
    'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g',
    'д': 'd', 'е': 'e', 'ё': 'e', 'ж': 'zh',
    'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k'
};

function transliterate(str) {
    return str
        .toLowerCase()
        .split('')
        .map(ch => map[ch] || ch)
        .join('');
}

И затем комбинированный фильтр:

Awesomplete.prototype.filter = function (text, input) {
    const t1 = transliterate(normalize(text));
    const t2 = transliterate(normalize(input));

    return t1.includes(t2);
};

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

Работа с направлением письма и RTL-алфавитами

При использовании арабского или иврита возникает проблема не только сравнения, но и визуального отображения. Awesomplete по умолчанию не управляет направлением текста, поэтому список может отображаться корректно, но ввод и подсветка совпадений требуют CSS-адаптации.

Ключевой параметр — direction:

.awesomplete {
    direction: rtl;
    text-align: right;
}

Однако этого недостаточно, если данные смешанные. В таких случаях направление задаётся на уровне элемента:

function detectRTL(text) {
    return /[\u0591-\u07FF]/.test(text);
}

И применяется динамически:

input.addEventListener('input', function () {
    input.dir = detectRTL(input.value) ? 'rtl' : 'ltr';
});

Кастомизация сортировки для разных алфавитов

Алгоритм сортировки по умолчанию в Awesomplete не учитывает языковую специфику. Для корректной работы с Unicode требуется использование localeCompare.

Awesomplete.prototype.sort = function (a, b) {
    return a.label.localeCompare(b.label, 'ru');
};

Для мультиалфавитных данных предпочтительно использовать нейтральный режим:

Awesomplete.prototype.sort = function (a, b) {
    return a.label.localeCompare(b.label, undefined, {
        sensitivity: 'base'
    });
};

Это снижает влияние диакритики и различий регистра.

Подсветка совпадений в разных системах письма

Стандартная подсветка Awesomplete опирается на индекс подстроки. В Unicode-контексте это может приводить к разрыву символов в комбинированных графемах.

Более устойчивый вариант — подсветка по нормализованной строке с сохранением исходного текста:

Awesomplete.prototype.item = function (text, input) {
    const html = text.label.replace(
        new RegExp(input, 'i'),
        match => `<mark>${match}</mark>`
    );

    const li = document.createElement('li');
    li.innerHTML = html;
    return li;
};

Однако в многоалфавитной среде необходимо использовать нормализованный input:

const safeInput = normalize(input);

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

Обработка комбинированных символов и grapheme clusters

JavaScript-строки оперируют UTF-16 кодовыми единицами, а не графемами. Это приводит к ошибкам при работе с эмодзи и сложными символами (например, индийские слоги или комбинированные диакритики).

Для корректного разбиения используется Intl.Segmenter:

const segmenter = new Intl.Segmenter('und', { granularity: 'grapheme' });

function splitGraphemes(str) {
    return [...segmenter.segment(str)].map(s => s.segment);
}

Это позволяет избегать разрыва символов при подсветке и сравнении.

Унификация источников данных в Awesomplete

При работе с разными алфавитами важно стандартизировать структуру входных данных. Awesomplete допускает как строки, так и объекты:

const list = [
    { label: 'Москва', value: 'moscow' },
    { label: 'München', value: 'munich' },
    { label: 'القاهرة', value: 'cairo' }
];

В таких случаях нормализация должна применяться только к label, сохраняя value без изменений.

function prepareItem(item) {
    return {
        label: item.label,
        value: item.value,
        norm: normalize(transliterate(item.label))
    };
}

И фильтр работает уже по norm:

Awesomplete.prototype.filter = function (text, input) {
    const nInput = normalize(transliterate(input));
    return text.norm.includes(nInput);
};

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

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

Решением является токенизация с разделением по Unicode-блокам:

function tokenize(str) {
    return str.match(/[\p{L}\p{N}]+/gu) || [];
}

Далее поиск осуществляется по пересечению токенов:

function match(item, input) {
    const iTokens = tokenize(normalize(input));
    const tTokens = tokenize(item.norm);

    return iTokens.every(t =>
        tTokens.some(x => x.includes(t))
    );
}

Итоговая архитектура слоя многоалфавитного поиска

В стабильной конфигурации Awesomplete в среде Unicode опирается на несколько последовательных уровней обработки:

  1. Unicode-нормализация
  2. Транслитерация (при необходимости)
  3. Токенизация
  4. Сравнение по нормализованным токенам
  5. Сортировка через localeCompare
  6. Отдельная обработка RTL и комбинированных графем

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