From 9e6c1a95893394d6080541bd54b22933d3f5f10f Mon Sep 17 00:00:00 2001 From: Soxasora Date: Sun, 26 Apr 2026 15:30:56 +0200 Subject: [PATCH 1/3] patch DOM insertBefore and removeChild, workaround for DOM translators --- lib/patch-dom-translate.js | 70 ++++++++++++++++++++++++++++++++++++++ pages/_app.js | 3 ++ 2 files changed, 73 insertions(+) create mode 100644 lib/patch-dom-translate.js diff --git a/lib/patch-dom-translate.js b/lib/patch-dom-translate.js new file mode 100644 index 000000000..a952935f3 --- /dev/null +++ b/lib/patch-dom-translate.js @@ -0,0 +1,70 @@ +/* global Node */ + +let translateActive = false + +function detectDOMTranslate () { + const html = document.documentElement + if (!html) return false + + // classes used by DOM translators + if (html.classList.contains('translated-ltr')) return true + if (html.classList.contains('translated-rtl')) return true + if (html.classList.contains('translated')) return true + + return false +} + +/** + * workaround for DOM translators, facebook/react#11538 + * browser translation engines wrap text nodes in tags, + * which desyncs React's reconciler from the real DOM and throws + * `NotFoundError` on `removeChild` and `insertBefore` + * + * We only suppress these errors while a DOM translator is active + */ +export function patchDOMTranslations () { + if (typeof Node === 'undefined' || typeof window === 'undefined') return + if (Node.prototype.__patchedDOMTranslations) return + + Node.prototype.__patchedDOMTranslations = true + + const originalRemoveChild = Node.prototype.removeChild + Node.prototype.removeChild = function (child) { + if (child && child.parentNode !== this) { + if (translateActive) { + console.log('translate, removeChild', child) + return child + } + console.log('no translate, removeChild', child) + } + return originalRemoveChild.call(this, child) + } + + const originalInsertBefore = Node.prototype.insertBefore + Node.prototype.insertBefore = function (newNode, referenceNode) { + if (referenceNode && referenceNode.parentNode !== this) { + if (translateActive) { + console.log('translate, insertBefore', newNode, referenceNode) + return newNode + } + console.log('no translate, insertBefore', newNode, referenceNode) + } + return originalInsertBefore.call(this, newNode, referenceNode) + } + + const update = () => { translateActive = detectDOMTranslate() } + update() + + const observer = new window.MutationObserver(update) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class', 'lang'] + }) + + // window.addEventListener('DOMContentLoaded', update) + + // return () => { + // observer.disconnect() + // window.removeEventListener('DOMContentLoaded', update) + // } +} diff --git a/pages/_app.js b/pages/_app.js index e2c53f4de..c3f3227b5 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -24,9 +24,12 @@ import { HasNewNotesProvider } from '@/components/use-has-new-notes' import { WalletsProvider } from '@/wallets/client/hooks' import FaviconProvider from '@/components/favicon' import { CookiesProvider } from '@/components/use-cookie' +import { patchDOMTranslations } from '@/lib/patch-dom-translate' const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false }) +patchDOMTranslations() + NProgress.configure({ showSpinner: false }) From 77560fbcb7e84ade764dfbf404f93b7ab4359595 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Sun, 26 Apr 2026 15:55:33 +0200 Subject: [PATCH 2/3] cleanup --- lib/patch-dom-translate.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/patch-dom-translate.js b/lib/patch-dom-translate.js index a952935f3..12819caeb 100644 --- a/lib/patch-dom-translate.js +++ b/lib/patch-dom-translate.js @@ -32,10 +32,8 @@ export function patchDOMTranslations () { Node.prototype.removeChild = function (child) { if (child && child.parentNode !== this) { if (translateActive) { - console.log('translate, removeChild', child) return child } - console.log('no translate, removeChild', child) } return originalRemoveChild.call(this, child) } @@ -44,10 +42,8 @@ export function patchDOMTranslations () { Node.prototype.insertBefore = function (newNode, referenceNode) { if (referenceNode && referenceNode.parentNode !== this) { if (translateActive) { - console.log('translate, insertBefore', newNode, referenceNode) return newNode } - console.log('no translate, insertBefore', newNode, referenceNode) } return originalInsertBefore.call(this, newNode, referenceNode) } @@ -60,11 +56,4 @@ export function patchDOMTranslations () { attributes: true, attributeFilter: ['class', 'lang'] }) - - // window.addEventListener('DOMContentLoaded', update) - - // return () => { - // observer.disconnect() - // window.removeEventListener('DOMContentLoaded', update) - // } } From 502e817339bdd23ac37460e1b7f1ae46ce9a9c73 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Mon, 27 Apr 2026 14:44:05 +0200 Subject: [PATCH 3/3] fallback to appendChild --- lib/patch-dom-translate.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/patch-dom-translate.js b/lib/patch-dom-translate.js index 12819caeb..8a4acf709 100644 --- a/lib/patch-dom-translate.js +++ b/lib/patch-dom-translate.js @@ -29,6 +29,8 @@ export function patchDOMTranslations () { Node.prototype.__patchedDOMTranslations = true const originalRemoveChild = Node.prototype.removeChild + const originalInsertBefore = Node.prototype.insertBefore + Node.prototype.removeChild = function (child) { if (child && child.parentNode !== this) { if (translateActive) { @@ -38,11 +40,11 @@ export function patchDOMTranslations () { return originalRemoveChild.call(this, child) } - const originalInsertBefore = Node.prototype.insertBefore Node.prototype.insertBefore = function (newNode, referenceNode) { if (referenceNode && referenceNode.parentNode !== this) { if (translateActive) { - return newNode + // append so the node still ends up under the expected parent, React will correct on the next render + return this.appendChild(newNode) } } return originalInsertBefore.call(this, newNode, referenceNode)