fix edge translation bug (#6158)

This commit is contained in:
Anthony Stirling
2026-04-22 16:31:43 +01:00
committed by GitHub
parent d71a2c3d81
commit 1d5b47fa9b
2 changed files with 104 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
// Browser page translators (Edge, Chrome, extensions) wrap text nodes in
// injected <font> elements, reparenting nodes React is holding. React's commit
// phase then throws NotFoundError on removeChild/insertBefore and the
// ErrorBoundary unmounts the app. https://github.com/facebook/react/issues/11538
//
// We watch for translator fingerprints (Google Translate's translated-* class
// on <html>, or any injected <font>) and install guards on Node.prototype only
// once one appears, so native DOM semantics are preserved when no translator
// is active.
declare global {
interface Node {
__stirlingTranslatorPatched?: boolean;
}
}
let patchApplied = false;
function isGoogleTranslateActive(): boolean {
const cls = document.documentElement.classList;
return cls.contains("translated-ltr") || cls.contains("translated-rtl");
}
function applyDomPatch(trigger: string): void {
if (patchApplied) return;
if (typeof Node === "undefined" || !Node.prototype) return;
if (Node.prototype.__stirlingTranslatorPatched) return;
patchApplied = true;
Node.prototype.__stirlingTranslatorPatched = true;
console.warn(
`[dom-patch] Browser page translator detected (${trigger}). ` +
"Installing removeChild/insertBefore guards to prevent React crashes. " +
"The UI may show minor glitches while the translator is active.",
);
const originalRemoveChild = Node.prototype.removeChild;
Node.prototype.removeChild = function patchedRemoveChild<T extends Node>(
this: Node,
child: T,
): T {
if (child.parentNode !== this) return child;
return originalRemoveChild.call(this, child) as T;
} as typeof Node.prototype.removeChild;
const originalInsertBefore = Node.prototype.insertBefore;
Node.prototype.insertBefore = function patchedInsertBefore<T extends Node>(
this: Node,
newNode: T,
referenceNode: Node | null,
): T {
if (referenceNode && referenceNode.parentNode !== this) return newNode;
return originalInsertBefore.call(this, newNode, referenceNode) as T;
} as typeof Node.prototype.insertBefore;
}
export function armTranslatorDetector(): void {
if (typeof window === "undefined" || typeof MutationObserver === "undefined")
return;
if (typeof document === "undefined" || !document.documentElement) return;
// Edge case: class already set (e.g., bfcache restore).
if (isGoogleTranslateActive()) {
applyDomPatch("html class was already translated-* on arm");
return;
}
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (
m.type === "attributes" &&
m.target === document.documentElement &&
isGoogleTranslateActive()
) {
applyDomPatch("<html> translated-* class appeared");
observer.disconnect();
return;
}
if (m.type === "childList") {
for (const n of m.addedNodes) {
if (n.nodeName === "FONT") {
applyDomPatch("<font> element injected into DOM");
observer.disconnect();
return;
}
}
}
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
childList: true,
subtree: true,
});
}
armTranslatorDetector();

View File

@@ -1,3 +1,8 @@
// Must be imported before React so the DOM-prototype patch is installed
// before React's commit phase runs. Prevents browser page translators
// (Edge / Google Translate / extensions) from crashing the app via
// parent-mismatch DOMExceptions. See the module for details.
import "@app/utils/patchDomForTranslators";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "../vite-env.d.ts"; // eslint-disable-line no-restricted-imports -- Outside app paths