<?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: Fix English</title>
    <description>The latest articles on DEV Community by Fix English (@fixenglish).</description>
    <link>https://dev.to/fixenglish</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%2F4207135%2F6098ebd5-f512-4e40-b888-64f0e26f99fb.png</url>
      <title>DEV Community: Fix English</title>
      <link>https://dev.to/fixenglish</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fixenglish"/>
    <language>en</language>
    <item>
      <title>The hardest part of our Chrome extension wasn't AI — it was replacing text safely</title>
      <dc:creator>Fix English</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:14:00 +0000</pubDate>
      <link>https://dev.to/fixenglish/the-hardest-part-of-our-chrome-extension-wasnt-ai-it-was-replacing-text-safely-j62</link>
      <guid>https://dev.to/fixenglish/the-hardest-part-of-our-chrome-extension-wasnt-ai-it-was-replacing-text-safely-j62</guid>
      <description>&lt;p&gt;We built a Chrome extension that fixes the English in selected text and puts the correction back in the same place.&lt;/p&gt;

&lt;p&gt;Sounds simple, right?&lt;/p&gt;

&lt;p&gt;Select a sentence in Gmail, press a shortcut, get corrected text, replace the selection. The AI call is straightforward.&lt;/p&gt;

&lt;p&gt;The hard part is everything that happens afterward.&lt;/p&gt;

&lt;p&gt;Gmail might wrap a misspelled word in a temporary span. Jira might rebuild the paragraph while we're waiting for the model. Slack might insert an extra space. Outlook might let us undo a correction, then silently put it back a moment later.&lt;/p&gt;

&lt;p&gt;And if we get the replacement wrong, we could delete someone's formatting, break a mention, or insert corrected text next to the original instead of replacing it.&lt;/p&gt;

&lt;p&gt;We ran into these problems while building Fix English, a Chrome extension that corrects selected text with minimal changes.&lt;/p&gt;

&lt;p&gt;Here's what we learned about safely editing text in Slack, Jira, Confluence, Gmail, GitHub, Teams, Outlook, and other web apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Plain text fields are easy. Until they're not.
&lt;/h2&gt;

&lt;p&gt;Let's start with a regular input or textarea.&lt;/p&gt;

&lt;p&gt;The obvious solution is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;correctedText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, that isn't a great way to edit someone else's application.&lt;/p&gt;

&lt;p&gt;Framework-controlled inputs might not recognize the update as expected. More importantly, directly assigning &lt;code&gt;.value&lt;/code&gt; doesn't preserve the browser's native undo history.&lt;/p&gt;

&lt;p&gt;We wanted users to be able to press Ctrl/Cmd+Z after a correction.&lt;/p&gt;

&lt;p&gt;Our approach uses the browser's normal editing path whenever possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dispatch a cancelable &lt;code&gt;beforeinput&lt;/code&gt; event with &lt;code&gt;inputType: 'insertReplacementText'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the application cancels that event, don't force the replacement.&lt;/li&gt;
&lt;li&gt;Restore the selection using &lt;code&gt;setSelectionRange()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Try &lt;code&gt;document.execCommand('insertText', false, text)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If that doesn't change the field, fall back to &lt;code&gt;setRangeText()&lt;/code&gt; and dispatch an &lt;code&gt;input&lt;/code&gt; event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our actual implementation also validates the selected text before writing and checks the result afterward.&lt;/p&gt;

&lt;p&gt;Yes, &lt;code&gt;execCommand()&lt;/code&gt; is deprecated.&lt;/p&gt;

&lt;p&gt;But it has one advantage that's difficult to replace: edits made through it can preserve the browser's undo history. Even &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand" rel="noopener noreferrer"&gt;MDN's documentation&lt;/a&gt; acknowledges this use case.&lt;/p&gt;

&lt;p&gt;One important detail: &lt;code&gt;execCommand()&lt;/code&gt; doesn't consistently fire &lt;code&gt;beforeinput&lt;/code&gt; across browsers and configurations. We dispatch it explicitly so applications have an opportunity to handle or cancel the proposed edit.&lt;/p&gt;

&lt;p&gt;That isn't a universal permission mechanism â€” synthetic events aren't trusted user input, and applications may ignore them â€” but respecting cancellation is better than blindly overwriting a field.&lt;/p&gt;

&lt;p&gt;We use the same general approach for React-controlled text fields. So far, we haven't needed a separate React-specific write mechanism.&lt;/p&gt;

