Как преобразовать текст в верхний или нижний регистр по правилам локали
Используйте JavaScript для правильного изменения регистра текста на разных языках и в разных системах письма
Введение
Когда вы преобразуете текст из верхнего регистра в нижний и наоборот, может показаться, что эта операция работает одинаково для всех языков. Но это не так. Разные системы письма следуют своим правилам преобразования регистра, и если их не учитывать, можно получить неожиданные результаты.
В JavaScript есть стандартные методы toUpperCase() и toLowerCase(), которые корректно работают для английского, но могут давать неверные результаты для других языков. Локализованные методы toLocaleUpperCase() и toLocaleLowerCase() применяют языковые правила преобразования регистра, что позволяет корректно изменять регистр текста независимо от языка.
В этом уроке объясняется, почему преобразование регистра различается в зависимости от языка, приводятся примеры проблем со стандартными методами и показывается, как использовать локализованные методы для правильного изменения регистра в международных приложениях.
Почему правила преобразования регистра зависят от локали
Верхний и нижний регистр букв — это не универсальные понятия, которые одинаково работают во всех системах письма. В разных языках сложились свои правила преобразования регистра, основанные на исторических традициях письма и типографике.
В английском языке преобразование регистра довольно простое. Буква i становится I при переводе в верхний регистр, а I превращается в i при переводе в нижний. Эта взаимосвязь сохраняется для всего английского алфавита.
В других языках правила сложнее. В турецком есть четыре разных варианта буквы i вместо двух. В немецком есть буква ß (эсцет), для которой существуют особые правила преобразования в верхний регистр. В греческом буква сигма имеет разные формы в зависимости от положения в слове.
Когда вы используете стандартные методы JavaScript, такие как toUpperCase() и toLowerCase(), преобразование происходит по английским правилам. Это приводит к неправильным результатам для текста на других языках. Методы, учитывающие локаль, применяют соответствующие правила для каждого языка, обеспечивая корректное преобразование.
Турецкая проблема с буквой i
Турецкий язык — самый наглядный пример того, почему локаль важна для преобразования регистра. В отличие от английского, в турецком есть четыре разные буквы, связанные с i:
- Строчная i с точкой:
i(U+0069) - Прописная İ с точкой:
İ(U+0130) - Строчная ı без точки:
ı(U+0131) - Прописная I без точки:
I(U+0049)
В турецком строчная i с точкой i становится прописной İ с точкой İ. Строчная ı без точки ı становится прописной I без точки I. Это две отдельные пары букв с разным произношением и значением.
Стандартные методы JavaScript следуют английским правилам и преобразуют i с точкой i в I без точки I. Это меняет смысл турецких слов и приводит к некорректному тексту.
const turkish = "istanbul";
console.log(turkish.toUpperCase());
// Output: "ISTANBUL" (incorrect - uses dotless I)
console.log(turkish.toLocaleUpperCase("tr"));
// Output: "İSTANBUL" (correct - uses dotted İ)
В названии города Istanbul используется буква i с точкой. При преобразовании в верхний регистр по турецким правилам получается İSTANBUL с буквой İ с точкой. Если использовать стандартный toUpperCase(), получится ISTANBUL с I без точки, что неверно для турецкого.
Та же проблема возникает и в обратную сторону при преобразовании турецкого текста из верхнего регистра в нижний.
const uppercase = "İSTANBUL";
console.log(uppercase.toLowerCase());
// Output: "i̇stanbul" (incorrect - creates i with combining dot above)
console.log(uppercase.toLocaleLowerCase("tr"));
// Output: "istanbul" (correct - produces dotted i)
İ с точкой должна становиться i с точкой при преобразовании в нижний регистр в турецком языке. Стандартный toLowerCase() не справляется с этим корректно и может выдать строчную i с комбинированной точкой, которая выглядит похоже, но технически неверна.
Другие языковые особенности преобразования регистра
Турецкий — не единственный язык со специальными правилами преобразования регистра. Для нескольких других языков также требуется учитывать локализацию.
В немецком есть буква ß (эсцет), которая традиционно не имела прописной формы. В 2017 году в Unicode добавили заглавную букву ẞ, но во многих системах при преобразовании в верхний регистр ß всё ещё заменяется на SS.
const german = "Straße";
console.log(german.toUpperCase());
// Output: "STRASSE" (converts ß to SS)
console.log(german.toLocaleUpperCase("de"));
// Output: "STRASSE" (also converts ß to SS)
В большинстве сред JavaScript оба способа дают одинаковый результат для немецкого текста. Параметр локали не влияет на результат, но использование метода, учитывающего локаль, гарантирует корректную работу кода, если в будущих реализациях изменится поддержка Unicode.
В греческом языке есть три формы буквы сигма. В середине слова используется строчная σ, а в конце — ς. Обе формы преобразуются в одну заглавную Σ.
В литовском есть особые правила для букв с точкой. Буква i сохраняет точку при сочетании с некоторыми диакритическими знаками, даже при преобразовании в верхний регистр. Это влияет на то, как методы, учитывающие локаль, обрабатывают определённые сочетания символов.
Использование toLocaleUpperCase для преобразования в верхний регистр с учётом локали
Метод toLocaleUpperCase() преобразует строку в верхний регистр с учётом языковых особенностей. Его вызывают у строки, при необходимости передавая идентификатор локали в качестве аргумента.
const text = "istanbul";
const result = text.toLocaleUpperCase("tr");
console.log(result);
// Output: "İSTANBUL"
Этот код преобразует строку в верхний регистр по турецким правилам. Точечная i становится заглавной İ, что корректно для турецкого языка.
Можно преобразовать тот же текст по разным языковым правилам.
const text = "istanbul";
console.log(text.toLocaleUpperCase("tr"));
// Output: "İSTANBUL" (Turkish rules - dotted İ)
console.log(text.toLocaleUpperCase("en"));
// Output: "ISTANBUL" (English rules - dotless I)
Параметр локали определяет, какие правила преобразования регистра применяются. Турецкие правила сохраняют точку у i, а английские — нет.
Если вызвать toLocaleUpperCase() без аргументов, будет использоваться системная локаль, определяемая средой выполнения JavaScript.
const text = "istanbul";
const result = text.toLocaleUpperCase();
console.log(result);
// Output depends on system locale
Результат зависит от локали по умолчанию в среде JavaScript, которая обычно совпадает с настройками операционной системы пользователя.
Использование toLocaleLowerCase для преобразования в нижний регистр с учётом локали
Метод toLocaleLowerCase() преобразует строку в нижний регистр с учётом языковых правил. Работает так же, как toLocaleUpperCase(), но переводит в нижний регистр вместо верхнего.
const text = "İSTANBUL";
const result = text.toLocaleLowerCase("tr");
console.log(result);
// Output: "istanbul"
Этот пример переводит турецкий текст в нижний регистр по турецким правилам. Буква İ с точкой становится i с точкой, что даёт правильную форму в нижнем регистре.
Без параметра локали стандартные toLowerCase() или toLocaleLowerCase() с настройками по умолчанию могут неправильно обработать турецкие символы.
const text = "İSTANBUL";
console.log(text.toLowerCase());
// Output: "i̇stanbul" (incorrect - i with combining dot above)
console.log(text.toLocaleLowerCase("tr"));
// Output: "istanbul" (correct - dotted i)
Для корректного преобразования турецкой буквы İ с точкой нужны турецкие правила регистра. Использование метода с поддержкой локали и локалью tr обеспечивает правильное преобразование.
Также можно обработать турецкую букву I без точки — при преобразовании в нижний регистр она должна остаться без точки.
const text = "IRAK";
console.log(text.toLocaleLowerCase("tr"));
// Output: "ırak" (Turkish rules - dotless ı)
console.log(text.toLocaleLowerCase("en"));
// Output: "irak" (English rules - dotted i)
В слове IRAK (Ирак по-турецки) используется буква I без точки. Турецкие правила регистра переводят её в нижний регистр как ı без точки, а английские — как i с точкой.
Указание идентификаторов локали
Оба метода toLocaleUpperCase() и toLocaleLowerCase() принимают идентификаторы локали в формате BCP 47. Это те же языковые теги, которые используются во всём API Intl и других функциях интернационализации.
const text = "Straße";
console.log(text.toLocaleUpperCase("de-DE"));
// Output: "STRASSE"
console.log(text.toLocaleUpperCase("de-AT"));
// Output: "STRASSE"
console.log(text.toLocaleUpperCase("de-CH"));
// Output: "STRASSE"
В этих примерах используются разные варианты немецкой локали для Германии, Австрии и Швейцарии. Правила преобразования регистра обычно совпадают для всех региональных вариантов одного языка, поэтому результат будет одинаковым.
Можно также передать массив идентификаторов локалей. Метод использует первую локаль из массива.
const text = "istanbul";
const result = text.toLocaleUpperCase(["tr", "en"]);
console.log(result);
// Output: "İSTANBUL"
Метод применяет турецкие правила, потому что tr — первая локаль в массиве. Если среда выполнения не поддерживает первую локаль, происходит переход к следующим локалям в массиве.
Использование языковых предпочтений браузера
В веб-приложениях можно использовать языковые предпочтения браузера пользователя, чтобы определить, какие правила преобразования регистра применять. Свойство navigator.language возвращает предпочитаемый язык пользователя.
const userLocale = navigator.language;
const text = "istanbul";
const result = text.toLocaleUpperCase(userLocale);
console.log(result);
// Output varies by user's locale
// For Turkish users: "İSTANBUL"
// For English users: "ISTANBUL"
Это автоматически применяет правильные правила регистра на основе языковых настроек пользователя. Турецкие пользователи видят текст, преобразованный по турецким правилам, англоязычные — по английским, и так далее.
Можно также передать весь массив языковых предпочтений для включения механизма резервирования.
const text = "istanbul";
const result = text.toLocaleUpperCase(navigator.languages);
console.log(result);
Метод использует первую локаль из предпочтений пользователя, обеспечивая более корректную обработку, если конкретные локали недоступны.
Сравнение стандартных и локализованных методов
Стандартные методы toUpperCase() и toLowerCase() работают корректно для английского, но могут давать сбои для других языков. Локализованные методы toLocaleUpperCase() и toLocaleLowerCase() корректно обрабатывают все языки, применяя правила конкретной локали.
const turkish = "Diyarbakır";
// Standard methods (incorrect for Turkish)
console.log(turkish.toUpperCase());
// Output: "DIYARBAKIR" (dotless I - incorrect)
console.log(turkish.toUpperCase().toLowerCase());
// Output: "diyarbakir" (dotted i - lost the dotless ı)
// Locale-aware methods (correct for Turkish)
console.log(turkish.toLocaleUpperCase("tr"));
// Output: "DİYARBAKIR" (dotted İ and dotless I - correct)
console.log(turkish.toLocaleUpperCase("tr").toLocaleLowerCase("tr"));
// Output: "diyarbakır" (preserves both i types - correct)
В названии турецкого города Diyarbakır встречаются оба типа буквы i. Стандартные методы не могут сохранить это различие при преобразовании регистра. Локализованные методы сохраняют правильные символы в обоих направлениях.
Для текста, содержащего только символы с простыми правилами регистра, оба подхода дают одинаковый результат.
const english = "Hello World";
console.log(english.toUpperCase());
// Output: "HELLO WORLD"
console.log(english.toLocaleUpperCase("en"));
// Output: "HELLO WORLD"
Английский текст преобразуется одинаково любым методом. Локализованная версия не обязательна для текста только на английском, но её использование гарантирует корректную работу кода, если в тексте появятся другие языки.
Когда использовать регистрозависимое преобразование с учётом локали
Используйте методы с учётом локали при работе с пользовательским контентом или текстом, который может содержать несколько языков. Это обеспечивает правильное преобразование регистра вне зависимости от языка текста.
function normalizeUsername(username) {
return username.toLocaleLowerCase();
}
Имена пользователей, email-адреса, поисковые запросы и другие пользовательские данные должны преобразовываться с учётом локали. Это корректно обрабатывает международные символы и предотвращает ошибки с турецким и другими особенностями.
Стандартные методы используйте только если уверены, что текст содержит только английские символы и вам важна максимальная производительность. Стандартные методы работают чуть быстрее, так как не проверяют правила локали.
const htmlTag = "<DIV>";
const normalized = htmlTag.toLowerCase();
// Output: "<div>"
Названия HTML-тегов, CSS-свойства, протоколы и другие технические идентификаторы используют только ASCII-символы и не требуют учёта локали. Для такого контента стандартные методы работают корректно.
Как длина строки может измениться после преобразования регистра
Преобразование регистра — это не всегда замена один к одному. Некоторые символы при переводе в верхний регистр превращаются в несколько символов, что влияет на длину строки.
const german = "groß";
console.log(german.length);
// Output: 4
const uppercase = german.toLocaleUpperCase("de");
console.log(uppercase);
// Output: "GROSS"
console.log(uppercase.length);
// Output: 5
В немецком слове groß четыре символа. При переводе в верхний регистр ß превращается в SS, и получается GROSS с пятью символами. Длина строки увеличивается на один символ при преобразовании.
Это влияет на операции, зависящие от длины строки или позиции символов. Не стоит предполагать, что строка в верхнем или нижнем регистре будет такой же длины, как исходная.
const text = "Maße";
const positions = [0, 1, 2, 3];
const uppercase = text.toLocaleUpperCase("de");
// "MASSE" (5 characters)
// Original position mapping no longer valid
Буква ß на позиции 2 превращается в SS при преобразовании в верхний регистр, из-за чего все последующие символы смещаются. Позиции символов в исходной строке не совпадают с позициями в преобразованной строке.
Повторное использование параметров локали
Если нужно преобразовать несколько строк с одной и той же локалью, можно сохранить идентификатор локали в переменной и использовать его повторно. Это делает код более удобным для поддержки и обеспечивает единообразную работу с локалью.
const userLocale = navigator.language;
const city = "istanbul";
const country = "türkiye";
console.log(city.toLocaleUpperCase(userLocale));
console.log(country.toLocaleUpperCase(userLocale));
Такой подход позволяет держать выбор локали в одном месте. Если потребуется изменить используемую локаль, достаточно обновить определение переменной.
Для приложений, которые обрабатывают большие объёмы текста, это не даёт прироста производительности. Каждый вызов toLocaleUpperCase() или toLocaleLowerCase() выполняет преобразование независимо. В отличие от форматтеров Intl API, здесь нет объекта форматтера для повторного использования.