Why Chrome's Translate feature breaks your React app (and what to do instead)

That 'Failed to execute removeChild on Node' crash in production? It's Chrome Translate rewriting the DOM under React's feet. Here's why it happens, how to stop the crash, and why browser translation was never localization in the first place.

6 min read← All articles

You ship a React app. Weeks later, an error starts showing up in Sentry from a handful of users you can't reproduce:

NotFoundError: Failed to execute 'removeChild' on 'Node':
The node to be removed is not a child of this node.

Sometimes it's insertBefore instead of removeChild. The stack trace points deep into React's reconciler, not your code. It only happens for some users. You can't trigger it locally. And it tends to cluster around users in countries where English isn't the first language.

That last clue is the tell. This is Chrome's built-in page translation fighting with React's virtual DOM — a crash that's widely reported and easy to misdiagnose.

What's actually happening

When Chrome decides a page is in a language different from the user's preference, it translates the page in place, in the browser, after your app has rendered. To do that, it walks the DOM and rewrites your text nodes — typically wrapping translated runs of text in <font> elements and swapping the original text node out.

React doesn't know any of this happened. React keeps its own virtual model of the DOM and assumes the real DOM still matches it. So the next time React tries to update or remove a node that Chrome quietly replaced, it reaches for a child that is no longer where React thinks it is — and the browser throws removeChild/insertBefore errors. In the worst cases, the component tree unmounts and the user sees a blank screen.

The long-running bug report is React issue #11538, filed in 2017, with more than 100 comments from engineers debugging exactly this in production. It is not a bug in your code, and it's not really a bug in React — it's two systems mutating the same DOM without coordinating.

The specific pattern that triggers it most often is a bare text node sibling to an element, where the text is dynamic:

// Fragile: "Hello, " and "!" are bare text nodes next to {name}.
// Chrome merges and rewraps them; React later can't find them.
<div>
  Hello, {name}!
</div>

When Chrome translates "Hello," it restructures those sibling nodes. When name changes and React re-renders, the reconciler trips over the rearranged children.

Stopping the crash (the band-aid)

There are two levels of fix. The first stops the bleeding.

1. Wrap dynamic text so there are no bare text-node siblings. Give React stable element boundaries Chrome won't merge across:

<div>
  <span>Hello, </span>
  <span>{name}</span>
  <span>!</span>
</div>

2. Mark subtrees as not-translatable. The HTML5 translate="no" attribute (and the notranslate class Google honors) tells the browser to leave a subtree alone. Apply it to the dynamic, React-managed parts:

<div translate="no">
  {/* React fully owns this subtree; the browser won't rewrite it */}
  <LiveDashboard />
</div>

You can also opt the whole document out with <html translate="no"> or <meta name="google" content="notranslate" /> — but that's the nuclear option, and it points at the deeper problem.

The band-aid reveals the real issue

Look at what those fixes actually do: they turn translation off. You've stopped the crash by ensuring your users don't get a translated page. Which means the original problem — your German, Japanese, or Brazilian users can't read your app — is still completely unsolved. You've just made it fail quietly instead of loudly.

This is the moment to step back and notice that Chrome Translate was never localization in the first place. Even when it doesn't crash your app, it's a fallback that breaks down in several ways that matter:

  • It only fires sometimes. Chrome's trigger is heuristic. Pages with mixed languages can confuse detection, and short screens — checkout, signup, error states — may not have enough text for confident detection, so the translate prompt may never appear.
  • Every browser does it differently. Safari, Edge, and Firefox have built-in translation too, each with its own language detection, its own prompts, and its own way of rewriting the page. You don't control which one your users have, whether they turn it on, or what it does to your DOM.
  • It doesn't exist in native apps. React Native, Flutter, Electron, a webview — there's no browser translate button to fall back on. The problem is still there, minus the workaround.
  • It mangles non-visible and dynamic text. placeholder, aria-label, alt, title, toast notifications rendered after the first pass — browser translation routinely misses them. Your user gets a translated page with an English "Enter your email" placeholder and an English "Invalid input" error.
  • The terminology is generic and inconsistent. The browser doesn't know your "Submit" means save the form in one place and send a payment in another. It has no glossary, so your brand terms get translated and your UI verbs drift between formal and informal across screens.
  • Search engines never see it. Browser translation happens after load, in the user's browser. Search engines index your English page. Users searching in Spanish find pages that are actually in Spanish, not your English one.

What to do instead

Real internationalization means your app renders correctly in each user's language regardless of their browser, device, or whether the browser offers to translate. Mechanically, that's three steps:

  1. Find every user-facing string — including the placeholders, aria-labels, and dynamically-generated messages browser translation chokes on.
  2. Wrap them with your framework's i18n calls (next-intl, react-i18next, etc.) and extract them into message files.
  3. Translate those message files — once, server-side, with context and a glossary — and ship them in your repo as plain locale JSON.

Now every user gets consistent, indexable copy in their language, and browser translation has no reason to touch your DOM. The translate="no" band-aid becomes unnecessary because you're no longer relying on the browser to paper over a gap.

The tedious part has always been steps 1 and 2 — finding the strings and wrapping them by hand across a real codebase. That's the part we built Polyglot to help with: polyglot scan walks your source with tree-sitter and reports hardcoded-string candidates (placeholders and aria-labels included), polyglot wrap applies a plan you've reviewed that rewrites them with t() calls and extracts the message files, and polyglot translate generates translations with context and your glossary, which you review in the dashboard before they're approved. Our June 2026 benchmark (AI-judged) documents how that pipeline scored, with the dataset published.

But the tool is secondary to the point: if you're reaching for translate="no" to stop a production crash, that's your signal to do i18n properly. Browser translation isn't a strategy — it's a thing that happens to your app when you haven't localized it.

Start in your terminal

Stop hunting for untranslated strings.

Install the CLI, run a scan, and see exactly what you're missing. Free, no account required.

$curl -fsSL https://getpolyglot.ai/install.sh | bash
Why Chrome's Translate feature breaks your React app (and what to do instead) - Polyglot Blog | Polyglot