Page MenuHomePhabricator

Ensure that Codex CSS supports both LTR and RTL directionalities
Closed, ResolvedPublic

Description

Background

Historically, some CSS properties and values have followed LTR-specific directional mappings and required conversion for RTL interfaces. For example, a margin-left rule would need to be converted to a margin-right rule in an RTL context.

CSSJanus is the solution currently employed in MediaWiki via ResourceLoader. It works by taking in a stylesheet, converting various properties and values for RTL, and generating a RTL version of the input stylesheet. This stable solution represents over a decade of learning and iteration. To lean more about what CSSJanus actually does, check out the well-commented source code.

A new CSS spec, CSS Logical Properties and Values, has been introduced to resolve this problem. Not only does this spec remove the need for much of the flipping and conversion done by a tool like CSSJanus, it also encourages the use of logical design language rather than directional. Though the spec is still in stage 1 ("editor's draft", aka early), browser support is progressing.

Considerations for Codex

CSS Janus and/or PostCSS plugins

Codex's largest use case is within MediaWiki, where ResourceLoader can use CSSJanus to create RTL stylesheets. However, we hope that Codex can also be used outside of MediaWiki, so we need to create a solution that will work in both contexts.

If we choose to use CSSJanus inside Codex:

  • There is a PostCSS plugin for it (not very active repo!)
  • Those using Codex inside MediaWiki can opt out of CSSJanus use by ResourceLoader

A different approach would be PostCSS-RTL plugin for RTL-adaptivity

CSS logical properties and values

Using this new spec in Codex would allow us to demonstrate the future of direction-agnostic CSS. However, it comes down to browser support. We could consider PostCSS plugins.

Client-side language switching

This was discussed in this PR; we need to determine if this is a use case we must cover.

Previous discussions

  • RTL support in WVUI was extensively discussed in this PR. Some highlights:
    • Timo's comment about the logical properties and values spec and browser support
    • Moriel's comment on CSSJanus and client-side language switching
    • Santhosh's comment on the importance of developing logical design language and CSS asset options
  • Santhosh's comments here and here on using PostCSS to support bidirectionality in stylesheets

Acceptance criteria

  • Decide how we will support bidirectionality in Codex's CSS
  • Consider when we might begin using CSS logical properties and values (moved to T304017)
  • Determine if we will support client-side language switching (and, if so, start talking about how)
  • Document these decisions and implementations in Codex for both Codex designers/developers and library users

Event Timeline

AnneT added a subscriber: Catrope.

@Volker_E @Catrope I'm providing the context for this as I know it, but y'all have lots more: feel free to edit this task or add comments.

AnneT updated the task description. (Show Details)
AnneT updated the task description. (Show Details)

The postcss-rtl plugin is no longer maintained, and also outputs "unified" CSS for both LTR and RTL at the same time(*), which bloats the output and doesn't work well in situations with nested directionality anyway.

Instead, I propose we use RTLCSS itself, which is compatible with PostCSS and simply flips its input.

Alternatively, we could use postcss-logical, which turns things like margin-inline-start into margin-left for LTR and margin-right for RTL. By default it outputs CSS for both LTR and RTL at the same time using the :dir() pseudo-class (which has such poor browser support that no version of Chrome supports it), but it can be configured to output only the LTR version or only the RTL version. I prefer this alternative, because CSS logical properties seem to be the future, and modern browsers have recently begun implementing them.

Either way, we would have to figure out a way to get Vite to generate two style files, one for LTR and one for RTL.

(*) Meaning input like .foo { margin-left: 1px; } would turn into:

[dir="ltr"] .foo {
    margin-left: 1px;
}

[dir="rtl"] .foo {
    margin-right: 1px;
}

Either way, we would have to figure out a way to get Vite to generate two style files, one for LTR and one for RTL.

This approach has the limitation that at any page, you can have only one directionality right? Either the page is full LTR or full RTL depending on which style file is loaded. Secondly we will not be able to change directionality of the content without page refresh. These requirements could be seen as fancy, but we have usecases of multi lingual content in same page with different directionalities.

