Главная » Хабрахабр » Справа налево. Как перевернуть интерфейс сайта под RTL

Справа налево. Как перевернуть интерфейс сайта под RTL

image

Мы недавно перевели онлайн-версию 2ГИС на арабский язык, и в прошлой статье я рассказал о необходимой для этого теории — что такое dir="rtl", по каким правилам отображается текст смешанной направленности и как держать себя в руках.

Настало время приступить к практике — перевернуть с минимальными усилиями весь интерфейс справа налево так, чтобы даже настоящий араб не почувствовал подвоха.

В этой статье я расскажу, как быстро сделать прототип, что сделать со сборкой CSS и какие костыли разложить в JS, замечу немного об особенностях перевода и локализации, напомню про логические свойства CSS и затрону тему RTL в CSS-in-JS.

Переворачиваем стили

С явно заданными в стилях значениями ничего не произошло.
Когда я применил к тегу атрибут dir="rtl", поменялся только неявный порядок элементов — например, порядок ячеек таблицы или flex-элементов.

Возьмём стили какой-нибудь нотификации, расположенной снизу справа:

.tooltip { position: 'absolute'; bottom: 10px; right: 10px;
}

dir="rtl" никак не повлияет на эти стили — в RTL-версии тултип будет так же справа, хотя ожидается он слева.

Нужно заменить right: 10px на left: 10px. Что делать? Абсолютное позиционирование, margin/padding, выравнивание текста — всё нужно для арабской версии перевернуть в другую сторону. И так со всеми остальными стилями.

Быстрый прототип

Для начала можно, не задумываясь, поменять все вхождения left на right и немножко поколдовать с shorthand значениями:

  • left: 0 → right: 0
  • padding-left: 4px → padding-right: 4px
  • margin: 0 16px 0 0 → margin: 0 0 0 16px

Удобно — нужно только вбросить его в список всех postcss-плагинов проекта. Для этого подходит плагин postcss-rtl. Например: Он заменяет все направленные правила на зеркальные и оборачивает это в [dir="rtl"].

/* input */ .foo { color: red; margin-left: 16px;
} /* output */ [dir] .foo { color: red;
} [dir="ltr"] .foo { margin-left: 16px;
} [dir="rtl"] .foo { margin-right: 16px;
}

Всё работает и кажется, что почти всё готово к продакшену, но это решение годится только для быстрого прототипа: После этого нужно только задать dir="rtl" и автоматически применятся только нужные правила.

  • увеличивается специфичность каждого правила. Это не обязательно будет проблемой, но хотелось бы этого избежать;
  • такие манипуляции порождают баги. Например, может ломаться порядок свойств;
  • заметно растёт размер css-файла. К каждому селектору добавляется [dir], каждое направленное свойство дублируется. В нашем случае размер увеличился на одном проекте на 21%, на другом — на 35%:

исходный размер (gzip)

двунаправленный размер (gzip)

распухло на

2gis.ru

272.3 kB

329.7 kB

21%

m.2gis.ru

24.5 kB

33.2 kB

35%

habr.com

33.1 kB

41.4 kB

25%

Есть вариант лучше?

Тогда не нужно будет трогать селекторы и размер css почти не изменится. Нужно собирать стили для LTR и RTL раздельно.

Для этого я выбрал:

  1. RTLCSS — эта библиотека под капотом у postcss-rtl.
  2. webpack-rtl-plugin — готовое решение для стилей, собираемых через ExtractTextPlugin. Тот же RTLCSS под капотом.

Единственный минус сборки в разные файлы — нельзя заменить dir на лету, не загружая предварительно нужный файл. И стал собирать RTL и LTR в разные файлы — styles.css и styles.rtl.css.

RTLCSS позволяет использовать директивы, чтобы контролировать обработку конкретных правил, например:

.foo { /*rtl:ignore*/ right: 0;
} .bar { font-size:16px/*rtl:14px*/;
}

Какие ещё есть решения?

Все существующие решения почти не отличаются от RTLCSS.

  • css-flip от Twitter;
  • cssjanus от Wikimedia;
  • да и postcss-rtl поддерживает параметр onlyDirection, с помощью которого можно собирать стили только для одной направленности, но размер всё равно растёт — например, для мобильного 2ГИС это 18% вместо 35% (24.5 kB → 29 kB).

Когда нужны директивы?

Когда стили не должны зависеть от направленности

Например, угол поворота стрелки, указывающей направление ветра:

image

.arrow._nw { /*rtl:ignore*/ transform: rotate(135deg);
}

Или фейд у номера телефона — числа всегда пишутся слева направо, значит, и градиент должен быть всегда справа:

image

image

Когда нужно центрировать иконку

