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

При локализации интерфейсов недостаточно перевести текст. Языки отличаются не только словарём и грамматикой, но и направлением письма. Большинство европейских языков используют направление слева направо (Left-to-Right, LTR), тогда как арабский, иврит, персидский и урду используют направление справа налево (Right-to-Left, RTL).

Библиотека FormatJS не занимается непосредственным изменением CSS или DOM-структуры, однако предоставляет инструменты для корректной интернационализации текста, чисел, дат и форматирования, которые должны учитывать направление письма и особенности письменности.


Направление текста: LTR и RTL

Основные режимы письма:

Направление Примеры языков
LTR английский, русский, французский
RTL арабский, иврит, персидский

HTML использует атрибут dir:

<html dir="ltr">

или:

<html dir="rtl">

Направление влияет на:

  • выравнивание текста;
  • порядок отображения элементов;
  • позиционирование иконок;
  • направление прокрутки;
  • отображение пунктуации;
  • смешивание разных языков внутри одной строки.

Связь FormatJS и направления письма

FormatJS работает поверх Intl API браузера. При выборе локали библиотека автоматически использует соответствующие правила форматирования:

import {IntlProvider} from 'react-intl';

<IntlProvider locale="ar">
  <App />
</IntlProvider>

Локаль "ar" сообщает:

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

Однако сам dir="rtl" необходимо задавать отдельно.


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

Направление можно определять по локали:

const rtlLocales = ['ar', 'he', 'fa', 'ur'];

function getDirection(locale) {
  return rtlLocales.includes(locale)
    ? 'rtl'
    : 'ltr';
}

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

const locale = 'ar';
const direction = getDirection(locale);

document.documentElement.dir = direction;

В React:

<html lang={locale} dir={direction}>

Интеграция с React Intl

Часто направление подключают одновременно с IntlProvider.

import {IntlProvider} from 'react-intl';

const locale = 'ar';

const direction =
  ['ar', 'he', 'fa', 'ur'].includes(locale)
    ? 'rtl'
    : 'ltr';

document.documentElement.dir = direction;

<IntlProvider
  locale={locale}
  messages={messages[locale]}
>
  <App />
</IntlProvider>

Так интерфейс получает:

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

Смешанный текст и bidi-проблемы

Одна из сложнейших проблем интернационализации — смешивание LTR и RTL внутри одной строки.

Пример:

تم إرسال الملف test.pdf بنجاح

Здесь арабский текст содержит латинское имя файла.

Браузер использует алгоритм Bidirectional Algorithm (BiDi), который пытается определить порядок символов автоматически. Иногда результат оказывается неожиданным.


Unicode Bidirectional Isolation

Для предотвращения ошибок используются специальные Unicode-символы:

Символ Назначение
LRI начало LTR-фрагмента
RLI начало RTL-фрагмента
PDI конец изолированного фрагмента

Однако вручную использовать Unicode-коды неудобно.

Современные браузеры поддерживают HTML-атрибут:

<bdi>test.pdf</bdi>

bdi изолирует направление текста.


Использование bdi вместе с FormatJS

Пример сообщения:

messages = {
  uploaded: 'تم إرسال الملف {filename} بنجاح'
};

Компонент:

<FormattedMessage
  id="uploaded"
  values={{
    filename: (
      <bdi>{filename}</bdi>
    )
  }}
/>

Теперь имя файла отображается корректно независимо от языка.


Форматирование чисел в RTL-языках

FormatJS автоматически использует локальные системы цифр.

Пример:

<FormattedNumber value={123456} />

Для локали "ar" результат может выглядеть так:

١٢٣٤٥٦

Это арабские цифры Unicode.


Управление системой цифр

Unicode locale extension позволяет выбирать систему письма:

locale="ar-u-nu-latn"

Здесь:

  • ar — арабская локаль;
  • nu — numbering system;
  • latn — латинские цифры.

Пример:

<IntlProvider locale="ar-u-nu-latn">

Результат:

123456

вместо:

١٢٣٤٥٦

Письменности и Unicode

Некоторые языки используют разные письменности.

Примеры:

Язык Письменности
сербский кириллица, латиница
китайский упрощённая, традиционная
узбекский кириллица, латиница

Unicode locale extension позволяет задавать письменность явно.


Script subtags

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

language-Script-region

Примеры:

Локаль Значение
sr-Cyrl сербский, кириллица
sr-Latn сербский, латиница
zh-Hans китайский, упрощённый
zh-Hant китайский, традиционный

Использование письменностей в FormatJS

<IntlProvider locale="sr-Cyrl">

или:

<IntlProvider locale="sr-Latn">

Это влияет на:

  • выбор переводов;
  • форматирование текста;
  • локализованные данные;
  • работу Intl.DisplayNames.

Разделение переводов по письменности

Сообщения можно хранить отдельно.

const messages = {
  'sr-Cyrl': {
    hello: 'Здраво'
  },

  'sr-Latn': {
    hello: 'Zdravo'
  }
};

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

<IntlProvider
  locale={locale}
  messages={messages[locale]}
>

Определение письменности пользователя

Браузер предоставляет языковые настройки:

navigator.language

Пример:

sr-Latn-RS

Можно извлечь script subtag:

const locale = navigator.language;

или использовать Intl.Locale.


Работа с Intl.Locale

Современный API:

const locale = new Intl.Locale('sr-Latn-RS');

console.log(locale.language);
console.log(locale.script);
console.log(locale.region);

Результат:

sr
Latn
RS

Автоматическое переключение сообщений

function resolveMessages(locale) {
  const intlLocale = new Intl.Locale(locale);

  const script = intlLocale.script;

  if (script === 'Cyrl') {
    return messages['sr-Cyrl'];
  }

  return messages['sr-Latn'];
}