In geneal I am not a fan of statically generated multiple style files which are created by flipping. They are not compatible with the future model of directionality abstraction in style(aka logical properties). I consider script directionality a run time variable for the content. If style asset(x.ltr.css, x.rtl.css)) need to vary for run time, then the run time content directionality need to be known in bundling stage, right? And that may even lead to create bundle/build variants(Infact people already went on that path).

I also observed RTLCSS appraoch is not popular among build systems that need to handle bidirectional content(I went through its issue tracker and there are unanswered questions about how to use it with frameworks like vue). Also see discussion on bidirectional output. To me RTLCSS looks like an approach to build a site primarily for RTL script. Not suitable for a site that can change directionality very often.

I liked the way we made our icon system and its bundling is agnostic about run time UI or content directionality.

Why can't we have one CSS file with both RTL and LTR in it?

[dir="ltr"] .foo {
    margin-left: 1px;
}

[dir="rtl"] .foo {
    margin-right: 1px;
}

This has clean upgrade path to

.foo {
 margin-inline-start: 1px;
}

... in same file. If we have separate style files, they remain to exist even when some of the logical properties are in wider use.

Alternatively, we could use postcss-logical, which turns things like margin-inline-start into margin-left for LTR and margin-right for RTL. By default it outputs CSS for both LTR and RTL at the same time using the :dir() pseudo-class (which has such poor browser support that no version of Chrome supports it), but it can be configured to output only the LTR version or only the RTL version. I prefer this alternative, because CSS logical properties seem to be the future, and modern browsers have recently begun implementing them.

The usage of pseudo class :dir() in that plugin is problematic. But can be converted to use directionality property [dir="ltr"] or [dir="rtl"] . See this plugin. (I wonder if somebody already merged these two plugins to single one)

In SectionTranslation, we are not using postcss, but we generate styles for both RTL and LTR in single style from a spacing system written in scss. It compress well too. For non standard(not as per grid) spacing, one need to write style for both RTL and LTR.

Why can't we have one CSS file with both RTL and LTR in it?

The main problem I see with this is performance. We can't deliver render-blocking CSS duplicated for both directionalities to the client, as long as logical properties are not widely-supported. Additionally logical properties don't enable us to do everything CSS Janus et al supports, but that's secondary.

The main problem I see with this is performance. We can't deliver render-blocking CSS duplicated for both directionalities to the client

If the size of style files is the concern, that need to be clearly measured. These things compress well in gzip because of repeating patterns. Also, there is no duplication, it is just directionality dependent styles are given with alternate directionalities. The CSS generated by Section translation for RTL/LTR, grid, utility classest comes under 10KB(Gzipped).

Either way, we would have to figure out a way to get Vite to generate two style files, one for LTR and one for RTL.

This approach has the limitation that at any page, you can have only one directionality right? Either the page is full LTR or full RTL depending on which style file is loaded. Secondly we will not be able to change directionality of the content without page refresh. These requirements could be seen as fancy, but we have usecases of multi lingual content in same page with different directionalities.

Yes, that is true, we would not be able to change the directionality of the page without either refreshing the page or loading different styles if we use this approach.

In geneal I am not a fan of statically generated multiple style files which are created by flipping. They are not compatible with the future model of directionality abstraction in style(aka logical properties). I consider script directionality a run time variable for the content. If style asset(x.ltr.css, x.rtl.css)) need to vary for run time, then the run time content directionality need to be known in bundling stage, right? And that may even lead to create bundle/build variants(Infact people already went on that path).

Yes, practically speaking we would have to generate separate LTR and RTL CSS files.

I liked the way we made our icon system and its bundling is agnostic about run time UI or content directionality.

It is, except that the icon system isn't able to detect the directionality of the surrounding context changing. So icons are only rendered for the directionality that the context had at the time the icon component was mounted. It's not reactive to the environment's (or the page's) directionality changing, as currently implemented. If :dir() was supported in browsers other than Firefox, I would have implemented it differently, but I didn't want to use the [dir="rtl"] approach because it breaks with nested directionality (see also below).

Why can't we have one CSS file with both RTL and LTR in it?

[dir="ltr"] .foo {
    margin-left: 1px;
}

[dir="rtl"] .foo {
    margin-right: 1px;
}

