Обработка сложных скриптов

Библиотека Intl предоставляет набор встроенных инструментов для корректной локализации текста, чисел, дат, валют и правил языка. При работе со сложными скриптами её значение особенно велико, поскольку многие письменные системы требуют специальной обработки:

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

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

  • арабская письменность;
  • иврит;
  • китайские иероглифы;
  • японские кандзи и кана;
  • тайский язык;
  • хинди и деванагари;
  • корейский хангыль;
  • бирманская письменность.

Архитектура интернационализации в Javascript

Модуль Intl основан на нескольких международных стандартах:

  • ECMAScript Internationalization API Specification;
  • ICU (International Components for Unicode);
  • Unicode CLDR;
  • Unicode BCP 47.

Большинство современных движков Javascript используют ICU как внутренний механизм локализации.

Пример определения локали:

const locale = new Intl.Locale("ar-EG");

console.log(locale.language); // ar
console.log(locale.region);   // EG

Объект Intl.Locale позволяет анализировать структуру локали и управлять параметрами языка.


Unicode и сложные системы письма

Проблема Unicode-нормализации

Многие символы могут быть представлены несколькими способами:

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

console.log(a === b); // false

Хотя визуально строки одинаковы, бинарно они различаются.

Для корректного сравнения используется нормализация:

const normalizedA = a.normalize("NFC");
const normalizedB = b.normalize("NFC");

console.log(normalizedA === normalizedB); // true

Формы нормализации

Форма Назначение
NFC Каноническая композиция
NFD Каноническая декомпозиция
NFKC Совместимая композиция
NFKD Совместимая декомпозиция

Для сложных скриптов нормализация особенно важна при:

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

Двунаправленный текст (BiDi)

Особенности RTL-языков

Арабский и иврит используют направление письма справа налево (RTL). При смешении RTL и LTR возникают сложные проблемы отображения.

Пример:

const text = "السعر 100 USD";

console.log(text);

Порядок отображения определяется алгоритмом Unicode Bidirectional Algorithm.


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

HTML-атрибут dir

<div dir="rtl">
  مرحبا بالعالم
</div>

Автоматическое определение направления

<div dir="auto">
  مرحبا Hello
</div>

Intl и RTL-локали

const formatter = new Intl.DateTimeFormat("ar-SA");

console.log(formatter.format(new Date()));

Результат будет содержать:

  • арабские цифры;
  • RTL-направление;
  • локализованный календарь.

Unicode Control Characters

Иногда необходимо вручную управлять направлением текста.

Основные символы

Символ Назначение
LRM Left-to-Right Mark
RLM Right-to-Left Mark
LRE Left-to-Right Embedding
RLE Right-to-Left Embedding
PDF Pop Directional Formatting

Пример:

const text = "\u200Fمرحبا 123";

Локализованные системы чисел

Разные системы цифр

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

Локаль Система
ar Арабские
fa Персидские
hi Деванагари
bn Бенгальские

Intl.NumberFormat

Арабские цифры

const formatter = new Intl.NumberFormat("ar-EG");

console.log(formatter.format(123456));

Результат:

١٢٣٤٥٦

Управление numberingSystem

const formatter = new Intl.NumberFormat("hi-IN-u-nu-deva");

console.log(formatter.format(2025));

Результат:

२०२५

Использование Unicode Extension

Формат локали:

language-region-u-key-value

Пример:

"ar-SA-u-nu-arab"

Где:

  • nu — numbering system;
  • arab — арабские цифры.

Локализованная сегментация текста

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

Во многих языках слова не отделяются пробелами:

  • китайский;
  • японский;
  • тайский.

Обычный split(" ") становится бесполезным.


Intl.Segmenter

API сегментации текста решает эту проблему.

Сегментация слов

const segmenter = new Intl.Segmenter("ja-JP", {
  granularity: "word"
});

const text = "私は学生です";

for (const item of segmenter.segment(text)) {
  console.log(item.segment);
}

Уровни сегментации

granularity Назначение
grapheme Символы
word Слова
sentence Предложения

Grapheme Clusters

Один визуальный символ может состоять из нескольких Unicode-кодов.

Пример эмодзи:

const text = "?‍?‍?‍?";

console.log(text.length);

Результат:

11

Но визуально это один символ.

Корректная сегментация

const segmenter = new Intl.Segmenter(undefined, {
  granularity: "grapheme"
});

for (const item of segmenter.segment(text)) {
  console.log(item.segment);
}

Сортировка в сложных языках

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

Простое сравнение строк работает некорректно:

["ä", "z", "a"].sort();

Unicode-порядок не соответствует языковым правилам.


Intl.Collator

Немецкая сортировка

const collator = new Intl.Collator("de");

const words = ["z", "ä", "a"];

words.sort(collator.compare);

console.log(words);

Китайская сортировка

Китайский язык имеет несколько вариантов:

  • по произношению;
  • по радикалам;
  • по количеству черт.
const collator = new Intl.Collator("zh");

const words = ["王", "李", "张"];

words.sort(collator.compare);

Чувствительность сравнения

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

Варианты sensitivity

Значение Поведение
base Игнорировать акценты
accent Учитывать акценты
case Учитывать регистр
variant Максимальная точность

Игнорирование пунктуации

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

Контекстные формы символов

Арабская письменность

В арабском символ меняет форму в зависимости от позиции:

  • начальная;
  • срединная;
  • конечная;
  • изолированная.

Javascript не занимается этим напрямую — рендеринг выполняет движок браузера и шрифты.

