How do I handle smart quotes and curly apostrophes? I'm building a new Drupal site, and the content authors and editors ared dyed-in-the-wool Microsoft Word users.  I've got WYSIWYG set up to force all pasting to be treated as &quot;Paste from Word&quot;, so much of Word's garbage seems to be handled well.  The one remaining problem is characters like smart quotes, curly apostrophes, em-dashes, and the like.  Although I'm shooting for end-to-end Unicode, and theoretically that should prevent those characters from showing up as weird garbage, I consider it safer to use HTML entities, in case at some future point someone else changes something that interferes with the end-to-end Unicode architecture.
Is there a way to convert non-ASCII characters into HTML entities, either when they are pasted or before they are displayed?
Converting to straight quotes (and using Typogrify to render them as smart quotes) would be fine too, if you can tell me how to convert to straight quotes.  Preferably, other characters should be as well handled.
Converting to straight quotes (and using Typogrify to render them as
smart quotes) would be fine too, if you can tell me how to convert to
straight quotes.

I had to do that for an online dictionary I built recently with Drupal 9.2. What I did is to create a drush command that updates all nodes (creating a new revision) converting all quote characters to straight quotes or ASCII quotes:
&lt;?php

namespace Drupal\diccionari_custom\Commands;

use Drush\Commands\DrushCommands;

/**
 * A Drush commandfile.
 */
class DiccionariCustomCommands extends DrushCommands {

  /**
   * Standarize quotes.
   *
   * @usage diccionari_custom:standarizeQuotes
   *   Usage description
   *
   * @command diccionari_custom:standarizeQuotes
   */
  public function standarizeQuotes() {

    $query = \Drupal::entityQuery('node');
    $entity_ids = $query-&gt;execute();
    foreach ($entity_ids as $nid) {
      $node = \Drupal\node\Entity\Node::load($nid);

      $new_title = str_replace('‘', &quot;'&quot;, $node-&gt;title-&gt;value);
      $new_title = str_replace('’', &quot;'&quot;, $new_title);
      $new_title = str_replace('ʹ', &quot;'&quot;, $new_title);
      $new_title = str_replace('ꞌ', &quot;'&quot;, $new_title);
      $new_title = str_replace('“', '&quot;', $new_title);
      $new_title = str_replace('”', '&quot;', $new_title);
      $new_title = str_replace('&amp;#x2018;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#x2019;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#x02B9;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#xA78C;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#x201C;', '&quot;', $new_title);
      $new_title = str_replace('&amp;#x201D;', '&quot;', $new_title);
      $node-&gt;setTitle($new_title);

      $new_content = str_replace('‘', &quot;'&quot;, $node-&gt;body-&gt;value);
      $new_content = str_replace('’', &quot;'&quot;, $new_content);
      $new_content = str_replace('ʹ', &quot;'&quot;, $new_content);
      $new_content = str_replace('ꞌ', &quot;'&quot;, $new_content);
      $new_content = str_replace('“', '&quot;', $new_content);
      $new_content = str_replace('”', '&quot;', $new_content);
      $new_content = str_replace('&amp;#x2018;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#x2019;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#x02B9;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#xA78C;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#x201C;', '&quot;', $new_content);
      $new_content = str_replace('&amp;#x201D;', '&quot;', $new_content);
      $node-&gt;body-&gt;value  = $new_content;
      $node-&gt;body-&gt;format = 'basic_html';

      $node-&gt;setNewRevision(TRUE);
      $node-&gt;revision_log = 'Converting quotes to straight quotes';
      $node-&gt;setRevisionCreationTime(REQUEST_TIME);
      $node-&gt;setRevisionUserId(1);
      $node-&gt;save();

      $this-&gt;logger()-&gt;success(dt($nid . ':' . $node-&gt;title-&gt;value));
    }
  }
}

Now, when outputting the content, you can use a text format and Typogrify module to do the conversion to smart quotes.
And if you have to perform that conversion in the title too, see my last comment in this Drupal.org issue.

Preferably your solution will handle other characters as well.

Both the drush command example and Typogrify settings can easily be extended to handle other characters.