Если центрируем несимметричную иконку через отступы/позиционирование, мы смещаем её блок в сторону, и если это смещение отразить, иконка «съедет» в другую сторону: Это частный случай предыдущего пункта.

image

Лучше в таких ситуациях центрировать иконку в самой svg:

Когда нужно изолировать целый виджет, который не должен реагировать на RTL

Мы оборачиваем все её стили при сборке в блочные директивы: /*rtl:begin:ignore*/ ... В нашем случае это карта. /*rtl:end:ignore*/.

Есть ли вариант ещё лучше?

Зависимость стилей от направления — естественная задача для современного веба, и её актуальность с каждым годом растёт всё больше. Решение с переворачиванием правил прекрасно работает, но возникает вопрос — а не костыль ли это? И нашло! Это должно было найти отражение в современных стандартах и подходах.

Logical properties

Он касается не только направлений слева направо и справа налево, но и направления сверху вниз, но его мы рассматривать не будем. Для адаптации вёрстки под разную направленность существует стандарт логических свойств в css.

Мы уже пользуемся чем-то похожим во флексах и гридах — например, flex-start, flex-end, grid-row-start, grid-column-end отвязаны от «лево/право».

Вместо width и heightinline-size и block-size. Вместо понятий left, right, top и bottom предлагается использовать inline-start, inline-end, block-start и block-end. Также для существующих шортхендов появляются новые парные версии — padding-block, margin-inline, border-color-inline, и т.д. А вместо шортхендов a b c dlogical a d c b (логические шортхенды идут против часовой стрелки).

  • left: 0 → inset-inline-start: 0
  • padding-left: 4px → padding-inline-start: 4px
  • margin: 0 16px 0 0 → margin: logical 0 0 0 16px
  • padding-top: 8px; padding-bottom: 16px → padding-block: 8px 16px
  • margin-left: 4px; margin-right: 8px → margin-inline: 4px 8px
  • text-align: right → text-align: end

А ещё появляется долгожданный шортхенд для позиционирования:

  • left: 4px; right: 8px → inset-inline: 4px 8px
  • top: 8px; bottom: 16px → inset-block: 8px 16px
  • top: 0; right: 2px; bottom: 2px; left: 0 → inset: logical 0 0 2px 2px

Это уже доступно в firefox без флагов и в вебкитовых браузерах под флагом.

Нет потребности в директивах — просто пиши left вместо inline-start, когда имеется в виду физическое «слева». Плюсы — решение нативное, будет работать вообще без сборки/плагинов, если нужные браузеры поддерживаются.

Минусы вытекают из плюсов — без плагинов код в большинстве браузеров невалиден, нужно проделать много работы, чтобы перевести большой существующий проект.

Как подключить?

Без параметра dir собирает стили для обоих направлений аналогично postcss-rtl, с заданным параметром dir — только для указанной направленности: Самый простой способ — postcss-logical.

.banner { color: #222222; inset: logical 0 5px 10px; padding-inline: 20px 40px; resize: block; transition: color 200ms;
} /* becomes */ .banner &:dir(rtl) { padding-right: 20px; padding-left: 40px; } resize: vertical; transition: color 200ms;
} /* or, when used with { dir: 'ltr' } */ .banner { color: #222222; top: 0; left: 5px; bottom: 10px; right: 5px; padding-left: 20px; padding-right: 40px; resize: vertical; transition: color 200ms;
}

Как убедить команду начать писать offset-inline-start вместо left?

Но мы у себя на проекте решили упростить — писать start: 0 вместо offset-inline-start: 0, как только все привыкнут, начну навязывать валидную запись 🙂 Никак.

RTL + CSS-in-JS = ️️<3

Значит, можно в рантайме определять направленность компонентов и выбирать, какие переворачивать, а какие нет. CSS-in-JS не нужно собирать заранее. Полезно, если нужно вставить какой-то виджет, не поддерживающий RTL вообще.

В целом задача состоит в том, чтобы превращать объекты типа { paddingInlineStart: '4px' } (или { paddingLeft: '4px' }, если не удалось перейти на логические свойства) в объекты типа { paddingRight: '4px' }:

  1. Вооружаемся bidi-css-js или rtl-css-js. Они предоставляют функцию, которая принимает объект стилей и возвращает трансформированный под нужную направленность.
  2. ???
  3. PROFIT!

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

Оборачиваем каждый стилизуемый компонент в HOC, принимающий стили:

export default withStyles(styles)(Button);

Он берёт из контекста направленность компонента и выбирает конечные стили:

function withStyles(styles) { const { ltrStyles, rtlStyles } = bidi(styles); return function WithStyles(WrappedComponent) { ... render() { return <WrappedComponent {...this.props} styles={this.context.dir === 'rtl' ? rtlStyles : ltrStyles} />; }; }; ... };
}

