1С справа налево: как мы поддержали RTL в платформе 1С: Предприятие

    Платформа 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, но это тема для отдельной статьи.
    Делаем средства разработки бизнес-приложений

    Комментарии 17

      0

      Вот Олег Филиппов обрадуется. Теперь ему снова ваш CSS реверсить ;)

        0
        все для него :)
        0
        Про RTL в вебе написано множество статей, а нативный клиент вы переводили на RTL? Что для этого сделали? Вот это было бы интересно!

        UPDATE: а вот строчку внизу я и не увидел, каюсь.
        Жду статью про нативный клиент!
          +2

          Спасибо! Буквально сейчас один из наших сотрудников готовит демку для потенциального заказчика из саудовской Аравии.

            +1
            Это поэтому вы меню «файл» в правый угол перенесли? А можно для европейцев и славян вернуть его в привычное место?
              0
              А разве 1С кто-то пользуется за пределами СНГ?
                +3
                Да. Есть внедрения в Австрии, Германии, Италии, Румынии, Польше, Испании, США, Канаде, Турции, Латинской Америке, ОАЭ, Англии, Швеции, Болгарии, Чехии, Китае и других странах.
                Список партнеров 1С за рубежом.
                  0
                  Я тут представил программирование на 1С на арабском…
                    +1
                    Программировать в 1С можно только на русском и английском :)
                      0
                      ты не поверишь программирование «на русском» для англоязычных выглядит аналогично
                        0
                        Я понимаю, но русский текст хотя бы идёт слева направо и влезает в моноширинные шрифты, а вот что делать с арабским с его RTL и обязательной связностью символов…
                          0

                          Так почему ты решил что там на нативном пишут? Может всеже на английском? Ты бы ещё про китайские иероглифы вспомнил, вообще сверху вниз ;)

                            0
                            Потому что это 1С, коль уж у него есть ЯП на основе русской письменности и лексики, мало ли что, может и на арабском есть. И вообще, дайте пофантазировать, ну!
                  +1
                  Хорошая статья — много деталей, фактически, мини-документация по использованию RTL-интерфейса. Но как я понимаю, основными её чтецами будут арабо-говорящие программисты. Поэтому, было бы здорово, чтобы ваш сотрудник ещё бы помог в переводе на арабский язык.
                    0
                    Спасибо за пример со шрифтом — тоже нахожусь сейчас в стадии поиска подходящей альтернативы для арабского — попробую ваш подход.

                    А по поводу переворачивания стилей — возможно, я что-то упустил, и в статье указаны причины конкретного подхода, но вы не пробовали использовать какие-то плагины для отзеркаливания CSS или, как альтернативу, значения start и end для выравнивания, отступов и т.п.?
                      0
                      Использование плагина для отзеркаливания CSS не упростит задачу, так как любое автоматическое преобразование CSS неминуемо приведёт к тому, что плагин преобразует right в left и наоборот там, где не нужно. Всё нужно будет перепроверять за плагином. Кроме того, автоматическое отзеркаливание CSS — это лишь часть работы, ведь нужно сделать аналогичное действие и с JS в тех местах, где идёт обращение к свойствам CSS, содержащим left или right.

                      start, end не поддерживаются в Internet Explorer, который по-прежнему поддерживается веб-клиентом 1С.
                      0
                      Согласен по второму пункту.

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

                      Для полноты картины читателям я бы рекомендовал рассмотреть подход, при котором весь CSS автоматически переворачивается, и следить надо только за исключениями (калькулятор, поле ввода телефона и прочие чисто LTR компоненты). Все приложения, конечно, уникальные, и у всех есть свои нюансы, но, честно говоря, впервые слышу что автоматическое переворачивание принесло больше проблем.

                      Также хочется предостеречь от использования глобального text-align на HTML. Желание поставить выравнивание текста на самый верх документа кажется поначалу логичным, но проблемы начнут возникать, как только в интерфейсе появится текст противоположной направленности (арабское имя или адрес в англоязычном, или, например, русское слово — в арабском интерфейсе). Наше общение с переводчиками на иврит и арабский показало, что хотя юзеры там уже привыкли к самым диким сочетаниям направления текста, наиболее привычно им читать по правилам языка. То есть носителю арабского, понимающему английский, наиболее удобно видеть арабский текст справа, а латиницу — слева (хотя и здесь будут исключения).

                      И в этом случае глобальный text-align начинает портить жизнь. Победить его можно c помощью text-align: initial, но, увы, это свойство не поддерживается в ИЕ<=11. Я бы предложил вообще не ставить его — браузер сам разместит текст как надо

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

                      Самое читаемое