Галоўная / Артыкулы

Баг у React, які выявляецца толькі калі чытальнікі перакладаюць вашу старонку

Адаптаванне сторынкі Chrome адсоюе вузлы тэксту React, чыяму выклікаюцца крахі з адзінам removeChild або тыха зупінка працы. Аналіз у разных браузерах паказвае, калі корыстна парадксаванне відвору, і калі безпечней запісваць даныя ў обгорткі перакладчыка.

Выдавец: REACTAPP.TOP6 хв чытання1432 слоў

Крыніцы: The bug that only appears for readers who translate your page

Першы квіток здаваўся простым. Кантар у React паказываў застарэлыя значэння, тады як усі суседзькі элементы ўсё ўсё рэагавалі. Стан прыкладнення мела правільнае значэнне; але паказаная строчка — няўсё. Для таго візітара быў увімкнуты вбудованы перакладчык Chrome.

Чэрэз тыдзень той жа продукт пачаў выдаляць помылку NotFoundError: Failed to execute 'removeChild' on 'Node' і разрушаць свой сабеўласны корэнь. Той самы механізм, але болей выражаная помылка.

Што робіць перакладчык

Перакладчык Chrome ніколі не мяніць ваш узел тэксту на тым месцы, дзе ён знаходзіцца. Ён стварае замену, загортае гэю замену ў элемент <font>, вставляе гэты заглушак на старую пазіцыю і від’еднуючы ваш узел ад жывага дрэва.

Автанскі узел застаецца выдыленым. React все ўсё зберагае да яго апуск. Узел проста не ўказаны у докумэнце.

Гэтыя структурныя змены адказваюць за оба спосабы выключэння. removeChild выклікае адказку, таму што вузел, які React хочаце з’явіць, больш не мае абяца. Значэнне nodeValue не выклікае адказак, пры тым зменяючы тэкст, які больш не ўвідны.

Адказкі залеююць следы стэка. Заморожаная UI не залеюе нічага корыстнага. Кантар, які перастае растаць, выглядае як баг стану, і самэ гэта зазвычай ёсць пунктам, дзе команды начынаюць расследаванне.

Рашэння, якое всі копіююць

Shuhei опісаў канфлікт DOM у трэйкеры проблем React у 2018 годзе. Dan Abramov пазначыў, што проблему нельга вылечыць, пры тым рашэння, якое было запропанаванае ў тых дыскусіях, застаецца стандартным спосабам — копіюваннем і вярненням. Патч перэкрывае дзейнасць removeChild і insertBefore, так што яны тыха вяртаюцца, калі вузел не ёсць дзецюм намечанага абяца.

Адказкі знікаюць.

Рэальныя апдэты таксама знікаюць разам з імі.

Той самы прыклад React быў парабялёваны ў трох конфігурацыях пад час адной сесіі перакладу. Калі дрэва не захаваць, выйшлі две незафіксаваныя адмоўкі, якія знішчылі корань, уключаючы кнопкі. Установка даданага захавальнага элемента пазбавіла ад усіх бядаў; лічылка застаяла, выдаленыя строкі засталіся на экране, а тэрнарны выраз, які зменяў стан, атрымоўваў выкарыстоўванне обох галужэй адразу.

Візыбельныя бяды заменяюцца невізыбельным застарэнням.

Мерыць заместа спадзявання

Адпаведныя адказы з форумаў былі адкладзены на другі час зарады прымітнага назірання. Chrome, Edge, Firefox, Yandex і самастоятны прыстрой для перакладу Google былі парабялёваны з адной стороны з стороннім сайтом-запісчыкам, які фіксаваў кожны тэкстовы вузел, паставляў працу на паузу, калі чалавек увімкнуў пераклад, а потым запускаў шанзеснаць тэстаў плюс пяць эксперыментаў. Для віміроўкі часу быў выкарыстоўваны Playwright проты рэальнага экземпляра Chrome з заданым профілем.

Першы рэзультат: баг не ўсюды.

У Edge і Firefox двіжак перапісвае тэкстовы вузел без яго ад’ёмлівання, таму пазнейшыя змены застаюцца. Лічылка, якая раней збіралася раз на секунду, у обох браузерах паднялася да 6. Корыстувальнікі этых браузераў ніколі не патраплялі ні на шлях збою, ні на шлях застою. Гэты факт падрыхтавае прычыны для сумневаў ў тых, хто продае універсальныя рашэнні, але гэта застаецца правдой, таму ў README і на сторанцы дэманстраціі гэта таксама зазначаецца.

