При локализации интерфейсов недостаточно перевести текст. Языки отличаются не только словарём и грамматикой, но и направлением письма. Большинство европейских языков используют направление слева направо (Left-to-Right, LTR), тогда как арабский, иврит, персидский и урду используют направление справа налево (Right-to-Left, RTL).
Библиотека FormatJS не занимается непосредственным изменением CSS или DOM-структуры, однако предоставляет инструменты для корректной интернационализации текста, чисел, дат и форматирования, которые должны учитывать направление письма и особенности письменности.
Основные режимы письма:
| Направление | Примеры языков |
|---|---|
| LTR | английский, русский, французский |
| RTL | арабский, иврит, персидский |
HTML использует атрибут dir:
<html dir="ltr">
или:
<html dir="rtl">
Направление влияет на:
FormatJS работает поверх Intl API браузера. При выборе
локали библиотека автоматически использует соответствующие правила
форматирования:
import {IntlProvider} from 'react-intl';
<IntlProvider locale="ar">
<App />
</IntlProvider>
Локаль "ar" сообщает:
Однако сам 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}>
Часто направление подключают одновременно с
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>
Так интерфейс получает:
Одна из сложнейших проблем интернационализации — смешивание LTR и RTL внутри одной строки.
Пример:
تم إرسال الملف test.pdf بنجاح
Здесь арабский текст содержит латинское имя файла.
Браузер использует алгоритм Bidirectional Algorithm (BiDi), который пытается определить порядок символов автоматически. Иногда результат оказывается неожиданным.
Для предотвращения ошибок используются специальные 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>
)
}}
/>
Теперь имя файла отображается корректно независимо от языка.
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 locale extension позволяет задавать письменность явно.
Формат локали:
language-Script-region
Примеры:
| Локаль | Значение |
|---|---|
| sr-Cyrl | сербский, кириллица |
| sr-Latn | сербский, латиница |
| zh-Hans | китайский, упрощённый |
| zh-Hant | китайский, традиционный |
<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-интерфейсах переносы могут выглядеть неожиданно:
السعر: 100 USD
Иногда USD переносится отдельно, нарушая визуальную
структуру.
Решение — использовать неразрывные пробелы:
'السعر: 100\u00A0USD'
FormatJS корректно форматирует валюты для RTL-языков.
<FormattedNumber
value={1000}
style="currency"
currency="USD"
/>
Для арабской локали отображение может быть:
١٬٠٠٠٫٠٠ US$
Обратить внимание следует на:
RTL-текст часто содержит невидимые символы:
| Символ | Назначение |
|---|---|
| LRM | Left-to-Right Mark |
| RLM | Right-to-Left Mark |
| ALM | Arabic Letter Mark |
Они помогают браузеру правильно отображать смешанный текст.
Пример:
const LRM = '\u200E';
Использование:
`${LRM}USD`
Пример:
<FormattedDate
value={new Date()}
year="numeric"
month="long"
day="numeric"
/>
Для арабской локали:
٢٨ مايو ٢٠٢٦
FormatJS автоматически:
Intl.ListFormat особенно важен для RTL.
<FormattedList
value={['أحمد', 'محمد', 'علي']}
type="conjunction"
/>
Результат:
أحمد ومحمد وعلي
FormatJS корректно подставляет союз и разделители.
FormatJS не управляет стилями интерфейса. RTL требует отдельной адаптации CSS.
Основные проблемы:
| Элемент | Изменение |
|---|---|
| margin-left | margin-right |
| padding-left | padding-right |
| flex-direction | reverse |
| text-align | right |
| position | mirror |
Современный подход:
margin-inline-start: 10px;
padding-inline-end: 20px;
Вместо:
margin-left: 10px;
padding-right: 20px;
Логические свойства автоматически адаптируются под 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:
ɥsıןƃuƎ
Тестирование помогает обнаружить:
Перед использованием RTL-языков полезно проверять поддержку:
Intl.NumberFormat.supportedLocalesOf([
'ar',
'he',
'fa'
]);
Пример:
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 не занимается рендерингом шрифтов, но корректная локаль влияет на:
Некоторые письменности не используют пробелы между словами.
Пример:
Intl.Segmenter помогает корректно разбивать текст.
const segmenter =
new Intl.Segmenter('ja', {
granularity: 'word'
});
Поддержка возможностей зависит от среды:
| Возможность | Поддержка |
|---|---|
| RTL | практически везде |
| Intl.Locale | современные браузеры |
| Intl.Segmenter | новые браузеры |
| Unicode extensions | современные движки |
Для старых браузеров FormatJS предоставляет polyfill-пакеты.
Intl.Localenpm install @formatjs/intl-locale
Использование:
import '@formatjs/intl-locale/polyfill';
Intl.Segmenternpm install @formatjs/intl-segmenter
Полноценная интернационализация включает:
FormatJS закрывает уровень локализации и форматирования, а направление интерфейса обычно реализуется совместно с: