Building a chat translator extension for WhatsApp Web: lessons from fighting a moving DOM
Web chat apps re-render constantly, class names change without notice, and a quoted message looks a lot like a new one. Here's what I learned building Chuyi, a small extension that puts translations under chat messages i
Web chat apps re-render constantly, class names change without notice, and a quoted message looks a lot like a new one. Here's what I learned building Chuyi, a small extension that puts translations under chat messages in WhatsApp Web, Telegram Web and Messenger — and lets you review your translated reply before you press Enter yourself.
(Chuyi is independent and not affiliated with WhatsApp, Meta or Telegram.)
The copy-paste problem
I kept watching people who sell online do the same dance: copy a buyer message, switch to a translator tab, paste, read, switch back, type a reply, translate that too, paste back, hit Enter. Do that across a busy afternoon and the rhythm of the conversation is gone.
Chrome's built-in "Translate this page" almost works — until it also translates the compose box, the sidebar, and every button label. A chat translator needs to touch message text, leave the UI alone, and stay out of the send path.
That last point mattered. If an extension fires Enter for you, one bad translation can go out as a wrong price or date. Chuyi's rule from day one: translate the draft, show a preview, and stop. You press Enter.
MutationObserver + per-site adapters
WhatsApp Web, Telegram Web, and Messenger are single-page apps. Messages appear, vanish on scroll, get replaced when a quote or reaction updates, and remount when you switch chats. A one-shot querySelectorAll at load time misses almost everything that matters.
The core loop:
- Attach a
MutationObserverto the chat pane (or a stable parent that survives re-renders). - On each batch of mutations, walk added nodes and find message containers.
- Extract the plain text of the message body — not the whole bubble.
- Skip anything already marked as translated.
- Ask the translation engine, then inject a small block under the original.
The hard part is not the observer — it is that each site structures messages differently. WhatsApp nests quotes, forward labels, and link previews in the same bubble; Telegram nests differently; Messenger changes often enough that brittle CSS selectors die quietly.
So Chuyi uses per-site adapters: a thin layer per host that answers questions the shared pipeline should not care about.
- Where is the scrollable chat list?
- What node is a message bubble?
- Which child is the primary body text?
- Which children are quotes, forwards, link previews, or system notices?
- Where is the compose box, and how do we read/write its value without breaking the page's own handlers?
Everything above the adapter — observer scheduling, dedupe, engine selection, rendering under the bubble — stays shared. WhatsApp renames a class? Fix one adapter. Telegram moves the compose box? Fix another.
A few practical lessons:
- Prefer attributes and roles that survive redesigns over long class chains.
- Debounce mutation handling. Chat UIs fire a lot of noise (presence, typing indicators, image placeholders).
- Mark translated nodes so a soft re-render of the bubble does not re-request the same sentence forever.
- Treat "chat switched" as a first-class event: reattach to the new pane, or you will translate ghost nodes.
Not re-translating quotes, forwards, and link previews
The first version translated every text node it could find. That produced three failure modes users notice immediately:
- Quoted replies got translated again inside the new message, so a short reply looked like a wall of duplicated text.
- Forwards often carry a header plus the original body; translating the header looks broken, and translating the body twice wastes engine calls.
- Link previews pull title/description from a URL. Those strings are not part of what the person typed, and translating them can invent meaning that was never in the chat.
The fix is adapter-level classification before any engine call. For each candidate message, the adapter returns the body text and regions to ignore. Quotes, forward labels, and preview cards are skipped. Only the primary body goes to the translator.
That means reading the DOM the way a human reads the bubble: indented block is context; larger block is what they just said. Getting that wrong is worse than translating nothing — the UI looks busy and untrustworthy.
A related trap: scrolling up remounts older messages that look "new" to the observer. Deduping by a stable message id (when available) or a hash of chat context plus body text avoids hammering the engine on every scroll.
Free engines: Google Translate vs Chrome's Translator API (138+)
Chuyi's free tier talks to translation engines from the browser. Two paths:
Google Translate (network)
Send the text from the extension to Google Translate and render the result. Pros: broad language coverage, familiar chat quality, works when on-device APIs are missing. Cons: text leaves the machine; network latency; partial failures must fail soft (show nothing rather than a half-wrong line under a price quote).
Chrome's built-in Translator API (Chrome 138+)
Newer Chromium builds expose an on-device translator. When available, text can stay on the device. Pros: stronger privacy, offline once language packs are present, no round trip for short messages. Cons: depends on browser version and packs; pairs differ from the cloud service; you still need a fallback when the API or pair is missing.
Chuyi treats engine choice as a setting with automatic fallback: prefer on-device when the user wants it and the pair is supported; otherwise Google Translate from the browser. Neither path routes chat text through Chuyi's servers.
I deliberately do not add a silent "AI rewrite" hop on the free tier. Pro offers tone controls (casual / business / short) on its separate AI translation path, with the same rule: the user reviews the draft and presses Enter.
Privacy choices: no server, no analytics
A chat translator sits next to some of the most sensitive text a person types all day. If that text goes through my backend "for convenience," I inherit a data problem I do not want — so I refused that design.
So the free tier is shaped around constraints:
- No Chuyi server in the translation path. The browser talks to Google Translate, or to Chrome's on-device translator.
- No contact list, phone number, or chat-title scraping for translation. The extension needs the message body and enough DOM context to place the translation — not an address book.
- No analytics SDK watching which chats you open. If I cannot measure something without reading private text, I do not measure it that way.
This is stricter than many assistant-style extensions, and it shows up as missing dashboards on my side. Trust is the product for a tool that lives inside WhatsApp Web.
Why it never sends messages
Automation is tempting: translate, fill the box, dispatch Enter, ship a demo GIF. I refused that path: sending is irreversible in a way that translating is not.
Buyers remember a wrong price; landlords remember a wrong date. A mistranslated "cancel" is a conversation event, not a UI bug. Outgoing flow:
- You type in your language (or theirs).
- You hit Alt+T (or the preview control) to translate the draft in place.
- You read the preview. Undo if needed.
- You press Enter.
The extension never owns the send key. Responsibility stays with the user, store review is happier, and money-sensitive chats stay "look once, then send."
It also simplifies the adapter: no need to synthesize trusted input events across each site's framework. Reading and writing the compose value, plus a preview surface, is enough.
What's next + feedback ask
Chuyi is a free Chrome extension, now on the Chrome Web Store. Homepage: translate.chuyi.ai. Most of my testing is on WhatsApp Web; Telegram Web and Messenger are adapted — bug reports from daily users of those clients are welcome.
Pro offers AI rewriting (casual / business / short), still with you pressing Enter. Chrome is the only browser store it's on today.
If you build content-script tools against moving chat DOMs, tell me how you handle quote detection and compose-box writes. If you sell across languages and try Chuyi, tell me where under-bubble placement feels wrong — that is taste as much as engineering.
Thanks for reading. I'm the developer of Chuyi, so take my enthusiasm with a grain of salt, and tell me what's broken.
Chuyi is independent and not affiliated with WhatsApp, Meta or Telegram.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.