Платформа 1С:Предприятие локализована на 22 языка, включая английский, немецкий, французский, китайский, вьетнамский. Недавно, в версии 8.3.17, мы поддержали арабский язык.

Одна из особенностей арабского языка в том, что текст на нём пишут и читают справа налево. UI для арабского языка надо отображать зеркально по горизонтали (но не всё и не всегда – тут есть тонкости), контекстное меню открывать слева от курсора и т.п.

Под катом – о том, как мы поддержали RTL (right-to-left) в веб-клиенте платформы 1С:Предприятие, а ещё – одна из гипотез, объясняющая, почему арабский мир пишет справа налево.

image

Немного истории


Для нас привычно письмо слева направо. Это направление письма во многом порождено тем фактом, что при записи текста на бумаге правши (а их, по статистике, около 85%) видят то, что уже написано – пишущая (правая) рука не закрывает написанный текст. Левшам же приходится мучиться.

Одна из гипотез «почему в арабском языке используется письмо справа налево» звучит так. Языки, от которых берет свое начало арабский, зародились в те времена, когда не было бумаги и ее аналогов (папируса, пергамента и т.п.). Был только один способ фиксации информации – высекать письмена на камне. А как правшам будет удобнее орудовать молотком и зубилом? Конечно же, держа зубило в левой руке и стуча по нему молотком, зажатым в правой. А в этом случае удобнее писать как раз справа налево.

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

Как мы приступали к задаче?


Никто из разработчиков платформы не говорил по-арабски и не имел опыта разработки RTL-интерфейсов. Мы перелопатили массу статей на тему RTL (особенно хочется поблагодарить компанию «2ГИС» за проделанную работу и тщательно проработанные статьи: статья 1, статья 2). По мере изучения материала пришло понимание, что без носителя языка нам никак не обойтись. Поэтому одновременно с поиском переводчиков на арабский язык мы стали искать себе сотрудника – носителя арабского языка, который бы имел нужный нам опыт, мог бы проконсультировать нас по арабской специфике интерфейсов. Просмотрев несколько кандидатов, мы нашли такого человека и приступили к работе.

Поиграем шрифтами


По умолчанию мы используем в платформе шрифт Arial, 10pt. Разработчик конкретной конфигурации может поменять шрифт у большинства элементов интерфейса, но, как показывает практика, делается этот нечасто. Т.е. в большинстве случаев пользователи программ 1С видят на экранах надписи, написанные Arial-ом.

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

100%:

image

Арабские пользователи, как правило, работают в увеличенном DPI – 125%, 150%. В этом DPI ситуация улучшается, но Arial по-прежнему остаётся плохо читаемым в силу особенностей шрифта.

125%:

image

150%:

image

Мы рассмотрели несколько вариантов решения этой проблемы:

  1. Поменять шрифт по умолчанию Arial на другой, одинаково хорошо отображающий все языки, поддерживаемые платформой (включая арабский).
  2. Увеличить размер шрифта Arial до 11pt в RTL-интерфейсе.
  3. Заменить шрифт по умолчанию с Arial на более подходящий для арабского текста, а в LTR-интерфейсе продолжать использовать Arial.

При выборе решения приходилось учитывать, что шрифт Arial размером 10pt используется в платформе 1С:Предприятие очень давно, на платформе создано нами и нашими партнёрами более 1300 тиражных решений, и во всех них шрифт Arial 10pt хорошо себя показал на всех поддерживаемых ОС (Windows, Linux и macOS различных версий), а также в браузерах. Смена шрифта и/или его размера означала бы необходимость массированного тестирования пользовательского интерфейса, и многое в этих тестах автоматизации не поддаётся. Смена шрифта также означала бы, что для текущих пользователей меняется привычный интерфейс программ.

