Votre build RTL passe tous vos tests, et il est quand même cassé
Les comparaisons de captures et les suites de régression visuelle sont structurellement aveugles aux bugs RTL. Voici pourquoi, et les vérifications qui les détectent vraiment.
Il existe deux sortes de bugs en droite-à-gauche. La première est bruyante : le texte part dans le mauvais sens, la mise en page s'effondre, et vous le voyez en cinq secondes. La seconde ne déclenche aucune erreur, paraît tout à fait plausible sur une capture, et part en production. Cet article traite de la seconde, et surtout de la raison pour laquelle vos outils de test actuels ne peuvent pas la voir.
En résumé : les défaillances RTL sont sémantiques plutôt que visuelles. Tous les outils de la chaîne de test front-end classique comparent des pixels ou vérifient le DOM, et cette classe de bugs produit des pixels corrects agencés selon un sens erroné. Ce n'est pas une lacune de votre couverture de tests. C'est une lacune de ce que la comparaison de pixels peut exprimer. Si vous en êtes plus tôt et recensez encore les défaillances elles-mêmes, la liste de ce qui reste discrètement orienté à l'envers vient d'abord.
Pourquoi une comparaison de captures ne voit pas un logo inversé
Une feuille de style RTL contient souvent une règle de miroir générale, destinée à retourner les icônes pour que les flèches pointent dans le bon sens. Écrite trop largement, elle attrape aussi le logo de la marque. Le logo est désormais à l'envers sur chaque page arabe.
Lancez maintenant une suite de régression visuelle dessus. La référence est la page anglaise, le candidat la page arabe, et les deux n'allaient de toute façon jamais correspondre : le texte diffère, la direction diffère, toute la colonne s'inverse. Vous établissez donc la référence de la page arabe sur elle-même. Dès cet instant, le logo inversé devient l'état attendu, et chaque exécution suivante le confirme.
Un logo inversé reste un objet en forme de logo occupant un espace en forme de logo. Rien dans l'image n'est anormal. Seul le sens est faux, et le sens n'est pas ce que mesure une comparaison.
Pourquoi un nombre réordonné passe la relecture
Placez la chaîne +180% dans un paragraphe arabe et l'algorithme bidirectionnel d'Unicode entre en action. Les chiffres sont fortement gauche-à-droite : 180 conserve donc son ordre. Le plus et le pourcent sont des caractères neutres, ce qui signifie que l'algorithme résout leur position d'après la direction du paragraphe environnant, pas d'après l'ordre de saisie. Les deux migrent du mauvais côté. Vous obtenez %180+.
Chaque glyphe de cette sortie est correct. Il y en a quatre, ce sont bien les quatre demandés, rendus proprement à la bonne taille dans la bonne police. Une comparaison de pixels voit quatre glyphes corrects dans un agencement plausible. Une assertion DOM voit textContent égal à +180%, car le DOM contient la chaîne logique et la réorganisation se produit au moment du rendu. Les deux passent.
La moitié que l'automatisation possède réellement
Les machines sont bonnes sur une partie du problème, et elle mérite d'être prise au sérieux car elle est peu coûteuse et s'exécute à chaque commit. Les propriétés CSS physiques ne s'inversent jamais. Les logiques s'inversent toujours. Analysez donc la feuille de style à la recherche du jeu physique et faites en sorte que le build vous les signale.
# Every physical property that will not flip under direction: rtl.
# Run it in CI and read the output as a list of candidates, not verdicts:
# a physical value is sometimes exactly what you meant.
rg -n --type css \
-e '\b(margin|padding|border|inset)-(left|right)\b' \
-e '^\s*(left|right)\s*:' \
-e '\b(text-align|float|clear)\s*:\s*(left|right)\b' \
-e '\bborder-(top|bottom)-(left|right)-radius\b' \
src/Soyez lucide sur ce que cela n'atteint pas. Cela ne trouvera pas une keyframe qui translate d'un pourcentage négatif, une image recadrée avec object-position, une icône dessinée vers la droite dans son propre SVG, ni une règle de miroir au périmètre trop large. Rien de tout cela ne s'écrit left ou right dans le fichier. Le scan est un plancher, pas un plafond.
Isoler un nombre, et pourquoi le style calculé ne prouve rien
Le correctif pour le nombre consiste à dire à l'algorithme que la valeur est un îlot autonome, doté de sa propre direction que le paragraphe environnant ne peut pas pénétrer.
.stat__value {
direction: ltr;
unicode-bidi: isolate;
/* pair it with tabular figures when the value counts up, or the
digits jitter as their widths change during the animation */
font-variant-numeric: tabular-nums;
}Et voici le piège. Une fois cette règle écrite, la façon évidente de vérifier est de relire le style calculé et de confirmer qu'il indique isolate. Cela prouve que la déclaration s'est appliquée. Cela ne prouve pas que les caractères sont peints dans l'ordre voulu, car la valeur calculée vous dit ce qu'on a demandé au navigateur, pas ce qu'il a dessiné. Les deux divergent plus souvent qu'on ne le souhaiterait, surtout dès qu'une police de repli ou un élément conteneur entre en jeu.
Lire où chaque caractère a réellement été peint
Un Range peut être réduit à un seul caractère, et son rectangle englobant indique où ce caractère a atterri à l'écran. Parcourez un nœud texte caractère par caractère, relevez chaque bord, puis triez selon celui-ci. La chaîne ainsi triée est l'ordre visuel. Comparez-la à la chaîne logique et vous obtenez une preuve, non une déduction.
/**
* The visual order of a text node, read from the rendered rects rather
* than from the string. Paste into DevTools on the real page.
*/
function visualOrder(node, paragraphDir = "ltr") {
const text = node.textContent;
const range = document.createRange();
const chars = [];
for (let i = 0; i < text.length; i++) {
range.setStart(node, i);
range.setEnd(node, i + 1);
const rect = range.getBoundingClientRect();
if (rect.width === 0) continue; // collapsed whitespace
chars.push({ char: text[i], logical: i, x: rect.left });
}
// Read the way the paragraph reads.
chars.sort((a, b) =>
paragraphDir === "rtl" ? b.x - a.x : a.x - b.x,
);
return chars.map((c) => c.char).join("");
}
const el = document.querySelector(".stat__value");
visualOrder(el.firstChild, "rtl");
// "%180+" before the isolate -> the bug, visible as a string
// "+180%" after -> proof, not inferenceCela mérite d'être intégré à un vrai test plutôt qu'exécuté à la main. C'est rapide, déterministe, et c'est la seule vérification de cet article qui produit un verdict plutôt qu'une liste de candidats.
La partie qui doit être un humain
Lisez-le dans la vraie locale, sur un vrai appareil, avec quelqu'un qui lit la langue. Tout ce qui reste en deçà est une vérification partielle. L'outillage automatisé gagne sa place sur la moitié mécanique du travail, en repérant les propriétés physiques codées en dur avant la mise en ligne, et la vérification bidi ci-dessus ferme un trou précis avec certitude. Les défaillances qui survivent jusqu'en production sont sémantiques, et une personne qui lit l'arabe les trouve en une minute environ.
La conclusion pratique relève du budget plus que de la technique. Cette relecture n'est pas une faveur que l'on demande une fois le build terminé. C'est une ligne du devis, et sur un projet trilingue elle y figure trois fois. C'est ainsi que nous cadrons les sites multilingues et RTL.

