Направление текста и a11y

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

FormatJS напрямую не управляет стилями или DOM-направлением, однако библиотека играет ключевую роль в определении текущей локали и построении инфраструктуры интернационализации, от которой зависит вся RTL/LTR-логика приложения.


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

Существует два основных режима отображения текста:

Режим Описание
ltr Текст отображается слева направо
rtl Текст отображается справа налево

Языки с направлением RTL

Наиболее распространённые RTL-языки:

  • арабский (ar)
  • иврит (he)
  • персидский (fa)
  • урду (ur)

Для них требуется:

  • изменение направления текста;
  • зеркалирование интерфейса;
  • корректная работа flex/grid;
  • изменение порядка иконок;
  • адаптация навигации;
  • корректное чтение скринридерами.

Определение направления по локали

FormatJS предоставляет текущую локаль через IntlProvider и хуки React Intl.

Пример получения локали

import { useIntl } from 'react-intl';

function CurrentLocale() {
  const intl = useIntl();

  return <div>{intl.locale}</div>;
}

На основе локали можно вычислить направление интерфейса.


Создание функции определения RTL

На практике обычно создают список RTL-языков.

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

export function isRTL(locale: string): boolean {
  const lang = locale.split('-')[0];

  return RTL_LANGUAGES.includes(lang);
}

Установка атрибута dir

HTML поддерживает специальный атрибут:

dir="ltr"
dir="rtl"

Именно он определяет направление текста и поведение layout-алгоритмов.

Динамическое применение

import { useEffect } from 'react';
import { useIntl } from 'react-intl';

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

export function DirectionProvider() {
  const { locale } = useIntl();

  useEffect(() => {
    const lang = locale.split('-')[0];

    document.documentElement.dir =
      RTL_LANGUAGES.includes(lang)
        ? 'rtl'
        : 'ltr';
  }, [locale]);

  return null;
}

После изменения локали браузер автоматически:

  • меняет направление текста;
  • перестраивает inline-flow;
  • меняет поведение курсора;
  • корректирует табуляцию;
  • изменяет поведение некоторых CSS-свойств.

Установка атрибута lang

Для accessibility критически важен атрибут:

<html lang="ar">

Он влияет на:

  • произношение скринридерами;
  • переносы строк;
  • правила чтения;
  • spellcheck;
  • voice engines;
  • синтез речи.

Интеграция с FormatJS

useEffect(() => {
  document.documentElement.lang = locale;
}, [locale]);

Одновременное управление lang и dir

Обычно обе настройки объединяют.

import { useEffect } from 'react';
import { useIntl } from 'react-intl';

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

export function HtmlI18nController() {
  const { locale } = useIntl();

  useEffect(() => {
    const lang = locale.split('-')[0];

    document.documentElement.lang = locale;

    document.documentElement.dir =
      RTL_LANGUAGES.includes(lang)
        ? 'rtl'
        : 'ltr';
  }, [locale]);

  return null;
}

Проблемы смешанного текста

Частая проблема — сочетание RTL и LTR внутри одной строки.

Пример:

مرحبا John Smith

Браузер может отображать подобные строки некорректно:

  • ломается порядок слов;
  • числа оказываются в неожиданных местах;
  • знаки препинания смещаются;
  • текст визуально «скачет».

Unicode Bidirectional Algorithm

Браузеры используют алгоритм Unicode BiDi для определения порядка текста.

Однако автоматического определения недостаточно при сложных сценариях:

  • email внутри арабского текста;
  • URL;
  • номера телефонов;
  • смешанные языки;
  • код;
  • даты и валюты.

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

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

<p>
  مرحبا <bdi>John Smith</bdi>
</p>

Это предотвращает нарушение порядка символов.

React-пример

function UserName({ name }: { name: string }) {
  return <bdi>{name}</bdi>;
}

Использование dir="auto"

HTML умеет автоматически определять направление текста.

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

Браузер анализирует первый сильный символ и выбирает направление.

React-пример

<div dir="auto">
  {message}
</div>

Особенно полезно для:

  • пользовательского контента;
  • комментариев;
  • чатов;
  • CMS;
  • сообщений поддержки.

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

FormatJS локализует числа через formatNumber.

intl.formatNumber(123456);

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

١٢٣٤٥٦

Это влияет на визуальное направление строки.


Работа с датами в RTL

intl.formatDate(date, {
  dateStyle: 'full',
});

В RTL-языках порядок компонентов даты может существенно отличаться.

Некорректное смешивание дат с LTR-контентом часто вызывает:

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

Избежание жёстких CSS-свойств

Главная ошибка RTL-поддержки — использование физических CSS-свойств:

margin-left: 16px;
padding-right: 12px;
text-align: left;

Они не адаптируются автоматически.


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

Современный CSS поддерживает логические свойства.

Вместо margin-left

margin-inline-start: 16px;

Вместо margin-right

margin-inline-end: 16px;

Вместо padding-left

padding-inline-start: 12px;

Вместо text-align: left

text-align: start;

Преимущества logical properties

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

LTR RTL
start → left start → right
end → right end → left

Это значительно упрощает поддержку RTL.


Flexbox и RTL

Flexbox автоматически учитывает dir.

Пример

.container {
  display: flex;
}

При dir="rtl" порядок элементов визуально инвертируется.


Контроль направления Flex

Иногда требуется сохранить исходный порядок.

.container {
  display: flex;
  direction: ltr;
}

Либо:

.container {
  display: flex;
  flex-direction: row-reverse;
}

Grid и RTL

CSS Grid тоже зависит от направления документа.

grid-auto-flow: column;

В RTL колонка может начинаться справа.

Это особенно важно для:

  • таблиц;
  • dashboards;
  • карточек;
  • data grids.

Иконки и зеркалирование

Не все элементы должны зеркалироваться автоматически.

Обычно зеркалируются

  • стрелки;
  • breadcrumbs;
  • navigation chevrons;
  • back buttons.

Обычно НЕ зеркалируются

  • логотипы;
  • видео;
  • фотографии;
  • графики;
  • музыкальные элементы.

Условное отображение иконок

const isRtl = isRTL(locale);

return (
  <ArrowIcon
    style={{
      transform: isRtl
        ? 'scaleX(-1)'
        : 'none',
    }}
  />
);

RTL и SVG

SVG не всегда автоматически зеркалируются.

Иногда требуется:

transform: scaleX(-1);

Либо отдельные RTL-версии иконок.


Доступность и направление текста

Screen readers используют:

  • lang;
  • dir;
  • структуру DOM;
  • Unicode direction markers.

Если направление указано неверно:

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

VoiceOver и RTL

В iOS VoiceOver особенно чувствителен к:

  • отсутствию lang;
  • смешанным строкам;
  • inline-элементам без bdi;
  • динамически изменяемому направлению.

NVDA и арабский текст

NVDA корректно читает RTL только при:

  • наличии lang;
  • корректном dir;
  • отсутствии конфликтующих CSS-direction.

Форматирование сообщений и a11y

FormatJS позволяет вставлять React-компоненты в перевод.

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

Это полезно для:

  • имён пользователей;
  • email;
  • mixed content;
  • username placeholders.

Работа с input в RTL

Поля ввода наследуют направление.

<input dir="auto" />

Это особенно важно для:

  • поисковых строк;
  • мессенджеров;
  • комментариев;
  • CRM;
  • админ-панелей.

Проблемы placeholder-текста

Placeholder может отображаться в неверном направлении.

Решение:

<input
  dir="auto"
  placeholder={intl.formatMessage({
    id: 'search',
  })}
/>

Табуляция и клавиатурная навигация

RTL влияет на:

  • порядок фокуса;
  • восприятие интерфейса;
  • keyboard navigation;
  • screen reader traversal.

Важно не ломать естественный DOM-порядок ради визуального RTL.


Ошибка визуального реверса

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

flex-direction: row-reverse;

для всего приложения.

Это нарушает:

  • tab order;
  • accessibility tree;
  • screen reader navigation.