This kind of transform breaks when dealing with nested directionality. If you have <html dir="rtl"> ... <div dir="ltr"> ... <span class="foo">, then the span matches both [dir="ltr"] .foo (because it's a descendant of the div) and [dir="rtl"] .foo (because it's a descendant of the html tag), and both the margin-left and margin-right rules will be set. Even if you try to fix that by canceling out the wrong-side margin property (which is dangerous because it could interfere with other rules), it still wouldn't work, because both the [dir="ltr"] .foo and [dir="rtl"] .foo have equal specificity, so the RTL one wins because it comes last, and in this context that's the wrong one. The LTR one "should" win because the dir="ltr" ancestor is closer than the dir="rtl" ancestor, but there's no good way to express that in CSS, except for :dir().

Without :dir() or logical properties I don't see a way to properly support nested directionality. Using html[dir="ltr"] .foo would follow the page directionality without causing these kinds of nesting bugs, but that takes you back to "the entire page has only one directionality", and the only remaining advantage is that you could dynamically change the page's directionality without reloading.

That may be a worthwhile benefit, and we should consider supporting it for consumers that would benefit from it. But I doubt that it would be a worthwhile benefit for MediaWiki in the short term, because changing the directionality of an entire MW page at runtime would break for lots of other reasons. We could support that by changing all RTL handling in MediaWiki to use the same html[dir="rtl"] approach, but that would require a bunch more work, and I'm not sure if it would be worth the performance impact (although you're right that we should quantify what that impact is, considering that it would likely gzip pretty well).

While browser support for :dir() is very disappointing, browser support for logical properties is pretty good nowadays, especially considering that most browsers that don't support it have very low usage (except for IE11 and Opera Mini, but Codex already doesn't support those anyway) and the partial support footnotes are pretty low-impact. So that leads me to the following proposal:

  • Use logical properties in our code; prohibit non-logical directional properties like margin-left using stylelint, only use them (with a stylelint-disable comment) when hard-coding the direction is necessary.
  • Provide a build for modern browsers that support logical properties. This build would not need any LTR/RTL transforms, except maybe for a few edge-case-y things that don't have good browser support, where we would duplicate the rule prefix the selector with html[dir="ltr"] / html[dir="rtl"] (either through a tool or manually).
  • Provide a build for less modern browsers that don't support logical properties. This build would duplicate rules that use logical properties and prefix the selectors with html[dir="ltr"] / html[dir="rtl"].
  • In MediaWiki, we would generally use the modern browsers build. Vue UIs in MW would not support browsers that don't support logical properties (which is pretty much the same set of browsers we already don't support because of ES6).
  • For server-rendered UIs, MW needs to support older browsers, so it would have to serve the processed build with duplicate selectors (or a directionality-specific build) to older browsers; the unprocessed one with logical properties could still be served to modern browsers. Serving different styles for different browsers isn't currently supported in ResourceLoader, and it would require some dark magic (especially since these styles would be render-blocking), but I think it could be done.

Since this proposal requires either new dark magic in ResourceLoader to serve different stylesheets to different clients (or to drop even basic CSS support for browsers that don't support logical properties, which we may do in the future, but not any time soon), we may not be able to implement it before the 0.5 release. If that's the case, we should also have a fallback option. For that, I would be OK with either the html[dir="ltr"] approach or the "separate LTR and RTL files" approach, as long as the former doesn't have a significant performance impact.

cc @Krinkle since we're discussing performance impacts and he's also shown an interest in this issue generally

This sounds generally good plan, I'm slightly worried about how exceptions may work with logical properties, but I think they should work in general.

As those who work with RTL/LTR know -- RTL has nice little set of best practices that work for 1% of cases, and 99% are basically outlier weirdness depends-what-you-want edge cases.... so the plan sounds good, and whatever you try -- you will likely find out if it's workable or not (IE, if you actually do need a css-janus-type system) fairly quickly either way....

  • Use logical properties in our code; prohibit non-logical directional properties like margin-left using stylelint, only use them (with a stylelint-disable comment) when hard-coding the direction is necessary.
  • Provide a build for modern browsers that support logical properties. This build would not need any LTR/RTL transforms, except maybe for a few edge-case-y things that don't have good browser support, where we would duplicate the rule prefix the selector with html[dir="ltr"] / html[dir="rtl"] (either through a tool or manually).
  • Provide a build for less modern browsers that don't support logical properties. This build would duplicate rules that use logical properties and prefix the selectors with html[dir="ltr"] / html[dir="rtl"].

Watch out from reading html-scope rather than component-scope. There are cases where you might want a specific set of components to be in a different scope especially if Codex is to support tools outside of the wiki page. For example, you might have a web page that's html[dir=rtl] but that has a login box that is surrounded by <div dir="ltr"> because email and password are usually latin... or you might have a page in LTR that has a section in RTL, etc.

The better way to measure whether a component should be ltr/rtl is as close of the context to that component as possible.

(Add) -- also, you'll need to take into account the ability to let component users (implementers?) force directionality for content (so, input direction regardless of interface) and also force and set based on interface. I know we all know this, I'm just pointing it out because it might get a bit more complex with the prefix selectors.

Ah, RTL support ;)

