<?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: 강명석</title>
    <description>The latest articles on DEV Community by 강명석 (@_e37583b7079a729834958).</description>
    <link>https://dev.to/_e37583b7079a729834958</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%2F4088738%2Ffb2aeed7-3e27-4a81-8942-13f2390ecd6c.png</url>
      <title>DEV Community: 강명석</title>
      <link>https://dev.to/_e37583b7079a729834958</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_e37583b7079a729834958"/>
    <language>en</language>
    <item>
      <title>Four places ffmpeg.wasm fails silently in a Next.js app (and the fixes)</title>
      <dc:creator>강명석</dc:creator>
      <pubDate>Fri, 21 Aug 2026 18:45:35 +0000</pubDate>
      <link>https://dev.to/_e37583b7079a729834958/four-places-ffmpegwasm-fails-silently-in-a-nextjs-app-and-the-fixes-4jo3</link>
      <guid>https://dev.to/_e37583b7079a729834958/four-places-ffmpegwasm-fails-silently-in-a-nextjs-app-and-the-fixes-4jo3</guid>
      <description>&lt;p&gt;I shipped four browser-only video tools with ffmpeg.wasm: trim, compress, video-to-GIF and MP3 extraction. Files never leave the browser, nothing to install.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://toolnjoy.com/video-trim" rel="noopener noreferrer"&gt;Trim&lt;/a&gt; · &lt;a href="https://toolnjoy.com/video-compress" rel="noopener noreferrer"&gt;Compress&lt;/a&gt; · &lt;a href="https://toolnjoy.com/video-to-gif" rel="noopener noreferrer"&gt;GIF&lt;/a&gt; · &lt;a href="https://toolnjoy.com/video-to-mp3" rel="noopener noreferrer"&gt;MP3&lt;/a&gt; (Korean UI, but the buttons are obvious)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting there, I hit four walls. Every one of them surfaced as a single "conversion failed" line in the UI and &lt;strong&gt;nothing in the console&lt;/strong&gt;. Writing them down for the next person. Stack: Next.js App Router + webpack, &lt;code&gt;@ffmpeg/ffmpeg&lt;/code&gt; 0.12, self-hosted core.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. webpack hijacks the dynamic import inside the worker
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@ffmpeg/ffmpeg&lt;/code&gt; spawns its worker like this:&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./worker.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;webpack recognises the pattern and bundles the worker. Fine. But it also rewrites the &lt;code&gt;import(coreURL)&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; that worker to go through its own module loader. The core URL arrives at runtime as a &lt;code&gt;blob:&lt;/code&gt; URL, which webpack's loader has never heard of, so it dies with &lt;code&gt;Cannot find module 'blob:...'&lt;/code&gt;. The error is thrown inside the worker, so the main-thread console stays empty.&lt;/p&gt;

&lt;p&gt;Fix: keep the worker out of the bundle. Copy &lt;code&gt;node_modules/@ffmpeg/ffmpeg/dist/esm/worker.js&lt;/code&gt; to &lt;code&gt;public/ffmpeg/&amp;lt;version&amp;gt;/lib/&lt;/code&gt; and pass it via &lt;code&gt;classWorkerURL&lt;/code&gt; in &lt;code&gt;load()&lt;/code&gt;. Now the untouched worker runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. classWorkerURL needs the origin
&lt;/h2&gt;

&lt;p&gt;Passing a path like &lt;code&gt;/ffmpeg/0.12.x/lib/worker.js&lt;/code&gt; is not enough. The library resolves it with &lt;code&gt;new URL(classWorkerURL, import.meta.url)&lt;/code&gt;, and inside the bundle &lt;code&gt;import.meta.url&lt;/code&gt; is a build-time &lt;code&gt;file:///C:/...&lt;/code&gt; path. So it goes looking for &lt;code&gt;file:///C:/ffmpeg/...&lt;/code&gt; and fails.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/ffmpeg/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;coreURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/core/ffmpeg-core.js`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;wasmURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/core/ffmpeg-core.wasm`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;classWorkerURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/lib/worker.js`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefix &lt;code&gt;location.origin&lt;/code&gt; and it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. You cannot build a GIF palette with -vf
&lt;/h2&gt;