А направленность в контекст прокидывает провайдер:

<DirectionProvider dir="rtl"> ... <Button /> ...

Похожий подход используют airbnb: https://github.com/airbnb/react-with-styles-interface-aphrodite#built-in-rtl-support, если на проекте уже используется aphrodite, можно воспользоваться этим готовым решением.

Для JSS всё ещё проще — нужно только подключить jss-rtl:

jss.use(rtl());

styled-components

const Button = styled.button` background: #222; margin-left: 12px;
`;

Всё сложно, но выход есть — вычислять название свойства из направления, заданного в props: Что делать, если мы работаем с шаблонными строками, а не с объектами?

const marginStart = props => props.theme.dir === "rtl" ? "margin-left" : "margin-right"; const Button = styled.button` background: #222; ${marginStart}: 12px;
`;

3. Но кажется проще перейти со строк на объекты, styled-components умеют это с версии 3. 0.

Особенности перевода и локализации

Изолировали контент неопределённой направленности, отзеркалили стили, расставили исключения в нужных местах, перевели тексты на арабский. Мы разобрались с технической частью. Кажется, всё готово — при переключении языка весь интерфейс оказывается с другой стороны экрана, никакая вёрстка не едет и всё выглядит лучше, чем на любом арабском сайте.

Показываем настоящему арабу, и...

Это касается почти всех слов на английском. Оказывается, не каждый носитель арабского языка знает, что такое Twitter. Для такого случая есть арабский транслит: «تويتر».

Оказывается, в арабском свои запятые, и то, что мы везде по коду конкатенировали через «,», на арабском нужно конкатенировать через «،».

Он лунный и обычной формулой перевода не обойтись. Оказывается, в некоторых мусульманских странах официальный календарь — исламский.

Оказывается, в Дубае не бывает отрицательной температуры и знак «плюс» в прогнозе «+40» не имеет никакого смысла.

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

Это можно просто вылечить, явно задав text-align: right. Если мы делаем dir="auto" на блочный элемент и его контент окажется LTR, текст прибьётся к левой стороне контейнера, даже если вокруг RTL. Можно даже применить это ко всей странице в арабской версии — значение этого свойства наследуется.

А без этого направленные иконки, такие как стрелки в галерее, могут смотреть в неправильную сторону. Иконки тоже автоматически не отзеркалятся. Представь себе, это единственный случай, в котором стрелки, сделанные через border, оправдали себя!

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

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

Тогда придётся сделать две разные иконки и вставлять их условно: Она, правда, не поможет, если в иконке содержатся буквы или цифры.

image

Например, в нашем случае мы решили оставить чекбоксы обычными: А ещё, оказывается, не все элементы интерфейса нужно зеркалить.

image

image

Тут может помочь только носитель языка, который скажет, что ему привычно, а что нет. Как из всего интерфейса выбрать такие элементы — не знаю.

Пользовательский ввод

Например, название файла. Даже если мы полностью контролируем все данные своего приложения, это может быть пользовательский ввод. Можно даже исхитриться и выдать .js файл за .png, — такая уязвимость была в Телеграме:

прикольная_картинка*U+202E*gnp.js → прикольная_картинкаsj.png

В таких случаях стоит отфильтровать неуместные utf-символы из строки.

Переворачиваем скрипты

Цикл, который выглядел так: В RTL-джаваскрипте синтаксис немного меняется.

