Работа с диакритическими знаками

Работа с диакритикой в интерфейсах выбора напрямую влияет на качество поиска и удобство пользователей, использующих языки с акцентами, умляутами, тильдами и другими модификаторами символов. В контексте Slim Select ключевая задача заключается в корректной нормализации строк перед сравнением, чтобы поисковый алгоритм не различал символы с диакритикой и без неё.

В основе поведения поиска Slim Select лежит строковое сопоставление значений опций с пользовательским вводом. Без дополнительной обработки символы é, è, ê, ë будут восприниматься как отличные от e, что приводит к снижению релевантности результатов.


Проблема различий между «базовыми» и «диакритическими» символами

В Unicode каждый символ с диакритикой может быть представлен либо как единый кодовый пункт (precomposed form), либо как комбинация базового символа и модификатора (decomposed form). Например:

  • é (U+00E9)
  • e + ́ (U+0065 + U+0301)

Для алгоритма поиска это два разных набора байтов, хотя визуально они эквивалентны.

В Slim Select, если не применять нормализацию, поиск по строке:

resume

не найдет:

résumé

что критично для мультиязычных интерфейсов.


Unicode нормализация как базовый инструмент обработки

Основной механизм унификации строк — String.prototype.normalize().

На практике используется форма NFD (Canonical Decomposition) или NFKD (Compatibility Decomposition).

Пример приведения строки к базовому виду

function normalizeString(str) {
  return str
    .normalize('NFD')
    .replace(/\p{Diacritic}/gu, '');
}

Здесь выполняются два ключевых шага:

  • декомпозиция символов в базовые компоненты;
  • удаление диакритических меток через Unicode property escape \p{Diacritic}.

Результат:

résumé → resume

Интеграция нормализации в Slim Select

Slim Select позволяет переопределять поведение поиска через конфигурацию searchText или кастомные обработчики фильтрации.

Стандартная модель поиска основана на приведении строк к нижнему регистру и проверке вхождения подстроки. Для корректной работы с диакритикой необходимо расширить этот слой.

Базовая стратегия переопределения поиска

new SlimSelect({
  select: '#select',
  searchText: (search, text) => {
    const normalize = (str) =>
      str.toLowerCase()
         .normalize('NFD')
         .replace(/\p{Diacritic}/gu, '');

    return normalize(text).includes(normalize(search));
  }
});

Такая реализация обеспечивает:

  • игнорирование регистра;
  • игнорирование диакритических знаков;
  • единообразное сравнение строк.

Поведение при различных языковых наборах

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

  • французский: é, è, ê, ë
  • немецкий: ä, ö, ü, ß
  • испанский: ñ, á, é
  • турецкий: ğ, ı, ş

После нормализации поведение поиска становится унифицированным:

Ввод Оригинал Совпадение
cafe café да
koln köln да
ano año да
gross groß да

Учет производительности при нормализации

Нормализация строк — операция не нулевая по стоимости. При большом количестве опций (тысячи и десятки тысяч элементов) прямое выполнение .normalize() на каждый ввод может стать узким местом.

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

Кэширование нормализованных данных

const cache = new Map();

function normalizeCached(str) {
  if (cache.has(str)) return cache.get(str);

  const normalized = str
    .toLowerCase()
    .normalize('NFD')
    .replace(/\p{Diacritic}/gu, '');

  cache.set(str, normalized);
  return normalized;
}

Применение в Slim Select:

new SlimSelect({
  select: '#select',
  searchText: (search, text) => {
    return normalizeCached(text)
      .includes(normalizeCached(search));
  }
});

Предобработка данных опций

Более эффективный подход — нормализовать данные один раз при инициализации списка.

const data = [
  { text: 'Café', value: '1' },
  { text: 'Résumé', value: '2' }
];

const preparedData = data.map(item => ({
  ...item,
  normalizedText: item.text
    .toLowerCase()
    .normalize('NFD')
    .replace(/\p{Diacritic}/gu, '')
}));

Далее поиск работает только с normalizedText:

new SlimSelect({
  select: '#select',
  data: preparedData,
  searchText: (search, text, data) => {
    const normalizedSearch = normalizeCached(search);
    return data.normalizedText.includes(normalizedSearch);
  }
});

Особенности работы с комбинированными символами

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

  • отдельными символами;
  • частью лигатур;
  • результатом визуальных преобразований.

Например:

ẞ → ss (в некоторых нормализациях)

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


Проблемы обратной совместимости

Использование Unicode property escapes (\p{Diacritic}) требует поддержки ECMAScript 2018+. В старых окружениях браузеров это может приводить к ошибкам.

Альтернативная реализация через диапазоны Unicode:

function removeDiacritics(str) {
  return str
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '');
}

Диапазон \u0300-\u036f охватывает большинство комбинируемых диакритических знаков.


Поведение поиска при частичном совпадении

Slim Select по умолчанию ориентирован на подстрочное совпадение. После нормализации это поведение становится более предсказуемым, но может привести к расширению результатов.

Например:

input: "res"
matches:
- résumé
- reseda
- response

Для ограничения результата используется дополнительная логика:

return normalize(text).startsWith(normalize(search));

Влияние регистронезависимости и диакритики одновременно

Совмещение двух преобразований:

  • toLowerCase()
  • удаление диакритики

создает единый слой сравнения, близкий к «фонетическому» поиску. Это особенно важно для интерфейсов автодополнения.

Порядок операций имеет значение:

  1. Приведение к нижнему регистру
  2. Unicode нормализация
  3. Удаление диакритики

Расширенные сценарии фильтрации

В некоторых интерфейсах требуется комбинировать несколько стратегий:

  • поиск без диакритики;
  • поиск с учетом порядка слов;
  • поиск по началу строки;
  • поиск по токенам.

Пример токенизированной фильтрации:

function tokenize(str) {
  return normalize(str).split(/\s+/);
}

function matchTokens(search, text) {
  const searchTokens = tokenize(search);
  const textTokens = tokenize(text);

  return searchTokens.every(token =>
    textTokens.some(t => t.includes(token))
  );
}

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

В многоязычных системах строки могут содержать одновременно:

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

Удаление диакритики применяется только к блокам Unicode, относящимся к комбинируемым знакам, не затрагивая остальные скрипты.

Это позволяет избежать повреждения строк вида:

Москва → остается без изменений
São Paulo → Sao Paulo

Стабилизация UX при вводе

После внедрения нормализации поиск становится менее чувствительным к ошибкам ввода:

  • отсутствие раскладки;
  • пропущенные диакритические знаки;
  • упрощённая транслитерация.

Это приводит к более устойчивому поведению автодополнения и снижает когнитивную нагрузку при поиске элементов в списках Slim Select.