Проблемы с переносом строк в RTL

В RTL-интерфейсах переносы могут выглядеть неожиданно:

السعر: 100 USD

Иногда USD переносится отдельно, нарушая визуальную структуру.

Решение — использовать неразрывные пробелы:

'السعر: 100\u00A0USD'

Форматирование валют

FormatJS корректно форматирует валюты для RTL-языков.

<FormattedNumber
  value={1000}
  style="currency"
  currency="USD"
/>

Для арабской локали отображение может быть:

‏١٬٠٠٠٫٠٠ US$

Обратить внимание следует на:

  • направление символов;
  • Unicode-маркеры;
  • специальные пробелы.

Invisible Unicode Characters

RTL-текст часто содержит невидимые символы:

Символ Назначение
LRM Left-to-Right Mark
RLM Right-to-Left Mark
ALM Arabic Letter Mark

Они помогают браузеру правильно отображать смешанный текст.

Пример:

const LRM = '\u200E';

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

`${LRM}USD`

Форматирование дат в RTL

Пример:

<FormattedDate
  value={new Date()}
  year="numeric"
  month="long"
  day="numeric"
/>

Для арабской локали:

٢٨ مايو ٢٠٢٦

FormatJS автоматически:

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

Локализованные списки

Intl.ListFormat особенно важен для RTL.

<FormattedList
  value={['أحمد', 'محمد', 'علي']}
  type="conjunction"
/>

Результат:

أحمد ومحمد وعلي

FormatJS корректно подставляет союз и разделители.


Интерфейсные проблемы RTL

FormatJS не управляет стилями интерфейса. RTL требует отдельной адаптации CSS.

Основные проблемы:

Элемент Изменение
margin-left margin-right
padding-left padding-right
flex-direction reverse
text-align right
position mirror

Использование логических CSS-свойств

Современный подход:

margin-inline-start: 10px;
padding-inline-end: 20px;

Вместо:

margin-left: 10px;
padding-right: 20px;

Логические свойства автоматически адаптируются под RTL.


RTL и иконки

Некоторые иконки должны отражаться:

  • стрелки;
  • кнопки навигации;
  • карусели;
  • индикаторы направления.

Пример:

[dir="rtl"] .icon-arrow {
  transform: scaleX(-1);
}

Форматирование сообщений с вложенными компонентами

FormatJS поддерживает React-компоненты внутри сообщений.

messages = {
  welcome:
    'مرحبا <b>{name}</b>'
};

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

<FormattedMessage
  id="welcome"
  values={{
    b: chunks => <b>{chunks}</b>,
    name: <bdi>{name}</bdi>
  }}
/>

Это особенно важно для смешанных направлений текста.


Псевдолокализация RTL

Для тестирования интерфейсов используют псевдолокали.

Пример:

[!!! مرحبا بالعالم !!!]

или искусственный RTL:

‮ɥsıןƃuƎ

Тестирование помогает обнаружить:

  • неправильные выравнивания;
  • обрезку текста;
  • ошибки bidi;
  • проблемы с flex/layout.

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

Перед использованием RTL-языков полезно проверять поддержку:

Intl.NumberFormat.supportedLocalesOf([
  'ar',
  'he',
  'fa'
]);

Полная RTL-конфигурация приложения

Пример:

import {
  IntlProvider
} from 'react-intl';

const locale = navigator.language;

const rtlLocales = [
  'ar',
  'he',
  'fa',
  'ur'
];

const language =
  locale.split('-')[0];

const direction =
  rtlLocales.includes(language)
    ? 'rtl'
    : 'ltr';

document.documentElement.lang =
  locale;

document.documentElement.dir =
  direction;

<IntlProvider
  locale={locale}
  messages={messages[locale]}
>
  <App />
</IntlProvider>

Структура локалей для разных письменностей

Пример организации проекта:

locales/
├── en.json
├── ru.json
├── ar.json
├── sr-Cyrl.json
├── sr-Latn.json
├── zh-Hans.json
└── zh-Hant.json

Особенности арабской письменности

Арабская письменность обладает дополнительными особенностями:

  • контекстные формы букв;
  • лигатуры;
  • диакритические знаки;
  • соединение символов.

FormatJS не занимается рендерингом шрифтов, но корректная локаль влияет на:

  • выбор шрифта браузером;
  • Unicode shaping;
  • правила отображения текста.

Intl.Segmenter и письменности

Некоторые письменности не используют пробелы между словами.

Пример:

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

Intl.Segmenter помогает корректно разбивать текст.

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

Совместимость браузеров

Поддержка возможностей зависит от среды:

Возможность Поддержка
RTL практически везде
Intl.Locale современные браузеры
Intl.Segmenter новые браузеры
Unicode extensions современные движки

Для старых браузеров FormatJS предоставляет polyfill-пакеты.


Polyfill для Intl.Locale

npm install @formatjs/intl-locale

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

import '@formatjs/intl-locale/polyfill';

Polyfill для Intl.Segmenter

npm install @formatjs/intl-segmenter

Архитектура RTL-ready приложения

Полноценная интернационализация включает:

  1. локализацию сообщений;
  2. поддержку направлений письма;
  3. поддержку письменностей;
  4. bidi-безопасность;
  5. адаптивный RTL-layout;
  6. локальные цифры и даты;
  7. тестирование псевдолокалей;
  8. Unicode-aware форматирование.

FormatJS закрывает уровень локализации и форматирования, а направление интерфейса обычно реализуется совместно с:

  • CSS logical properties;
  • RTL-aware UI frameworks;
  • React context;
  • системами темизации;
  • bidi-safe компонентами.