Регистр букв с учетом локали

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

Базовые методы работы с регистром в Jav * aScript:

  • String.prototype.toLowerCase()

  • String.prototype.toUpperCase()

  • локализованные версии:

    • String.prototype.toLocaleLowerCase()
    • String.prototype.toLocaleUpperCase()

Стандартные методы без локали используют Unicode Simple Case Mapping. Он быстрый и предсказуемый, но не учитывает языковые особенности. Например, в некоторых языках существуют контекстные преобразования, которые невозможно выразить одной статической таблицей соответствий.

Unicode определяет не только простое сопоставление символов, но и полное case folding — более сложный процесс, используемый для сравнения строк без учета регистра. Однако case folding не всегда совпадает с визуальным преобразованием регистра, что важно при работе с локализованными интерфейсами.

Локализация преобразования регистра

Методы toLocaleLowerCase(locales) и toLocaleUpperCase(locales) учитывают языковые правила. Параметр locales позволяет указать BCP 47 тег языка:

str.toLocaleLowerCase('tr')
str.toLocaleUpperCase('tr')

Если параметр не указан, среда выполнения использует локаль по умолчанию.

Ключевая особенность: в некоторых языках преобразование регистра зависит от контекста символов, а не только от их кода.

Турецкий и азербайджанский i

Наиболее известный пример локально-зависимого поведения — турецкий и азербайджанский языки.

В этих языках существуют четыре различных символа:

  • i (латинская i с точкой)
  • I (латинская I без точки)
  • İ (I с точкой сверху)
  • ı (i без точки)

Стандартное преобразование без локали:

"I".toLowerCase()
// "i"

Однако в турецкой локали:

"I".toLocaleLowerCase('tr')
// "ı"

И наоборот:

"i".toLocaleUpperCase('tr')
// "İ"

Такое поведение критично при обработке пользовательского ввода, поиске и нормализации строк.

Различие между case mapping и case folding

В контексте Intl API важно различать два процесса:

Case mapping

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

  • используется в toUpperCase, toLowerCase, toLocaleLowerCase, toLocaleUpperCase
  • ориентировано на визуальный результат
  • зависит от локали

Case folding

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

  • применяется в алгоритмах сравнения
  • ориентирован на равенство, а не отображение
  • часто более агрессивен, чем case mapping

Пример различия:

"Straße".toLowerCase()
// "straße"

"Straße".toLocaleLowerCase('de')
// "straße"

Но при case folding для сравнения:

  • ß может быть эквивалентен "ss" в некоторых алгоритмах
  • визуальное преобразование не всегда отражает это

Intl.Collator и регистронезависимое сравнение

Для корректной работы с регистром в сравнении строк используется Intl.Collator.

Базовое использование

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

collator.compare('a', 'A')
// 0

Параметр sensitivity управляет учетом регистра:

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

Пример влияния регистра

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

c1.compare('a', 'A')
// 0 (регистронезависимо)

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

c2.compare('a', 'A')
// -1 или 1 (зависит от порядка)

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

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

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

Немецкий язык

В немецком языке символ ß имеет особое поведение:

"ß".toLocaleUpperCase('de')
// "SS"

При этом обратное преобразование может не быть симметричным:

"SS".toLocaleLowerCase('de')
// "ss"

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

Французский и испанский языки

В этих языках регистр в основном следует стандартному Unicode-мэппингу, но сортировка может учитывать дополнительные правила акцентуации, что влияет на поведение Intl.Collator.

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

Преобразования регистра не гарантируют обратимость:

const a = "Straße";
const b = a.toLocaleUpperCase('de').toLocaleLowerCase('de');

a === b
// false

Причина:

  • ß → SS при upper-case
  • SS не всегда возвращается обратно в ß

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

Использование локали по умолчанию

Если локаль не указана:

str.toLocaleLowerCase()
str.toLocaleUpperCase()

движок использует системную локаль окружения. Это может приводить к различным результатам в разных средах выполнения (браузер, Node.js, серверные контейнеры).

Поэтому в прикладной логике часто фиксируют локаль явно:

str.toLocaleLowerCase('en')

Сравнение с приведением регистра в идентификаторах

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

  • email-адреса обычно приводятся к lower-case
  • логины могут быть case-insensitive
  • URL-пути чаще case-sensitive

Однако использование toLowerCase() без локали может привести к ошибкам в международных системах. Более корректный подход — сочетание:

  • нормализации Unicode (NFC/NFKC)
  • локализованного приведения регистра при необходимости
  • Intl.Collator для сравнения
const normalized = str.normalize('NFKC').toLocaleLowerCase('tr');

Особенности производительности

Локализованные методы:

  • toLocaleLowerCase
  • toLocaleUpperCase
  • Intl.Collator

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

В высоконагруженных системах это учитывается при выборе стратегии:

  • массовая нормализация → простые методы
  • пользовательский ввод и поиск → Intl API

Роль Intl API в управлении регистром

Хотя Intl API напрямую не предоставляет отдельного “case conversion service”, он определяет поведение через:

  • локализацию строковых операций
  • правила сравнения (Intl.Collator)
  • языкозависимые особенности отображения

Таким образом, работа с регистром в JavaScript является не изолированной операцией, а частью более широкой системы локализованных правил обработки текста, где Intl API выступает координирующим слоем между Unicode-стандартом и языковыми особенностями.