{"_id":"translation-resilience","_rev":"7-16f10cfd342ae1b4fe4e0d6cb1ffb230","name":"translation-resilience","dist-tags":{"latest":"0.4.1"},"versions":{"0.1.0":{"name":"translation-resilience","version":"0.1.0","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"license":"MIT","_id":"translation-resilience@0.1.0","maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"homepage":"https://github.com/alexspeller/translation-resilience#readme","bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"dist":{"shasum":"24c7c059b3391f266e8fb28e432d7ee3dc6be368","tarball":"https://registry.npmjs.org/translation-resilience/-/translation-resilience-0.1.0.tgz","fileCount":15,"integrity":"sha512-5NxqDsssUsJ4SGeO3m+L5ubHnIxhLfG8f6yEnzkDFs3EqlpPW4LsQN8OgWPG0JfTGqPb9w91b+NLusjAvk/Qvw==","signatures":[{"sig":"MEQCIDiR/GBkokgYpM7BX6XFx5Gr1bi8HIDh2XRm97a+k0azAiBJvV7L4okLL7UGUxxQpJ0NVDOiGzF+y9PQvlU0MKl9Ew==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":149090},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./simulator":{"types":"./dist/simulator.d.ts","import":"./dist/simulator.js","require":"./dist/simulator.cjs"}},"gitHead":"929445946dae41b4c1ec2e4ae98d814840fa856f","scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"alexspeller","email":"alex@alexspeller.com"},"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"_npmVersion":"11.16.0","description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","directories":{},"sideEffects":false,"_nodeVersion":"26.3.1","allowScripts":{"esbuild":true,"fsevents":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^29.1.1","react":"^18.3.1","vitest":"^4.1.10","react-dom":"^18.3.1","typescript":"^5.9.3","@types/react":"^18.3.31","@biomejs/biome":"^2.5.3","@types/react-dom":"^18.3.7","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/translation-resilience_0.1.0_1783722386756_0.1181486880708571","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"translation-resilience","version":"0.1.1","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"license":"MIT","_id":"translation-resilience@0.1.1","maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"homepage":"https://github.com/alexspeller/translation-resilience#readme","bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"dist":{"shasum":"1f0b7ba7ff877fb39fdca18b7dc0df36eeac35c4","tarball":"https://registry.npmjs.org/translation-resilience/-/translation-resilience-0.1.1.tgz","fileCount":15,"integrity":"sha512-soucSrLAFj11MX3+XJwvwLVhWz2WXNuoH8YIVgTLI/ztl6mRMrEpiEk5e7YaowghLIoeoDg+WEL+2o/L+TjsQg==","signatures":[{"sig":"MEQCIAJCjL9FlFDomIdZbofvijTdgEytA5xfWwhcxKQcdFhmAiAexQt9LakOITQjYume5reUTKmEMvYFbHKQH9/ET1DdhA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":153621},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./simulator":{"types":"./dist/simulator.d.ts","import":"./dist/simulator.js","require":"./dist/simulator.cjs"}},"gitHead":"c1bb3a4f3671d84144fc53eb03625ce8a58811f5","scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"alexspeller","email":"alex@alexspeller.com"},"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"_npmVersion":"11.16.0","description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","directories":{},"sideEffects":false,"_nodeVersion":"26.3.1","allowScripts":{"esbuild":true,"fsevents":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^29.1.1","react":"^18.3.1","vitest":"^4.1.10","react-dom":"^18.3.1","typescript":"^5.9.3","@types/react":"^18.3.31","@biomejs/biome":"^2.5.3","@types/react-dom":"^18.3.7","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/translation-resilience_0.1.1_1783723583062_0.7119177181445795","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"translation-resilience","version":"0.1.2","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"license":"MIT","_id":"translation-resilience@0.1.2","maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"homepage":"https://github.com/alexspeller/translation-resilience#readme","bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"dist":{"shasum":"01238dbd46ceb373da6f46a7bf3921eca5027e10","tarball":"https://registry.npmjs.org/translation-resilience/-/translation-resilience-0.1.2.tgz","fileCount":15,"integrity":"sha512-kW+Ficrm3E5EpQvp5Y7lyUFVgdcKS+ecLCiazgGR/JTn65Li9eUuJKDICAp0rDN3u18y+9p2kezAmFV8cNynZw==","signatures":[{"sig":"MEYCIQDHQGXkNF6QyQ1XcwqiaooAyFNHjQXZnvnpBPQvcWL1+gIhAI78XYnvdB8y8MiWicNjRqO+G5+Jmlvm0jhpd5+dyFLu","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":156164},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./simulator":{"types":"./dist/simulator.d.ts","import":"./dist/simulator.js","require":"./dist/simulator.cjs"}},"gitHead":"2dc7760cbc77c1650250655e244632689ffe1714","scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"alexspeller","email":"alex@alexspeller.com"},"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"_npmVersion":"11.16.0","description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","directories":{},"sideEffects":false,"_nodeVersion":"26.3.1","allowScripts":{"esbuild":true,"fsevents":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^29.1.1","react":"^18.3.1","vitest":"^4.1.10","react-dom":"^18.3.1","typescript":"^5.9.3","@types/react":"^18.3.31","@biomejs/biome":"^2.5.3","@types/react-dom":"^18.3.7","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/translation-resilience_0.1.2_1783724645797_0.11223306141451439","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"translation-resilience","version":"0.2.0","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"license":"MIT","_id":"translation-resilience@0.2.0","maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"homepage":"https://github.com/alexspeller/translation-resilience#readme","bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"dist":{"shasum":"7f25cf863cf4f9ee34bd742dd8df81ae23256730","tarball":"https://registry.npmjs.org/translation-resilience/-/translation-resilience-0.2.0.tgz","fileCount":15,"integrity":"sha512-IxTjhpHGp1SJxVEEPBu/YbBaHnymMRJdYxUY1i5qtYADuz9b8fdWhZSE/EeRS2aVIiuQjRqtYDk69ruS/3fzTg==","signatures":[{"sig":"MEUCIG7br16uHbD/uxiQOZMOoH3EuBRy48/+ngyHNHjMDtYTAiEA8M/D7W8wKi/fHZYUkPA0huz6Xwte7jhSwzarHlvHor0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":172401},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./simulator":{"types":"./dist/simulator.d.ts","import":"./dist/simulator.js","require":"./dist/simulator.cjs"}},"gitHead":"c2df5ff197aa914bd1d5921b8d4d28218609ab6d","scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"alexspeller","email":"alex@alexspeller.com"},"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"_npmVersion":"11.16.0","description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","directories":{},"sideEffects":false,"_nodeVersion":"26.3.1","allowScripts":{"esbuild":true,"fsevents":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^29.1.1","react":"^18.3.1","vitest":"^4.1.10","react-dom":"^18.3.1","typescript":"^5.9.3","@types/react":"^18.3.31","@biomejs/biome":"^2.5.3","@types/react-dom":"^18.3.7","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/translation-resilience_0.2.0_1783775695162_0.32320042892756273","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"translation-resilience","version":"0.3.0","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"license":"MIT","_id":"translation-resilience@0.3.0","maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"homepage":"https://github.com/alexspeller/translation-resilience#readme","bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"dist":{"shasum":"4c95d3e7cf9596022bea241c83b1700dc21c38f3","tarball":"https://registry.npmjs.org/translation-resilience/-/translation-resilience-0.3.0.tgz","fileCount":15,"integrity":"sha512-Z557+xYfS8BGXyCHIfKzTxjwyNocgi5FQ3Symr0Lo0lrZdx+dtfHrLWgPTD3yq1pxhYxLoCh7EO2iiUaGBM6jQ==","signatures":[{"sig":"MEYCIQCE9JW+XtIF5tjT+FHMaVuH2UM7zASwMYwv8AcrUfub9wIhAPJq1g0QGqAqg7MHO/EV+lOWjRhndJoQRnEkLMHDanTJ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":181968},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./simulator":{"types":"./dist/simulator.d.ts","import":"./dist/simulator.js","require":"./dist/simulator.cjs"}},"gitHead":"622276073e6fa8f37655dc0faa42c3db8248cbe6","scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"alexspeller","email":"alex@alexspeller.com"},"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"_npmVersion":"11.17.0","description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","allowScripts":{"esbuild":true,"fsevents":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^29.1.1","react":"^18.3.1","vitest":"^4.1.10","react-dom":"^18.3.1","typescript":"^5.9.3","@types/react":"^18.3.31","@biomejs/biome":"^2.5.3","@types/react-dom":"^18.3.7","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/translation-resilience_0.3.0_1787843749289_0.48816217258189254","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"translation-resilience","version":"0.4.0","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"license":"MIT","_id":"translation-resilience@0.4.0","maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"homepage":"https://github.com/alexspeller/translation-resilience#readme","bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"dist":{"shasum":"3cd767cc3cf5bdbc80e9095d4f5ba00ba1e8905f","tarball":"https://registry.npmjs.org/translation-resilience/-/translation-resilience-0.4.0.tgz","fileCount":15,"integrity":"sha512-fH93ZdG6uzZm4FNA3AZPM3Wz6/4lw9bQHPD6Ccd2LLdrWfMAbeJWkOLIeh9Hmg5XDCj9l3uC/l+zcXoN1myU5A==","signatures":[{"sig":"MEYCIQCNQ6u3/wnrtdONV0QczOAvpnsAEONBMx3AY1JTDEI5LgIhANwvOyRAJcwhgT5jxjI5KCJhO4EX34YrrdNDrdNhduOK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":224996},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./simulator":{"types":"./dist/simulator.d.ts","import":"./dist/simulator.js","require":"./dist/simulator.cjs"}},"gitHead":"cff5986ee3c7b5f2c8ef557097b30b619fb56299","scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:browser":"node scripts/browser-test.mjs","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build && npm run test:browser"},"_npmUser":{"name":"alexspeller","email":"alex@alexspeller.com"},"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"_npmVersion":"11.17.0","description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","allowScripts":{"esbuild":true,"fsevents":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^29.1.1","react":"^18.3.1","vitest":"^4.1.10","react-dom":"^18.3.1","typescript":"^5.9.3","@types/react":"^18.3.31","@biomejs/biome":"^2.5.3","@types/react-dom":"^18.3.7","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/translation-resilience_0.4.0_1789561279148_0.2642736382643298","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"_id":"translation-resilience@0.4.1","bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"dist":{"shasum":"904b3345ba58a431a80388229fad4e28d9c634ec","tarball":"https://registry.npmjs.org/translation-resilience/-/translation-resilience-0.4.1.tgz","fileCount":15,"integrity":"sha512-LYOJfWBXptYFB0egNundI8AcLVqrEU25ehExkNdIxR4V9b0ynnkJ52GrLoEHqIOTUljxwHecMeuIBgoE/VCLiQ==","signatures":[{"sig":"MEUCIEMJJUOVH9DDdkj60l3nvbMPF6FFJSqGX4/3dZ9fFP4BAiEAvakf62ZcHbigTLWXwUEB5F+Vxey57j4HMasBEe8XxIs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCppBO3Y4CDhmzsEw8h173VgvpnW+B+9uHH80ydRCL3LgIgH2+k8TQ8B60DTpDnP/n9I1QXrMDZsBpOUS1M9p11xSY="}],"unpackedSize":226026},"main":"./dist/index.cjs","name":"translation-resilience","type":"module","types":"./dist/index.d.ts","author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./simulator":{"types":"./dist/simulator.d.ts","import":"./dist/simulator.js","require":"./dist/simulator.cjs"}},"gitHead":"99addcd4ebed29a8e77c8d819fdf18a49ecd7d43","license":"MIT","scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:browser":"node scripts/browser-test.mjs","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build && npm run test:browser"},"version":"0.4.1","_npmUser":{"name":"alexspeller","email":"alex@alexspeller.com"},"homepage":"https://github.com/alexspeller/translation-resilience#readme","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"_npmVersion":"11.17.0","description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","directories":{},"maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"sideEffects":false,"_nodeVersion":"26.5.0","allowScripts":{"esbuild":true,"fsevents":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^29.1.1","react":"^18.3.1","vitest":"^4.1.10","react-dom":"^18.3.1","typescript":"^5.9.3","@types/react":"^18.3.31","@biomejs/biome":"^2.5.3","@types/react-dom":"^18.3.7","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/translation-resilience_0.4.1_1790357436203_0.728666343619679"}}},"time":{"created":"2026-07-10T22:26:26.689Z","modified":"2026-09-25T17:30:36.492Z","0.1.0":"2026-07-10T22:26:26.910Z","0.1.1":"2026-07-10T22:46:23.192Z","0.1.2":"2026-07-10T23:04:05.962Z","0.2.0":"2026-07-11T13:14:55.320Z","0.3.0":"2026-08-27T15:15:49.495Z","0.4.0":"2026-09-16T12:21:19.282Z","0.4.1":"2026-09-25T17:30:36.299Z"},"bugs":{"url":"https://github.com/alexspeller/translation-resilience/issues"},"author":{"name":"Alex Speller","email":"alex@alexspeller.com"},"license":"MIT","homepage":"https://github.com/alexspeller/translation-resilience#readme","keywords":["google-translate","page-translation","react","notfounderror","removechild","insertbefore","mutationobserver","chrome","i18n","dom"],"repository":{"url":"git+https://github.com/alexspeller/translation-resilience.git","type":"git"},"description":"Stops browser page translation (Google Translate) from crashing React and silently freezing translated text, by re-adopting displaced text nodes instead of swallowing errors.","maintainers":[{"name":"alexspeller","email":"alex@alexspeller.com"}],"readme":"# translation-resilience\n\nStops browser page translation (Chrome's built-in Google Translate) from **crashing React apps** and **silently freezing translated text** — by repairing the DOM instead of swallowing errors.\n\n```ts\nimport { installTranslationResilience } from 'translation-resilience';\n\ninstallTranslationResilience();\n```\n\nFramework-agnostic (it patches the DOM layer, not React), dependency-free, ~2.6 kB min+gzip, and lazily activated — near-zero cost until a translator actually touches the page.\n\n## The problem\n\nWhen Chrome translates a page, it rewrites the DOM underneath your framework:\n\n1. Adjacent `Text` nodes are merged (`normalize()`) — so `Lights: {count}` collapses into one run.\n2. The merged run is split into segments (numbers isolated) and each segment is wrapped in nested `<font style=\"vertical-align: inherit;\">` elements containing **new** text nodes.\n3. The original text nodes are removed from the document — but React still owns and references them.\n4. Translation can also delete text nodes outright and reorder inline elements to match target-language word order ([Chromium bug 872770](https://issues.chromium.org/issues/41407169)).\n\nReact keeps operating on the original, now-detached nodes:\n\n- **Crash**: unmounting translated conditional text throws `NotFoundError: Failed to execute 'removeChild' on 'Node'`. On React ≤ 18 this takes down the whole tree; React 19 catches it and tears down to the nearest error boundary — still broken, just politer.\n- **Crash**: mounting an element before translated text throws the same from `insertBefore`.\n- **Silent freeze**: updating translated text (counters, timers, live data) writes into the detached node. The visible page never updates again, with no error anywhere.\n\nBackground reading: [facebook/react#11538](https://github.com/facebook/react/issues/11538), [Everything about Google Translate crashing React](https://martijnhols.nl/blog/everything-about-google-translate-crashing-react). The same mechanism breaks Vue ([vuejs/core#14810](https://github.com/vuejs/core/issues/14810)) and Ember/Glimmer ([glimmerjs/glimmer-vm#1372](https://github.com/glimmerjs/glimmer-vm/issues/1372)).\n\n## Why not the well-known monkey-patch?\n\nThe widely copied [error-swallowing patch](https://github.com/facebook/react/issues/11538#issuecomment-417504600) catches the `removeChild`/`insertBefore` exceptions. That stops the crash but **not the freeze**: text React updates still never reaches the screen, and \"removed\" text stays visible. Other mitigations — wrapping every text interpolation in a `<span>`, or `<meta name=\"google\" content=\"notranslate\">` — are invasive or hostile to users who genuinely need translation.\n\n## How this is different: re-adoption\n\nInstead of swallowing errors, this shim puts the original text nodes **back** the moment the renderer touches them:\n\n1. A document-wide `MutationObserver` recognizes translation's displacement pattern (merge, wrap, remove — a pattern renderer commits never produce) and tracks each replaced text run as a *displacement group*: the ordered renderer-owned originals with their pre-translation values, plus the wrapper nodes currently standing in for them.\n2. Patched `Node.prototype.removeChild` / `insertBefore` / `appendChild` and the `nodeValue` / `data` setters detect operations on displaced text nodes and first **restore the group** — originals go back into the wrappers' position, wrappers are removed — then let the native operation proceed on a consistent tree.\n3. The translator's own observer notices the restored (now updated) text and re-translates it, so the user sees fresh, translated content. The loop is self-healing: update → restore → re-translate.\n\nThe result: no crashes, **and** live data keeps updating on translated pages — in the visitor's language.\n\n## Usage\n\nInstall:\n\n```sh\nnpm install translation-resilience\n```\n\nCall once in your client entrypoint, before your framework renders:\n\n```ts\nimport { installTranslationResilience } from 'translation-resilience';\n\nconst uninstall = installTranslationResilience();\n```\n\n### Options\n\n```ts\ninstallTranslationResilience({\n  // Which document to observe (defaults to the global `document`).\n  document,\n\n  // Observability hook: called with a short message on every non-native\n  // path taken, e.g. 'translation signal detected, observing document',\n  // 'translation activity detected',\n  // 'removeChild: displaced text already gone, removal skipped'.\n  onEvent: (message) => {\n    Bugsnag.leaveBreadcrumb('translation-resilience', { message }, 'log');\n  },\n\n  // Install the document-wide observer immediately instead of waiting for a\n  // translation signal (see \"Performance cost\"). Costs more on never-translated\n  // pages. Rarely needed: lazy activation covers translators that mark <html>\n  // (Chrome) and, via the detection stylesheet, translators that only wrap text\n  // in <font> (Edge, the Google Translate extension). eager buys earlier\n  // tracking for a translator that displaces text without marking <html> and\n  // without an identifiable <font> wrapper — that case is otherwise caught on\n  // the repair path, which prevents the crash but cannot re-adopt the first\n  // displacement it never saw.\n  eager: false,\n});\n```\n\n`onEvent` is the recommended way to learn how often translation actually happens to your users — the `'translation activity detected'` message fires once per page load, only when a translator starts displacing text.\n\n## Safety properties\n\n- **Inert until translation happens.** The document-wide observer isn't even created until a translation signal appears — the translator marking `<html>`, or its first signature `<font>` entering the tree (see \"Performance cost\") — and until translation activity is detected every operation behaves natively, including throwing `NotFoundError` on genuine `removeChild`/`insertBefore` bugs in your code. The one deliberate exception is the repair-path sweep: on an operation that is *already* about to throw, finding translator markup anywhere in the document is taken as evidence that a translator, not your code, moved the node. A genuine DOM bug committed while the page is being translated is therefore repaired rather than reported — the trade that keeps a translated page from crashing. On an untranslated page nothing is masked.\n- **Fault-contained.** Every non-native code path runs inside a guard; an internal error in the shim falls back to stock browser behavior and reports through `onEvent` (`internal error in …`). A bug in the shim can never make things worse than not having it.\n- **Reversible.** `installTranslationResilience()` returns an uninstall function that restores all prototypes and disconnects the observer. Calling install twice returns the same uninstall (idempotent).\n- **SSR-safe to import.** Native DOM entry points are captured lazily, so importing the module in Node is fine; only *calling* install requires a DOM.\n\n## Performance cost\n\nThe shim is **lazily activated**. At install it patches the DOM methods and watches for two kinds of signal, whichever comes first:\n\n- **`<html>` markers.** It watches the `class` attribute of `<html>`. Chrome's built-in translator announces itself there — it adds a `translated-ltr`/`translated-rtl` class — a few hundred milliseconds *before* it touches any text (~275–500 ms measured against real Chrome). The class *value* is checked (`translated-…`), not merely \"class changed\", because browser extensions routinely add unrelated classes to `<html>`. `lang` is deliberately *not* watched, for the same reason: apps write it themselves — i18n libraries sync `<html lang>` once language detection resolves and on every language switch, as WCAG 3.1.1 asks — so arming on it would run the full observer on every such page. Watching it would not arm any earlier, either: Chrome adds the class first, in the same call, and only rewrites a `lang` the page already has.\n- **A detection stylesheet keyed to the translator's own `<font>` wrappers.** Not every translator marks `<html>`: Microsoft Edge's built-in translator and the Google Translate browser extension wrap text in `<font>` elements without ever touching the class or `lang`. The shim installs a stylesheet giving a 1 ms CSS animation to a translator's *signature* `<font>` — `style=\"vertical-align: inherit;\"` (Google) or the `_msttexthash`/`_msthash`/`_mstmutation` attributes (Edge) — and arms when `animationstart` fires. Matching the signature rather than the bare tag matters because `<font>` is deprecated but not impossible for an app to render itself (via JSX, `dangerouslySetInnerHTML`, or rendered HTML/markdown); an app's own `<font>` has neither marker and is ignored. The stylesheet is installed as a constructable stylesheet (`adoptedStyleSheets`) so that a `style-src 'self'` Content-Security-Policy — which blocks an injected `<style>` element outright — does not disable detection, with a `<style>` fallback for engines without constructable stylesheet support. The rule uses `!important` so a global `* { animation: none !important }` reset cannot silence it.\n\nOnly on a signal does the shim create the document-wide `MutationObserver` that does the real work. Installing on an already-marked document activates immediately, and a synchronous fallback in the patched methods covers same-realm translators (like the bundled simulator) that signal and displace in the same task.\n\nAnything that still slips through — a translator whose wrappers carry no recognisable signature, or a renderer commit landing in the same task as the displacement, before `animationstart` is delivered — is caught on the way to the exception: when a patched method is about to throw `NotFoundError`, the shim sweeps the document for translator markup and degrades gracefully if it finds any. That sweep runs *only* on a path that would otherwise crash, so it costs nothing in normal operation. `eager: true` remains available to arm the full observer from the start.\n\n**Why detection cannot simply hook the patched methods.** A browser's page translator does not run in your page's JavaScript world. Chromium executes the translate script in an *isolated world* (`ExecuteScriptInIsolatedWorld`, [`translate_agent.cc`](https://chromium.googlesource.com/chromium/src/+/main/components/translate/content/renderer/translate_agent.cc)) — a separate copy of every JS builtin, including `Node.prototype`, over the *same* DOM. Patching `Node.prototype.insertBefore` in the page therefore has no effect on the translator's own DOM calls: they are simply invisible to it. Only signals the shared DOM itself raises — attribute changes, mutation records, style/animation events — cross that boundary. This is why detection is a stylesheet rather than a check inside the patched methods, and why any test that displaces text by calling the patched methods directly proves nothing about real browsers (see the simulator's `displaceFromIsolatedWorld`).\n\nMeasured numbers, with an honest caveat: these are microbenchmarks from one machine (Apple Silicon, headless Chrome, production React 18 build; a 500-row × 4-column table, medians of 7 runs). The shim has **not yet been benchmarked inside a large production app** — if you measure something different, please open an issue.\n\n**Before any translation signal (the overwhelmingly common case) the cost is near zero:**\n\n- Re-rendering the table with three cells changing in every row, 50 commits: 26.9 ms → 29.1 ms (+8%, ≈ 40 µs per full-table commit). Repeated mount+unmount of the whole table: +3% (noise).\n- Writes to an attached `text.data`: ~0.05 µs → ~0.08 µs each. The worst case we could construct — a tight synchronous loop appending and removing individual detached text nodes, a pattern frameworks don't produce — ~0.2 µs → ~0.26 µs per pair.\n- The detection stylesheet adds one selector for the style engine to evaluate against elements it is already matching. Measured on a 500 × 4 grid under sustained churn (50 commits of 2,000 text writes + 1,000 insert/remove each, medians of 9 interleaved trials, headless Chrome on Apple Silicon): **+1.1%** against no detection at all. The alternatives for detecting an out-of-world translator are much more expensive — a `childList`/`subtree` `MutationObserver` measured +7.2% and the full observer +9.5% on the same benchmark. A cheap cross-world signal is the whole reason detection is a stylesheet.\n- What remains is the patched-method call indirection plus one attribute read per operation while dormant. There is no observer, so the browser allocates no mutation records.\n\n**After translation starts**, the full machinery is live: one document-wide `MutationObserver` (childList + characterData + oldValue, subtree) means a record per DOM mutation, and inserting a *detached* text node drains and processes pending records. Correlation state expires after 100 ms and is purged incrementally (amortized O(1) per entry), so steady-state memory is effectively zero. On a translated page, operations on *untranslated* content cost roughly: ≈ 0.3 ms per pathological full-table commit, ~0.3 µs per text write, ~1.8 µs per worst-case churn op. Updates to *translated* text pay the restore → re-translate cycle: ≈ 14 µs per updated text run, so a commit updating 1,500 translated runs at once costs ≈ 20 ms extra — the alternative without the shim is those updates never becoming visible at all. Translated pages that are idle cost nothing beyond the observer.\n\n### How React core could fix this (nearly for free)\n\nAlmost everything this package pays on the happy path is an *outsider tax*: the cost of reconstructing, from mutation records, knowledge the renderer already has. React knows which text nodes it owns (each HostText fiber holds its DOM node), what they should contain (`memoizedProps`), and what the DOM around them should look like (the fiber tree). A fix inside React would need none of this package's machinery:\n\n- **Detection is one pointer comparison.** A text node displaced by translation has `parentNode === null`, so the silent-freeze case is caught by a single check inside `commitTextUpdate`, on an operation React is already performing — instead of a document-wide `MutationObserver` allocating a record (plus an `oldValue` string copy) for every DOM mutation on the page.\n- **The crash half can cost literally nothing.** React 19 already wraps commit-phase deletions in try/catch — that is why it tears down to the nearest error boundary instead of white-screening. Upgrading that catch from \"tear down\" to \"repair and continue\" adds zero instructions to the non-throwing path.\n- **Repair needs no archaeology.** This package tracks displacement groups through a correlation window because, from the outside, \"which foreign nodes replaced mine, and what did mine say?\" can only be answered by watching the mutations happen. React can instead rebuild the affected host element's children from fiber state — the moral equivalent of hydration-mismatch recovery — and let the translator re-translate the result. Same self-healing loop as this shim, none of the bookkeeping.\n- **Nobody else pays.** The prototype patches here are page-global: every DOM user, React or not, pays the (small) toll. An in-core fix is scoped to React's own commit operations.\n\nAgainst the numbers above: lazy activation already makes the never-translated case nearly free, but once translation is active the userland machinery has real costs that an in-core fix would not — and even the dormant-state residue (patched-call indirection, an attribute read per op) would drop to a branch-predicted pointer compare. A fix has been requested since 2017 ([facebook/react#11538](https://github.com/facebook/react/issues/11538)); the traditional objections — defensive checks don't belong in the commit hot path, and third-party DOM mutation is outside React's contract — are worth weighing against what userland has to do instead, which is this entire package. The same reasoning applies to any renderer that keeps references to the text nodes it creates (Vue, Ember, Svelte). We would be delighted for this package to be made obsolete.\n\n## Testing your app against translation: the simulator\n\nThe package ships the mutation simulator used to test the shim itself — it reproduces Google Translate's exact DOM mutations (merge, segment-split, nested `<font>` wrappers, detached originals, deletions, word-order moves, and the ongoing re-translation observer) so you can write deterministic tests without a real browser translation session:\n\n```ts\nimport { render } from '@testing-library/react';\nimport { installTranslationResilience } from 'translation-resilience';\nimport { startTranslateObserver, translateSubtree } from 'translation-resilience/simulator';\n\nit('keeps counters updating on translated pages', async () => {\n  const uninstall = installTranslationResilience();\n  const { container, rerender } = render(<Counter count={4} />);\n  const stopTranslator = startTranslateObserver(container);\n  translateSubtree(container);\n\n  rerender(<Counter count={5} />);\n  await Promise.resolve();\n\n  expect(container.textContent).toContain('5');\n  stopTranslator();\n  uninstall();\n});\n```\n\n`translateSubtree(root, translate?, options?)` performs the initial translation pass (the default `translate` wraps text as `[text]` so assertions are easy); `options` can simulate the nastier behaviors: `deleteTextNodes` (translation dropping nodes) and `moveToParentEnd` (word-order element moves). `startTranslateObserver(root)` keeps re-translating changed content, like the real thing.\n\nLike real Chrome, `translateSubtree` marks the document before touching any text — it adds `translated-ltr` and flips `lang` on `<html>` (the class is what activates the lazily-installed shim). Remember to reset those attributes between tests if your assertions depend on them.\n\n### Testing the out-of-world case\n\n`translateSubtree` mutates the DOM from your own JavaScript realm, which is fine for the Chrome-style flow but is **not** how a browser translator behaves — a real one runs in an isolated world and never calls the patched methods (see \"Why detection cannot simply hook the patched methods\"). A test that displaces text in-realm can arm the shim through a path no browser can reach, and will pass even if the shim would crash in production.\n\n`displaceFromIsolatedWorld(textNode, translatedText?, signature?)` reproduces that constraint faithfully: it performs the wrapper-in/original-out displacement using DOM natives captured at import time, so the patched methods never observe it. `signature` is `'google'` (nested `<font style=\"vertical-align: inherit;\">`) or `'edge'` (`_msttexthash`/`_msthash`, no `<html>` markers).\n\n```ts\nimport { displaceFromIsolatedWorld } from 'translation-resilience/simulator';\n\nit('survives Edge translating the page', () => {\n  const { container, rerender } = render(<Row show={false} />);\n  const text = findTextNode(container, 'trailing text');\n\n  displaceFromIsolatedWorld(text, 'nachlaufender Text', 'edge');\n\n  expect(() => rerender(<Row show />)).not.toThrow();\n});\n```\n\nImport the simulator before calling `installTranslationResilience()`, so the natives it captures are the unpatched ones (ES module imports are hoisted, so a normal top-level `import` already guarantees this).\n\nNote that jsdom runs no CSS animations, so the detection stylesheet never fires there; under jsdom these cases are caught by the repair-path sweep instead. The stylesheet's own behaviour is covered by `npm run test:browser`, which drives real headless Chrome and displaces text from a genuine isolated world via `Page.createIsolatedWorld`.\n\n## Compatibility and limitations\n\n- **Renderers**: developed and tested against React 18 (the tests render real components with `react-dom` and assert both crash-avoidance and update-visibility). React 19 benefits equally — its error-boundary teardown and the silent freeze both disappear. The patch layer is framework-agnostic, so Vue/Svelte/Ember apps should benefit too, but the test suite currently covers React.\n- **Translators**: built against Chrome's translation mutations (the merge/wrap/remove pattern, `<font>` wrappers). Covers both translators that mark `<html>` (Chrome's built-in Google Translate) and translators that only wrap text in `<font>` (Microsoft Edge's built-in translator, the Google Translate browser extension). A translator that mutates in some entirely different way simply isn't recognized — unrecognized mutations degrade to stock behavior, never worse than not having the shim (and `eager: true` widens coverage further).\n- **`event.target` inside translated text is a `<font>` element.** That's inherent to page translation, not this shim. React's synthetic event dispatch is unaffected (handlers fire on the right components with the right `currentTarget`), but avoid comparing `event.target` by identity or tag — use `closest()`/`contains()`.\n- **Word-order moves are cosmetically imperfect**: text the translator deleted for a word-order change is restored at the best position still known, which may differ from the translator's chosen ordering until re-translation catches up.\n- **Per-frame**: iframes have their own prototypes and documents; install the shim inside each frame that renders your app. (This is separate from the translator's isolated world, which shares the document and is handled.)\n\n## Demo\n\n`npm run build`, serve the repo root over HTTP (`npx serve .`), and open `/demo/`. Translate the page via Chrome's right-click → \"Translate\", or use the \"Simulate Google Translate\" button in browsers without it. `?shim=off` shows stock behavior: the toggles crash the app and the counter freezes.\n\n## License\n\nMIT © Alex Speller\n","readmeFilename":"README.md"}