Engineering notes · October 2026
Translating a web page in place without breaking it
A translation extension has two jobs that pull against each other: show the reader their language as fast as possible, and leave the page working exactly as its authors built it. Here is how Native Translate does both.
Write into the text the page already has
The usual approaches either replace a block’s content with new markup or insert a translated copy next to it. Both fight the site’s own code. A React or Vue app keeps references to the nodes it rendered; when an extension wraps or removes them, the next re-render calls removeChild on a node that has moved, and the app crashes. It is a long-standing issue with browser page translation (facebook/react#11538).
Native Translate never wraps, re-parents or removes a node the page owns. It finds the text runs inside each block, sends them as one unit to translate, and writes the result back into the existing text nodes by setting nodeValue. Inline elements such as links and bold text travel through the model as numbered placeholders (<a1>the Moon</a1>), and the response is accepted only if it comes back with the same placeholder tree. The link element stays the same element, with the same listeners and the same href; only its words change.
Every write is recorded with the value it replaced. That gives exact restoration (“Show original” puts back the page byte for byte) and lets the mutation observer tell our own writes apart from the page’s, so a framework re-rendering a node is translated again rather than fought over.
Translate what you can see, first
A long article can hold a thousand text units. Translating them in document order would make a reader wait for the navigation bar and the footer. A scheduler ranks every unit instead:
| Priority | What | When |
|---|---|---|
| P0 | In the viewport, the title | Sent within 50 ms, in small first batches |
| P1 | The next one and a half screens | After a 250 ms pause in scrolling |
| P2 | The rest of the main content | When the browser is idle, up to a budget |
| P3 | Everything else on long pages | Only when it scrolls near |
Within the viewport, readable text (a headline, a sentence) goes before short labels, which matters on news homepages that have no main-content landmark. Long paragraphs are cut into sentence pieces, and each piece is written into the page as the model streams it, so the first words appear while the rest of the paragraph is still being generated.
Draft first, then refine
The first pass optimises for time to first word. The article body then gets a second pass that sees the page title, the site and a short summary, plus a glossary of names already used, so a person or product keeps one spelling through a long read. The refined text replaces the draft in place, but never under the reader’s cursor or while a field has focus.
No model menu: a router instead
We benchmarked 25 models on Chinese-to-English page text. The fast tier and the refinement tier are different models, chosen for time to first token and for quality respectively, and bigger models did not score better for this task. A small gateway owns that decision: text in view goes to the fastest model, text below the fold to a cheaper one, and the article body to the refiner. Each model’s recent error rate and time to first token are tracked; a model that degrades is demoted for a cooldown and the next one takes over, even halfway through a batch. Changing the table is a server change, never an extension release.
A cache that cannot leak one person’s reading
Popular pages are read by many people, so a shared cache saves time and cost. But a cache of what people translate is also a record of what they read. Lookups are by hash only, the gateway never receives a URL, and a cached translation is served to anyone only after three different installs have asked for the same text. Rows are encrypted at rest, sensitive sites never write to it, and privacy mode turns it off entirely.
Testing against the real web
A breakage harness loads the built extension into Chromium and drives fixtures that are hostile on purpose: a React app with adjacent text nodes and conditional rendering, a single-page app that changes routes, an infinite feed. It fails on any thrown removeChild or insertBefore error, checks that links keep their elements and that “Show original” restores the exact HTML, and measures time to first paint. A second suite runs the production path against real sites in several languages.
Try it
Open any page in another language and press Alt+Shift+T. The first 20 pages need no account. If a site breaks, “Report broken site” in the popup comes straight to us.
Get Native Translate