All posts

Fixing the Google Translate crash in React without turning translation off

Google Translate swaps a page's text for <font> tags, and React crashes on the nodes it lost. Why the usual patch leaves the page broken, and the fix we shipped.

On 3 November our error tracking caught 31 crash reports in under two and a half hours, all from two accounts in Brazil. The pages had nothing in common: first-run setup, a computer's page, the desktop viewer. The errors were always the same two:

NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Both accounts were in Brazil, and the pattern matched a well-known one: Google Translate.

What Google Translate does to a page

Google Translate does not edit text in place. For each text node, it inserts a <font> holding the translation just before it, then removes the original:

<!-- before -->
<span role="status"><svg></svg>Starting…</span>

<!-- after -->
<span role="status"><svg></svg><font><font>Iniciando…</font></font></span>

React still holds the original text node, which is now off the page. When the component next updates, React inserts beside that node or removes it, and the browser throws because the node is no longer where React put it. Our app does this all the time: a button gains a spinner, a loading message gives way to the page, a status changes from "Starting" to "Connected".

We wanted proof, not a likely story. Google's website translation script can be loaded into any page, so we ran it in headless Chromium against the same React and Radix versions we ship. It crashed all four sites with the errors from production.

The usual fix leaves the page broken

The well-known workaround comes from React issue #11538: patch removeChild and insertBefore to skip the call when the node has moved. That stops the crash. Under the real translator, it also left our pages wrong:

Skip patchWhat we shipped
Desktop statusStuck on "Starting" after it connectedShows the current status
A computer's page"Loading your computer" stays above the loaded pageRemoved
ButtonsThe spinner never appearsAppears before the translated label

A skipped removal leaves the translated text behind. A skipped insertion drops the element. And React's text updates still go to the detached node, which nobody can see.

Turning translation off with translate="no" would also stop the crash, but anyone reading BoxCompute in another language relies on it.

Let the font stand in for the text

Recording the translator's DOM changes showed it works the same way every time: the <font> goes in immediately before the text, then the text comes out. So the mutation record that removes a text node names its replacement as previousSibling. We keep that pair in a WeakMap, and the font acts for the text:

Node.prototype.removeChild = function (child) {
  if (child.parentNode === this) return removeChild.call(this, child)
  const standIn = standIns.get(child)
  if (standIn?.parentNode === this) removeChild.call(this, standIn)
  return child
}

Inserting before the text inserts before its font, and removing the text removes its font. Changing the text puts the original node back with its new value, so a status is always current, though it can show in English until something translates it again.

None of this is installed until the page is translated. In our recordings, Google set class="translated-ltr" and the new lang on <html> about 190 ms before it swapped any text, and a small observer waits for that. An untranslated page keeps the browser's own DOM methods, so a real bug still crashes and still reaches our error tracking.

What we have not tested

  • Chrome's built-in Translate menu cannot be driven from a headless browser, so we tested with Google's website translation script. If the built-in menu marks the page after it starts swapping text, pages still do not crash, but they can show the leftover text from the table above.
  • Other translators, such as Edge's, are untested.

The fix shipped in BoxCompute 0.153.2 the same morning. A browser test replays the translator's recorded sequence against our real components, and it fails both without the fix and with the skip patch.

Back to the blog