1C da direita para a esquerda: como suportamos RTL na plataforma 1C: Enterprise

Plataforma 1C: Enterprise está localizada em 22 idiomas , incluindo inglês, alemão, francês, chinês, vietnamita. Recentemente, na versão 8.3.17, oferecemos suporte ao idioma árabe.



Uma das peculiaridades da língua árabe é que o texto é escrito e lido da direita para a esquerda. A interface do usuário para o idioma árabe deve ser espelhada horizontalmente (mas nem tudo e nem sempre - existem sutilezas), abra o menu de contexto à esquerda do cursor, etc.



Debaixo do corte - sobre como suportamos RTL (da direita para a esquerda) no cliente web da plataforma 1C: Enterprise, e também uma das hipóteses que explicam porque o mundo árabe escreve da direita para a esquerda.



imagem



Um pouco de historia



Estamos acostumados a escrever da esquerda para a direita. Essa direção da escrita é em grande parte gerada pelo fato de que, ao escrever um texto no papel, os destros (e de acordo com as estatísticas, cerca de 85% deles) veem o que já foi escrito - a escrita (direita) não cobre o texto escrito. Os canhotos têm que sofrer.



Uma das hipóteses "por que a língua árabe é escrita da direita para a esquerda" soa assim. As línguas de origem do árabe originaram-se na época em que não existia papel e seus análogos (papiro, pergaminho, etc.). Havia apenas uma maneira de registrar informações - entalhar letras em pedra. Como será mais conveniente para os destros empunhar um martelo e um cinzel? Claro, segurando um cinzel na mão esquerda e batendo nele com um martelo preso na direita. E neste caso é mais conveniente escrever da direita para a esquerda.



E agora - sobre como lidamos com esse legado de séculos.



Como começamos a tarefa?



Nenhum dos desenvolvedores da plataforma falava árabe e não tinha experiência no desenvolvimento de interfaces RTL. Recolhemos muitos artigos sobre o tema RTL (quero agradecer especialmente à empresa 2GIS pelo trabalho realizado e artigos cuidadosamente elaborados: artigo 1 , artigo 2 ). Ao estudar o material, percebi que não poderíamos viver sem um falante nativo. Portanto, em simultâneo com a procura de tradutores para o árabe, passamos a procurar um funcionário - um falante nativo da língua árabe, que tivesse a experiência de que precisávamos, pudesse aconselhar-nos nas especificidades árabes das interfaces. Depois de analisar vários candidatos, encontramos essa pessoa e começamos a trabalhar.



Vamos brincar com fontes



Por padrão, usamos a fonte de plataforma Arial, 10pt. O desenvolvedor de uma configuração específica pode alterar a fonte para a maioria dos elementos da interface, mas, como mostra a prática, isso raramente é feito. Essa. na maioria dos casos, os usuários de programas 1C veem inscrições escritas por Arial em suas telas.



Arial exibe bem 21 idiomas (incluindo chinês e vietnamita). Mas, como ficou claro graças ao nosso colega árabe, o texto árabe renderizado nesta fonte é muito pequeno e difícil de ler:



100%:



imagem



os usuários árabes tendem a trabalhar com um DPI maior - 125%, 150%. A situação melhora neste DPI, mas o Arial ainda é difícil de ler devido à natureza da fonte.



125%:



imagem



150%:



imagem



Vimos várias opções para resolver este problema:



  1. Arial , , ( ).
  2. Arial 11pt RTL-.
  3. Arial , LTR- Arial.


Na hora de escolher uma solução, tivemos que levar em consideração que a fonte Arial 10pt está sendo usada na plataforma 1C: Enterprise há muito tempo, na plataforma nós e nossos parceiros criamos mais de 1300 soluções de edição, e em todas elas a fonte Arial 10pt se mostrou bem em todos os sistemas operacionais suportados (Windows, Linux e macOS de várias versões), bem como em navegadores. Alterar a fonte e / ou seu tamanho significaria um teste massivo da interface do usuário, e muitos desses testes não podem ser automatizados. Alterar a fonte também significaria alterar a interface familiar dos programas para os usuários atuais.



Além disso, não foi possível encontrar uma fonte universal que representasse bem todos os idiomas, incluindo o árabe. Por exemplo, a fonte Segoe UI renderiza bem o árabe, mesmo em 10pt, mas não oferece suporte ao chinês e também não é compatível com alguns sistemas operacionais. Tahoma é bom em renderizar texto em árabe em 10pt, mas tem problemas com suporte em Linux e latim / cirílico "muito negrito" no caso de negrito (negrito árabe parece bom). Etc.