Watch out from reading html-scope rather than component-scope. There are cases where you might want a specific set of components to be in a different scope especially if Codex is to support tools outside of the wiki page. For example, you might have a web page that's html[dir=rtl] but that has a login box that is surrounded by <div dir="ltr"> because email and password are usually latin... or you might have a page in LTR that has a section in RTL, etc.

The better way to measure whether a component should be ltr/rtl is as close of the context to that component as possible.

It would be great if that was possible to reliably do that in CSS, but it's not currently (except in Firefox). See the last few paragraphs of T295189#7501327.

(Add) -- also, you'll need to take into account the ability to let component users (implementers?) force directionality for content (so, input direction regardless of interface) and also force and set based on interface. I know we all know this, I'm just pointing it out because it might get a bit more complex with the prefix selectors.

This is a great point: we might not always be able to handle complex nested directionality situations, but it would be nice to be able to handle <html dir="rtl"> ... <div class="mw-content-text" dir="ltr"> correctly, or otherwise provide consumers the ability to override the directionality of a component (e.g. by wrapping it in <div class="cdx-dir-rtl">).

We do currently measure the directionality of a component at runtime in some cases (in the Icon component to determine whether to flip the icon, and in the Tabs component to determine whether a right arrow keypress should advance forwards or backwards). Another strategy we could consider is to do this for every component, set class="cdx-dir-ltr" or class="cdx-dir-rtl" on the component based on that calculation, and use those classes in our generated styles. One downside of that approach is that our directionality detection happens at mount time (when the component is inserted into the DOM) and is not reactive to the surrounding context changing, but I don't think it's possible to implement that kind of reactivity anyway (unless you're using native logical properties or :dir()).

Change 752759 had a related patch set uploaded (by Catrope; author: Catrope):

[design/codex@main] build: Generate RTL version of Codex CSS using rtlcss

https://gerrit.wikimedia.org/r/752759

After discussing this with @Volker_E, @AnneT and @egardner, we decided to keep it simple for now: write styles for LTR, and generate a flipped version of the stylesheet for RTL. The attached patch generates dist/codex.style-rtl.css alongside dist/codex.style.css, using the rtlcss package.

Not sure how helpful it is to this conversation, but I wanted you to be aware that at least on fawiki, we have some CSS logic that is specifically for "ltr content within an rtl container".

For instance, the default content is RTL and numbered lists such as ref lists are numbered using Eastern Arabic numerals. However, when LTR references are included in between them, those are numbered using Western Arabic numerals. The latter occurs when you have <li dir=ltr> tags within the <ol> tag for references and the <ol> tag, by default, has dir=rtl in the case of fawiki. To see an example, check the references section of this article and notice that references 2-7 are differently numbered than 1, 8 or 9.

Why does it matter? It matters because in a "logical" CSS rule, we would want a rule that says "if LI's direction is opposite that of the parent OL tag, then use such and such numbering" but to the extent I understand it, this is not supported by postcss.

Even though this CSS logic is currently handled locally in fawiki's interface messages, ideally it would be moved to Cite extension modules at some point. I cannot imagine completely getting rid of "rlt/ltr" rules in CSS.

Change 752759 merged by jenkins-bot:

[design/codex@main] build: Generate RTL version of Codex CSS using rtlcss

https://gerrit.wikimedia.org/r/752759

Why does it matter? It matters because in a "logical" CSS rule, we would want a rule that says "if LI's direction is opposite that of the parent OL tag, then use such and such numbering" but to the extent I understand it, this is not supported by postcss.

Even though this CSS logic is currently handled locally in fawiki's interface messages, ideally it would be moved to Cite extension modules at some point. I cannot imagine completely getting rid of "rlt/ltr" rules in CSS.

Thank you for this comment! It's always great to hear from people who have real-life experiences on wikis that deal with mixed directionality.

We certainly aren't proposing to completely get rid of rtl/ltr rules in CSS, because of the kind of use case that you pointed out. What we decided was that for Codex (the new Vue component library, you can think of it as a Vue equivalent of OOUI), we will generate a separate RTL stylesheet by flipping the LTR stylesheet, just like we currently do for every other interface stylesheet in MediaWiki.

We had had a discussion about maybe generating a single stylesheet for both directions that looks like [dir='ltr'] .foo { padding-left: 4px; } [dir='rtl'] .foo { padding-right: 4px }, but we decided against that because it would cause issues and wouldn't actually support the kind of "nested LTR in RTL" situations you describe any better (your example is perfect to illustrate that: both rules would apply, the RTL rule would win because it came second, but that's wrong because we want LTR). If we could use .foo:dir( ltr ) instead, then the generated bidirectional stylesheet strategy could have worked, but browser support for :dir() is very poor right now. If that improves in the future, we may revisit this decision then.