&lt;p&gt;The fallback is less ideal for native undo, which is why we prefer &lt;code&gt;insertText&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Rich editors don't consider the DOM their source of truth
&lt;/h2&gt;

&lt;p&gt;Now things get interesting.&lt;/p&gt;

&lt;p&gt;A regular &lt;code&gt;contenteditable&lt;/code&gt; element looks like an editable DOM tree. But modern rich-text editors often maintain their own internal document model.&lt;/p&gt;

&lt;p&gt;The DOM is just a rendered representation of that model.&lt;/p&gt;

&lt;p&gt;If you directly change its text nodes or &lt;code&gt;innerHTML&lt;/code&gt;, the editor might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Re-render and undo your change.&lt;/li&gt;
&lt;li&gt;Lose track of its selection.&lt;/li&gt;
&lt;li&gt;Remove formatting or embedded content.&lt;/li&gt;
&lt;li&gt;Accept the visual change without updating the document it eventually saves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We needed to interact with the editor's own editing pipeline rather than treating its DOM as a text file.&lt;/p&gt;

&lt;h3&gt;
  
  
  One generic strategy, several editor families
&lt;/h3&gt;

&lt;p&gt;We identify common editor families through DOM markers, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ProseMirror&lt;/li&gt;
&lt;li&gt;Lexical&lt;/li&gt;
&lt;li&gt;CKEditor 5&lt;/li&gt;
&lt;li&gt;Draft.js&lt;/li&gt;
&lt;li&gt;Native contenteditable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the write logic is shared. The detected editor family changes the order in which we try different methods.&lt;/p&gt;

&lt;p&gt;The right order depends on the editor type.&lt;/p&gt;

&lt;p&gt;CKEditor can restore its internal model over DOM changes it didn't initiate. In our Teams web testing, using its input handling path worked better than forcing &lt;code&gt;execCommand()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synthetic paste is not the same as pasting
&lt;/h3&gt;

&lt;p&gt;We construct a &lt;code&gt;ClipboardEvent&lt;/code&gt; using a &lt;code&gt;DataTransfer&lt;/code&gt; object containing the correction.&lt;/p&gt;

&lt;p&gt;This doesn't modify the user's real clipboard.&lt;/p&gt;

&lt;p&gt;There's an important distinction, though: dispatching a synthetic paste event doesn't make the browser insert text automatically. The editor must actually handle the event.&lt;/p&gt;

&lt;p&gt;That's also documented in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event" rel="noopener noreferrer"&gt;MDN's paste event reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We treat &lt;code&gt;event.defaultPrevented&lt;/code&gt; as a signal that the editor may have claimed the event, not proof that the replacement succeeded.&lt;/p&gt;

&lt;p&gt;After dispatching, we verify the resulting text.&lt;/p&gt;

&lt;p&gt;If the editor handled the event but produced no acceptable change, we don't blindly retry a different insertion method. That could duplicate text or fight the editor's internal state.&lt;/p&gt;

&lt;h3&gt;
  
  
  A ProseMirror surprise
&lt;/h3&gt;

&lt;p&gt;In Jira and Confluence, inserting HTML through a paste event initially created a new paragraph instead of replacing text inside the existing one.&lt;/p&gt;

&lt;p&gt;Adding the editor's own slice metadata to the clipboard data solved it.&lt;/p&gt;

&lt;p&gt;Other editors needed different handling.&lt;/p&gt;

&lt;p&gt;In CKEditor, line breaks inside a paragraph can map to &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; elements, while blank lines may need paragraph elements.&lt;/p&gt;

&lt;p&gt;Gmail introduced another challenge: temporary spelling and grammar spans. A word we're correcting can cross one of those spans. We added narrowly scoped handling for the observed Gmail proofreading markers rather than treating arbitrary formatting as disposable.&lt;/p&gt;

&lt;p&gt;Slack occasionally normalizes a replacement by adding a trailing space. Our first verification logic interpreted that as a failed edit.&lt;/p&gt;

&lt;p&gt;The lesson: matching the corrected text isn't always enough. You also need to understand how the host editor represents it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. DOM Ranges can become stale without warning
&lt;/h2&gt;

&lt;p&gt;This was one of the most frustrating bugs.&lt;/p&gt;

&lt;p&gt;Imagine a user selects:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I has a meeting tomorow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They press our shortcut.&lt;/p&gt;

&lt;p&gt;We save the selection, send the text for correction, and receive:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have a meeting tomorrow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But while the correction is running, the editor re-renders.&lt;/p&gt;

