<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chuyi</title>
    <description>The latest articles on DEV Community by Chuyi (@chuyi_ai).</description>
    <link>https://dev.to/chuyi_ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4225227%2F1a3ac7f4-b11d-428c-b4ad-abc02800409a.jpg</url>
      <title>DEV Community: Chuyi</title>
      <link>https://dev.to/chuyi_ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chuyi_ai"/>
    <language>en</language>
    <item>
      <title>Building a chat translator extension for WhatsApp Web: lessons from fighting a moving DOM</title>
      <dc:creator>Chuyi</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:21:01 +0000</pubDate>
      <link>https://dev.to/chuyi_ai/building-a-chat-translator-extension-for-whatsapp-web-lessons-from-fighting-a-moving-dom-36ho</link>
      <guid>https://dev.to/chuyi_ai/building-a-chat-translator-extension-for-whatsapp-web-lessons-from-fighting-a-moving-dom-36ho</guid>
      <description>&lt;p&gt;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 &lt;strong&gt;Chuyi&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;(Chuyi is independent and not affiliated with WhatsApp, Meta or Telegram.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The copy-paste problem
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;message text&lt;/em&gt;, leave the UI alone, and stay out of the send path.&lt;/p&gt;

&lt;p&gt;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: &lt;strong&gt;translate the draft, show a preview, and stop. You press Enter.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  MutationObserver + per-site adapters
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;querySelectorAll&lt;/code&gt; at load time misses almost everything that matters.&lt;/p&gt;

&lt;p&gt;The core loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attach a &lt;code&gt;MutationObserver&lt;/code&gt; to the chat pane (or a stable parent that survives re-renders).&lt;/li&gt;
&lt;li&gt;On each batch of mutations, walk added nodes and find message containers.&lt;/li&gt;
&lt;li&gt;Extract the plain text of the message body — not the whole bubble.&lt;/li&gt;
&lt;li&gt;Skip anything already marked as translated.&lt;/li&gt;
&lt;li&gt;Ask the translation engine, then inject a small block under the original.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So Chuyi uses &lt;strong&gt;per-site adapters&lt;/strong&gt;: a thin layer per host that answers questions the shared pipeline should not care about.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where is the scrollable chat list?&lt;/li&gt;
&lt;li&gt;What node is a message bubble?&lt;/li&gt;
&lt;li&gt;Which child is the primary body text?&lt;/li&gt;
&lt;li&gt;Which children are quotes, forwards, link previews, or system notices?&lt;/li&gt;
&lt;li&gt;Where is the compose box, and how do we read/write its value without breaking the page's own handlers?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A few practical lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer attributes and roles that survive redesigns over long class chains.&lt;/li&gt;
&lt;li&gt;Debounce mutation handling. Chat UIs fire a lot of noise (presence, typing indicators, image placeholders).&lt;/li&gt;
&lt;li&gt;Mark translated nodes so a soft re-render of the bubble does not re-request the same sentence forever.&lt;/li&gt;
&lt;li&gt;Treat "chat switched" as a first-class event: reattach to the new pane, or you will translate ghost nodes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Not re-translating quotes, forwards, and link previews
&lt;/h2&gt;

&lt;p&gt;The first version translated every text node it could find. That produced three failure modes users notice immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quoted replies&lt;/strong&gt; got translated again inside the new message, so a short reply looked like a wall of duplicated text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forwards&lt;/strong&gt; often carry a header plus the original body; translating the header looks broken, and translating the body twice wastes engine calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link previews&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is adapter-level classification before any engine call. For each candidate message, the adapter returns the body text &lt;em&gt;and&lt;/em&gt; regions to ignore. Quotes, forward labels, and preview cards are skipped. Only the primary body goes to the translator.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Free engines: Google Translate vs Chrome's Translator API (138+)
&lt;/h2&gt;

&lt;p&gt;Chuyi's free tier talks to translation engines &lt;strong&gt;from the browser&lt;/strong&gt;. Two paths:&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Translate (network)
&lt;/h3&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;h3&gt;
  
  
  Chrome's built-in Translator API (Chrome 138+)
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I deliberately do &lt;strong&gt;not&lt;/strong&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privacy choices: no server, no analytics
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So the free tier is shaped around constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Chuyi server in the translation path.&lt;/strong&gt; The browser talks to Google Translate, or to Chrome's on-device translator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No contact list, phone number, or chat-title scraping&lt;/strong&gt; for translation. The extension needs the message body and enough DOM context to place the translation — not an address book.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No analytics SDK&lt;/strong&gt; watching which chats you open. If I cannot measure something without reading private text, I do not measure it that way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it never sends messages
&lt;/h2&gt;

&lt;p&gt;Automation is tempting: translate, fill the box, dispatch &lt;code&gt;Enter&lt;/code&gt;, ship a demo GIF. I refused that path: &lt;strong&gt;sending is irreversible in a way that translating is not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Buyers remember a wrong price; landlords remember a wrong date. A mistranslated "cancel" is a conversation event, not a UI bug. Outgoing flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You type in your language (or theirs).&lt;/li&gt;
&lt;li&gt;You hit &lt;strong&gt;Alt+T&lt;/strong&gt; (or the preview control) to translate the draft in place.&lt;/li&gt;
&lt;li&gt;You read the preview. Undo if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You&lt;/strong&gt; press Enter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next + feedback ask
&lt;/h2&gt;

&lt;p&gt;Chuyi is a free Chrome extension, now on the &lt;a href="https://chromewebstore.google.com/detail/ddgapahiegjpoblofkeapnojkjcefphj" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;. Homepage: &lt;a href="https://translate.chuyi.ai" rel="noopener noreferrer"&gt;translate.chuyi.ai&lt;/a&gt;. Most of my testing is on WhatsApp Web; Telegram Web and Messenger are adapted — bug reports from daily users of those clients are welcome.&lt;/p&gt;

&lt;p&gt;Pro offers AI rewriting (casual / business / short), still with you pressing Enter. Chrome is the only browser store it's on today.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Thanks for reading. I'm the developer of Chuyi, so take my enthusiasm with a grain of salt, and tell me what's broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chuyi is independent and not affiliated with WhatsApp, Meta or Telegram.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>extensions</category>
    </item>
  </channel>
</rss>
