Баг у React, які выявляецца толькі калі чытальнікі перакладаюць вашу сторонку
Адаптаванне сторынкі Chrome адсоюе вузлы тексту React, чыяму выклікаюцца крахі з адзінам removeChild або тыха зупінка працы. Аналіз у разных браузерах паказвае, калі корыстна парадксаванне відвору, і калі безпечней запісваць даныя ў обгорткі перакладчыка.
Першы квіток здаваўся простым. Кантар у 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