&lt;p&gt;For decent GIF quality you run &lt;code&gt;palettegen&lt;/code&gt; first and &lt;code&gt;paletteuse&lt;/code&gt; second. Doing it in one pass needs &lt;code&gt;split&lt;/code&gt; to fork the stream, and a filter graph with two outputs is something &lt;code&gt;-vf&lt;/code&gt; cannot take. It fails silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# does not work&lt;/span&gt;
&lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=12,scale=480:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse"&lt;/span&gt;

&lt;span class="c"&gt;# works&lt;/span&gt;
&lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v]fps=12,scale=480:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch to &lt;code&gt;-filter_complex&lt;/code&gt; and label the input &lt;code&gt;[0:v]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Permissions-Policy: display-capture
&lt;/h2&gt;

&lt;p&gt;This one is from the screen-recorder next door. The security headers had &lt;code&gt;display-capture=()&lt;/code&gt;. With that, &lt;code&gt;getDisplayMedia()&lt;/code&gt; rejects immediately &lt;strong&gt;without showing the permission prompt&lt;/strong&gt;. To the user it looks like the button does nothing. It has to be &lt;code&gt;display-capture=(self)&lt;/code&gt;. Same story for camera and microphone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: there is no denominator for the progress bar
&lt;/h2&gt;

&lt;p&gt;The core wasm is 30+ MB. Over compressed transfer there is no &lt;code&gt;Content-Length&lt;/code&gt;, so you have nothing to divide by. I keep the uncompressed wasm size as a constant and use that. The byte count you read from &lt;code&gt;fetch&lt;/code&gt; is post-decompression, so the units match. Actual transfer is about 9.8 MB gzip, 8 MB brotli.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing with a canvas-generated video
&lt;/h2&gt;

&lt;p&gt;Tools that need a file are awkward to test automatically. A canvas that changes background colour every second, recorded with &lt;code&gt;captureStream()&lt;/code&gt; + &lt;code&gt;MediaRecorder&lt;/code&gt; into a 6-second clip and injected via &lt;code&gt;DataTransfer&lt;/code&gt;, turns out to be enough. Trim is verified by reading pixel colours from the output (cut 2–4 s, first frame green, last frame blue). GIFs are verified by counting &lt;code&gt;21 F9 04&lt;/code&gt; (frames) and &lt;code&gt;21 FF 0B&lt;/code&gt; (loop block) in the bytes. One gotcha: &lt;code&gt;requestAnimationFrame&lt;/code&gt; pauses in background tabs, so draw with &lt;code&gt;setInterval&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything is live. If a file fails, tell me the format.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://toolnjoy.com/video-trim" rel="noopener noreferrer"&gt;https://toolnjoy.com/video-trim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://toolnjoy.com/video-to-gif" rel="noopener noreferrer"&gt;https://toolnjoy.com/video-to-gif&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Other tools on the site: &lt;a href="https://toolnjoy.com" rel="noopener noreferrer"&gt;https://toolnjoy.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webassembly</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A rule-based Korean dialect converter with no LLM: eight guards that keep standard Korean intact</title>
      <dc:creator>강명석</dc:creator>
      <pubDate>Fri, 21 Aug 2026 18:40:22 +0000</pubDate>
      <link>https://dev.to/_e37583b7079a729834958/a-rule-based-korean-dialect-converter-with-no-llm-eight-guards-that-keep-standard-korean-intact-216n</link>
      <guid>https://dev.to/_e37583b7079a729834958/a-rule-based-korean-dialect-converter-with-no-llm-eight-guards-that-keep-standard-korean-intact-216n</guid>
      <description>&lt;p&gt;I built a Korean dialect converter that runs entirely in the browser and uses no LLM. Type a standard Korean sentence and it rewrites it the way people speak in Gyeongsang, Jeolla, Chungcheong, Gangwon or Jeju. Paste a dialect sentence and it converts back to standard Korean. Nothing leaves the browser.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://toolnjoy.com/dialect-converter" rel="noopener noreferrer"&gt;https://toolnjoy.com/dialect-converter&lt;/a&gt; (Korean UI)&lt;/p&gt;

