Учет регистра и диакритических знаков

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

Стандарт ECMAScript решает эту задачу через Internationalization API, где центральную роль играет механизм коллации (collation), реализованный в Intl.Collator.


Unicode, нормализация и базовая проблема сравнения

Строки в JavaScript представлены в UTF-16, но визуально одинаковые символы могут кодироваться по-разному.

Пример:

"é" !== "e\u0301"

Первый вариант — предкомпозированный символ (NFC), второй — буква e + комбинирующий акцент (NFD). С точки зрения пользователя они идентичны, но побайтовое сравнение даёт различие.

Для устранения подобных расхождений применяется нормализация Unicode:

const a = "é";
const b = "e\u0301";

a === b; // false
a.normalize("NFC") === b.normalize("NFC"); // true

Однако нормализация решает только проблему представления, но не семантики сравнения, где важны регистр и диакритика.


Intl.Collator как основа языкового сравнения

Intl.Collator предоставляет механизм локализованного сравнения строк с учётом языковых правил.

const collator = new Intl.Collator("en");
collator.compare("a", "b"); // -1

Главное отличие от обычного сравнения — использование языковых правил, а не кодовых точек.


Учет регистра: параметр sensitivity

Регистрозависимость управляется параметром sensitivity. Он определяет, насколько различия в регистре влияют на сравнение строк.

Значения sensitivity

  • base — игнорируются диакритика и регистр
  • accent — учитываются диакритика, но игнорируется регистр
  • case — учитывается регистр, но игнорируются диакритика
  • variant — учитываются и регистр, и диакритика (максимальная строгость)

Поведение sensitivity = “base”

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

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

collator.compare("a", "A"); // 0
collator.compare("cafe", "café"); // 0

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


Поведение sensitivity = “accent”

Диакритика учитывается, но регистр игнорируется:

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

collator.compare("cafe", "café"); // -1
collator.compare("A", "a"); // 0

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


Поведение sensitivity = “case”

Регистр становится значимым фактором, но диакритика игнорируется:

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

collator.compare("a", "A"); // -1
collator.compare("cafe", "café"); // 0

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


Поведение sensitivity = “variant”

Максимально строгий режим сравнения:

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

collator.compare("a", "A"); // -1
collator.compare("cafe", "café"); // -1

Учитываются все различия: регистр, диакритика и дополнительные языковые вариации.


Диакритические знаки и их семантика

Диакритики (акценты, умляуты, тильды) в разных языках могут:

  • изменять произношение (é vs e)
  • изменять значение слова
  • быть орфографическими вариантами

Примеры:

  • café / cafe (французский и английский контексты)
  • naïve / naive
  • São / Sao

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


Локаль и языковая специфика сравнения

Разные языки по-разному трактуют регистр и диакритические знаки.

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

В немецком языке:

  • ä может рассматриваться как ae в сортировке
  • ß может сопоставляться с ss

В шведском:

  • å, ä, ö имеют собственный порядок сортировки

localeCompare как альтернатива Collator

Метод localeCompare использует те же механизмы, что и Intl.Collator, но в более компактной форме.

"a".localeCompare("A", "en", { sensitivity: "base" }); // 0

Поведение полностью эквивалентно:

const collator = new Intl.Collator("en", { sensitivity: "base" });
collator.compare("a", "A");

Практика игнорирования регистра и диакритики

В реальных приложениях часто требуется поиск без учета регистра и акцентов.

Пример фильтрации массива:

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

const data = ["Ёж", "еж", "ель", "яблоко"];

const result = data.filter(item => collator.compare(item, "ЕЖ") === 0);

Здесь строки считаются равными независимо от регистра и диакритики.


Использование sensitivity в сортировке

Коллация применяется не только для сравнения, но и для сортировки:

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

const words = ["Zebra", "apple", "Álpha", "banana"];

words.sort(collator.compare);

Результат зависит от выбранной локали и уровня чувствительности, а не от Unicode-порядка.


Игнорирование регистра при сохранении диакритики

Частый сценарий — сортировка без учета регистра, но с сохранением акцентов:

const collator = new Intl.Collator("fr", { sensitivity: "accent" });

Здесь:

  • e и E считаются равными
  • e и é различаются

Это соответствует французским орфографическим правилам, где акценты несут смысловую нагрузку, но регистр вторичен.


Турецкий язык и особый случай регистра

Турецкая локаль демонстрирует важность языковых правил:

  • iİ
  • Iı
const collator = new Intl.Collator("tr", { sensitivity: "case" });

Простое приведение к нижнему регистру (toLowerCase) здесь даёт некорректные результаты, поэтому используется только локализованная коллация.


Диакритика и поиск по пользовательскому вводу

При реализации поиска часто требуется «мягкое» сравнение:

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

function matches(query, value) {
  return collator.compare(query, value) === 0;
}

Такой подход позволяет игнорировать:

  • регистр
  • акценты
  • вариативность написания

Сравнение через numeric и punctuation

Хотя основной фокус — регистр и диакритика, на поведение влияет и дополнительная настройка:

new Intl.Collator("en", {
  sensitivity: "base",
  numeric: true,
  ignorePunctuation: true
});

numeric: true изменяет сравнение числовых подстрок:

  • “file2” < “file10”

ignorePunctuation: true исключает знаки препинания из сравнения.


Взаимодействие нормализации и Collator

Intl.Collator работает поверх Unicode, но не заменяет нормализацию.

Корректный подход:

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

function compare(a, b) {
  return collator.compare(a.normalize("NFC"), b.normalize("NFC"));
}

Это особенно важно при работе с внешними источниками данных, где форма Unicode может быть нестабильной.


Поведение variant как строгая модель сравнения

Режим variant используется в ситуациях, где требуется точное различие всех символов:

  • криптографические идентификаторы
  • внутренние ключи систем
  • строгая валидация строк
const collator = new Intl.Collator("en", { sensitivity: "variant" });

Любое отличие, включая регистр и акценты, приводит к неравенству.


Итоговая логика сравнения строк в Intl API

Механизм обработки регистра и диакритики в Intl строится на трёх уровнях:

  • Unicode-нормализация
  • локализованные правила коллации
  • параметры чувствительности (sensitivity)

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