for (let i = 0; i < arr.length; i++) {

Теперь нужно писать так:

for (++i; length.arr > i; let 0 = i) {

Шутка.

Например, мы столкнулись с проблемами в подсчёте координат центра экрана — раньше все карточки висели слева, а теперь справа, но код приложения об этом не знал. Всё, что нужно делать — избегать понятий «left» и «right» в коде. Все расчёты и инлайн-стили нужно проводить, учитывая базовую направленность.

Смекалочка

Тогда нужно попробовать адаптировать этот компонент под RTL снаружи, но оставить LTR внутри. В некоторых ситуациях сложно внедрить поддержку RTL в какой-то компонент системы.

Он поддерживает только положительные упорядоченные значения. Например, у нас есть слайдер. Нужно отразить его с сохранением поведения шкалы. Он может быть линейным и логарифмическим (в начале плотность значений меньше, чем в конце).

image

Тогда придётся инвертировать работу с мышью (клики и драги) относительно центра слайдера. Можно отразить слайдер с помощью transform: scaleX(-1). Плохой вариант.

Если это линейная шкала, вместо набора значений [10, 100, 1000] передадим набор [N-1000, N-100, N-10], а в хендлере преобразуем обратно. Есть другой вариант — развернуть ось в другую сторону, изменив только передачу и получение значений из слайдера. Для логарифмической шкалы вместо набора [10, 100, 1000] передадим [1/1000, 1/100, 1/10]:

function flipSliderValues(values, scale, isRtl) { if (!isRtl) { return values; } if (scale === 'log') { // [A, B] --> [1/B, 1/A] return values.map(x => 1 / x).reverse(); } // [A, B] --> [MAX-B, MAX-A] return values.map(x => Number.MAX_SAFE_INTEGER - x).reverse();
};

Вот так слайдер стал поддерживать RTL, хотя он сам об этом не знает.

Storybook

Можно даже верстать и видеть вёрстку LTR и RTL одновременно в одном окне. В отличие от проверки вёрстки под каким-нибудь IE9, для проверки вёрстки под RTL не нужно запускать отдельный браузер. Для этого можно, например, сделать декоратор в сторибуке, который рендерит сразу две версии истории:

image

На скриншоте видно, что без изоляции text-overflow: ellipsis ведёт себя не так, как хотелось бы — лучше сразу пофиксить.

Намного проще поддерживать RTL сразу при вёрстке, чем потом тестировать абсолютно весь проект.

Нерешаемые проблемы

Приведу один пример. Знание теории не помогает решить абсолютно все задачи.

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


В данном кейсе можно при навигации по подсказкам подставлять весь текст целиком (как, например, делает Яндекс или Google). Нужно стараться избегать таких проблем на этапе дизайна и иногда отказываться от очевидных для LTR решений, никак не применимых в RTL.

Выводы

RTL — это не просто «перевернуть всё»

Где-то в логике совсем нужно отказаться от «право»/«лево». Нужно учитывать особенности языка, что-то переворачивать не нужно, что-то нужно адаптировать иначе.

Очень сложно что-то сделать без знания языка

Разрабатывай с точки зрения человека, не знающего никакого языка. Ты будешь думать, что всё готово, пока не покажешь свой проект настоящему носителю языка. И знаки препинания, оказывается, не на всей планете одинаковые. Ведь даже такие очевидные для тебя слова, как, например, «Twitter», возможно, придётся переводить.

Итоговый рецепт

Пошагового руководства не будет, но вот главное, что нужно сделать: Сложно описать в одном списке всё, о чём шла речь в двух статьях.

  • обязательно найди носителя языка и покажи ему прототипы как можно раньше;
  • собирай стили для LTR и RTL в разные файлы. Для этого подходят rtlcss и webpack-rtl-plugin;
  • добавь исключения для всего, что переворачивать не нужно и явно отрази то, что не перевернулось само;
  • изолируй весь контент произвольной направленности с помощью <bdi> и dir="auto";
  • явно задай text-align на всю страницу;
  • избегай left/right в js-коде, когда имеешь в виду начало и конец.

Готовься к тому, что больше всего времени придётся потратить на самые мелкие детали.

Быть заранее готовым — не сложно

И немного советов для тех, кто пока что не собирается адаптировать сайт под RTL, но хочет подстелить соломку:

  • не используй свойство direction не по назначению;
  • на всякий случай всё-таки изолируй весь произвольный контент (да и ведь даже в английском интерфейсе юзеры могут что-нибудь написать на арабском и всё сломать);
  • если есть возможность, используй логические css-свойства;
  • проверяй вёрстку не только в разных браузерах, но и иногда в RTL, хотя бы ради любопытства. А лучше ненавязчиво контролируй вёрстку под RTL при помощи инструментов вроде storybook;
  • не допускай хардкода языковых конструкций (например, конкатенация строк через запятую), по возможности конфигурируй всё, включая знаки препинания. Это пригодится не только для RTL — к примеру, на греческом языке вопросительный знак — «;».

Но если внезапно придёт желание запустить RTL-версию, это достанется намного дешевле, чем могло бы. Эти правила не должны доставить хлопот.


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

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Как в Сингапуре работают с инновациями: от госрегулирования до ночных клубов

Привет, Хабр! Меня зовут Сергей Лукашкин, я директор по управлению проектами в управлении цифровой трансформации ДИТ ВТБ. Много лет занимаюсь финтехом и инновациями в финансовой сфере. Конечно, уделяю много времени изучению лучших российских и зарубежных финтех-практик. В мире есть несколько ...

[Из песочницы] Ticket to Ride.Европа — скромные шаги в арифметику игры

День первый. Нам подарили игру «Ticket to ride. Европа». Это моё первое знакомство с игрой данной серии, надо обязательно попробовать и заценить. Как-то надоело регулярно проигрывать, пора бы призвать на помощь математику и попробовать таким образом одержать заслуженную победу. День ...