&lt;p&gt;The hard part was not producing dialect. It was &lt;strong&gt;not breaking standard Korean&lt;/strong&gt;. Every time I widened a rule, a perfectly normal sentence got mangled somewhere. Here is what I ended up with. Most of it applies to any rule-based text rewriter (honorific converters, profanity filters, spelling normalizers).&lt;/p&gt;

&lt;h2&gt;
  
  
  Shape of the thing
&lt;/h2&gt;

&lt;p&gt;Four modules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;file&lt;/th&gt;
&lt;th&gt;job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;core&lt;/td&gt;
&lt;td&gt;sentence splitting, sentence-type detection, ending replacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;endings&lt;/td&gt;
&lt;td&gt;per-region ending tables (declarative / interrogative / propositive / imperative × politeness)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;words&lt;/td&gt;
&lt;td&gt;per-region vocabulary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reverse&lt;/td&gt;
&lt;td&gt;dialect → standard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Forward conversion goes "decide the sentence type, then pick the region's ending". Reverse goes the other way: "look at the ending, infer the sentence type". You cannot just invert the forward table, so reverse has its own table. Each reverse rule carries a &lt;code&gt;type&lt;/code&gt;, which removes the detection step entirely.&lt;/p&gt;

&lt;p&gt;In reverse mode the user does not pick a region. I run all five and keep the one with the highest score (4 points per matching ending + syllable count of matched words). People who paste dialect usually do not know which region it is from. That is why they came.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight guards, each from a real failure
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Never reverse a form that appears on the standard side of any table.&lt;/strong&gt; The Gyeongsang table had &lt;code&gt;함께 → 같이&lt;/code&gt; (together). Reverse flipped it and rewrote "같이 보게" into "함께 보게", except 같이 is perfectly standard. Fix: anything that ever appears in a &lt;em&gt;standard&lt;/em&gt; column anywhere is excluded from reverse candidates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Question endings only count when there is a question mark or a wh-word.&lt;/strong&gt; Gyeongsang &lt;code&gt;-나&lt;/code&gt; / &lt;code&gt;-노&lt;/code&gt; mark questions. But 하나 ("one") also ends in &lt;code&gt;-나&lt;/code&gt;. Without the guard, "하나 주세요" (give me one) became "하니 주세요".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Count the prefix in words, not in the sentence.&lt;/strong&gt; In "왜 그랴?" the ending &lt;code&gt;그랴&lt;/code&gt; has a one-character stem. Counted at sentence level it looked like two characters ("왜 그") and passed a minimum-length check it should have failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Leave single characters alone.&lt;/strong&gt; I added the particle 와 as vocabulary and "PDF와 이미지" became "PDF왜 이미지". Particles attach to Latin letters and digits too, and a single character gives you no boundary to anchor on. Only two single-character entries survived, both nouns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Nouns matched mid-word.&lt;/strong&gt; 벼락 (lightning) became 나락락 because &lt;code&gt;벼 → 나락&lt;/code&gt; (rice plant) matched inside it. Same story for 오이소박이, 부담, 벽지. Noun entries get a marker and their own boundary check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Single-character particles punch through boundaries.&lt;/strong&gt; 도 was in the particle list, so 가재도구 (household goods) was split as 가재 + 도. Rule: if more Hangul follows a one-character particle, it is not a particle. Also, particles must agree with the final consonant: the 이 in 가재이 cannot be a subject particle because 가재 has no final consonant (it would need 가). That single check stopped the propositive 가재이 from turning into 까재이.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. &lt;code&gt;-으니까&lt;/code&gt; is not a sentence end.&lt;/strong&gt; Treating 니까 as a terminal ending turned "왔으니까 드세요" (since you came, please eat) into a question-then-imperative in Gyeongsang. 니까 terminates only after a ㅂ final.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Greetings before endings.&lt;/strong&gt; If the ending rule hits 감사합니다 first you get 감사합니더 and the real Gyeongsang 고맙심더 never appears. Vocabulary of four or more characters is applied before endings. Four, because anything shorter at the end of a sentence would swallow the ending and the ending rule would never fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest hole was punctuation
&lt;/h2&gt;

&lt;p&gt;All my test sentences had periods. A real user typed "안녕하세요 저는 백수입니다 돈을 주세요" with none, and not a single character changed. The whole thing was one sentence so only the last ending matched.&lt;/p&gt;