&lt;p&gt;The original DOM nodes may no longer exist.&lt;/p&gt;

&lt;p&gt;A saved &lt;code&gt;Range&lt;/code&gt; can become detached, collapsed, or otherwise no longer represent the original selection. The dangerous part is that it doesn't necessarily throw an error.&lt;/p&gt;

&lt;p&gt;You might insert the corrected sentence without deleting the original.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we do instead
&lt;/h3&gt;

&lt;p&gt;At capture time, we store the original selected text and its character offsets within the editor.&lt;/p&gt;

&lt;p&gt;Before writing, we resolve the selection again.&lt;/p&gt;

&lt;p&gt;We try to locate it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use the original live Range if it's still connected and still contains the expected text.&lt;/li&gt;
&lt;li&gt;Check the original character offsets against the current content.&lt;/li&gt;
&lt;li&gt;If the text has moved, search for an exact, unique occurrence of the original selection.&lt;/li&gt;
&lt;li&gt;If no unambiguous match exists, stop and show a correction preview.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third step matters.&lt;/p&gt;

&lt;p&gt;Suppose someone adds a sentence before the selected text during the correction. The original offsets are now wrong, even if the selection itself hasn't changed.&lt;/p&gt;

&lt;p&gt;A unique text match can let us recover safely.&lt;/p&gt;

&lt;p&gt;But if the same selected sentence appears twice, we refuse to guess which one the user intended.&lt;/p&gt;

&lt;p&gt;We then reconstruct a fresh Range from the resolved offsets, synchronize the selection with the editor, and verify the expected text again before writing.&lt;/p&gt;

&lt;p&gt;For editors that maintain their own selection state, even setting the browser selection isn't always enough. We may need to wait for selection synchronization and confirm it hasn't been overwritten by the framework.&lt;/p&gt;

&lt;p&gt;This is more work than saving &lt;code&gt;selection.getRangeAt(0)&lt;/code&gt; and calling it a day.&lt;/p&gt;

&lt;p&gt;It's also the difference between replacing the intended text and modifying the wrong part of someone's document.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Don't replace an entire paragraph to fix one word
&lt;/h2&gt;

&lt;p&gt;Another problem came from the model output itself.&lt;/p&gt;

&lt;p&gt;AI models love to clean up formatting, even when you explicitly tell them not to.&lt;/p&gt;

&lt;p&gt;They might collapse repeated spaces, normalize non-breaking spaces, remove tabs, or reflow line breaks.&lt;/p&gt;

&lt;p&gt;Consider this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello    John,

I  will send the report
    tomorrow.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the correction only needs to change a misspelled word, we don't want to rebuild that layout.&lt;/p&gt;

&lt;p&gt;And in a rich editor, replacing a whole paragraph might also remove a link, a mention, or inline formatting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our approach: diff the words, preserve the layout
&lt;/h3&gt;

&lt;p&gt;We tokenize the original text and corrected text, then use a longest-common-subsequence (LCS) diff to identify unchanged words.&lt;/p&gt;

&lt;p&gt;Those unchanged words become anchors.&lt;/p&gt;

&lt;p&gt;Instead of applying the entire model response, we build a correction plan against the original text.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I has a meting tomorrow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Model output&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have a meeting tomorrow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Our replacement plan&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;has     â†’ have
meting  â†’ meeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The surrounding whitespace and punctuation stay where they were.&lt;/p&gt;

&lt;p&gt;We apply mapped edits from the highest character offset to the lowest, so an earlier edit doesn't invalidate the offsets of later edits.&lt;/p&gt;

&lt;p&gt;We also avoid merging edits across protected separators such as tabs, repeated spaces, non-breaking spaces, and line breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatting makes this harder
&lt;/h3&gt;

&lt;p&gt;A changed word might span multiple text nodes.&lt;/p&gt;

&lt;p&gt;Sometimes that's harmless, like a Gmail spelling annotation.&lt;/p&gt;

&lt;p&gt;Sometimes those nodes represent an actual formatting boundary, link, or embedded object.&lt;/p&gt;

&lt;p&gt;We distinguish between those cases instead of assuming every text-node boundary is equivalent.&lt;/p&gt;

&lt;p&gt;Our preferred path is to modify only the smallest safe text regions.&lt;/p&gt;

&lt;p&gt;The extension also has a broader whole-selection fallback when targeted edits can't be applied. That improves compatibility, but it's a tradeoff: verifying the resulting text alone cannot guarantee every piece of rich formatting survived.&lt;/p&gt;

&lt;p&gt;That's one of the areas we're continuing to tighten.&lt;/p&gt;

&lt;p&gt;For selections we can't safely resolve or that contain unsupported structures, we show a correction preview instead of guessing.&lt;/p&gt;

&lt;p&gt;A preview is an extra step for the user.&lt;/p&gt;

&lt;p&gt;Silently damaging their message is much worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A successful write isn't necessarily a successful edit
&lt;/h2&gt;

&lt;p&gt;We originally thought we could check the DOM immediately after inserting text and call the operation successful.&lt;/p&gt;

&lt;p&gt;That turned out to be optimistic.&lt;/p&gt;

&lt;p&gt;An editor might update its DOM synchronously, then reconcile its internal model on the next task.&lt;/p&gt;

&lt;p&gt;For a moment, the correction appears.&lt;/p&gt;

&lt;p&gt;Then the original text comes back.&lt;/p&gt;

&lt;p&gt;Our write pipeline therefore distinguishes between three outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OK:&lt;/strong&gt; The resulting text matches what we expected, including after a short settling period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unchanged:&lt;/strong&gt; The editor didn't produce an acceptable change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different:&lt;/strong&gt; The editor changed the document in a way we didn't expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those outcomes matter.&lt;/p&gt;

&lt;p&gt;If nothing changed and the editor didn't claim the event, trying another write method may be reasonable.&lt;/p&gt;

&lt;p&gt;If the editor handled the event, or changed the content differently than expected, blindly trying again is dangerous.&lt;/p&gt;

&lt;p&gt;For multi-edit corrections, we also track which individual edits succeeded. If a later edit fails, we attempt to roll back the changes already applied.&lt;/p&gt;

&lt;p&gt;Even rollback needs verification.&lt;/p&gt;

&lt;p&gt;We don't want to turn a partially successful correction into a corrupted draft.&lt;/p&gt;

&lt;p&gt;And if we can't confidently restore the original state, we tell the user to review the editor rather than claiming everything is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Undo is harder than the original correction
&lt;/h2&gt;

&lt;p&gt;Native text fields have a useful advantage: &lt;code&gt;execCommand('insertText')&lt;/code&gt; can participate in the browser's undo history.&lt;/p&gt;

&lt;p&gt;Rich editors are more complicated.&lt;/p&gt;

&lt;p&gt;They might maintain their own undo stack, group transactions differently, or re-render from internal state.&lt;/p&gt;

&lt;p&gt;So Fix English also provides an Undo button in its confirmation toast.&lt;/p&gt;

&lt;p&gt;That sounds straightforward: save the original text, and put it back if the user clicks Undo.&lt;/p&gt;

&lt;p&gt;In practice, we needed several rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Only undo our own edit
&lt;/h3&gt;

&lt;p&gt;Before restoring anything, we verify that the editor still contains the correction we applied.&lt;/p&gt;

&lt;p&gt;If the user has since changed the content, we don't overwrite those changes with an old snapshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignore the editor's own cleanup
&lt;/h3&gt;

&lt;p&gt;Some editors normalize their DOM after insertion.&lt;/p&gt;

&lt;p&gt;We don't want that internal activity to make the Undo button disappear.&lt;/p&gt;

&lt;p&gt;Our listener distinguishes trusted user activity from the synthetic events involved in our own operation. It also checks whether an observed change is simply the editor normalizing the same corrected text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Outlook web gave us a particularly strange bug
&lt;/h3&gt;

&lt;p&gt;We clicked Undo.&lt;/p&gt;

&lt;p&gt;The original text came back.&lt;/p&gt;

&lt;p&gt;Everything looked correct.&lt;/p&gt;

&lt;p&gt;Then, a moment later, the corrected text reappeared.&lt;/p&gt;

&lt;p&gt;Outlook's editor had restored its own model snapshot over our Undo operation.&lt;/p&gt;

&lt;p&gt;The workaround wasn't particularly elegant.&lt;/p&gt;

&lt;p&gt;After restoring, we verify the result again after a short delay. If the editor overwrote our restoration, we attempt to restore the original again and verify after another short delay.&lt;/p&gt;

&lt;p&gt;This isn't meant to be an infinite fight with the editor. The retry is bounded, and we verify whether the state still matches what we're trying to restore.&lt;/p&gt;

&lt;p&gt;If we can't confidently finish, Undo reports a failure instead of pretending it worked.&lt;/p&gt;