Правильный подход — использовать dir="rtl".


ARIA и локализация

ARIA-атрибуты тоже должны локализоваться.

<button
  aria-label={intl.formatMessage({
    id: 'close',
  })}
>
  ×
</button>

Перевод скрытого текста

Часто забывают локализовать:

  • aria-label;
  • aria-description;
  • title;
  • alt;
  • placeholder.

Это создаёт серьёзные проблемы доступности.


Локализация alt

<img
  src="/hero.png"
  alt={intl.formatMessage({
    id: 'heroAlt',
  })}
/>

Локализация live regions

<div aria-live="polite">
  {intl.formatMessage({
    id: 'saved',
  })}
</div>

Screen readers должны получать сообщения на корректном языке.


Форматирование списков

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

intl.formatList([
  'React',
  'Vue',
  'Angular',
]);

В разных языках правила соединения отличаются.


Форматирование для screen readers

Иногда визуальный текст и текст для screen reader различаются.

<span aria-hidden="true">
  €
</span>

<span className="sr-only">
  {intl.formatMessage({
    id: 'euro',
  })}
</span>

RTL и анимации

Анимации должны учитывать направление.

Ошибка

transform: translateX(100%);

В RTL движение может выглядеть нелогично.


Направление-aware анимации

const offset = isRTL(locale)
  ? '-100%'
  : '100%';

Локализация canvas и charts

Canvas и графики не наследуют dir.

Направление приходится задавать вручную:

ctx.direction = 'rtl';

если библиотека поддерживает API.


SSR и направление документа

При серверном рендеринге dir должен присутствовать сразу.

Next.js пример

<html
  lang={locale}
  dir={isRTL(locale) ? 'rtl' : 'ltr'}
>

Иначе возможны:

  • hydration mismatch;
  • layout jump;
  • FOUC;
  • некорректный initial render.

RTL и CSS-in-JS

Некоторые библиотеки поддерживают RTL автоматически:

  • Emotion RTL;
  • Stylis RTL;
  • Material UI RTL plugins.

Однако они не заменяют корректный dir.


Material UI и FormatJS

Пример интеграции:

const theme = createTheme({
  direction: isRTL(locale)
    ? 'rtl'
    : 'ltr',
});

RTL-тестирование

Проверять необходимо:

  • desktop;
  • mobile;
  • screen readers;
  • keyboard navigation;
  • mixed content;
  • forms;
  • tables;
  • animations.

Автоматическое тестирование

RTL-сценарии можно тестировать через Jest и Testing Library.

render(
  <div dir="rtl">
    <App />
  </div>
);

Проверка accessibility

Полезные инструменты:

  • axe;
  • Lighthouse;
  • NVDA;
  • VoiceOver;
  • TalkBack.

Типичные ошибки RTL

Жёсткое выравнивание

text-align: left;

Абсолютное позиционирование

left: 0;

Неправильные иконки

Стрелки остаются LTR.

Смешанный текст без bdi

Ломается порядок символов.

Отсутствие lang

Скринридеры читают текст неправильным голосом.


Рекомендуемая архитектура

Обычно создаются:

I18nProvider

Отвечает за:

  • locale;
  • messages;
  • IntlProvider.

DirectionProvider

Отвечает за:

  • dir;
  • lang;
  • RTL detection.

RTL-aware UI layer

Содержит:

  • logical CSS;
  • mirrored icons;
  • accessibility helpers.

Пример полной интеграции

import { IntlProvider } from 'react-intl';

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

function isRTL(locale: string) {
  return RTL_LANGUAGES.includes(
    locale.split('-')[0]
  );
}

export function AppI18n({
  locale,
  messages,
  children,
}) {
  useEffect(() => {
    document.documentElement.lang =
      locale;

    document.documentElement.dir =
      isRTL(locale)
        ? 'rtl'
        : 'ltr';
  }, [locale]);

  return (
    <IntlProvider
      locale={locale}
      messages={messages}
    >
      {children}
    </IntlProvider>
  );
}