Change 755521 had a related patch set uploaded (by Catrope; author: Catrope):

[design/codex@main] build: Make the sandbox (npm run dev) switchable between LTR and RTL

https://gerrit.wikimedia.org/r/755521

Glad to have helped with this discussion, and sorry that I didn't fully understand the scope of this task (your description of what Codex is was very helpful).

Change 755521 merged by jenkins-bot:

[design/codex@main] build: Make the sandbox (npm run dev) switchable between LTR and RTL

https://gerrit.wikimedia.org/r/755521

Change 757783 had a related patch set uploaded (by Catrope; author: Catrope):

[design/codex@main] build: Fix RTL flipping so that comment directives work

https://gerrit.wikimedia.org/r/757783

Change 757783 merged by jenkins-bot:

[design/codex@main] build: Fix RTL flipping so that comment directives work

https://gerrit.wikimedia.org/r/757783

Catrope closed this task as Resolved.EditedMar 16 2022, 9:54 PM

Wrapping up this task: we have implemented the following in Codex:

  • We author styles based on LTR, and generate RTL styles by flipping those.
  • For the documentation website, where we have a directionality switcher, we use postcss-rtlcss to generate "bidirectional" styles that look like [dir='ltr'] .foo { padding-left: 4px; } [dir='rtl'] .foo { padding-right: 4px; }. This is necessary to make directionality switching work, but it has the annoying requirement that every component has to have exactly one ancestor with a dir attribute, otherwise the styles break. We painstakingly ensure this on the documentation website, but it's not a reasonable constraint to expect consumers of the library to enforce.
  • For consumers of the library, we produce two stylesheets: codex.style.css with LTR styles, and codex.style-rtl.css with flipped RTL styles. Internally, we do that by generating a bidirectional stylesheet like the one for the documentation website, then stripping the [dir='ltr'] and [dir='rtl'] selectors to extract the LTR and RTL styles; but we would like to find a less complex/fragile way of doing this, see T304020: Consider less fragile / easier to understand ways to generate separate CSS files for LTR and RTL.
  • We do not use logical properties at this time. Instead, we author LTR-based styles: e.g. padding-left: 4px; rather than margin-inline-start: 4px;.
  • Client-side language/directionality switching is not supported, except on the documentation website.

I've started a discussion about how and when we should use logical properties in the future at T304017: Consider whether and when to start using CSS logical properties in Codex. Using logical properties would most likely be the main blocker for supporting client-side directionality switching as well (the alternative is the :dir() pseudo-class, but browser support for that is way worse).

Since we have functional LTR/RTL support in Codex now, and have a task for future enhancements, this task can be closed.

Bidirectionality support in Codex is documented here for code contributors and here for library consumers.