&lt;p&gt;Now sentences split on polite terminal endings followed by whitespace (습니다, 세요, 어요…). Casual one-character endings (-다, -어) are deliberately not in the list: in "이거 다 먹어" the 다 would become a sentence end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the whole repository as a regression corpus
&lt;/h2&gt;

&lt;p&gt;This tool never throws. It produces quietly wrong output. So the only safety net is a corpus check: scrape every Korean string literal and every JSX prose node from the codebase (about 9,400 snippets, all assumed to be standard Korean), run reverse over all of them, and assert that &lt;strong&gt;not one character changes&lt;/strong&gt;. Every rule widening gets caught here. Jeju &lt;code&gt;-게&lt;/code&gt;, Chungcheong &lt;code&gt;-유&lt;/code&gt;, Gyeongsang &lt;code&gt;-니라&lt;/code&gt; were all caught this way.&lt;/p&gt;

&lt;p&gt;Scraping only string literals misses things. "오직 → 오죽" only showed up in JSX prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importing 8,886 dialect headwords from the national dictionary
&lt;/h2&gt;

&lt;p&gt;I widened the reverse vocabulary with the National Institute of Korean Language's open dictionary (Urimalsaem). You cannot import it blindly: many dialect forms are homographs of standard words.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;패기: 覇氣 (spirit) in standard Korean, "hiccup" in one dialect&lt;/li&gt;
&lt;li&gt;새똥: bird droppings, or "dawn"&lt;/li&gt;
&lt;li&gt;영신: a given name, listed as "shaman"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A corpus filter cannot catch these because 패기 does not appear in my codebase. So the import is two-layered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;rewrite layer&lt;/strong&gt;: exactly one standard equivalent, and the dictionary API confirms there is no non-dialect sense&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;annotate layer&lt;/strong&gt;: text untouched, the meaning is only listed ("this word also means X in region Y")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two-syllable homographs were the danger zone. I first demoted every two-syllable entry, then queried the dictionary API for 7,766 headwords asking "does a standard sense exist", which restored 1,340 of them. Two things the dictionary cannot catch: personal names (handled with a name list) and real-world text (I ran 890k Korean sentences through reverse and dropped any entry that changed even one character).&lt;/p&gt;

&lt;h2&gt;
  
  
  Still weak
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Jeju changes both endings and vocabulary, so a round trip (standard → Jeju → standard) does not return the original&lt;/li&gt;
&lt;li&gt;Jeju propositive &lt;code&gt;-게&lt;/code&gt; (먹게 = let's eat) is identical to standard &lt;code&gt;-게&lt;/code&gt;, so it is not in. Adding it turns 유일하게 (uniquely) into 유일하자&lt;/li&gt;
&lt;li&gt;Splitting only looks at polite endings and ㅆ past-tense forms; "이거 다 먹어 그리고 자" is still one sentence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything above came from actually using the thing, not from the test suite. If you read Korean and get a weird result, tell me the sentence.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converter: &lt;a href="https://toolnjoy.com/dialect-converter" rel="noopener noreferrer"&gt;https://toolnjoy.com/dialect-converter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Regional pages: &lt;a href="https://toolnjoy.com/dialect-converter/gyeongsang" rel="noopener noreferrer"&gt;Gyeongsang&lt;/a&gt; · &lt;a href="https://toolnjoy.com/dialect-converter/jeolla" rel="noopener noreferrer"&gt;Jeolla&lt;/a&gt; · &lt;a href="https://toolnjoy.com/dialect-converter/chungcheong" rel="noopener noreferrer"&gt;Chungcheong&lt;/a&gt; · &lt;a href="https://toolnjoy.com/dialect-converter/gangwon" rel="noopener noreferrer"&gt;Gangwon&lt;/a&gt; · &lt;a href="https://toolnjoy.com/dialect-converter/jeju" rel="noopener noreferrer"&gt;Jeju&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dictionary lookup only: &lt;a href="https://toolnjoy.com/dialect-dictionary" rel="noopener noreferrer"&gt;https://toolnjoy.com/dialect-dictionary&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>nlp</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