Аўтарыфікацыя, якая зменіла характар проблемы

Перакладчык у Chrome працуе з тым, што ў даны момент відображаецца, а не з усім старонкам разам.

Працюючы проты неактыўнага перакладчыка, было адправлена дзесяць спроб актывацыі, а таксама выкананы тылу контрольныя дзеяння: прымусовыя чытанні лейауту; фальшывыя змены размеру, фокуса, стану відразлівасці і руху мышы; прасуванне вікна вярх і назад; плюс аутэнтычныя дзеяння колеса мышы і курсора, якія генеруе сам браузер.

Толькі element.scrollIntoView() спрыяў перакладу, пасля 168 мілісекунд. Іншыя дзевяць спроб засталіся без рэакцыі працоўніка прыблізна на дзесяць секунд.

Две неудачныя спробы былі справжнімі запускамі дзеянняў браузера, што выключае можлівасць таго, што “павержаны вхідны дадзеныя” ёсць едынай прычыной. Скроллінг на рэжыме вікна таксама не спрацаваў. Элемент, які павінен быў быць перакладзеным, сам мусіць стаць відным.

Дзе пачатковы вывар быў некарэктны

У праекце была сформульавана адважная тэза: пасля вярнення выдаленага тэкстовага вузла Chrome больш ніколі яго не перакладае. Здавалася, што праба падтвердзіць гэта. Праба была адбыта, вузел застаўся на англійскай мове, і гэты факт быў занесены ў даследчыя матэрыялы.

Эта праба засталася непрыгляднаю для аналізу.

Супрацоўка выявілася толькі пасля таго, як была запісана правілам для вікна перагляду: обе тэзы не можаць існаваць адночасна. Перамішчанне прыбора ў зону перагляду і павтарэнне тэста паказалі, што Chrome відновіў пошкоджаны вузол за 210 мілісекунд. Калі прыбор знаходзіўся за межам екрана, ён ніколі не відновляўся, незалежна ад часу чакання.

Гэта тверджэнне было некалькі дзён неправым у дакуменце, які ставіць тэзу пра тое, што вімеры перажываюць заўважэння.

Ішлі ўсьмо тыя ж помылкі. Пораўнанне з існуючай бібліятэкай па-першаму не мела сенсу, таму што гэтая бібліятэка ніколі не завантажвалася. Іе пакет заканчваецца коментарыем //# sourceMappingURL=, а рэчка, якая дадавалася для ўсунення гэтага коментарыя, опінавалася ў самам гэтым коментарые. Цяпер кожны тэст паказвае, якія методы DOM фактычна былі відновлены кожнай з частакаў прычымоў першыя, прычымоў вімеры.

Firefox таксама быў перацэнены. У трох прымёрах вылічэнныя значэння паспаўляліся з правільнымі, пры тым два завершыліся на французскай мове, а адна — на англійскай. З-за сталых апдэйтаў двіжак, які працуе на месцы, можа сповольняцца і на час паказваць первісную мову.

Усе тры пасправленні засталіся віднымі ў звёрненні, а таксама тое, чым яны былі заменены. Скрытыя правкі вымагалі б ад чытача павернення ў дзеянні неконтрольваных частак.

Калі коштае пасправленне для чытача

Калі вузол знікае з дрэва, ўтварэнне яго зноў ёсць два. Можна вярнуць первісны вузел і чакаць, пакі ў перакладчыку з’явіцца можлівасць і ён перакладзе зноў. Але можна таксама запісаць новую значэнне ў кантэйнер, які ўжо быў вставлены перакладчыкам.

Большасць існуючых бібліятэкаў выбірае варыянт вярнення. Функцыйна ён работае, пры тым чытач вынужаны ўжываць відныя зусіллі. У п’яты рэплікацыях чатырох апдэйтаў, калі тэкст перазбіраўся кожныя 50 мілісекунд:

Працэўнае вярненне паказала строкі мовы выходу пры кожнай адчынцы за 100–150 мс, а пры чатыроэтапнай последовальнасці — за 500–600 мс. Запіс у обгортку паказаў тэкст мовы выходу за 0 мс пры кожнай з двадцяці адчынэнняў.

