API Intl.Collator
Корректная сортировка и сравнение строк на разных языках
Введение
Сортировка строк в JavaScript кажется простой задачей, пока не сталкиваешься с интернациональным текстом. По умолчанию строки сравниваются по значениям кодовых точек Unicode, что приводит к ошибочным результатам для многих языков. API Intl.Collator обеспечивает сравнение строк с учётом локали, соблюдая культурные правила сортировки и корректно обрабатывая специальные символы.
Почему стандартная сортировка не работает
Рассмотрим сортировку списка немецких имён:
const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort());
// ["Ava", "Zoe", "Änder", "Ärzte"]
Такой результат неверен для носителей немецкого языка. В немецком буквы с умлаутами, такие как ä, должны сортироваться рядом с их базовой буквой a, а не в конце. Проблема в том, что JavaScript сравнивает значения кодовых точек Unicode, где Ä (U+00C4) идёт после Z (U+005A).
В разных языках свои правила сортировки. В шведском ä ставится в конец алфавита, в немецком — рядом с a, а во французском акцентированные буквы обрабатываются иначе. Бинарное сравнение игнорирует эти культурные особенности.
Как работает колlation строк
Колляция — это процесс сравнения и упорядочивания строк по языковым правилам. Алгоритм унифицированной колляции Unicode определяет, как сравнивать строки, анализируя символы, диакритические знаки, регистр и пунктуацию по отдельности.
При сравнении двух строк функция колляции возвращает число:
- Отрицательное значение: первая строка идёт перед второй
- Ноль: строки считаются эквивалентными на текущем уровне чувствительности
- Положительное значение: первая строка идёт после второй
Такое трёхстороннее сравнение работает с Array.sort и позволяет точно контролировать, какие различия важны.
Использование localeCompare для базовой сортировки с учётом локали
Метод localeCompare обеспечивает сравнение строк с учётом локали:
const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort((a, b) => a.localeCompare(b, "de")));
// ["Ava", "Änder", "Ärzte", "Zoe"]
Это обеспечивает правильную сортировку по-немецки. Первый параметр указывает локаль, а localeCompare автоматически учитывает культурные правила.
В качестве третьего параметра можно передать опции:
const items = ["File10", "File2", "File1"];
console.log(items.sort((a, b) =>
a.localeCompare(b, "en", { numeric: true })
));
// ["File1", "File2", "File10"]
Опция numeric включает естественную сортировку, при которой "2" идёт перед "10". Без неё "10" будет раньше "2", потому что "1" стоит перед "2".
Проблема с производительностью при многократном использовании localeCompare
Каждый вызов localeCompare заново обрабатывает настройки локали. При сортировке больших массивов это создаёт значительные издержки:
// Inefficient: processes locale for every comparison
const sorted = items.sort((a, b) => a.localeCompare(b, "de"));
Сортировка 1000 элементов требует примерно 10000 сравнений. Каждое сравнение пересоздаёт конфигурацию локали, что многократно увеличивает затраты на производительность. Эта нагрузка становится заметной в интерфейсах с большими наборами данных.
Эффективное сравнение строк с помощью Intl.Collator
Intl.Collator создаёт объект сравнения, который обрабатывает настройки локали один раз и может использоваться повторно:
const collator = new Intl.Collator("de");
const sorted = items.sort((a, b) => collator.compare(a, b));
Экземпляр collator хранит конфигурацию локали и правила сравнения. Метод compare использует эти заранее вычисленные правила для каждого сравнения, устраняя повторную инициализацию.
Производительность увеличивается на 60–80% при сортировке больших массивов по сравнению с многократными вызовами localeCompare.
Прямой доступ к методу compare
Метод compare можно передать напрямую в sort:
const collator = new Intl.Collator("de");
const sorted = items.sort(collator.compare);
Это работает, потому что compare привязан к экземпляру collator. Метод принимает две строки и возвращает результат сравнения, что соответствует сигнатуре, ожидаемой Array.sort.
Как работает опция sensitivity
Опция sensitivity определяет, какие различия между символами учитываются при сравнении. Есть четыре уровня:
Базовая чувствительность
Базовая чувствительность игнорирует акценты и регистр:
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("a", "b")); // -1
Отличаются только базовые буквы. Этот уровень хорошо подходит для нестрогого поиска, если пользователь может не ввести акценты.
Учет акцентов
Учет акцентов различает символы с акцентами, но игнорирует регистр:
const collator = new Intl.Collator("en", { sensitivity: "accent" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("á", "A")); // 1
Символы с акцентами и без считаются разными. Прописные и строчные буквы считаются одинаковыми.
Учет регистра
Учет регистра различает прописные и строчные буквы, но игнорирует акценты:
const collator = new Intl.Collator("en", { sensitivity: "case" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1
Различие в регистре важно, но акценты не учитываются. Такой уровень встречается реже.
Учет всех различий
Учет всех различий принимает во внимание любые отличия:
const collator = new Intl.Collator("en", { sensitivity: "variant" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1
Это стандартный режим сортировки. Любое отличие символов приводит к разному результату сравнения.
Выбор чувствительности в зависимости от задачи
Для разных сценариев нужны разные уровни чувствительности:
- Сортировка списков: используйте учет всех различий для строгого порядка
- Поиск по содержимому: используйте базовую чувствительность, чтобы находить совпадения независимо от акцентов и регистра
- Фильтрация вариантов: используйте учет акцентов, если регистр не важен
- Поиск с учетом регистра: используйте учет регистра, если акценты не важны
Опция использования задает стандартные настройки чувствительности для типовых сценариев.
Использование опции usage для режимов сортировки и поиска
Опция usage оптимизирует работу коллатора для сортировки или поиска:
// Optimized for sorting
const sortCollator = new Intl.Collator("en", { usage: "sort" });
// Optimized for searching
const searchCollator = new Intl.Collator("en", { usage: "search" });
Для сортировки по умолчанию используется учет всех различий, чтобы любое отличие влияло на порядок. Для поиска обычно применяется более мягкая чувствительность для удобства поиска совпадений.
Для поиска без учета регистра и акцентов:
const collator = new Intl.Collator("en", {
usage: "search",
sensitivity: "base"
});
const items = ["Apple", "Äpfel", "Banana"];
const matches = items.filter(item =>
collator.compare(item, "apple") === 0
);
console.log(matches); // ["Apple"]
Такой подход позволяет искать "на глаз", не вводя точные символы.
Включение числовой сортировки для естественного порядка
Опция numeric воспринимает встроенные числа как числовые значения:
const collator = new Intl.Collator("en", { numeric: true });
const files = ["File1", "File10", "File2"];
console.log(files.sort(collator.compare));
// ["File1", "File2", "File10"]
Без числовой сортировки "File10" будет идти перед "File2", потому что строка "10" начинается с "1". Числовая сортировка разбирает числовые последовательности и сравнивает их математически.
Это обеспечивает естественный порядок, который соответствует ожиданиям людей для имён файлов, номеров версий и пронумерованных списков.
Обработка десятичных чисел при числовой сортировке
У числовой сортировки есть ограничение при работе с десятичными числами:
const collator = new Intl.Collator("en", { numeric: true });
const values = ["1.5", "1.10", "1.2"];
console.log(values.sort(collator.compare));
// ["1.2", "1.5", "1.10"]
Десятичная точка считается знаком препинания, а не частью числа. Каждый сегмент между знаками препинания сортируется отдельно. Для сортировки десятичных чисел преобразуйте значения в числа и используйте числовое сравнение.
Управление порядком регистра с помощью caseFirst
Опция caseFirst определяет, какие буквы — заглавные или строчные — будут идти первыми при сортировке:
// Uppercase first
const upperFirst = new Intl.Collator("en", { caseFirst: "upper" });
console.log(["a", "A", "b", "B"].sort(upperFirst.compare));
// ["A", "a", "B", "b"]
// Lowercase first
const lowerFirst = new Intl.Collator("en", { caseFirst: "lower" });
console.log(["a", "A", "b", "B"].sort(lowerFirst.compare));
// ["a", "A", "b", "B"]
По умолчанию установлено значение false, что использует стандартный порядок для локали. Эта опция не влияет на сортировку, если sensitivity установлено в base или accent, потому что эти уровни игнорируют регистр.
Игнорирование знаков препинания при сравнении
Опция ignorePunctuation пропускает знаки препинания при сравнении строк:
const collator = new Intl.Collator("en", { ignorePunctuation: true });
console.log(collator.compare("hello", "he-llo")); // 0
console.log(collator.compare("hello", "hello!")); // 0
Для тайского языка по умолчанию эта опция включена, для других языков — выключена. Используйте её, если знаки препинания не должны влиять на порядок или совпадение строк.
Указание типов колляции для языковых правил
Некоторые локали поддерживают несколько типов колляции для специализированной сортировки:
// Chinese pinyin ordering
const pinyin = new Intl.Collator("zh-CN-u-co-pinyin");
// German phonebook ordering
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");
// Emoji grouping
const emoji = new Intl.Collator("en-u-co-emoji");
Тип колляции указывается в строке локали с помощью синтаксиса расширения Unicode. Часто используемые типы:
pinyin: китайская сортировка по романизированному произношениюstroke: китайская сортировка по количеству чертphonebk: немецкий телефонный порядокtrad: традиционные правила сортировки для некоторых языковemoji: группировка эмодзи по категориям
Проверьте Intl.supportedValuesOf, чтобы узнать, какие типы сортировки доступны в вашей среде.
Повторное использование экземпляров collator в приложении
Создавайте экземпляры collator один раз и используйте их повторно по всему приложению:
// utils/collation.js
export const germanCollator = new Intl.Collator("de");
export const searchCollator = new Intl.Collator("en", {
sensitivity: "base"
});
export const numericCollator = new Intl.Collator("en", {
numeric: true
});
// In your components
import { germanCollator } from "./utils/collation";
const sorted = names.sort(germanCollator.compare);
Такой подход повышает производительность и обеспечивает единое поведение сравнения во всём коде.
Сортировка массивов объектов по свойству
Используйте collator в функции сравнения, которая обращается к свойствам объекта:
const collator = new Intl.Collator("de");
const users = [
{ name: "Zoe" },
{ name: "Änder" },
{ name: "Ava" }
];
const sorted = users.sort((a, b) =>
collator.compare(a.name, b.name)
);
Этот способ работает с любой структурой объекта. Извлеките строки для сравнения и передайте их в collator.
Сравнение производительности Intl.Collator и localeCompare
Intl.Collator обеспечивает лучшую производительность при сортировке больших наборов данных:
// Slower: recreates locale settings for each comparison
const slow = items.sort((a, b) => a.localeCompare(b, "de"));
// Faster: reuses precomputed locale settings
const collator = new Intl.Collator("de");
const fast = items.sort(collator.compare);
Для небольших массивов (менее 100 элементов) разница незначительна. Для больших массивов (тысячи элементов) Intl.Collator может быть быстрее на 60–80%.
Есть исключение для браузеров на базе V8, например Chrome. Для строк только из ASCII localeCompare использует оптимизацию с таблицами поиска. При сортировке только ASCII-строк localeCompare может работать так же быстро, как Intl.Collator.
Когда использовать Intl.Collator или localeCompare
Используйте Intl.Collator, если:
- Сортируете большие массивы (сотни или тысячи элементов)
- Часто сортируете (пользователь меняет порядок, виртуальные списки)
- Создаёте переиспользуемые функции сравнения
- Для вас важна производительность
Используйте localeCompare, если:
- Нужно сделать одно сравнение
- Сортируете небольшие массивы (менее 100 элементов)
- Простота важнее производительности
- Нужно сравнить "на лету" без подготовки
Оба API поддерживают одинаковые опции и дают одинаковый результат. Различие только в производительности и организации кода.
Проверка применённых опций
Метод resolvedOptions возвращает реальные опции, которые использует collator:
const collator = new Intl.Collator("de", { sensitivity: "base" });
console.log(collator.resolvedOptions());
// {
// locale: "de",
// usage: "sort",
// sensitivity: "base",
// ignorePunctuation: false,
// collation: "default",
// numeric: false,
// caseFirst: "false"
// }
Это помогает отлаживать поведение сортировки и понимать значения по умолчанию. Итоговая локаль может отличаться от запрошенной, если система не поддерживает именно эту локаль.
Проверка поддержки локалей
Проверьте, какие локали поддерживаются в текущем окружении:
const supported = Intl.Collator.supportedLocalesOf(["de", "fr", "xx"]);
console.log(supported); // ["de", "fr"]
Неподдерживаемые локали будут заменены на системную по умолчанию. Этот способ помогает определить, когда нужная локаль недоступна.
Поддержка браузеров и окружений
Intl.Collator широко поддерживается с сентября 2017 года. Все современные браузеры и версии Node.js поддерживают этот API. Он работает одинаково во всех средах.
Некоторые типы сортировки и опции могут быть ограничены в старых браузерах. Тестируйте критичные функции или смотрите таблицы совместимости на MDN, если поддерживаете старые окружения.
Частые ошибки, которых стоит избегать
Не создавайте новый collator для каждого сравнения:
// Wrong: creates collator repeatedly
items.sort((a, b) => new Intl.Collator("de").compare(a, b));
// Right: create once, reuse
const collator = new Intl.Collator("de");
items.sort(collator.compare);
Не думайте, что сортировка по умолчанию подходит для интернационального текста:
// Wrong: breaks for non-ASCII characters
names.sort();
// Right: use locale-aware sorting
names.sort(new Intl.Collator("de").compare);
Не забывайте указывать чувствительность для поиска:
// Wrong: variant sensitivity requires exact match
const collator = new Intl.Collator("en");
items.filter(item => collator.compare(item, "apple") === 0);
// Right: base sensitivity for fuzzy matching
const collator = new Intl.Collator("en", { sensitivity: "base" });
items.filter(item => collator.compare(item, "apple") === 0);
Практические примеры использования
Используйте Intl.Collator для:
- Сортировки пользовательского контента (имена, заголовки, адреса)
- Реализации поиска и автодополнения
- Построения таблиц данных с сортируемыми столбцами
- Создания фильтрованных списков и выпадающих опций
- Сортировки имён файлов и номеров версий
- Алфавитной навигации в списках контактов
- Мультиязычных интерфейсов приложений
Любой интерфейс, где пользователю показывается отсортированный текст, выигрывает от сортировки с учётом локали. Это делает приложение более привычным и корректным для любого языка пользователя.