RTL Support in Web Applications

Some languages of the world (Arabic, Hebrew etc.) are RTL, meaning they are read right-to-left, instead of left-to-right. Typically in web applications supporting one of these languages, everything is reversed, meaning scroll bars, progress indicators, buttons etc.

I recently took part in a discussion with the jQuery UI team about how they would start to implement RTL support and I thought many of the issues were generic across any web application or library, so this is a blog post about my thoughts on the discussion.

How RTL support is done

Lets start with an example. Here is a little html fragment and how it looks in LTR.

Make a choice

and the code is

<div class="widget">
  <span>Make a choice</span>
  <div class="buttons-bar">
    <button>Ok</button>
    <button>Cancel</button>
  </div>
</div>
.widget {
  border: black 1px solid;
  width: 200px;
  height: 60px;
}
.buttons-bar {
  float: right;
  margin-right: 10px;
}
button {
  border: green 1px solid;
}

Now, if it were in a RTL language, it would look like this…

Make a choice

Which has the following code..

<div class="widget" dir="rtl">
  <span>What do you want to do?</span>
  <div class="buttons-bar">
    <button>Ok</button>
    <button>Cancel</button>
  </div>
</div>
.widget {
  border: black 1px solid;
  width: 200px;
  height: 60px;
}
.buttons-bar {
  float: left;
  margin-left: 10px;
}
button {
  border: green 1px solid;
}

Lets go through the differences. Firstly the element has dir="rtl" on it. This tells the browser that all text should be laid out from right to left, but it doesn’t just effect text, it effects all inline and inline-block code. This means that because the button elements default to inline-block, they are reversed, so Cancel is on the left and Ok on the right (without any css changes). Note that this dir attribute is inherited and is not a CSS style.

However the rest of the differences have to be done with CSS and involve basically reversing all spacial attributes, so float: right becomes float: left and margin-left becomes margin-right.

Further Changes

In an ideal world, all the work would be done in the CSS - which would mean no JavaScript changes and it would keep everything simple, however that isn’t always possible. An example from jQuery UI was the progress bar which would have an absolutely positioned div for the progress and which is increasing the size. You either need to change the CSS so that the element is not absolutely positioned or you need to change the JavaScript to know whether the layout is RTL and if it is ,also change the left position (remember that the RTL attribute only changes inline, inline-block directions - not positions, floats, margins, borders, padding etc.)

There are further complications and browser differences which mean that a complex web application can’t refactor everything on to the CSS - but it is good to try, because changes in the JavaScript can be pervasive, with an isRTL boolean starting to appear everywhere.

LTR inside RTL

The initial approach that the jQuery UI team had thought to go with was that each widget determines its own direction (traversing the DOM backwards to see if an ancestor has the attribute), stores it somewhere at start up and then uses the boolean wherever it is needed. This approach works fine if we just consider the JavaScript, however with the CSS it becomes tricky.

At first you might think you could write selectors like so.

.widget {
  float: right;
}
[dir="rtl"] .widget {
  float: left;
}

However, this approach (having the RTL and LTR CSS co-existing on the same page) is so that different widgets can have different directions, but some widgets contain other widgets, like a tab control, so what do you do if you have RTL tab which has LTR content? A first approach might be CSS like so…

.widget {
  float: right;
}
[dir="rtl"] .widget {
  float: left;
}
[dir="ltr"] .widget {
  float: left; /* override a LTR widget inside a RTL one */
}

But you are only really supporting one redirection level (e.g. not RTL in LTR in RTL) and you are trebling the size of your CSS (OK so you could increase the specificity of the last selector so you can put it with the first, but things are starting to look awful).

Another approach would be to change the names of the css classes - it looks nicer at first..

.widget {
  float: right;
}
.rtl_widget {
  float: left;
}

But consider that if you go for this approach, you cannot use the CSS ancestor selector anywhere (or if you do, the ancestor CSS class must be marked rtl_) and furthermore, every class that is used across more than 1 widget, must be marked rtl_ and therefore have it’s css class changed.

You could concede that only css classes used in widgets which have ancestors need to be treated like this, but in my opinion, things get very messy, very fast and you see the size of the CSS and the JavaScript jump in size.

Solutions

In my opinion, the use-cases across the web for web applications which mix both LTR and RTL are limited. The only viable one I heard during the meeting was a translation or language learning application. I also think that applications that can change their language without a refresh are limited (and not only that a change from a LTR language to a RTL one) so, the simplest thing is to have one LTR stylesheet and one RTL stylesheet and therefore not impact the performance and size of either language set for most users and keep the required changes to the JavaScript to an absolute minimum. You could tell users that they should use iframes for mixing script or else support it only in the JavaScript and let people deal with the CSS themselves if they need to.

One bonus you get from this approach (although not completely excluded from a different approach - just harder) is that you can look at automating the changes to the CSS. It would be easy to create a Less plugin which automatically reversed all the CSS properties (I’m sure you could do this in postcss too, used for autoprefixer). I’d do this with the use of a plugin and a variable, so that you had ultimate control in a single set of CSS.

That way the input would be..

.widget {
  float: right;
  & when (@rtl) {
    margin-left: 10px; // for some obscure reason, not applied in LTR mode
  }
}

Generating

.widget {
  float: right;
}

or

.widget {
  float: left;
  margin-left: 10px;
}

Depending on the direction.

Further Problems

Scrollbars cross browser

When using scrollbars in overflow divs, like above, all browsers will show the scrollbar on the left, rather than the right. However, when it comes to setting the direction on the body, only IE will move the main page scroll bar over to the left - Chrome and Firefox will keep it on the right.

LTR characters mixed with RTL characters

When you combine characters that can be used in both RTL and LTR languages (punctuation for instance) then where the punctuation is displayed, depends on the direction. So, below I’ve used Google Translate to convert the same text to Arabic and English, and displayed them both in RTL.

جعل خيار؟ ثم سنعرف.
Make a choice? Then we will know.

The full-stop ‘.’ is the last character in both strings, after “Know” in English, but because we have told the browser the direction is RTL and it is the last character in the string and is punctuation so of indeterminate direction, the browser puts it on the far left hand side. Here is the same text but with the direction RTL.

جعل خيار؟ ثم سنعرف.
Make a choice? Then we will know.

Essentially the data format is start-to-beginning, but the browser is still rendering the RTL script RTL and the punctuation in whatever direction you have defined - so the full stop is always on the end of the string, which is on the right hand side. The reason that only punctuation at the end of the sentence is effected is because indeterminate direction characters that are surrounded on both sides by the same direction will inherit that direction, rather than the over-ridden direction of the HTML node. The easiest way to see what is happening is to do a substring in JavaScript, because even your LTR editor will be displaying RTL script, RTL.

"جعل خيار؟ ثم سنعرف.".substr(0, 4)
"جعل "

This means - do not use RTL with non RTL script or vice-versa or you can end up with something you do not expect.

MORE BY LUKE

Seven Surprising JavaScript 'Features'

Aurelia, less2css and bundling

blog comments powered by Disqus