Aumentar o tamanho da fonte padrão para 11pt na interface RTL significaria uma grande quantidade de testes de IU - temos que nos certificar de que tudo seja renderizado corretamente, todos os rótulos sejam colocados no espaço fornecido para eles, etc. E mesmo em 11pt, Arial não exibe caracteres árabes perfeitamente.



Como resultado, a terceira forma revelou-se ótima em termos de custos de mão-de-obra e efeito obtido: continuamos a usar Arial para todos os caracteres, exceto o árabe. E para caracteres árabes, usamos uma fonte adequada para isso - Almarai . Para fazer isso, adicione ao 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;
}


e sempre que você precisar usar a fonte padrão, defina a fonte desta forma:



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


A beleza dessa abordagem é que, se não houver um único caractere na interface que se enquadre no intervalo de Unicode, essa fonte nem carregará. Mas assim que esse símbolo aparecer, o navegador fará o download da própria fonte (ou usará sua versão local) e exibirá o símbolo na fonte desejada.



Interface "Flip"



Como você pode esperar, o layout HTML do cliente da Web não estava pronto para uma mudança. Depois de dar o primeiro passo, definindo o atributo dir = ”rtl” no elemento raiz e adicionando o estilo html [dir = rtl] {text-align: right;} , começamos o trabalho meticuloso. No decorrer deste trabalho, desenvolvemos uma série de práticas que queremos compartilhar aqui.



Simetria



Vejamos o exemplo de botões. Os botões da plataforma podem conter uma imagem, um texto e um marcador de lista suspensa. E tudo isso em qualquer composição a critério dos desenvolvedores de soluções de aplicativos baseadas na plataforma.



A coluna "antes do RTL" representa graficamente o preenchimento inicial dos elementos do botão. A dependência da quantidade de indentações com a presença de elementos no botão, bem como com a seqüência de sua disposição, é óbvia. Se houver uma imagem, o texto não precisará de um recuo à esquerda, se a imagem estiver à direita, a imagem terá um deslocamento negativo, se houver um marcador na lista suspensa, o contêiner com o texto terá mais recuo à direita, se o marcador estiver imediatamente após a imagem, também terá uma margem à direita. Muitos ifs, exceto para um botão somente texto com preenchimento simétrico. Simétrico! Se você distribuir os recuos simetricamente, não há nada para inverter. Essa se tornou a ideia principal.



A coluna "após RTL" mostra os novos recuos simétricos nos mesmos botões. Resta resolver a nuance com o recuo entre a imagem e o marcador de lista. Eu queria uma solução universal para qualquer orientação. O triângulo em si é desenhado com a borda superior do pseudoelemento e só precisa de um recuo se estiver depois da imagem. Sob esta condição, outro pseudo-elemento é adicionado com a largura para o recuo necessário. O triângulo e o preenchimento se trocarão quando a orientação for alterada.



imagem



Nota. Todos os exemplos abaixo são, por padrão, para a interface LTR. Para ver a aparência do exemplo na interface RTL, altere dir = "ltr" para 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>


Tentamos evitar elementos, pseudoelementos e invólucros desnecessários. Mas, ao escolher, neste caso, entre aumentar as condições em CSS e adicionar um pseudoelemento, a solução com um pseudoelemento venceu por sua versatilidade. Não há muitos desses botões no formulário, portanto, o desempenho ao adicionar elementos não será prejudicado nem mesmo no Internet Explorer.



O princípio de simetria também se mostrou útil para rolar nossos painéis. Para mover o conteúdo horizontalmente, aplicamos anteriormente uma única propriedade margin-left: -Npx; ...



imagem



O valor agora está definido para a margem simétrica : 0 -Npx; , ou seja, para esquerda e direita ao mesmo tempo, e para onde mover - o próprio navegador sabe, dependendo da direção especificada.



Classes atômicas



Um dos recursos de nossa plataforma é a capacidade de alterar dinamicamente o conteúdo e sua localização no formulário "na hora" de acordo com o gosto de cada usuário. Um caso comum de alterações é o alinhamento horizontal do texto: esquerda, direita ou centro. Isso é obtido simplesmente alinhando o alinhamento de texto com o valor apropriado. Uma reversão para RTL significaria expandir as condições em scripts e estilos para cada controle e para cada caso de seu posicionamento. A solução mínima custa 4 linhas:



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


