Поддержка разных языков в интерфейсе затрагивает не только перевод строк, но и корректное направление текста. Для языков вроде арабского или иврита применяется направление справа налево — RTL (Right-to-Left), тогда как большинство европейских языков используют LTR (Left-to-Right). Неправильная работа с направлением приводит к визуальным ошибкам, нарушению навигации, проблемам со скринридерами и ухудшению доступности.
FormatJS напрямую не управляет стилями или DOM-направлением, однако библиотека играет ключевую роль в определении текущей локали и построении инфраструктуры интернационализации, от которой зависит вся RTL/LTR-логика приложения.
Существует два основных режима отображения текста:
| Режим | Описание |
|---|---|
ltr |
Текст отображается слева направо |
rtl |
Текст отображается справа налево |
Наиболее распространённые RTL-языки:
ar)he)fa)ur)Для них требуется:
FormatJS предоставляет текущую локаль через IntlProvider
и хуки React Intl.
import { useIntl } from 'react-intl';
function CurrentLocale() {
const intl = useIntl();
return <div>{intl.locale}</div>;
}
На основе локали можно вычислить направление интерфейса.
На практике обычно создают список RTL-языков.
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];
export function isRTL(locale: string): boolean {
const lang = locale.split('-')[0];
return RTL_LANGUAGES.includes(lang);
}
dirHTML поддерживает специальный атрибут:
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;
}
После изменения локали браузер автоматически:
langДля accessibility критически важен атрибут:
<html lang="ar">
Он влияет на:
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 BiDi для определения порядка текста.
Однако автоматического определения недостаточно при сложных сценариях:
bdiТег <bdi> изолирует направление текста.
<p>
مرحبا <bdi>John Smith</bdi>
</p>
Это предотвращает нарушение порядка символов.
function UserName({ name }: { name: string }) {
return <bdi>{name}</bdi>;
}
dir="auto"HTML умеет автоматически определять направление текста.
<div dir="auto">
مرحبا John
</div>
Браузер анализирует первый сильный символ и выбирает направление.
<div dir="auto">
{message}
</div>
Особенно полезно для:
FormatJS локализует числа через formatNumber.
intl.formatNumber(123456);
Для арабской локали результат может выглядеть так:
١٢٣٤٥٦
Это влияет на визуальное направление строки.
intl.formatDate(date, {
dateStyle: 'full',
});
В RTL-языках порядок компонентов даты может существенно отличаться.
Некорректное смешивание дат с LTR-контентом часто вызывает:
Главная ошибка RTL-поддержки — использование физических CSS-свойств:
margin-left: 16px;
padding-right: 12px;
text-align: left;
Они не адаптируются автоматически.
Современный CSS поддерживает логические свойства.
margin-leftmargin-inline-start: 16px;
margin-rightmargin-inline-end: 16px;
padding-leftpadding-inline-start: 12px;
text-align: lefttext-align: start;
Логические свойства автоматически адаптируются под направление:
| LTR | RTL |
|---|---|
| start → left | start → right |
| end → right | end → left |
Это значительно упрощает поддержку RTL.
Flexbox автоматически учитывает dir.
.container {
display: flex;
}
При dir="rtl" порядок элементов визуально
инвертируется.
Иногда требуется сохранить исходный порядок.
.container {
display: flex;
direction: ltr;
}
Либо:
.container {
display: flex;
flex-direction: row-reverse;
}
CSS Grid тоже зависит от направления документа.
grid-auto-flow: column;
В RTL колонка может начинаться справа.
Это особенно важно для:
Не все элементы должны зеркалироваться автоматически.
const isRtl = isRTL(locale);
return (
<ArrowIcon
style={{
transform: isRtl
? 'scaleX(-1)'
: 'none',
}}
/>
);
SVG не всегда автоматически зеркалируются.
Иногда требуется:
transform: scaleX(-1);
Либо отдельные RTL-версии иконок.
Screen readers используют:
lang;dir;Если направление указано неверно:
В iOS VoiceOver особенно чувствителен к:
lang;bdi;NVDA корректно читает RTL только при:
lang;dir;FormatJS позволяет вставлять React-компоненты в перевод.
<FormattedMessage
id="welcome"
values={{
name: chunks => <bdi>{chunks}</bdi>,
}}
/>
Это полезно для:
Поля ввода наследуют направление.
<input dir="auto" />
Это особенно важно для:
Placeholder может отображаться в неверном направлении.
Решение:
<input
dir="auto"
placeholder={intl.formatMessage({
id: 'search',
})}
/>
RTL влияет на:
Важно не ломать естественный DOM-порядок ради визуального RTL.
Плохая практика:
flex-direction: row-reverse;
для всего приложения.
Это нарушает:
Правильный подход — использовать dir="rtl".
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',
})}
/>
<div aria-live="polite">
{intl.formatMessage({
id: 'saved',
})}
</div>
Screen readers должны получать сообщения на корректном языке.
FormatJS поддерживает локализованные списки.
intl.formatList([
'React',
'Vue',
'Angular',
]);
В разных языках правила соединения отличаются.
Иногда визуальный текст и текст для screen reader различаются.
<span aria-hidden="true">
€
</span>
<span className="sr-only">
{intl.formatMessage({
id: 'euro',
})}
</span>
Анимации должны учитывать направление.
transform: translateX(100%);
В RTL движение может выглядеть нелогично.
const offset = isRTL(locale)
? '-100%'
: '100%';
Canvas и графики не наследуют dir.
Направление приходится задавать вручную:
ctx.direction = 'rtl';
если библиотека поддерживает API.
При серверном рендеринге dir должен присутствовать
сразу.
<html
lang={locale}
dir={isRTL(locale) ? 'rtl' : 'ltr'}
>
Иначе возможны:
Некоторые библиотеки поддерживают RTL автоматически:
Однако они не заменяют корректный dir.
Пример интеграции:
const theme = createTheme({
direction: isRTL(locale)
? 'rtl'
: 'ltr',
});
Проверять необходимо:
RTL-сценарии можно тестировать через Jest и Testing Library.
render(
<div dir="rtl">
<App />
</div>
);
Полезные инструменты:
text-align: left;
left: 0;
Стрелки остаются LTR.
bdiЛомается порядок символов.
langСкринридеры читают текст неправильным голосом.
Обычно создаются:
Отвечает за:
Отвечает за:
dir;lang;Содержит:
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>
);
}