Однако Intl должен учитывать эти особенности при:

  • сегментации;
  • сортировке;
  • сравнении;
  • поиске.

Лигатуры

Некоторые символы объединяются в единый графический элемент.

Пример арабской лигатуры:

لا

В Unicode это может быть:

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

Нормализация помогает избежать ошибок сравнения.


Локализованные календари

Альтернативные календарные системы

Многие локали используют не григорианский календарь.

Примеры

Локаль Календарь
ar-SA Исламский
th-TH Буддийский
ja-JP Японский
fa-IR Персидский

Intl.DateTimeFormat

Исламский календарь

const formatter = new Intl.DateTimeFormat("ar-SA");

console.log(formatter.format(new Date()));

Японские эры

const formatter = new Intl.DateTimeFormat("ja-JP-u-ca-japanese", {
  era: "long",
  year: "numeric"
});

console.log(formatter.format(new Date()));

Результат может содержать:

令和8年

Управление календарём

const locale = new Intl.Locale("fa-IR-u-ca-persian");

console.log(locale.calendar);

Форматирование времени в сложных локалях

Локальные правила отображения

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

  • 12-часовой формат;
  • 24-часовой формат;
  • особые разделители;
  • нестандартные обозначения AM/PM.

Пример тайской локали

const formatter = new Intl.DateTimeFormat("th-TH", {
  dateStyle: "full",
  timeStyle: "long"
});

console.log(formatter.format(new Date()));

Отображение имён и языков

Intl.DisplayNames

API локализует:

  • названия стран;
  • валют;
  • языков;
  • регионов.

Пример

const languageNames = new Intl.DisplayNames(["ar"], {
  type: "language"
});

console.log(languageNames.of("en"));

Результат:

الإنجليزية

Работа с множественным числом

Сложные правила plural forms

Арабский язык содержит шесть форм множественного числа.


Intl.PluralRules

const pluralRules = new Intl.PluralRules("ar");

console.log(pluralRules.select(0));
console.log(pluralRules.select(1));
console.log(pluralRules.select(2));
console.log(pluralRules.select(3));

Категории plural rules

Категория Назначение
zero Ноль
one Один
two Два
few Несколько
many Много
other Остальное

Обработка регистров

Проблемы uppercase/lowercase

Некоторые языки имеют нестандартные правила регистра.

Турецкий язык

console.log("i".toUpperCase());

Результат:

I

Но в турецком правильно:

İ

locale-aware casing

console.log("i".toLocaleUpperCase("tr"));

Локализованное сравнение строк

localeCompare

Метод использует правила локали.

const result = "ä".localeCompare("z", "de");

Использование numeric

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

const files = ["file2", "file10", "file1"];

files.sort(collator.compare);

console.log(files);

Результат:

["file1", "file2", "file10"]

Поддержка Unicode Extension Keys

Основные ключи

Ключ Назначение
ca Календарь
nu Система цифр
hc Формат часов
co Сортировка

Пример сложной локали

const locale =
  "zh-Hans-CN-u-ca-chinese-nu-hanidec";

Здесь:

  • zh — китайский;
  • Hans — упрощённый;
  • CN — Китай;
  • ca-chinese — китайский календарь;
  • nu-hanidec — китайские десятичные числа.

Производительность Intl API

Высокая стоимость создания объектов

Создание форматтеров дорогостоящее.

Плохая практика:

for (const number of numbers) {
  const formatter =
    new Intl.NumberFormat("ar");

  console.log(formatter.format(number));
}

Кэширование форматтеров

Правильный подход:

const formatter =
  new Intl.NumberFormat("ar");

for (const number of numbers) {
  console.log(formatter.format(number));
}

Поддержка браузеров

Современная поддержка

Большинство возможностей поддерживаются:

  • Chrome;
  • Firefox;
  • Safari;
  • Edge;
  • Node.js.

Частичные ограничения

Некоторые API появились позже:

API Поддержка
Intl.Segmenter Новые браузеры
Intl.DisplayNames Современные версии
Intl.Locale ES2020+

ICU и Node.js

Полная и сокращённая ICU

Node.js может собираться:

  • с полной ICU;
  • с минимальной ICU.

Минимальная версия ограничивает локализацию.


Проверка поддержки

console.log(Intl.DateTimeFormat.supportedLocalesOf([
  "ar",
  "ja",
  "th",
  "fa"
]));

Частые ошибки при работе со сложными скриптами

Использование substring для Unicode

const text = "?‍?‍?‍?";

console.log(text.substring(0, 1));

Результат может повредить grapheme cluster.


Использование length

console.log("?".length);

Результат:

2

Причина — UTF-16 surrogate pairs.


Неправильная сортировка

words.sort();

Для локализованных данных необходимо:

words.sort(collator.compare);

Практика построения интернационализированных приложений

Основные рекомендации

Всегда использовать Intl вместо самописных решений

Неправильно:

date.toString()

Правильно:

new Intl.DateTimeFormat(locale)

Избегать предположений о структуре текста

Недопустимые предположения:

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

Хранить данные в Unicode NFC

text.normalize("NFC");

Проверять работу RTL

Особенно важно тестировать:

  • формы;
  • таблицы;
  • mixed content;
  • числовые значения;
  • inline-элементы.

Использовать locale-aware API

Задача API
Числа Intl.NumberFormat
Даты Intl.DateTimeFormat
Сортировка Intl.Collator
Сегментация Intl.Segmenter
Plural rules Intl.PluralRules
Названия Intl.DisplayNames