Assim, nos locais necessários, a classe é instalada com o alinhamento necessário e sua fácil reposição, se necessário. Resta apenas substituir a configuração de alinhamento com style = "text-align: ..." com a classe apropriada.



O mesmo princípio é usado para definir outro tipo de alinhamento - float .



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


E, como sem ela, uma classe de espelhamento, por exemplo, ícones, que também é instalada em qualquer container que precise de espelhamento na interface RTL.



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


Anti escala



Tendo lidado com os elementos lineares "simples", é hora de passar para os "complexos". Existem também alguns em nossa plataforma, por exemplo, interruptores de alternância. Eles podem ter diferentes formas geométricas. O navegador lidou com a disposição dos elementos, os recuos em nossas chaves de alternância são inicialmente simétricos. Então qual é o problema? O problema está no arredondamento das molduras.

O arredondamento dos frames é calculado para cada elemento da chave seletora, dependendo de sua posição. "Parte superior esquerda", "parte superior direita", "parte superior direita e parte inferior direita" - as variações são diferentes.



Você pode virar o contêiner inteiro com o botão de alternância, mas e o texto, que também muda? Chamamos essa técnica de "anti-escala" . Adicione a classe atômica rtlScale ao contêiner que precisa ser espelhado, e adicione a propriedade de herança de transformação a seu elemento filho : herança; ... Na interface LTR, este método será ignorado, mas para a interface RTL, o texto, invertido duas vezes, será exibido conforme necessário.



imagem



<!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



Claro, infelizmente, não criamos essa tecnologia incrível, mas com grande prazer usamos seus recursos para nossos propósitos. Por exemplo, no painel de seção. Os botões de rolagem deste painel não ocupam espaço, eles aparecem na parte superior do painel quando é possível rolar para um lado ou outro. Implementação bastante lógica da posição: absoluta; direita / esquerda: 0; acabou por não ser universal, então o abandonamos. Como resultado, a solução universal começou a se parecer com isto: definir o contêiner pai do botão de rolagem com largura zero para que não ocupe espaço, e a orientação do botão de rolagem localizado no final foi alterada através de flex-direction: row-reverse; ...



imagem



Assim, o botão no final da linha é pressionado contra o final da linha do contêiner de largura zero e é exibido "para trás" sobre o painel.



<!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>


A propósito, a ideia de largura zero acabou sendo útil para outras tarefas também. Os elementos suspensos (menus de contexto, listas suspensas etc.) são amplamente usados ​​na plataforma. O cálculo de posicionamento é complexo e sutil, portanto, o espelhamento requer ainda mais complexidade e sutileza.



imagem



A solução é colocar o menu suspenso em um contêiner de tamanho zero (chamado de âncora). A âncora é posicionada absolutamente no ponto requerido da interface, e seu conteúdo com sua borda inicial é pressionado contra a borda inicial da âncora, posicionando o conteúdo na direção desejada.



<!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>


Elementos absolutamente posicionados



Onde o posicionamento absoluto dos elementos não pode ser evitado ( estilo = ”posição: absoluto;” ou estilo = ”posição: fixo;” ), dir = ”rtl” é impotente. Uma abordagem vem em socorro quando a coordenada horizontal é aplicada não ao estilo esquerdo , mas ao direito .



imagem



Além disso, se em JS, ao calcular coordenadas, existe um apelo aos scrollLeft e propriedades offsetLeft de elementos, em seguida, na interface RTL, utilizando estas propriedades directamente pode conduzir a consequências inesperadas. Você precisa calcular o valor dessas propriedades de uma maneira diferente. A implementação desta funcionalidade na Biblioteca Google Closure, que usamos no cliente web, tem se mostrado bem: veja.https://github.com/google/closure-library/blob/master/closure/goog/style/bidi.js .



Eventualmente



Conseguimos! Viramos e salvamos nosso código-fonte em uma única versão para interfaces LTR e RTL. A necessidade ainda não surgiu, mas se desejarmos, podemos exibir duas formas de direções diferentes simultaneamente em uma página. E por falar nisso, usando nossas técnicas, acabamos com o arquivo CSS final 25% mais leve.



Também oferecemos suporte a RTL em um cliente 1C thin (nativo) que funciona em Windows, Linux e macOS, mas este é um tópico para um artigo separado.



All Articles