Le bug de React qui n’apparaît que lorsque les lecteurs traduisent votre page
La traduction des pages Chrome sépare les nœuds de texte React, provoquant des plantages liés à la méthode removeChild ou des freezes silencieux. Des tests effectués sur différents navigateurs montrent quand la réparation de la zone de visualisation est utile et quand il est plus sûr d’écrire directement dans les enveloppes du traducteur.
Le premier cas semblait anodin. Un compteur React affichait une valeur obsolète tandis que tous les contrôles voisins répondaient encore. L’état de l’application contenait la valeur correcte ; la chaîne affichée, en revanche, non. Le traducteur intégré de Chrome était activé pour ce visiteur.
Une semaine plus tard, le même produit a commencé à générer l’erreur NotFoundError: Failed to execute 'removeChild' on 'Node' et à détruire sa propre racine. Même mécanisme, mais erreur plus grave.
Fonctionnement du traducteur
Le traducteur de Chrome ne modifie jamais le nœud de texte à son emplacement initial. Il crée un remplaçant, l’encadre dans un élément <font>, insère cet encadrement à l’ancienne position, puis déconnecte le nœud d’origine de l’arbre en temps réel.
Le nœud d’origine reste alloué. React conserve toujours sa référence. Le nœud est simplement absent du document.
Cette substitution structurelle explique les deux modes de défaillance. removeChild provoque une erreur car le nœud que React souhaite supprimer n’a plus de parent. L’attribution de nodeValue ne provoque aucune erreur, mais modifie du texte qui n’est plus visible.
Les plantages laissent des traces d’exception. Une interface utilisateur figée ne fournit aucune information utile. Un compteur qui cesse d’avancer semble être un bug d’état, ce qui est généralement le point de départ des recherches des équipes.
La solution que tout le monde copie-colle
Shuhei a décrit ce conflit DOM sur le suivi des problèmes de React en 2018. Dan Abramov a indiqué que ce problème ne pouvait pas être résolu, mais la solution temporaire issue de cette discussion reste la réponse par défaut à copier-coller. Le correctif contourne removeChild et insertBefore afin qu’ils renvoient silencieusement lorsque le nœud n’est pas un enfant du parent prévu.
Les plantages disparaissent.
Les mises à jour en direct disparaissent avec eux.
Le même échantillon React a été comparé sous trois configurations au cours d’une seule session de traduction. Laisser l’arborescence sans protection a provoqué deux exceptions non capturées qui ont détruit la structure racine, y compris les boutons. L’installation du mécanisme de protection collé a éliminé tous les erreurs ; le compteur s’est figé, les chaînes de caractères supprimées sont restées à l’écran, et un opérateur ternaire qui modifiait l’état affichait simultanément les deux branches.
L’échec visible est remplacé par une stagnation invisible.
Mesurer plutôt que deviner
Les réponses des forums ont été mises de côté au profit d’une observation directe. Chrome, Edge, Firefox, Yandex ainsi que le widget de traduction indépendant de Google ont été comparés à une page enregistreuse qui prenait des captures d’écran de chaque nœud de texte, mettait en pause l’affichage jusqu’à ce qu’un utilisateur active la traduction, puis exécutait seize tests ainsi que cinq expériences. Les mesures de temps ont été effectuées à l’aide de Playwright sur une instance réelle de Chrome dotée d’un profil prédéfini.
Le premier résultat concret : le bug n’est pas présent partout.
Au Edge et au Firefox, le moteur réécrit le nœud de texte sans le séparer, ce qui fait que les modifications ultérieures persistent. Un compteur fonctionnant une fois par seconde est passé à 6 dans les deux navigateurs. Les utilisateurs de ces moteurs n’ont jamais rencontré de plantage ni de blocage. Ce fait remet en question ceux qui proposent une solution universelle, mais il reste vrai, c’est pourquoi le README ainsi que la page de démonstration l’indiquent tous deux.
La découverte qui a changé la nature du problème
Le traducteur de Chrome fonctionne sur ce qui est actuellement affiché, et non sur toute la page en même temps.
Face à un traducteur inactif, dix tentatives de mise en marche ont été envoyées, accompagnées de tests discrets : lectures forcées du layout ; événements fictifs de redimensionnement, de mise en surbrillance, de changement de visibilité et de déplacement de la souris ; défilement de la fenêtre vers l’avant et vers l’arrière ; ainsi que des actions réelles de la roulette et du curseur émises par le navigateur lui-même.
Seul element.scrollIntoView() a déclenché la traduction, après 168 millisecondes. Les neuf tentatives restantes sont restées silencieuses pendant dix secondes entières.
Deux tentatives infructueuses correspondaient à de véritables événements du navigateur, ce qui exclut l’« entrée fiable » comme unique déclencheur. Le défilement au niveau de la fenêtre a également échoué. L’élément traduit doit devenir visible par lui-même.
Où la première conclusion a échoué
Une affirmation audacieuse figurait dans le brouillon : une fois un nœud de texte séparé restauré, Chrome ne le traduit plus jamais. Une vérification semblait la confirmer. La vérification a été effectuée, le nœud est resté en anglais, et la phrase a été intégrée à la documentation.
Cette vérification se trouvait en bas de page.
La contradiction n’est apparue qu’après que la règle concernant la zone de visualisation ait été écrite : les deux affirmations ne peuvent pas coexister. En déplaçant le curseur dans la zone de visualisation et en répétant l’exécution, on a vu Chrome réparer le nœud restauré en 210 millisecondes. En dehors de la zone de visualisation, il ne le réparait jamais, quel que soit le temps d’attente.
Cette affirmation était fausse depuis deux jours au sein d’un document dont la thèse est que les mesures l’emportent sur les suppositions.
Deux autres erreurs ont suivi le même schéma. Une comparaison directe avec une bibliothèque existante était sans signification lors de la première exécution, car cette bibliothèque ne s’chargeait jamais. Son fichier bundle se termine par un commentaire //# sourceMappingURL=, et la ligne ajoutée pour l’exposer globalement se trouvait à l’intérieur de ce commentaire. Chaque exécution indique désormais quels méthodes DOM chaque version a réellement corrigées avant le début de la mesure.
Firefox a également fait l’objet d’une surestimation. Trois essais de contrôle ont tous affiché le nombre correct, mais deux se sont terminés en français et un en anglais. Avec des mises à jour continues, le moteur en place peut ralentir et afficher temporairement la langue originale.
Toutes les trois corrections sont restées visibles dans le rapport, ainsi que ce qui les a remplacées. Cacher les modifications obligerait les lecteurs à faire confiance à des sections non vérifiées.
Le coût pour le lecteur d’une correction
Lorsqu’un nœud disparaît de l’arbre, il existe deux façons de le récupérer : restaurer le nœud original et attendre que le traducteur s’en aperçoive pour le traduire à nouveau, ou bien insérer la nouvelle valeur dans l’encadrement déjà ajouté par le traducteur.
La plupart des bibliothèques existantes choisissent la restauration. Cela fonctionne, mais le lecteur subit un coût visible. Sur cinq répétitions de quatre mises à jour, avec un échantillonnage du texte visible tous les 50 millisecondes :
La restauration affichait des chaînes dans la langue source pendant 100–150 ms à chaque mise à jour, et de 500–600 ms pour une séquence en quatre étapes. L’écriture dans l’enveloppe affichait du texte dans la langue source pendant 0 ms à chacune des vingt mises à jour.
Un éclairage bref est facile à ignorer. Un compteur en temps réel met en évidence cet éclairage à chaque battement.
Un brouillon précédent indiquait 150–200 ms par mise à jour et 700 ms pour la séquence. Ces chiffres provenaient d’une seule exécution et n’ont pas tenu face à cinq répétitions. Le rapport publié conserve donc l’ensemble complet des cinq exécutions : un seul échantillon de chronométrage ne constitue pas une preuve.
Où l’approche plus efficace cesse de fonctionner
Injecter un nouveau chiffre dans une phrase déjà traduite fonctionne en néerlandais. En russe, cela peut altérer la grammaire.
Intl.PluralRules('ru') classe 4 comme peu et 7 comme beaucoup, et les terminaisons des noms suivent cette catégorie. Une phrase en russe qui utilise la terminaison peu pour quatre ampoules ne doit pas conserver cette terminaison lorsque le nombre devient sept. Une version précoce a engendré précisément cette corruption silencieuse dans une langue que l’implementateur ne pouvait pas lire.
La logique actuelle refuse toute modification lorsque la catégorie du pluriel, la longueur du chiffre ou la structure de la phrase change, ou lorsque le paysage linguistique n’est pas reconnu. Lorsque l’heuristique échoue, l’interface affiche le nombre exact dans la langue non traduite. Cette dégradation est intentionnelle : un chiffre correct en anglais vaut mieux qu’une inflexion erronée dans une langue que personne sur l’équipe ne peut corriger.
En néerlandais et en allemand, Intl.PluralRules renvoie other pour tout entier, de sorte que le piège lié à la catégorie du pluriel ne se produit jamais lors des tests uniquement sur ces locales.
Ce qui reste non mesuré
La ligne Safari est intentionnellement vide. WebKit dans Playwright ne dispose pas de traducteur, et aucune version de Safari pour Windows n’a été publiée depuis 2012, ce qui rend impossible une couverture automatisée. La collecte de données nécessite un Mac physique ainsi que quelqu’un capable d’ignorer manuellement la demande de traduction. Deviner un résultat serait pire que de laisser la case vide.
Lorsqu’une enregistrement ne détecte jamais de traduction en cours, la valeur est stockée sous forme de null plutôt que de false. Cette distinction empêche les lecteurs ultérieurs de considérer une session silencieuse comme une preuve que le moteur est inoffensif.
La bibliothèque
Le package associé est translate-shield sur npm. Lorsque Chrome remplace un nœud de texte par un conteneur <font>, la bibliothèque enregistre cette relation et redirige les écritures ultérieures de React vers ce conteneur, afin que l’écran reste dans la langue choisie par le visiteur. Il n’y a pas de dépendances en temps d’exécution et la taille du fichier compressé est d’environ 15 kB. Edge et Firefox reçoivent un chemin de comportement vide, puisque ces navigateurs ne séparent jamais le nœud à l’origine.
Le package ne traduit pas de chaînes de caractères et ne remplace pas non plus une bibliothèque i18n.
Une démonstration interactive place un document protégé à côté d’un double non protégé, tandis que le navigateur du visiteur traduit les deux. Des documents distincts sont obligatoires : la correction s’applique à l’ensemble du document, de sorte qu’une seule page ne peut pas servir de groupe témoin fiable.
Chaque statistique citée ici est étayée par un fichier JSON généré à partir d’un test réexécutable dans le dépôt, y compris les mesures qui ont été corrigées suite à des erreurs antérieures.
Lectures complémentaires
Comparaison interactive : https://google-translate-simulation.netlify.app/
Dépôt contenant les enregistrements bruts des sondages : https://github.com/alievdavlat/translate-shield
Page du paquet publié : https://www.npmjs.com/package/translate-shield