&lt;p&gt;The broader lesson was that Undo is another editor transaction, not just a reverse string operation.&lt;/p&gt;

&lt;p&gt;And it needs the same validation as the original edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The right frame matters too
&lt;/h2&gt;

&lt;p&gt;Some web applications place editable content inside iframes.&lt;/p&gt;

&lt;p&gt;An earlier version of our extension occasionally required two shortcut presses in Teams and Jira.&lt;/p&gt;

&lt;p&gt;The first press didn't reach the frame holding the active selection.&lt;/p&gt;

&lt;p&gt;We changed the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspect the current selection.&lt;/li&gt;
&lt;li&gt;Inject the content script into accessible frames when needed.&lt;/li&gt;
&lt;li&gt;Ask the frames which one actually contains an editable selection.&lt;/li&gt;
&lt;li&gt;Re-read the selection before starting the correction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important part is not assuming the top-level document owns the text the user selected.&lt;/p&gt;

&lt;p&gt;Of course, Chrome extension permissions still apply. We don't attempt to bypass inaccessible cross-origin frames, protected browser pages, password fields, or read-only content.&lt;/p&gt;

&lt;p&gt;And this is a browser extension. It doesn't operate inside native desktop applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Knowing when not to write is a feature
&lt;/h2&gt;

&lt;p&gt;After working through all these issues, we ended up with a few principles that guide the implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The editor owns its text.&lt;/strong&gt; We should use its editing mechanisms whenever possible, rather than modifying the DOM directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selections expire.&lt;/strong&gt; A selection captured before an asynchronous operation must be validated again afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text equality isn't structural equality.&lt;/strong&gt; The words might look correct while formatting or editor state has changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries need evidence.&lt;/strong&gt; If an editor accepted an event or changed its content unexpectedly, another blind write can make the situation worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Undo deserves the same safety checks as insertion.&lt;/strong&gt; Restoring an old snapshot shouldn't destroy a newer user edit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A preview is a valid result.&lt;/strong&gt; Sometimes the safest automatic edit is no automatic edit.&lt;/p&gt;

&lt;p&gt;We're still improving compatibility.&lt;/p&gt;

&lt;p&gt;Multi-paragraph corrections in Gmail can still stop partway. Confluence sometimes falls back to a preview. Complex formatting remains a challenging area, particularly when an editor doesn't expose a reliable external editing API.&lt;/p&gt;

&lt;p&gt;And that's okay. We'd rather make these limitations visible than claim every editor behaves the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;When we started building Fix English, we expected prompt quality and model latency to be the hardest problems.&lt;/p&gt;

&lt;p&gt;Instead, we spent a surprising amount of time understanding browser editing behavior, DOM selections, framework state, clipboard events, whitespace preservation, and undo.&lt;/p&gt;

&lt;p&gt;The AI generates the correction.&lt;/p&gt;

&lt;p&gt;But the surrounding application decides whether that correction can safely become part of the user's document.&lt;/p&gt;

&lt;p&gt;If you're building a grammar tool, translation extension, AI writing assistant, or anything else that replaces text inside third-party web applications, I'd strongly recommend treating text replacement as its own engineering problem.&lt;/p&gt;

&lt;p&gt;Because generating the right words is only half the job.&lt;/p&gt;

&lt;p&gt;Putting them back without breaking anything is the other half.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; We built &lt;a href="https://chromewebstore.google.com/detail/fix-english-grammar-fixer/ohbbfibfkgjgajgffnfkifghjadoleic?utm_source=devto" rel="noopener noreferrer"&gt;Fix English&lt;/a&gt;, the Chrome extension discussed in this article.&lt;/p&gt;

&lt;p&gt;It's free, requires no account, and focuses on minimal grammar and spelling corrections rather than rewriting your message. Select text and press Cmd/Ctrl+Shift+E to fix it in place, with Undo or a correction preview when needed.&lt;/p&gt;

&lt;p&gt;When Chrome's built-in on-device model is already available, Private mode is enabled by default and corrections run locally. Otherwise, the extension uses its cloud service. If an active Private mode correction can't run locally, sending that selection to the cloud requires an explicit user action. Fix English doesn't save your text.&lt;/p&gt;

&lt;p&gt;I'd love to hear from anyone who's dealt with similar problems in ProseMirror, Lexical, CKEditor, or other rich-text editors. What was the most unexpected editing behavior you ran into?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>chrome</category>
      <category>contenteditable</category>
    </item>
  </channel>
</rss>