Более того, найти универсальный шрифт, хорошо отображающий все языки, включая арабский, нам не удалось. Например, шрифт Segoe UI хорошо отображает арабский даже при 10pt, но не поддерживает китайский язык, а также не поддерживается в ряде ОС. Tahoma неплохо отображает арабский текст при 10pt, но имеет проблемы с поддержкой в Linux и «слишком жирное» начертание латиницы/кириллицы в случае жирного шрифта (арабский жирный текст выглядит хорошо). И т.д., и т.п.

Увеличение размера шрифта по умолчанию до 11pt в RTL-интерфейсе означало бы серьёзный объём тестирования пользовательского интерфейса – мы должны убедиться, что всё отрисовывается корректно, все надписи помещаются в отведённое для них место и т.п. И даже при размере 11pt Arial показывает арабские символы не идеально.

В итоге оптимальным с точки зрения трудозатрат и достигаемого эффекта оказался третий путь: мы продолжаем использовать Arial для всех символов, кроме арабских. А для арабских символов используем хорошо подходящий для этого шрифт – Almarai. Для этого в CSS добавляем:

@font-face {
  font-family: 'Almarai';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('Almarai'), 
       local('Almarai-Regular'),
       url(https://fonts.gstatic.com/s/almarai/v2/tsstApxBaigK_hnnQ1iFo0C3.woff2) 
            format('woff2');
  unicode-range: 
       U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
}

и далее везде, где нужно использовать шрифт по умолчанию, задаём шрифт таким образом:

font-family: 'Almarai', Arial, sans-serif;

Прелесть этого подхода в том, что, если в интерфейсе нет ни одного символа, попадающего в диапазон unicode-range, то такой шрифт даже не загрузится. Но как только такой символ появится, браузер сам загрузит шрифт (или использует его локальную версию) и отобразит символ нужным шрифтом.

«Переворот» интерфейса


Как и следовало ожидать, HTML-вёрстка веб-клиента не была готова к «перевороту». Совершив первый шаг, поставив на корневом элементе атрибут dir=”rtl” и добавив стиль html[dir=rtl] {text-align: right;}, мы приступили к кропотливой работе. В ходе этой работы мы выработали ряд практик, которыми хотим здесь поделиться.

Симметрия


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

В колонке «до RTL» графически представлены исходные отступы элементов кнопки. Очевидна зависимость величины отступов от наличия элементов в кнопке, а также от последовательности их расположения. Если есть картинка, то тексту не нужен левый отступ, если картинка справа, то у картинки отрицательный сдвиг, если есть маркер выпадающего списка – контейнеру с текстом больше отступ справа, если маркер сразу после картинки – у него еще отступ справа. Слишком много «если», за исключением кнопки только с текстом, у которого симметричные отступы. Симметричные! Если распределить отступы симметрично, то и переворачивать нечего. Это и стало основной идеей.

В колонке «после RTL» показаны новые симметричные отступы на тех же самых кнопках. Осталось решить нюанс с отступом между картинкой и маркером списка. Решение хотелось универсальное для любой ориентации. Сам треугольник рисуется верхним бордером на псевдоэлементе, а отступ ему нужен, только если он после картинки. Вот под таким условием добавляется еще псевдоэлемент шириной в необходимый отступ. Треугольник и отступ сами поменяются местами при смене ориентации.

image

Примечание. Все приведённые ниже примеры по умолчанию приведены для LTR-интерфейса. Чтобы увидеть, как пример выглядит в RTL-интерфейсе, смените dir=”ltr” на dir=”rtl”.

<!DOCTYPE html>
<html dir="ltr">
<head>
<style>
.button {
    display: inline-flex;
    align-items: center;
    border: 1px solid #A0A0A0;
    border-radius: 3px;
    height: 26px;
    padding: 0 8px;
}
.buttonImg {
    background: #A0A0A0;
    width: 16px;
    height: 16px;
}
.buttonBox {
    margin: 0 8px;
}
.buttonDrop {
    display: flex;
}
.buttonDrop:after {
    content: '';
    display: block;
    border-width: 3px 3px 0;
    border-style: solid;
    border-left-color: transparent;
    border-right-color: transparent;
}
.buttonImg + .buttonDrop::before {
    content: '';
    display: block;
    width: 8px;
    overflow: hidden;
}
</style>
</head>
<body>
<a class="button">
    <span class="buttonImg"></span>
    <span class="buttonBox">Настройки</span>
    <span class="buttonDrop"></span>
</a>
<a class="button">
    <span class="buttonImg"></span>
    <span class="buttonDrop"></span>
</a>
</body>
</html>

Мы стараемся избегать лишних элементов, псевдоэлементов и обёрток. Но, делая выбор в данном случае между увеличением условий в CSS и добавлением псевдоэлемента, победило решение с псевдоэлементом в силу своей универсальности. Таких кнопок бывает на форме немного, поэтому производительность при добавлении элементов не пострадает даже в Internet Explorer.

Принцип симметрии оказался полезен и при прокрутке наших панелей. Чтобы сдвинуть содержимое по горизонтали, ранее применялось единичное свойство margin-left: -Npx;.

image

Теперь устанавливается значение симметричное margin: 0 -Npx;, т.е. для левого и правого сразу, а куда сдвинуть — знает сам браузер, в зависимости от указанного направления.

Атомарные классы


Одной из возможностей нашей платформы является возможность динамически менять контент и его расположение на форме «на лету» по вкусу каждого пользователя. Нередкий случай изменений – выравнивание текста по горизонтали: слева, справа или по центру. Достигается это простым выравниваем text-align с соответствующим значением. Разворот для RTL означал бы расширение условий в скриптах и стилях для каждого контрола и для каждого случая его позиционирования. Минимальное решение обошлось в 4 строчки:

.taStart {
    text-align: left;
} 
html[dir=rtl] .taStart {
    text-align: right;
}
.taEnd {
    text-align: right;
}
html[dir=rtl] .taEnd {
    text-align: left;
}

Таким образом, в необходимых местах происходит установка класса с необходимым выравниванием и его легкая замена в случае необходимости. Осталось только заменить в установку выравнивания с style=”text-align: ...” на соответствующий класс.

По такому же принципу происходит установка другого вида выравнивания – float.

.floatStart {
    float: left;
} 
html[dir=rtl] .floatStart {
    float: right;
}
.floatEnd {
    float: right;
}
html[dir=rtl] .floatEnd {
    float: left;
}

И, как же без него, класс для зеркального отображения, например, иконок, который так же устанавливается в любые контейнеры, где необходим зеркальное отображение в RTL-интерфейсе.

html[dir=rtl] .rtlScale {
    transform: scaleX(-1);
}

Антискейл


Разобравшись с «простыми» линейными элементами, пришло время переходить к «сложным». Есть и такие в нашей платформе, например, тумблеры. Они могут оказаться разной геометрической формы. С расположением элементов справился браузер, отступы в наших тумблерах изначально симметричные. Так в чем же проблема? Проблема в скруглениях рамок.
Скругления рамок рассчитываются для каждого элемента тумблера в зависимости его положения. «Слева-сверху», «справа-сверху», «справа-сверху и справа-снизу» – вариации различны.

Можно перевернуть контейнер с тумблером целиком, но что делать с текстом, который тоже перевернется? Этот прием мы назвали «антискейл». Контейнеру, которому необходимо отобразиться зеркально, добавляем атомарный класс rtlScale, а его дочернему элементу добавляем свойство наследования transform: inherit;. В LTR-интерфейсе данный метод будет проигнорирован, а для RTL-интерфейса, текст, перевернувшись дважды, отобразится как надо.

image

<!DOCTYPE html>
<html dir="ltr">
<head>
<style>
html[dir=rtl] .rtlScale {
    transform: scaleX(-1);
}
.tumbler {
    display: inline-flex;
    border-radius: 4px 0 0 4px;
    border: 1px solid #A0A0A0;
    padding: 4px 8px;
}
.tumblerBox {
    transform: inherit;
}
</style>
</head>
<body>
<div class="tumbler rtlScale">
    <div class="tumblerBox">не знаю</div>
</div>
</body>
</html>

Flexbox


Конечно же, к сожалению, не мы придумали эту потрясающую технологию, но с большим удовольствием использовали её возможности в наших целях. Например, в панели разделов. Кнопки прокрутки этой панели не занимают места, появляются поверх панели при возможности прокрутки в ту или иную сторону. Вполне логичная реализация position: absolute; right/left: 0; оказалась не универсальной, поэтому мы от неё отказались. В итоге универсальное решение стало выглядеть так: родительскому контейнеру кнопки прокрутки устанавливаем нулевую ширину, чтобы не занимал место, а кнопке прокрутке, расположенной в конце, сменили ориентацию через flex-direction: row-reverse;.

image

Таким образом, кнопка в конце строки прижимается к концу строки контейнера с нулевой шириной и отображается «назад» поверх панели.

<!DOCTYPE html>
<html dir="ltr">
<head>
<style>
.panel {
    display: inline-flex;
    background: #fbed9e;
    height: 64px;
    width: 250px;
}
.content {
    width: 100%;
}
.scroll {
    display: flex;
    position: relative; 
    width: 0; 
}
.scrollBack {
    order: -1; 
}
.scrollNext {
    flex-direction: row-reverse; 
}
.scroll div {
    display: flex; 
    flex: 0 0 auto; 
    justify-content: center; 
    align-items: center; 
    background: rgba(255,255,255,0.5); 
    width: 75px; 
}
</style>
</head>
<body>
<div class="panel">
    <div class="content">Контент панели</div>
    <div class="scroll scrollBack">
        <div>Назад</div>
    </div>
    <div class="scroll scrollNext">
        <div>Вперёд</div>
    </div>
</div>
</body>
</html>

Кстати, идея с нулевой шириной оказалась полезна и при решении других задач. В платформе повсеместно используются выпадающие элементы (контекстные меню, выпадающие списки и др.). Расчёт позиционирования сложный и тонкий, следовательно, зеркальное отображение требует еще большей сложности и тонкости.

image

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

<!DOCTYPE html>
<html dir="ltr">
<head>
<style>
.anchor {
    border: 1px solid red; 
    position: absolute; 
    width: 100px; 
    height: 50px; 
    max-width: 0; 
    max-height: 0; 
    top: 25%;
    left: 50%;
}
.anchorContent {
    background: #FFF; 
    border: 1px solid #A0A0A0; 
    width: inherit; 
    height: inherit; 
    padding: 4px 8px; 
}
</style>
</head>
<body>
<div class="anchor">
    <div class="anchorContent">Контент якоря</div>
</div>
</body>
</html>

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


Там, где нельзя обойтись без абсолютного позиционирования элементов (style=”position: absolute;” или style=”position: fixed;”), dir=”rtl” бессилен. На помощь приходит подход, когда горизонтальная координата применяется не к стилю left, а right.

image

При этом если в JS при расчётах координат идёт обращение к свойствам scrollLeft, offsetLeft у элементов, то в RTL-интерфейсе использование этих свойств напрямую может привести к неожиданным последствиям. Нужно высчитывать значение этих свойств по-другому. Хорошо зарекомендовала себя реализация подобного функционала в Google Closure Library, которую мы используем в веб-клиенте: см. https://github.com/google/closure-library/blob/master/closure/goog/style/bidi.js.

В итоге


Мы это сделали! Перевернули и сохранили наш исходный код в едином варианте под LTR и RTL-интерфейсы. Необходимости пока не возникло, но при желании мы сможем на одной странице отобразить две формы разной направленности одновременно. И кстати, применив наши приёмы, в итоге мы получили итоговый CSS-файл легче на 25%.

А ещё мы поддержали RTL в тонком (нативном) клиенте 1C, который работает в Windows, Linux и macOS, но это тема для отдельной статьи.