Однакі спалах лёгка ігнораваць. Жывы лічылак показвае гэты спалах пры кожным цікле.

У пачатковай версіі было зазначана 150–200 мс пры кожнай адчынцы і 700 мс для всей последовальнасці. Гэтыя цыфры былі апрацаваны за адну практыку і не перастаўалі бутыць актуальнымі пасля пяці парадоў. Таму у опублікованым адчыненні застаўся весь набор пяці практык: адзін прыклад часу не ўважаецца фактам.

Дзе лепшы падход перастае работаць

Даўжэнне новай цыфры ў вялікануюцы ўжо перакладзеную рэчэнне працюе у голандскай мове. У рускай жа гэта можа нарабіць паталогій у граматыцы.

Intl.PluralRules('ru') класіфікуе 4 як некальканосныя, а 7 — як кальканосныя, і заканчванні іменаў паследуюць гэтай катагорыі. Рускія рэчч, у якой для чатырох лампочак іспользуецца заканчванне некальканосных, не можа застаўляцца з гэтым заканчваннем, калі колькасць стае сям. У ранней версіі прыстрою выйшла самэ гэтая таятная парадоксальнасць у мове, якую распрацоўшчык не можа прачытаць.

Часовая логіка адмовляецца кожны раз, калі зменяецца катагорыя множкі, дужнасць цыфры чы ўзор рэччы, або калі локацыя не ўпішваецца. Калі гэтая хюрыстыка не работае, інтэрфейс паказвае точную колькасць на ненадтаўленай мове. Такое пахібнае рашэнне ўмышленае: правільная цыфра на англійскай мове ў безпека, чым неправільная форма іменаў у мове, якую ніхто з команды не можа пераканаліць.

У голандскай і нямецкай мовах Intl.PluralRules вяртае other для кожнага цэлага чысла, таму падчас тэставання толькі з гэтымі локаламі прыгонаў, связаных з катэгорыяй множкі, ніколі не выступае.

Што застаецца незамераным

Строка для Safari спецыяльна застае пустой. WebKit у Playwright не мае перакладчыка, а жаднага выдання Safari для Windows не выпускалі з 2012 года, таму автаматызаваная перакрыценасць недаступна. Збір даных апяроўвае наявнасць фізычнага Mac і чалавека, які можа ручная адхіліць запрос на пераклад. Адгадваўшы рэзультат, можна будзе парадкаваць становішча, больш ніж якшы ўзостаць у клетцы пустой.

Калі запіс ніколі не спазірае на актыўны пераклад, значэнне зберагаецца як null уместо false. Гэта разлік не дазволяе пазнейшым чытальнікам спрыяжваць тыхую сесію з доказам таго, што двайнік нешкодны.

Бібліятэка

Супакет, які лягаецца ў комплект, — гэта translate-shield у npm. Калі Chrome заменяе вузел тэкста на обгортку <font>, бібліятэка фіксуе гэтыя зв’язкі і перэнаправляе всі пазначкі React у гэтую обгортку, так што экран застаецца на мове, якую выбраў візітар. Не існуе залежнасцяў пад час выканання, а розмер запакаванага файлу становіць апошней 15 kB. Edge і Firefox отрымаюць порожняе правіле працы, адтуды што гэтыя браузеры з самага пачатку не ад’еднуюць такі вузлы.

Гэты супакет не перакладае стрынкі і не заменяе бібліятэку i18n.

Інтэрактыўная дама ставіць захішчаны документ падле незахічанага аналога, пры чым браузер візітара перакладае як адны, так і другі. Неабходныя разлучныя документы: патч праіснуе для усяго документа, таму адна стораніца не можа служыць для керавання.

Кожная статыстыка, прыведзеная тут, падтрымвана файлам JSON, створаным за дапамою тэсту, які можна запускаць знова ў рэпазітарые, укладаючы ў сябе таксама паказнікі, якія былі скорэгаваны пасля ранейшых адклеканняў.

Да дапамогі

Інтэрактыўныя порэвананні: https://google-translate-simulation.netlify.app/

Рэпазітарые з неапранутымі запісамі проб: https://github.com/alievdavlat/translate-shield

Старонка пакета: https://www.npmjs.com/package/translate-shield