<?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: ggwork</title>
    <description>The latest articles on DEV Community by ggwork (@ggwork).</description>
    <link>https://dev.to/ggwork</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%2F4070458%2Fc1960c8f-08ea-49bc-b4b7-1c0f82814c7f.jpeg</url>
      <title>DEV Community: ggwork</title>
      <link>https://dev.to/ggwork</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ggwork"/>
    <language>en</language>
    <item>
      <title>Building a Position Averaging Calculator: Getting the Math Right Matters</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Tue, 01 Sep 2026 01:37:42 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-position-averaging-calculator-getting-the-math-right-matters-2694</link>
      <guid>https://dev.to/ggwork/building-a-position-averaging-calculator-getting-the-math-right-matters-2694</guid>
      <description>&lt;p&gt;Last week, I found myself staring at a spreadsheet trying to figure out my average cost after buying the same stock three times at different prices. You know the drill — you buy at 10, it drops, you buy more at 8, then it dips again and you're thinking about averaging down further. The math isn't hard, but it's tedious, and I kept second-guessing whether I was accounting for fees correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Existing Solutions
&lt;/h2&gt;

&lt;p&gt;I searched for online calculators, and honestly, most of them were disappointing. Either they required sign-up, were buried in financial portal websites with more ads than functionality, or they ignored trading fees entirely. A few had the fee calculation, but they made assumptions that didn't match how my broker actually charged me.&lt;/p&gt;

&lt;p&gt;The real issue? &lt;strong&gt;Most calculators ignore the fact that your break-even price isn't just your average cost.&lt;/strong&gt; When you sell, you pay fees too. So if your average cost is 9.0051, you can't actually break even by selling at 9.0051 — you need to sell slightly higher to cover the sell-side fees.&lt;/p&gt;

&lt;p&gt;That's the gap I wanted to fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Math
&lt;/h2&gt;

&lt;p&gt;The calculation itself is straightforward weighted averaging, but the fee handling is where things get interesting. Here's the essential logic:&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;function&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentRaw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;commRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stampRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minComm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;buys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actualFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minComm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;actualFee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Break-even includes sell-side fees&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sellFeePerShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;stampRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;breakEven&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sellFeePerShare&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;breakEven&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;The key insight here is the break-even calculation. Most tools stop at &lt;code&gt;avgCost&lt;/code&gt;, but that's misleading. If you sell at your average cost, you'll actually lose money because of sell-side fees. The break-even price needs to be slightly higher — it's the price where your sell proceeds (after fees) exactly equal your total buy cost (including buy fees).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose Pure Frontend
&lt;/h2&gt;

&lt;p&gt;I could have built this with a backend. I could have added user accounts, saved calculation history, all that stuff. But honestly, this is a privacy-sensitive calculation. Do you really want your stock positions floating around on some server?&lt;/p&gt;

&lt;p&gt;Pure frontend means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No data leaves the browser&lt;/li&gt;
&lt;li&gt;No server costs for me&lt;/li&gt;
&lt;li&gt;Instant load times&lt;/li&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off? No persistent history, no cross-device sync. For this use case, that's a trade-off I'm comfortable with. If you need to save results, just screenshot it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-Assisted Development Experience
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. I built this with heavy AI assistance, and the experience was... educational.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI Got Right
&lt;/h3&gt;

&lt;p&gt;I described the requirements conversationally: "I need a calculator that takes multiple buy records with price and shares, calculates weighted average cost including fees, and shows break-even price." The AI generated a solid initial structure — the HTML layout, the dynamic row addition, the basic styling. It even handled the dark mode CSS without me asking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where AI Struggled
&lt;/h3&gt;

&lt;p&gt;The first version had a subtle bug in the fee calculation. It was applying the minimum commission to every trade individually, which is correct, but it was also applying the minimum to the &lt;em&gt;sell&lt;/em&gt; fee estimation in the break-even calculation. That's wrong — the minimum commission should only apply if the trade actually happens.&lt;/p&gt;

&lt;p&gt;Here's the conversation that followed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: The break-even price is too high. If I buy 100 shares at 10 with a 5 yuan minimum commission, the break-even shouldn't include the minimum commission because the sell fee is proportional to the sell amount.

AI: You're right. Let me fix the break-even calculation to use the percentage rate only, not the minimum.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the pattern I've noticed with AI coding — it's great at generating the 80% solution, but the edge cases and financial logic accuracy require human domain knowledge. The AI doesn't "know" that minimum commissions don't apply proportionally; it just patterns from typical code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Another AI Miss: Input Validation
&lt;/h3&gt;

&lt;p&gt;The AI initially assumed all inputs would be valid numbers. It took me pointing out that someone might type "abc" or leave a field empty before it added proper validation. And even then, the first validation attempt was too aggressive — it blocked legitimate edge cases like zero shares (which should be an error, but the error message was confusing).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Refinement Process
&lt;/h2&gt;

&lt;p&gt;After the initial AI-generated version, I went through several iterations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fixed the fee calculation&lt;/strong&gt; — the minimum commission logic needed adjustment for the break-even price&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added input validation&lt;/strong&gt; — with clear error messages in Chinese (the tool targets Chinese-speaking users)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved the UX&lt;/strong&gt; — the first version had all inputs on one line, which was cramped on mobile. I restructured to a grid layout that stacks on small screens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added the optional current price field&lt;/strong&gt; — this lets users see floating profit/loss without needing a separate calculator&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The floating P&amp;amp;L calculation was another spot where I had to correct the AI:&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="c1"&gt;// AI's first version — wrong&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floatPL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Correct version — includes sell fees&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sellFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;stampRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floatPL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;sellFee&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference matters. If you're showing someone their unrealized profit, it should reflect what they'd actually get if they sold right now, not a naive price-minus-cost calculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Domain Knowledge Still Matters
&lt;/h3&gt;

&lt;p&gt;AI can write code, but it can't know that in Chinese stock markets, there's a stamp tax on sells (0.05%) that doesn't apply to buys, or that some brokers have a 5 yuan minimum commission. These domain-specific rules are where human input becomes critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Test with Real Numbers
&lt;/h3&gt;

&lt;p&gt;The AI generated test cases, but they were too clean. I had to create edge cases myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if the user enters a price of 0?&lt;/li&gt;
&lt;li&gt;What if shares are negative?&lt;/li&gt;
&lt;li&gt;What if the commission rate is 0 but the minimum is 5?&lt;/li&gt;
&lt;li&gt;What about very small trades where the minimum commission dominates?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These edge cases exposed bugs that would have shipped otherwise.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI Is Great for UI, Less Great for Logic
&lt;/h3&gt;

&lt;p&gt;For this project, the AI's HTML/CSS was immediately usable. The JavaScript logic needed multiple rounds of correction. I've found this pattern holds across projects — AI excels at structure and boilerplate, but business logic requires careful human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After several hours of iteration, I had a working tool that handles the edge cases I care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple buy records with dynamic add/remove&lt;/li&gt;
&lt;li&gt;Commission, stamp tax, and transfer fees included in cost&lt;/li&gt;
&lt;li&gt;Break-even price that accounts for sell-side fees&lt;/li&gt;
&lt;li&gt;Optional current price for floating P&amp;amp;L&lt;/li&gt;
&lt;li&gt;Dark mode (because I'm a developer, of course I added dark mode)&lt;/li&gt;
&lt;li&gt;Pure frontend — no data leaves the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The calculation runs in milliseconds, the UI is responsive, and the whole thing is a single HTML file. No build step, no dependencies, no server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts on AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;This project reinforced my belief that AI coding tools are incredibly useful, but they're not a replacement for understanding the problem domain. The AI saved me maybe 60% of the time compared to writing everything from scratch, but the remaining 40% required me to actually understand what the tool needed to do.&lt;/p&gt;

&lt;p&gt;The pattern I've settled into:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Describe the problem clearly to the AI&lt;/strong&gt; — the more specific, the better&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review all generated logic carefully&lt;/strong&gt; — especially anything involving money or dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create edge case tests&lt;/strong&gt; — the AI won't do this for you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate with targeted prompts&lt;/strong&gt; — "fix the fee calculation" works better than "something's wrong"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building a similar tool, whether it's a calculator or something else, the same principles apply. AI gets you 80% there quickly, but the last 20% is where the real engineering happens.&lt;/p&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's free to use, handles all the fee calculations correctly (I hope), and respects your privacy since everything runs locally in your browser. If you're curious, you can find it &lt;a href="https://craftvo.app/en/tool/stock-average-cost" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I need to go check whether my actual average cost matches what the calculator says. Spoiler: it probably won't, because my broker's fee structure is apparently a mystery even to their own support team.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to Calculate Exact Age in the Browser Without Cheating with Math</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:41:34 +0000</pubDate>
      <link>https://dev.to/ggwork/how-to-calculate-exact-age-in-the-browser-without-cheating-with-math-5225</link>
      <guid>https://dev.to/ggwork/how-to-calculate-exact-age-in-the-browser-without-cheating-with-math-5225</guid>
      <description>&lt;p&gt;While working on a utility site recently, I needed a precise age calculator. Sounds trivial, right? Pick two dates, subtract, done. Except it's not. The moment you try to calculate "25 years, 3 months, and 12 days" accurately, you realize that naive date arithmetic is a rabbit hole of edge cases.&lt;/p&gt;

&lt;p&gt;Let me walk you through the engineering decisions that turned a seemingly simple tool into a proper date-handling exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Simple Subtraction
&lt;/h2&gt;

&lt;p&gt;Here's the trap I initially fell into:&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;ageInMilliseconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageInYears&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ageInMilliseconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;365.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works... until it doesn't. The 365.25 days approximation drifts. Leap years aren't uniform. And try explaining to a user born on February 29th why their age is "wrong" on non-leap years.&lt;/p&gt;

&lt;p&gt;The real issue is that calendars aren't arithmetic — they're cultural constructs with rules. Months have different lengths. Leap years follow a specific pattern. You can't just divide milliseconds by a constant and get a meaningful answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Correct" Way: Component-wise Calculation
&lt;/h2&gt;

&lt;p&gt;The key insight is to calculate years, months, and days separately, adjusting for borrows like you learned in elementary school subtraction:&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;function&lt;/span&gt; &lt;span class="nf"&gt;calculateAge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;daysInMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;days&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;This is the part that matters — the borrow logic. When your birthday hasn't happened yet this month, you borrow days from the previous month. When it's not your birthday month yet, you borrow months from the previous year. Simple, elegant, and handles all the calendar weirdness correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Birthday Problem
&lt;/h2&gt;

&lt;p&gt;The next challenge was calculating days until the next birthday. The naive approach:&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;nextBirthday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks for people born on February 29th. What's their birthday in non-leap years? March 1st? February 28th?&lt;/p&gt;

&lt;p&gt;The pragmatic answer: check if the birthday exists in the current year; if not, use March 1st. It's not perfect, but it's predictable and defensible:&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;function&lt;/span&gt; &lt;span class="nf"&gt;getBirthdayInYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthMonth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthDay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthMonth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthDay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;birthMonth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// March 1st&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;date&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;h2&gt;
  
  
  The Real-Time Seconds Problem
&lt;/h2&gt;

&lt;p&gt;Here's where things get interesting. I wanted to show total seconds alive, and it needed to tick in real-time. The naive approach would recalculate everything every second. That's wasteful and causes jank.&lt;/p&gt;

&lt;p&gt;The solution: calculate the static values once, then only update the seconds display:&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;let&lt;/span&gt; &lt;span class="nx"&gt;totalSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initialSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;totalSeconds&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;totalSeconds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formatNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalSeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a micro-optimization, but it's the kind of thinking that prevents your tool from feeling sluggish. The rest of the calculation happens once; only the seconds counter updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration Experience
&lt;/h2&gt;

&lt;p&gt;Now for the part that made this project actually fun — building it with AI assistance. I've been experimenting with using AI as a pair programmer for utility tools, and this was a perfect test case.&lt;/p&gt;

&lt;h3&gt;
  
  
  The First Attempt
&lt;/h3&gt;

&lt;p&gt;I gave the AI my requirements: "Create a single-file HTML age calculator with precise year/month/day calculation, total days/hours/minutes, and next birthday countdown."&lt;/p&gt;

&lt;p&gt;The AI's first attempt was... surprisingly good. It nailed the structure, the styling, and the basic logic. But it had one critical bug: it used the millisecond division approach for age calculation. The output looked right for most dates, but it was subtly wrong for edge cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Iteration Process
&lt;/h3&gt;

&lt;p&gt;This is where the real collaboration began. Instead of pointing out the bug directly, I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happens for someone born on February 29th, 2000, calculating age on March 1st, 2024?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI paused, recalculated, and immediately recognized the issue. It suggested the component-wise approach. But its first implementation had a bug in the borrow logic — it didn't handle the case where the target date's month has fewer days than the birth month's day.&lt;/p&gt;

&lt;p&gt;I had to guide it through the borrow logic step by step. The AI was great at generating code quickly, but it struggled with the edge case reasoning that comes naturally to experienced developers. It would optimize for the common case and miss the exceptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Worked Well
&lt;/h3&gt;

&lt;p&gt;The AI excelled at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating the complete HTML structure with proper i18n support&lt;/li&gt;
&lt;li&gt;Creating responsive CSS with dark mode support&lt;/li&gt;
&lt;li&gt;Writing the structured data for SEO&lt;/li&gt;
&lt;li&gt;Implementing the real-time seconds counter&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where I Had to Step In
&lt;/h3&gt;

&lt;p&gt;The AI struggled with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calendar edge cases (February 29th, month-end boundaries)&lt;/li&gt;
&lt;li&gt;The borrow logic in date arithmetic&lt;/li&gt;
&lt;li&gt;Understanding that not all dates are valid in all years&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The i18n Trap
&lt;/h2&gt;

&lt;p&gt;One thing the AI got right from the start: proper internationalization. It automatically used a key-value system with &lt;code&gt;data-i18n&lt;/code&gt; attributes and a JavaScript dictionary. This was smart because it anticipated the need for multiple languages without over-engineering.&lt;/p&gt;

&lt;p&gt;The language detection logic is simple but effective:&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;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&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;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&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;URL parameter takes priority, then browser language, then default to Chinese. This pattern is simple enough to be maintainable but powerful enough for edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Calendar math is deceptively complex.&lt;/strong&gt; What looks like a simple subtraction problem has more edge cases than you'd expect. Always test with boundary dates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. AI is a great pair programmer, but not a replacement for understanding.&lt;/strong&gt; The AI generated code quickly, but I had to understand the domain deeply to guide it correctly. It's like having a brilliant intern who's fast but needs supervision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Real-time features need incremental updates.&lt;/strong&gt; Don't recalculate everything when you only need to update one value. Think about what actually needs to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Internationalization should be baked in from the start.&lt;/strong&gt; Retrofitting i18n is painful. Designing for it from day one is barely any extra work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;I ended up with a clean, single-file tool that handles all the date edge cases correctly. It's fast, responsive, and works in both Chinese and English. The whole thing is about 300 lines of HTML/CSS/JS — no frameworks, no dependencies.&lt;/p&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. You can find it at &lt;a href="https://craftvo.app/en/tool/age-calculator" rel="noopener noreferrer"&gt;craftvo.app&lt;/a&gt; if you want to see the final result.&lt;/p&gt;

&lt;p&gt;The real takeaway isn't about age calculators specifically. It's about the mindset: don't trust naive arithmetic when dealing with human constructs like calendars. And when using AI assistance, remember that it's a tool for accelerating your thinking, not replacing it. The best results come from understanding the problem deeply and using AI to execute your understanding faster.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building an ASCII Table Tool: Why I Ditched the Spreadsheet and Made My Own</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Mon, 31 Aug 2026 01:57:27 +0000</pubDate>
      <link>https://dev.to/ggwork/building-an-ascii-table-tool-why-i-ditched-the-spreadsheet-and-made-my-own-426f</link>
      <guid>https://dev.to/ggwork/building-an-ascii-table-tool-why-i-ditched-the-spreadsheet-and-made-my-own-426f</guid>
      <description>&lt;p&gt;I was debugging a network protocol implementation last month when I hit a wall. I needed to quickly verify what byte value corresponded to a specific control character — was it &lt;code&gt;0x1B&lt;/code&gt; for ESC or was that &lt;code&gt;0x1C&lt;/code&gt;? My go-to approach was to open a random ASCII chart website, but every single one I found was either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buried in ads and popups&lt;/li&gt;
&lt;li&gt;Missing the control characters entirely (you know, the actually useful ones)&lt;/li&gt;
&lt;li&gt;So cluttered with unnecessary features that the table was half the screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I did what any reasonable developer would do: I spent an afternoon building my own. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Approach: Data First, UI Later
&lt;/h2&gt;

&lt;p&gt;The first decision was the data structure. I needed all 128 standard ASCII characters (0-127), each with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decimal, hex, octal, and binary representations&lt;/li&gt;
&lt;li&gt;HTML entity (for the printable ones)&lt;/li&gt;
&lt;li&gt;Standard name (like "Capital A" or "Line Feed")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I started by hardcoding an array of objects:&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;ASCII_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Capital A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;char&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 126 more entries&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was tedious but straightforward. The real challenge came with control characters — you can't just display &lt;code&gt;\x00&lt;/code&gt; and expect users to know what it means. I needed proper names: NUL, SOH, STX, and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration: Where It Helped and Where It Didn't
&lt;/h2&gt;

&lt;p&gt;This is where AI-assisted development got interesting. I described the requirements to Claude — "I need an ASCII table with all 128 characters, multi-format display, search, and copy functionality" — and it generated a solid foundation on the first try.&lt;/p&gt;

&lt;p&gt;The data generation was where AI really shined. I asked it to generate the complete character metadata, and it produced accurate names, hex codes, and HTML entities. This saved me probably 30 minutes of manual data entry and potential typos.&lt;/p&gt;

&lt;p&gt;But here's where it went wrong: the AI's first version had a subtle bug. It was using &lt;code&gt;String.fromCharCode()&lt;/code&gt; directly to display characters, which works fine for printable characters but breaks for control characters — they render as invisible or garbled. The AI didn't catch this because it never actually rendered the table.&lt;/p&gt;

&lt;p&gt;I had to step in and add explicit handling:&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;function&lt;/span&gt; &lt;span class="nf"&gt;formatChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dec&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;dec&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;␀&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Use visual placeholder&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dec&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;This is the difference between "code that compiles" and "code that actually works." The AI was great at generating the skeleton, but it missed the edge cases that only become obvious when you're looking at a rendered page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: The Sticky Header Problem
&lt;/h2&gt;

&lt;p&gt;One thing I insisted on was a sticky table header — when you're scrolling through 128 rows, you need to see what column you're in. This turned out to be surprisingly tricky.&lt;/p&gt;

&lt;p&gt;The naive approach is &lt;code&gt;position: sticky; top: 0&lt;/code&gt; on the &lt;code&gt;th&lt;/code&gt; elements. But this breaks when the table is inside a scrollable container with a border. The header would scroll away or get clipped by the container's overflow.&lt;/p&gt;

&lt;p&gt;The fix was to ensure the scroll container is a direct parent with proper overflow settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.table-wrap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;th&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&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;Simple in hindsight, but it took me a few iterations to get the z-index right — otherwise the sticky header would appear under the table rows when scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search: The Unexpected Complexity
&lt;/h2&gt;

&lt;p&gt;Search seemed straightforward: filter by decimal, hex, or name. But I quickly realized that users think in different formats. Some search for "65", others for "0x41", others for "A", and others for "Capital A".&lt;/p&gt;

&lt;p&gt;The solution was to normalize the input and search across multiple fields:&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;function&lt;/span&gt; &lt;span class="nf"&gt;filterRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;allRows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;allRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&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;The AI initially generated a search that only matched exact decimal values — completely missing the use case where someone types "A" or "0x41". I had to iterate on the prompt: "Search should be fuzzy, matching partial strings across multiple fields."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Copy Feature: Clipboard API Gotchas
&lt;/h2&gt;

&lt;p&gt;Click-to-copy sounds simple, but there's a catch: the Clipboard API only works in secure contexts (HTTPS or localhost). Since this is a static tool, I need to handle the fallback for HTTP environments:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;copyHex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Copied: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hex&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fallback for older browsers&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&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;hex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&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;The AI didn't consider this edge case at all — it just used &lt;code&gt;navigator.clipboard&lt;/code&gt; without any error handling. This would have silently failed for users on HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile Responsiveness: The Table That Wouldn't Fit
&lt;/h2&gt;

&lt;p&gt;Tables are inherently non-responsive, and ASCII tables are no exception. With 7 columns (DEC, HEX, OCT, BIN, CHAR, NAME, HTML), it's impossible to fit everything on a 320px screen.&lt;/p&gt;

&lt;p&gt;My solution was progressive disclosure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.hide-sm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&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;This hides the HTML entity column on small screens. Not perfect, but functional. The table becomes scrollable horizontally if needed, but at least the essential columns are visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Got Right (and What It Didn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI nailed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating the complete character dataset with zero typos&lt;/li&gt;
&lt;li&gt;Setting up the basic table structure and i18n scaffolding&lt;/li&gt;
&lt;li&gt;Producing a clean, consistent CSS foundation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI struggled with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge cases (control characters, clipboard fallbacks)&lt;/li&gt;
&lt;li&gt;Understanding the actual user experience (search that works the way people think)&lt;/li&gt;
&lt;li&gt;Performance considerations (sticky headers, DOM rendering at scale)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern I've noticed: AI is excellent at generating the "happy path" but consistently misses the "sad path" — the error handling, edge cases, and browser quirks that make production code actually production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Decision
&lt;/h2&gt;

&lt;p&gt;Since I'm building this for a global audience, I needed both Chinese and English support. The lightweight approach was a simple dictionary-based system:&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;I18N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASCII 表&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;搜索...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASCII Table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Search...&lt;/span&gt;&lt;span class="dl"&gt;"&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;This is intentional — I didn't want to pull in a full i18n library for a single tool. The trade-off is that adding a new language requires duplicating all strings, but for a tool with maybe 20 user-facing strings, it's manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI is a great pair programmer, not a replacement.&lt;/strong&gt; It saved me time on boilerplate and data generation, but I still needed to understand the domain well enough to catch its mistakes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge cases are where the real work lives.&lt;/strong&gt; Any developer can render an ASCII table. Making it actually useful — with proper control character handling, intuitive search, and graceful fallbacks — is the difference between a toy and a tool.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sometimes the old ways are fine.&lt;/strong&gt; I briefly considered using a framework like React or Vue, but for a static reference tool, vanilla JS was simpler, faster to load, and had zero dependencies to maintain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance matters even for simple tools.&lt;/strong&gt; The sticky header and efficient filtering make the tool feel instant. Users notice when a "simple" tool is sluggish.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file with no dependencies — just open it in a browser and it works. The complete ASCII table (0-127) with multi-format display, fuzzy search, category filtering, and click-to-copy functionality.&lt;/p&gt;

&lt;p&gt;If you find yourself constantly opening random ASCII chart websites that are cluttered with ads, or if you just want a clean, fast reference tool, you might find it useful. &lt;a href="https://craftvo.app/en/tool/ascii-table" rel="noopener noreferrer"&gt;Try it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The irony isn't lost on me — I spent four hours building a tool that saves me maybe 30 seconds each time I need to look up a character code. But those 30 seconds add up, and now I have a tool that works exactly the way I want it to. Sometimes reinventing the wheel is worth it.&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>tools</category>
    </item>
    <item>
      <title>How I Built a Compound Interest Calculator Without Any Backend</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:20:57 +0000</pubDate>
      <link>https://dev.to/ggwork/how-i-built-a-compound-interest-calculator-without-any-backend-2j2c</link>
      <guid>https://dev.to/ggwork/how-i-built-a-compound-interest-calculator-without-any-backend-2j2c</guid>
      <description>&lt;p&gt;While working on a side project recently, I needed a way to help users quickly understand how their savings could grow over time. Nothing fancy — just input a principal, an interest rate, and a timeframe, and see the future value. But here's the thing: I didn't want to spin up a server, manage a database, or deal with API rate limits. I wanted something that would work instantly, even on a slow connection, and that wouldn't cost me a cent in hosting.&lt;/p&gt;

&lt;p&gt;So I built a compound interest calculator that runs entirely in the browser. No backend. No dependencies. Just pure JavaScript and a bit of math.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Why Not Just Use an Existing Tool?"
&lt;/h2&gt;

&lt;p&gt;Good question. There are plenty of compound interest calculators out there. But most of them are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buried in a finance website with a million other widgets&lt;/li&gt;
&lt;li&gt;Require JavaScript from external CDNs that might fail&lt;/li&gt;
&lt;li&gt;Don't let you customize the compounding frequency&lt;/li&gt;
&lt;li&gt;Or my personal favorite: they call an API to do the math. Seriously. A compound interest calculation is about 10 lines of code, and some sites will make a network request to compute it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something self-contained. A single HTML file that could be hosted anywhere, even on a static file server, and just work. Plus, I'm an indie developer — I enjoy reinventing wheels when the wheel design is fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math (It's Not That Scary)
&lt;/h2&gt;

&lt;p&gt;The core formula for compound interest with regular contributions isn't as intimidating as it looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FV = P(1 + i)^N + PMT × ((1 + i)^N − 1) / i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P&lt;/strong&gt; is the initial principal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i&lt;/strong&gt; is the periodic interest rate (annual rate divided by compounding periods per year)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N&lt;/strong&gt; is the total number of periods (years × periods per year)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PMT&lt;/strong&gt; is the periodic contribution amount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tricky part? When &lt;code&gt;i&lt;/code&gt; is zero. If you have a 0% interest rate, the formula &lt;code&gt;((1+i)^N − 1) / i&lt;/code&gt; becomes &lt;code&gt;0/0&lt;/code&gt;, which is undefined. Classic division by zero situation.&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;function&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rPct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;monthlyAdd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rPct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PMT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;monthlyAdd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;principalFV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contribFV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;PMT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PMT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// fallback for 0% interest&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principalFV&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;contribFV&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;monthlyAdd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fv&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;invested&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateSeries&lt;/span&gt;&lt;span class="p"&gt;(...)&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;That fallback for &lt;code&gt;i = 0&lt;/code&gt; was something I initially got wrong. The AI I was working with (more on that in a bit) wrote the formula without the edge case, and the first time I tested with 0% interest, I got &lt;code&gt;NaN&lt;/code&gt; displayed. That's not a great user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision: Pure Frontend vs. API
&lt;/h2&gt;

&lt;p&gt;Let me explain why I went with pure frontend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: Zero server costs. Host it on any static host (GitHub Pages, Netlify, even a simple CDN).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt;: Users' financial data never leaves their browser. No tracking, no analytics, no "we store your data for research purposes."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: Instant response. No network latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: No server to crash, no API to rate-limit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off? I can't update the calculation logic without redeploying. But honestly, the math for compound interest hasn't changed in centuries. I think it's safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Chart Without a Library
&lt;/h2&gt;

&lt;p&gt;The second challenge was the growth chart. Most people would reach for Chart.js or D3. But I wanted to keep this dependency-free. So I wrote a tiny SVG chart generator.&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;function&lt;/span&gt; &lt;span class="nf"&gt;drawChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxVal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maxVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&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;y&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="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;svg viewBox="0 0 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&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;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;
    &amp;lt;polyline points="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" fill="none" stroke="#3b82f6" stroke-width="2"/&amp;gt;
  &amp;lt;/svg&amp;gt;`&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;That's the essence of it. A polyline with points scaled to fit the viewBox. It's not going to win any design awards, but it shows the growth curve clearly, and it's about 15 lines of code.&lt;/p&gt;

&lt;p&gt;The challenge here was handling the edge case where all values are zero. If &lt;code&gt;maxVal&lt;/code&gt; is 0, you get a division by zero in the scaling. I had to add a check: if the max is 0, just draw a flat line at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-Assisted Development Experience
&lt;/h2&gt;

&lt;p&gt;Now, the part I promised I'd talk about: building this with AI assistance. I used ChatGPT for most of the initial implementation, and honestly, it was a mixed bag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I asked for:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Write a compound interest calculator in vanilla JavaScript. It should take principal, annual rate, years, compounding frequency, and monthly contribution. Return the future value and a series of values for each year to plot on a chart."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What I got:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A pretty solid implementation of the formula, with the series generation, and a basic chart. The AI handled the core math correctly on the first try — which was impressive. But here's where it went wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The 0% interest edge case&lt;/strong&gt; — as I mentioned, it returned &lt;code&gt;NaN&lt;/code&gt; without the fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The series generation&lt;/strong&gt; — it initially generated points for every compounding period, not every year. For daily compounding over 30 years, that's 10,950 points in the SVG. The chart was... let's say "visually noisy."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation&lt;/strong&gt; — it didn't handle negative numbers or non-numeric input gracefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How I iterated:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I went back with specific feedback:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The 0% case returns NaN. Also, I only want yearly points in the series for the chart, not every compounding period."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI fixed both issues. But it introduced a new one: it started generating the series using a different formula than the main calculation, so the chart didn't match the final value. Classic inconsistency bug.&lt;/p&gt;

&lt;p&gt;At that point, I stepped in and rewrote the series generation to use the same iterative formula as the main calculation. This is where I think the human still has an edge — understanding that two separate code paths computing the "same" thing will inevitably drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Got Right vs. Where I Had to Step In
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI handled well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The core math formula&lt;/li&gt;
&lt;li&gt;The HTML/CSS structure for the form&lt;/li&gt;
&lt;li&gt;The SVG chart generation (once I specified the requirements clearly)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where AI struggled:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge cases (zero interest, zero principal, zero contribution)&lt;/li&gt;
&lt;li&gt;Consistency between different parts of the calculation&lt;/li&gt;
&lt;li&gt;Understanding the UX implications of its choices (like generating 10,000 data points)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My honest take: for a tool like this, AI-assisted development saved me maybe 60-70% of the time. It's excellent for getting a working skeleton quickly. But the last 30% — the edge cases, the polish, the consistency — that's where I had to bring my own experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always test the edge cases.&lt;/strong&gt; The AI didn't, and I nearly shipped a calculator that showed &lt;code&gt;NaN&lt;/code&gt; for a very valid input (0% interest).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep it dependency-free when you can.&lt;/strong&gt; For a single-purpose tool, a full charting library is overkill. A polyline SVG does the job.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "periodic contribution" assumption matters.&lt;/strong&gt; I chose to model contributions at the end of each period (which is standard for most investment scenarios). But this is a subtle detail that affects results, and it's worth documenting for users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI is a great pair programmer, not a replacement.&lt;/strong&gt; It wrote the first draft in minutes. But I had to review every line with a skeptical eye.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Final Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that calculates compound interest with optional monthly contributions and displays a growth chart. You can try it out if you want to see the approach in action.&lt;/p&gt;

&lt;p&gt;The whole thing is about 200 lines of HTML/CSS/JavaScript. No build step, no dependencies, no server. Just open the file and it works.&lt;/p&gt;

&lt;p&gt;If you're building similar utility tools, I'd encourage you to think about whether you really need a backend. For calculation-heavy tools, the browser is surprisingly capable. And with AI to help you scaffold the initial implementation, you can go from idea to working tool in a single afternoon.&lt;/p&gt;

&lt;p&gt;Just remember to test the edge cases. The AI probably didn't.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Date Difference Calculator: The Hidden Complexity of Time</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Fri, 28 Aug 2026 06:25:52 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-date-difference-calculator-the-hidden-complexity-of-time-329d</link>
      <guid>https://dev.to/ggwork/building-a-date-difference-calculator-the-hidden-complexity-of-time-329d</guid>
      <description>&lt;p&gt;I was working on a project timeline feature when I hit an unexpected wall. The requirement seemed simple: "Show how many days are between these two dates." Easy, right? &lt;code&gt;(end - start) / 86400000&lt;/code&gt;. Done.&lt;/p&gt;

&lt;p&gt;Except it wasn't done. Because "days between" means completely different things depending on whether you're planning a sprint, calculating payroll, or tracking a contract period. And that's before we even get into time zones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with "Just Subtract the Dates"
&lt;/h2&gt;

&lt;p&gt;Here's the thing that trips up most developers (myself included): a date difference isn't just one number. It's several different numbers, each answering a different question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total days&lt;/strong&gt;: The raw duration in 24-hour blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calendar breakdown&lt;/strong&gt;: How many full years, months, and days? (Spoiler: it's not just &lt;code&gt;totalDays / 365&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working days&lt;/strong&gt;: How many weekdays? (Because weekends don't exist when you're on a deadline)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed all three, and I needed them to be accurate. Existing libraries like &lt;code&gt;date-fns&lt;/code&gt; or &lt;code&gt;dayjs&lt;/code&gt; handle some of this, but they're heavy for what I needed. Plus, I'm a bit of a minimalist when it comes to dependencies — adding a 50KB library to calculate what should be a few simple functions felt like using a chainsaw to cut a piece of string.&lt;/p&gt;

&lt;p&gt;So I decided to build it myself. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Time Zone Trap
&lt;/h2&gt;

&lt;p&gt;The first thing that bit me: time zones. JavaScript's &lt;code&gt;Date&lt;/code&gt; object is notoriously tricky with this. If you parse &lt;code&gt;"2026-08-01"&lt;/code&gt; with &lt;code&gt;new Date("2026-08-01")&lt;/code&gt;, you get UTC midnight — but if you're in a different timezone, that's actually the previous evening for you.&lt;/p&gt;

&lt;p&gt;The fix was to use &lt;code&gt;datetime-local&lt;/code&gt; inputs and parse everything in local time. No UTC conversions, no &lt;code&gt;getTimezoneOffset()&lt;/code&gt; gymnastics. Here's the core parsing function that saved my sanity:&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;function&lt;/span&gt; &lt;span class="nf"&gt;parseLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&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;That's it. The &lt;code&gt;datetime-local&lt;/code&gt; input format (&lt;code&gt;YYYY-MM-DDTHH:mm&lt;/code&gt;) is designed to be parsed as local time, so &lt;code&gt;new Date()&lt;/code&gt; handles it correctly. The key insight: &lt;strong&gt;don't try to be clever with timezones when you don't need to be&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Calendar Breakdown Problem
&lt;/h2&gt;

&lt;p&gt;Now for the interesting part. I needed to break down a date range into years, months, and days. This sounds straightforward until you realize that months have varying lengths, and "1 month from January 31" is genuinely ambiguous.&lt;/p&gt;

&lt;p&gt;I went with a calendar-push approach: increment the start date by years until you can't go further without exceeding the end date, then months, then days. Here's the logic that made it work:&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;function&lt;/span&gt; &lt;span class="nf"&gt;calendarBreakdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addMonths&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&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;The &lt;code&gt;addMonths&lt;/code&gt; function handles the edge cases — like February 29 — by clamping to the end of the month. It's not perfect (what is, with dates?), but it gives results that match human intuition about calendar spans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working Days: The Weekend Problem
&lt;/h2&gt;

&lt;p&gt;The workday calculation sounded easy: iterate through dates, skip Saturdays and Sundays. But here's a subtlety that caught me: should the workday count include the start and end dates? Most people think "between" means excluding endpoints, but in business contexts, you usually want inclusive counting.&lt;/p&gt;

&lt;p&gt;I went with inclusive counting, and the implementation is refreshingly simple:&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;function&lt;/span&gt; &lt;span class="nf"&gt;countWorkdays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;count&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;The &lt;code&gt;getDay()&lt;/code&gt; method returns 0 for Sunday and 6 for Saturday, so checking for those two values handles the entire weekend logic. No arrays, no constants, no lookup tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Actually Helped
&lt;/h2&gt;

&lt;p&gt;Now, the part you're probably curious about: how much of this did I build with AI assistance?&lt;/p&gt;

&lt;p&gt;I'll be honest — I used Claude for a significant portion of this. The interesting part was the iterative process. My first prompt was something like: "Write a date difference calculator that shows days, hours, and workdays."&lt;/p&gt;

&lt;p&gt;What I got back was... technically correct but practically useless. It used UTC everywhere, which would have been fine if I was building for a server, but this was a browser tool. The AI didn't think about timezones because I didn't ask it to.&lt;/p&gt;

&lt;p&gt;The back-and-forth went something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Me&lt;/strong&gt;: "The dates are off by one day when I test with local times."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: "Ah, you need to use local time parsing. Here's the fix." (And it was right.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me&lt;/strong&gt;: "Now the calendar breakdown doesn't match what I expect for ranges crossing month boundaries."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: "You need to use a calendar-push approach instead of just dividing total days."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI was genuinely helpful for catching edge cases I hadn't considered and for refactoring the logic. But it also made mistakes — the first version of the workday calculator excluded the start date, which I had to catch and fix.&lt;/p&gt;

&lt;p&gt;My honest take: AI is excellent at generating correct code for well-defined problems, but it's terrible at understanding what "well-defined" means for your specific use case. You still need to be the domain expert.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool That Emerged
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a date difference calculator that handles all three modes — total days, calendar breakdown, and workday counting — with a clean interface that doesn't require any server-side processing.&lt;/p&gt;

&lt;p&gt;The UI is deliberately minimal: two date inputs, a checkbox for workday mode, and a results panel. The entire thing runs in the browser with zero dependencies, which means it's instant and works offline.&lt;/p&gt;

&lt;p&gt;One design decision I'm particularly happy about: defaulting to showing "today → 30 days from now" as an example. It gives users immediate feedback and demonstrates the tool's capabilities without requiring them to figure out the interface first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time zones are the root of all evil&lt;/strong&gt;. If you don't need to handle them, don't. Local time parsing with &lt;code&gt;datetime-local&lt;/code&gt; inputs sidesteps an entire class of bugs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Days between" is ambiguous&lt;/strong&gt;. Always clarify whether you need total days, calendar days, or working days. They're all different numbers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI is a pair programmer, not a replacement&lt;/strong&gt;. It caught edge cases I missed, but it also missed cases I caught. We complemented each other.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dependencies aren't always worth it&lt;/strong&gt;. For something this focused, a few well-written functions beat a library every time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tool is live at &lt;a href="https://craftvo.app/en/tool/date-difference" rel="noopener noreferrer"&gt;craftvo.app&lt;/a&gt; if you want to see the final result. It's free, it's fast, and it handles the date math so you don't have to.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I need to go explain to my PM why "2 days" and "48 hours" are apparently different numbers.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>Building a Braille Translator in the Browser: When Unicode Bit Manipulation Meets AI Pair Programming</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:20:50 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-braille-translator-in-the-browser-when-unicode-bit-manipulation-meets-ai-pair-7g</link>
      <guid>https://dev.to/ggwork/building-a-braille-translator-in-the-browser-when-unicode-bit-manipulation-meets-ai-pair-7g</guid>
      <description>&lt;p&gt;While working on a collection of browser-based utility tools, I stumbled upon an interesting challenge: building a Braille translator. Not because I have any personal connection to Braille — I'm just a developer who loves Unicode puzzles. But the problem turned out to be more fascinating than I expected, with a hidden layer of complexity that made me rethink how I approach character encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Asked Me to Solve
&lt;/h2&gt;

&lt;p&gt;I was building a suite of small, focused web tools. You know the type — URL encoders, JSON formatters, maybe a base64 decoder. Useful, boring, done a thousand times. But Braille? That's different.&lt;/p&gt;

&lt;p&gt;Here's the thing: most "Braille translators" online are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flash relics from 2005 that barely work on modern browsers&lt;/li&gt;
&lt;li&gt;Server-side tools that require uploading your text somewhere&lt;/li&gt;
&lt;li&gt;Overly complex accessibility suites when all you need is "text → Braille → text"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that runs entirely in the browser, works offline, and doesn't send sensitive text to a server. Because apparently, I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment I Realized Braille is Just Binary
&lt;/h2&gt;

&lt;p&gt;Here's what blew my mind: Braille isn't some mystical code. It's literally binary.&lt;/p&gt;

&lt;p&gt;A Braille cell has 6 dots, arranged in 2 columns and 3 rows. Each dot is either raised or flat. That's 2^6 = 64 possible combinations. And in Unicode, those 64 combinations map directly to code points U+2800 through U+283F.&lt;/p&gt;

&lt;p&gt;The mapping is beautifully simple:&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="c1"&gt;// Dot 1 is bit 0, dot 2 is bit 1, dot 3 is bit 2, etc.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;brailleCodePoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x2800&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dotPatternNumber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if you know the dot pattern for a letter, you just add the corresponding number to 0x2800. The letter 'a' is dot 1, so it's 0x2800 + 1 = 0x2801, which is ⠁. The letter 'b' is dots 1 and 2, so it's 0x2800 + 3 = 0x2803, which is ⠃.&lt;/p&gt;

&lt;p&gt;Wait, did I just say "the letter 'a' is dot 1"? Because that's where the fun begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alphabet Is Not What You Think
&lt;/h2&gt;

&lt;p&gt;Here's where my initial assumptions fell apart. I thought I'd just create a simple dictionary mapping letters to their Unicode Braille characters. Then I discovered the actual Braille alphabet pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a = dot 1&lt;/li&gt;
&lt;li&gt;b = dots 1,2&lt;/li&gt;
&lt;li&gt;c = dots 1,4&lt;/li&gt;
&lt;li&gt;d = dots 1,4,5&lt;/li&gt;
&lt;li&gt;e = dots 1,5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first 10 letters (a-j) follow a pattern, then k-t add dot 3 to the first 10, and u-z follow another pattern. It's almost systematic, but not quite. There's a method to the madness, but you can't derive it from a simple formula — you need the actual mapping.&lt;/p&gt;

&lt;p&gt;I initially tried to write a clever algorithmic solution. That lasted about 15 minutes before I gave up and hardcoded the mapping. Sometimes the simple solution is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Number Problem: A Trap for the Unwary
&lt;/h2&gt;

&lt;p&gt;Just when I thought I had it figured out, I hit the numbers issue.&lt;/p&gt;

&lt;p&gt;In Braille, the letters a-j double as numbers 1-0. But you can't just type "123" and get Braille numbers — you need a special number sign (⠼) before them. So "123" becomes "⠼⠁⠃⠉", not just "⠁⠃⠉".&lt;/p&gt;

&lt;p&gt;This creates an interesting ambiguity: if you see "⠁⠃⠉" without the number sign, is it "abc" or "123"? The answer is context-dependent, which makes bidirectional translation genuinely tricky.&lt;/p&gt;

&lt;p&gt;My solution: when translating text to Braille, detect digits and prepend the number sign once for a run of consecutive digits. When translating Braille to text, if the number sign appears, treat the next characters as digits until you hit a space or non-letter character.&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;function&lt;/span&gt; &lt;span class="nf"&gt;textToBraille&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;inNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;inNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;BRAILLE_NUMBER_SIGN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;inNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;BRAILLE_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;inNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;BRAILLE_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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;This was one of those moments where I realized: "I'm not just building a translator, I'm building a state machine."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dot Pattern Visualization
&lt;/h2&gt;

&lt;p&gt;Now for the part that made this tool actually useful: showing the dot patterns. Because if you're learning Braille, just seeing "⠓" doesn't help — you need to know that's dots 1, 2, and 5.&lt;/p&gt;

&lt;p&gt;The visualization is straightforward once you have the dot pattern number:&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;function&lt;/span&gt; &lt;span class="nf"&gt;getDots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codePoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&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;codePoint&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x2800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dots&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;Bit manipulation. Again. It's like binary is following me around.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Assisted Development: The Good, The Bad, The Ugly
&lt;/h2&gt;

&lt;p&gt;Now for the part I'm most honest about: I used AI to build much of this. And it was a mixed experience.&lt;/p&gt;

&lt;p&gt;The first prompt I gave was something like: "Create a Braille translator with text to Braille and Braille to text conversion, with a reference chart."&lt;/p&gt;

&lt;p&gt;The AI nailed the basic structure in one shot. It created the HTML layout, the CSS styling, the i18n system — all the scaffolding. I was impressed.&lt;/p&gt;

&lt;p&gt;But then came the bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #1: The Case Sensitivity Trap
&lt;/h3&gt;

&lt;p&gt;The AI initially made the translation case-sensitive. "HELLO" would produce different results than "hello". In Braille, there's no such thing as lowercase — it's all one case. The AI hadn't thought about normalizing input to lowercase before mapping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #2: The Number Sign Placement
&lt;/h3&gt;

&lt;p&gt;The AI's first attempt at numbers was a disaster. It would put a number sign before every single digit, so "123" became "⠼⠁⠼⠃⠼⠉" instead of "⠼⠁⠃⠉". That's like writing "one hundred twenty three" as "one-one-hundred-twenty-three". Technically understandable, but completely wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #3: The Reverse Translation Mapping
&lt;/h3&gt;

&lt;p&gt;The biggest issue was reverse translation. The AI had a static map for text-to-Braille but didn't properly handle the reverse lookup. When I asked it to translate Braille back to text, it would get stuck on ambiguous characters. Is "⠁" an "a" or a "1"? Without context, you can't tell.&lt;/p&gt;

&lt;p&gt;The AI's solution was to always treat it as a letter unless the number sign appeared. That's actually correct, but the AI couldn't articulate why it made that choice — it just happened to be right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Had to Step In
&lt;/h2&gt;

&lt;p&gt;The AI was great at generating boilerplate and basic logic, but it kept making the same class of mistakes: not understanding the domain. It didn't know that Braille is case-insensitive, that numbers need special handling, or that the dot pattern visualization would be the most useful feature for learners.&lt;/p&gt;

&lt;p&gt;I had to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Explain the number sign rule explicitly, multiple times&lt;/li&gt;
&lt;li&gt;Provide the actual Braille mapping table (the AI kept hallucinating wrong patterns)&lt;/li&gt;
&lt;li&gt;Design the dot visualization myself — the AI's first attempt was a mess of nested divs that looked like a CSS nightmare&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson? AI is great for generating code, but it's terrible at understanding domain-specific rules it wasn't explicitly trained on. Braille is a niche topic, and the AI's training data apparently had conflicting information about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Challenge
&lt;/h2&gt;

&lt;p&gt;Building bilingual support (Chinese/English) added another layer of complexity. The tool needed to work for both Chinese and English speakers, which meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All UI strings needed translation&lt;/li&gt;
&lt;li&gt;The language switcher needed to persist (I used localStorage)&lt;/li&gt;
&lt;li&gt;The Braille reference chart needed to be language-agnostic (it is — Braille is Braille)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The i18n system itself was straightforward:&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;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;盲文翻译器&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;textInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;文本输入&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;brailleOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;盲文输出&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Braille Translator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;textInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Text Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;brailleOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Braille Output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&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;The tricky part was making the AI understand that the default language should be Chinese, not English. It kept defaulting to English and I had to keep reminding it. Classic "works on my machine" situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;For a tool like this, performance is almost a non-issue. The entire translation is O(n) — you're just iterating over characters and doing a lookup. Even a 10,000-character document would translate in milliseconds.&lt;/p&gt;

&lt;p&gt;But there was one performance consideration: the reference chart. Rendering 26 letters + 10 digits + punctuation as individual DOM elements adds up. I used a simple CSS grid with &lt;code&gt;auto-fill&lt;/code&gt; columns, which handles responsiveness gracefully without JavaScript. No virtual scrolling needed for 60 items, but it's worth thinking about if you ever scale this to include contractions (the full Braille system has hundreds of contractions).&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Why" Behind the Architecture
&lt;/h2&gt;

&lt;p&gt;I chose vanilla JavaScript over React or Vue for a few reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — this tool needs to work forever, even if npm packages disappear&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single file&lt;/strong&gt; — easy to embed, easy to share, easy to deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No build step&lt;/strong&gt; — I can just open the HTML file and it works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off? I had to write more boilerplate for things like state management and event handling. But for a tool this small, it's the right call. A React app with a build step would be overkill for what's essentially a &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; with a mapping function.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bit manipulation is everywhere&lt;/strong&gt; — even in something as human as Braille&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain knowledge beats clever code&lt;/strong&gt; — you can't algorithmically derive the Braille alphabet; you need the actual mapping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI is a junior developer&lt;/strong&gt; — great at scaffolding, bad at domain-specific edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple tools are underrated&lt;/strong&gt; — sometimes the best solution is a single HTML file that just works&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Final Product
&lt;/h2&gt;

&lt;p&gt;The result is a browser-based Braille translator that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converts text to Braille and back&lt;/li&gt;
&lt;li&gt;Shows dot patterns for each character&lt;/li&gt;
&lt;li&gt;Handles numbers correctly (with the number sign)&lt;/li&gt;
&lt;li&gt;Works in both Chinese and English&lt;/li&gt;
&lt;li&gt;Has a complete reference chart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. You can find it &lt;a href="https://craftvo.app/en/tool/braille-translator" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you're curious.&lt;/p&gt;

&lt;p&gt;The best part? I learned something unexpected about Braille — it's not a mystery code, it's just binary with a different face. And that's the kind of discovery that makes building these little tools worthwhile.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. If you're wondering why I chose this project: I wanted to build something that would make me think about Unicode in a new way. Mission accomplished. The fact that it might actually help someone learn Braille is just a bonus.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>javascript</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From "What's the MIME for That?" to a 200+ Entry Reference Tool</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:08:25 +0000</pubDate>
      <link>https://dev.to/ggwork/from-whats-the-mime-for-that-to-a-200-entry-reference-tool-1a5j</link>
      <guid>https://dev.to/ggwork/from-whats-the-mime-for-that-to-a-200-entry-reference-tool-1a5j</guid>
      <description>&lt;p&gt;You know that moment when you're building an upload feature and you need to validate file types, and you find yourself Googling "what's the MIME type for .webp" for the fifth time that week? That was me last month. I was working on a file upload handler and kept hitting the same wall: I'd remember common ones like &lt;code&gt;image/jpeg&lt;/code&gt;, but then I'd need something obscure like &lt;code&gt;.avif&lt;/code&gt; or &lt;code&gt;.m4a&lt;/code&gt; and I'd be back to searching.&lt;/p&gt;

&lt;p&gt;The worst part? The answers were scattered across MDN docs, Stack Overflow threads from 2012, and random GitHub gists with questionable accuracy. I wanted a single source of truth I could reference quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Just Build It" Temptation
&lt;/h2&gt;

&lt;p&gt;I considered just hardcoding the few types I needed. But I knew that was a band-aid. I'd be back in the same spot next week with a different project. So I did what any reasonable developer would do: I decided to build a comprehensive reference tool.&lt;/p&gt;

&lt;p&gt;Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Problem
&lt;/h2&gt;

&lt;p&gt;The first challenge was obvious: I needed a solid MIME type database. Not just the common ones — I wanted coverage across documents, images, video, audio, archives, code, fonts, and "other" stuff that doesn't fit neatly anywhere.&lt;/p&gt;

&lt;p&gt;Here's what I learned: there's no official "MIME type registry" that's both comprehensive and easy to consume. The IANA registry is the authority, but it's a massive HTML table that's painful to scrape. I ended up cross-referencing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The IANA registry (for official types)&lt;/li&gt;
&lt;li&gt;MDN's MIME type documentation (for practical usage)&lt;/li&gt;
&lt;li&gt;My own experience (for the "what people actually use" factor)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data structure ended up being refreshingly simple:&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebP 图片&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;That's it. Four fields. No relational database, no API calls — just a plain array in JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Search Logic: Simpler Than You'd Think
&lt;/h2&gt;

&lt;p&gt;The search was the fun part. I wanted bidirectional lookup — type an extension and get the MIME, or type a MIME and get extensions. My first approach was embarrassingly over-engineered. I was thinking about fuzzy matching, Levenshtein distances, the whole nine yards.&lt;/p&gt;

&lt;p&gt;Then I realized: developers searching for MIME types aren't typos. They know exactly what they're looking for. They just need quick verification. So I kept it simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strip leading dots from the input&lt;/li&gt;
&lt;li&gt;Case-insensitive substring matching&lt;/li&gt;
&lt;li&gt;Check against extension, MIME type, and description&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The debounce was the only "fancy" part — 200ms to prevent the render function from firing on every keystroke. The whole search logic fits in about 15 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration Experiment
&lt;/h2&gt;

&lt;p&gt;Here's where things got interesting. I decided to build this with AI assistance — not as a gimmick, but because I genuinely wanted to see how well it could handle a well-specified, self-contained project. This felt like the perfect test case: clear requirements, no external dependencies, pure frontend.&lt;/p&gt;

&lt;p&gt;I gave the AI a detailed spec: the exact data structure, the interaction patterns, the UI layout, even the SEO tags. The first attempt was... surprisingly good. It nailed the structure and the search logic on the first try.&lt;/p&gt;

&lt;p&gt;But it completely missed the dark mode support. Classic "works on my machine" situation — the AI assumed everyone uses light mode.&lt;/p&gt;

&lt;p&gt;The iteration process was where the real value came through. I'd say "add prefers-color-scheme media queries" and it would generate the CSS. Then I'd notice the category badges were unreadable in dark mode, and it would fix those specific color values. It was like pair programming with a very fast, slightly forgetful junior developer.&lt;/p&gt;

&lt;p&gt;The part where I had to step in? The data. The AI's initial MIME database was thin — maybe 60 entries. I had to explicitly list the file types I wanted covered: "add these: cpp, java, py, go, rs, woff, woff2, ttf, otf..." That's the kind of domain knowledge that AI still can't fully replicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Surprise
&lt;/h2&gt;

&lt;p&gt;I initially thought internationalization would be the hardest part. I was planning a full i18n library integration. But for a tool like this, a lightweight approach made more sense:&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;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type 查询&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;输入扩展名或MIME...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type Lookup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter extension or MIME...&lt;/span&gt;&lt;span class="dl"&gt;'&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;Just a simple dictionary with a language switcher. The descriptions in the database stay in their original language — which actually makes sense because MIME types themselves are language-agnostic. The tool is for developers, and developers know what &lt;code&gt;image/jpeg&lt;/code&gt; means regardless of their native language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Copy Interaction
&lt;/h2&gt;

&lt;p&gt;The click-to-copy feature was a requirement I initially underestimated. I thought &lt;code&gt;navigator.clipboard.writeText()&lt;/code&gt; was a solved problem. Turns out, it has a subtle gotcha: it only works in secure contexts (HTTPS or localhost). For a tool that people might open as a local HTML file, I needed a fallback.&lt;/p&gt;

&lt;p&gt;The fallback is the old-school approach:&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;textarea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;textarea&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;mime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's deprecated, but it works everywhere. The lesson here: don't assume modern APIs are universally available, even for a "modern" tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: The "It's Just an Array" Moment
&lt;/h2&gt;

&lt;p&gt;I spent way too long thinking about performance optimization. Should I use a Map? A binary search tree? Index the data by extension?&lt;/p&gt;

&lt;p&gt;Then I realized: the entire dataset is 215 entries. The search runs in milliseconds even with a naive filter. The only performance consideration that actually mattered was the debounce on the search input, and even that was more about avoiding excessive DOM updates than actual CPU usage.&lt;/p&gt;

&lt;p&gt;The table rendering? I'm just regenerating the HTML string and setting &lt;code&gt;innerHTML&lt;/code&gt;. It's not React, but it's also not 10,000 rows. Sometimes the simplest solution is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stats That Matter
&lt;/h2&gt;

&lt;p&gt;One feature I'm glad I added: the result counter. It shows "X results out of Y total entries." It sounds trivial, but when you're searching for something and get zero results, seeing "0 results out of 215 entries" is much more reassuring than just "No results found." It confirms the tool is working — you just searched for something that doesn't exist in the database.&lt;/p&gt;

&lt;p&gt;Speaking of no results: I made sure to include a friendly empty state. Because nothing kills developer trust faster than a blank screen with no explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were to rebuild this, I'd probably add a "random MIME type" button. Not because it's useful, but because it's fun. Sometimes you want to test how your code handles unexpected input.&lt;/p&gt;

&lt;p&gt;I'd also consider adding copy feedback per-row instead of a global status message. Right now, clicking any row copies the MIME and shows a status bar message. It works, but a per-row "copied!" tooltip would be more immediate feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict on AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;This project was a genuine test of AI-assisted development, and my honest take is: it's great for boilerplate and structure, but it still needs a human for judgment calls.&lt;/p&gt;

&lt;p&gt;The AI handled the CSS layout, the search logic, the i18n structure, and even the SEO meta tags without complaint. It made mistakes — the dark mode oversight being the biggest one — but they were easy to catch and fix.&lt;/p&gt;

&lt;p&gt;What the AI couldn't do was understand the subtle requirements that come from real-world usage. It didn't know that &lt;code&gt;.docx&lt;/code&gt; is more common than &lt;code&gt;.doc&lt;/code&gt; in 2024. It didn't know that &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; deserves a spot in the database even though it's not a file extension. Those insights come from experience, not from training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;I built this tool because I was tired of searching for MIME types. The process taught me that even "boring" developer tools have interesting engineering decisions lurking beneath the surface. The data structure matters more than the algorithm. The UX details (like the result counter and empty states) matter more than the feature list.&lt;/p&gt;

&lt;p&gt;And if you're wondering: yes, I still Google MIME types sometimes. But now I have a better starting point.&lt;/p&gt;




&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. If you're dealing with the same problem, you can find it at &lt;a href="https://craftvo.app/en/tool/content-type-lookup" rel="noopener noreferrer"&gt;Craftvo's Content-Type Lookup&lt;/a&gt;. It's just a plain HTML file with vanilla JS — no build tools, no frameworks, no server. Open it, search, copy, move on with your life.&lt;/p&gt;




&lt;p&gt;Tags: &lt;code&gt;mime-types&lt;/code&gt;, &lt;code&gt;web-development&lt;/code&gt;, &lt;code&gt;javascript&lt;/code&gt;, &lt;code&gt;developer-tools&lt;/code&gt;, &lt;code&gt;ai-assisted-development&lt;/code&gt;&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Base64 Tool That Actually Handles UTF-8 (Without Losing Your Mind)</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Tue, 25 Aug 2026 06:54:20 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-base64-tool-that-actually-handles-utf-8-without-losing-your-mind-d78</link>
      <guid>https://dev.to/ggwork/building-a-base64-tool-that-actually-handles-utf-8-without-losing-your-mind-d78</guid>
      <description>&lt;h2&gt;
  
  
  The Problem That Started It All
&lt;/h2&gt;

&lt;p&gt;I was working on a project that required sending configuration data between different services. The payload contained Chinese characters, emojis, and the occasional accented character from a French colleague's name in the comments. Classic stuff.&lt;/p&gt;

&lt;p&gt;The first time I tested it, everything worked. The second time, I got back a string of &lt;code&gt;Ã©&lt;/code&gt; and &lt;code&gt;â€™&lt;/code&gt; instead of actual text. You know the drill — mojibake. The kind of thing that makes you question every life decision that led you to this moment.&lt;/p&gt;

&lt;p&gt;The root cause? I was using &lt;code&gt;btoa()&lt;/code&gt; and &lt;code&gt;atob()&lt;/code&gt; directly on strings containing non-ASCII characters. These functions only handle Latin-1 (ISO-8859-1) natively. Feed them a Chinese character and they'll happily mangle it into something that looks like it fell out of a corrupted file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use an Existing Tool?
&lt;/h2&gt;

&lt;p&gt;I could have opened any of the dozens of online Base64 converters. But I had specific requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I needed something that handles UTF-8 correctly (the mojibake problem above)&lt;/li&gt;
&lt;li&gt;I wanted it to work offline&lt;/li&gt;
&lt;li&gt;I didn't want to paste sensitive config data into some random website&lt;/li&gt;
&lt;li&gt;I was tired of tools that required clicking a "Convert" button when I just wanted live feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I decided to build a small browser-based tool. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UTF-8 Problem (The Part That Actually Matters)
&lt;/h2&gt;

&lt;p&gt;Here's the core issue with &lt;code&gt;btoa()&lt;/code&gt; and &lt;code&gt;atob()&lt;/code&gt;:&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="c1"&gt;// This works fine&lt;/span&gt;
&lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// This throws an error or produces garbage&lt;/span&gt;
&lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;你好世界&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;The fix is straightforward but easy to miss if you're in a hurry:&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;function&lt;/span&gt; &lt;span class="nf"&gt;encodeUTF8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decodeUTF8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&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;The &lt;code&gt;TextEncoder&lt;/code&gt; converts the string to UTF-8 bytes, and &lt;code&gt;String.fromCharCode(...)&lt;/code&gt; turns those bytes into a string that &lt;code&gt;btoa()&lt;/code&gt; can handle. The decode path reverses this process.&lt;/p&gt;

&lt;p&gt;One gotcha: for very large inputs, &lt;code&gt;String.fromCharCode(...array)&lt;/code&gt; can blow the call stack. In production, you'd want a chunked approach. But for typical text inputs, this works fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real-Time Conversion Decision
&lt;/h2&gt;

&lt;p&gt;The most interesting design decision was whether to convert on every keystroke or wait for user action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Real-time conversion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Instant feedback, feels magical&lt;/li&gt;
&lt;li&gt;Con: Can be janky with large inputs, potentially wasteful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option B: Manual conversion button&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Predictable, performant&lt;/li&gt;
&lt;li&gt;Con: Extra click, feels dated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up going with a hybrid: real-time by default, but with a manual override. Users can toggle it off if they're pasting a massive file. The implementation is simple — just listen to the input event and debounce:&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;let&lt;/span&gt; &lt;span class="nx"&gt;debounceTimer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;realTimeToggle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debounceTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;debounceTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&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;The debounce is critical. Without it, every keystroke triggers a full conversion cycle, which causes noticeable lag on longer inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Encoding/Decoding Direction Problem
&lt;/h2&gt;

&lt;p&gt;Here's something I didn't think about until I started building: when you swap between encode and decode modes, what happens to the existing input?&lt;/p&gt;

&lt;p&gt;If someone types "hello" in encode mode, then switches to decode mode, should "hello" be decoded as Base64? That would produce garbage. The sensible behavior is to clear the input when the mode changes.&lt;/p&gt;

&lt;p&gt;But wait — there's a more interesting UX pattern. What if the user wants to decode something they just encoded? They'd have to copy the output, switch modes, and paste it back. That's annoying.&lt;/p&gt;

&lt;p&gt;This is where the swap button comes in. Instead of manually copying and switching, one click moves the current output into the input field and flips the mode. It's a small touch, but it makes the tool feel much more fluid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Error Handling Trap
&lt;/h2&gt;

&lt;p&gt;Decoding invalid Base64 is a surprisingly common failure mode. Users paste in text that looks like Base64 but isn't — maybe it has a space in it, or they copied a URL fragment by accident.&lt;/p&gt;

&lt;p&gt;The naive approach:&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid Base64&lt;/span&gt;&lt;span class="dl"&gt;"&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;This works, but it's not very helpful. The better approach is to validate the input format first and give specific feedback:&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;function&lt;/span&gt; &lt;span class="nf"&gt;isValidBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9+&lt;/span&gt;&lt;span class="se"&gt;/]&lt;/span&gt;&lt;span class="sr"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;{0,2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&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;This catches the most common issues: wrong length and invalid characters. The error message can then tell the user &lt;em&gt;what's wrong&lt;/em&gt; rather than just "that didn't work."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI Got Right and Wrong
&lt;/h2&gt;

&lt;p&gt;I built this tool with AI assistance, and I want to be honest about the experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the AI nailed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The initial structure and layout&lt;/li&gt;
&lt;li&gt;The i18n setup with language detection&lt;/li&gt;
&lt;li&gt;The dark mode implementation using CSS variables&lt;/li&gt;
&lt;li&gt;The basic encode/decode logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where I had to step in:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first version of the UTF-8 handling was wrong. The AI used &lt;code&gt;encodeURIComponent&lt;/code&gt; as a workaround, which produces a URL-encoded string that isn't proper Base64. It worked for basic cases but broke with certain character combinations.&lt;/p&gt;

&lt;p&gt;I had to explain the issue and iterate:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The current implementation uses encodeURIComponent which produces incorrect Base64. We need to use TextEncoder/TextDecoder instead. Here's the correct approach..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI then generated the correct implementation. But here's the thing — I only knew it was wrong because I'd hit this exact bug before. If I hadn't, I would have shipped broken code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; AI is great at scaffolding and boilerplate, but you still need to understand the domain well enough to catch subtle bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Decision
&lt;/h2&gt;

&lt;p&gt;I'm building this tool for a global audience, so I needed English and Chinese support. The requirement was simple: detect the browser language, allow a URL override, and default to Chinese.&lt;/p&gt;

&lt;p&gt;The implementation was straightforward:&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;translations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Base64 编码/解码&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;encode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;编码&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;解码&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Base64 Encode/Decode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;encode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Encode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Decode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectLanguage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urlLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&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;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlLang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;urlLang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;urlLang&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&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;The &lt;code&gt;t('key')&lt;/code&gt; function then looks up translations:&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;function&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentLang&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;key&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;Simple, but it works. No external dependencies, no build step, just a plain object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;For a tool like this, performance is mostly about not being stupid. The main concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Large inputs&lt;/strong&gt;: A 10MB file will freeze the browser if you try to process it synchronously on every keystroke. The debounce helps, but you might also need to cap the input size.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;String operations&lt;/strong&gt;: &lt;code&gt;String.fromCharCode(...array)&lt;/code&gt; is fine for typical inputs but will throw a RangeError for arrays larger than about 65K elements. For robustness, you'd chunk it:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bytesToBinaryString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x8000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;DOM updates&lt;/strong&gt;: Don't update the output textarea on every keystroke. Batch updates or use &lt;code&gt;requestAnimationFrame&lt;/code&gt; if needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The "it works on my machine" moment:&lt;/strong&gt; The first version of the tool worked perfectly with English text. It wasn't until I tested with Chinese characters that the encoding bug surfaced. This is a reminder to always test with the character sets your users will actually use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "it's always a CSS issue" moment:&lt;/strong&gt; The dark mode implementation initially had a bug where textareas kept a light background in dark mode. It was a specificity issue — the &lt;code&gt;background&lt;/code&gt; property on &lt;code&gt;textarea&lt;/code&gt; was overriding the CSS variable. The fix was to use the variable consistently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--bg-input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--text&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;&lt;strong&gt;The "AI is a junior developer" moment:&lt;/strong&gt; AI-assisted development is powerful, but it's not a replacement for understanding the fundamentals. The AI could write the code, but I had to know enough to catch the subtle bugs. It's like having a very fast junior dev who occasionally makes confident mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It handles UTF-8 correctly, supports real-time conversion, and works entirely in the browser — no data ever leaves your machine.&lt;/p&gt;

&lt;p&gt;The full implementation is a single HTML file with inline CSS and JavaScript, which makes it easy to deploy or even save locally for offline use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Base64 encoding seems simple until it isn't. The UTF-8 handling is the kind of thing that looks trivial in theory but causes real production issues. If you're building anything that processes text, take five minutes to test with non-ASCII characters. Your future self will thank you.&lt;/p&gt;

&lt;p&gt;And if you're using AI to write code, remember: it's a tool, not a replacement for understanding. The best workflow is to use AI for the parts that are tedious and mechanical, then apply your own knowledge to catch the subtle issues.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;You can try the tool at &lt;a href="https://craftvo.app/en/tool/base64-encoder" rel="noopener noreferrer"&gt;Craftvo&lt;/a&gt; if you're curious about the implementation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Countdown Timer Without External Dependencies: A Vanilla JS Journey</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Tue, 25 Aug 2026 01:12:48 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-countdown-timer-without-external-dependencies-a-vanilla-js-journey-25m7</link>
      <guid>https://dev.to/ggwork/building-a-countdown-timer-without-external-dependencies-a-vanilla-js-journey-25m7</guid>
      <description>&lt;p&gt;There's something oddly satisfying about building a tool that thousands of people might use daily, especially when you decide to do it with zero dependencies. No React, no Vue, no moment.js — just good old-fashioned vanilla JavaScript, HTML, and CSS.&lt;/p&gt;

&lt;p&gt;Last month, I needed a countdown timer for an upcoming project deadline. I could have downloaded any of the hundreds of countdown apps available. But as developers, we often find ourselves thinking, "I could build this myself in an afternoon." Spoiler alert: it took longer than an afternoon, but the journey was worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Existing Solutions
&lt;/h2&gt;

&lt;p&gt;Most countdown timer tools online are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bloated with features I didn't need (who needs a countdown with confetti animations?)&lt;/li&gt;
&lt;li&gt;Require desktop installation (I wanted something browser-based)&lt;/li&gt;
&lt;li&gt;Have terrible mobile responsiveness (nobody wants to pinch-zoom on a timer)&lt;/li&gt;
&lt;li&gt;Are riddled with ads and tracking scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something simple: set a date, see the time remaining, get a visual progress indicator. That's it. No account creation, no email signup, no "premium features" paywall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Decision: Why Vanilla?
&lt;/h2&gt;

&lt;p&gt;When I started planning, I caught myself reaching for React out of habit. But then I thought about it — this is a single-purpose tool with minimal state. Do I really need a virtual DOM for a timer?&lt;/p&gt;

&lt;p&gt;The answer was no. Here's my reasoning:&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="c1"&gt;// The entire state management for this app&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Two variables and a timer interval. Adding React would have meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A build process&lt;/li&gt;
&lt;li&gt;Bundle size overhead&lt;/li&gt;
&lt;li&gt;More complexity than the actual problem requires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the simplest solution is the right one. This was one of those times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Challenge: Time Calculation
&lt;/h2&gt;

&lt;p&gt;The heart of any countdown timer is the time calculation logic. It sounds simple — subtract one date from another — but there are edge cases that'll bite you if you're not careful.&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;function&lt;/span&gt; &lt;span class="nf"&gt;updateCountdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Handle expired state&lt;/span&gt;
        &lt;span class="nf"&gt;showExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Update DOM&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tricky part? Timezone handling. If someone in Tokyo sets a target date, and someone in New York views it, the calculations need to be consistent. I initially used &lt;code&gt;new Date()&lt;/code&gt; everywhere, but quickly realized I needed to be more careful with how I stored and compared dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Works on My Machine" Moment
&lt;/h2&gt;

&lt;p&gt;Here's where things got interesting. I tested the timer locally, and everything worked perfectly. Then I deployed it, and a user in Australia reported that the countdown was off by a day.&lt;/p&gt;

&lt;p&gt;Classic "works on my machine" situation.&lt;/p&gt;

&lt;p&gt;The issue? I was using local timezone offsets implicitly. When a user selected a date in their local timezone, I needed to store it as a timestamp (UTC) to ensure consistent calculations across timezones.&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="c1"&gt;// The fix: convert to UTC timestamp when storing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetTimestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dateInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a humbling reminder that even "simple" time calculations have hidden complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Assisted Development: The Honest Truth
&lt;/h2&gt;

&lt;p&gt;Now, here's where I need to be completely transparent. I used AI tools (mostly Claude and ChatGPT) throughout this project. Here's how it actually went — the good, the bad, and the ugly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI Did Well
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The initial skeleton&lt;/strong&gt;: I described my requirements — a single-file HTML tool with dark mode support, i18n, and responsive design — and the AI generated a solid starting point in minutes. The CSS variables structure, the basic layout, and the initial JavaScript logic were all well-structured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The i18n implementation&lt;/strong&gt;: I asked for a lightweight internationalization system that supported English and Chinese, with language detection based on URL parameters and browser settings. The AI came up with a clean solution:&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;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;倒计时器&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;天&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;时&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Countdown Timer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;days&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hrs&lt;/span&gt;&lt;span class="dl"&gt;'&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;&lt;strong&gt;Dark mode&lt;/strong&gt;: The media query for &lt;code&gt;prefers-color-scheme&lt;/code&gt; was generated perfectly. No complaints there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where AI Struggled
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The timezone bug&lt;/strong&gt;: I spent two hours debugging a timezone issue that AI kept getting wrong. Every time I described the problem, it would suggest a fix that worked for the specific case but broke another edge case. Eventually, I had to reason through it myself and implement the solution manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progress bar logic&lt;/strong&gt;: The AI's initial implementation of the progress bar was mathematically incorrect for the "expired" state. It took several iterations of me explaining the requirements before it got the logic right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance optimization&lt;/strong&gt;: The AI initially used &lt;code&gt;setInterval&lt;/code&gt; with DOM updates every 100ms, which caused unnecessary reflows. I had to point out that we only need to update once per second, and that we should batch DOM updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Iterative Process
&lt;/h2&gt;

&lt;p&gt;Working with AI felt like pair programming with a very enthusiastic junior developer. It's fast, eager, and sometimes confidently wrong. Here's what my workflow looked like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Describe the feature&lt;/strong&gt;: "I need a preset button that sets the target date to next New Year's Day"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get the initial implementation&lt;/strong&gt;: AI generates code that mostly works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test and break it&lt;/strong&gt;: Find the edge case that fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report the bug&lt;/strong&gt;: Describe what happened and what should have happened&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate&lt;/strong&gt;: Repeat until it works correctly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, the "New Year" preset initially set the date to January 1st of the current year, which meant it was always in the past. The fix required understanding the business logic:&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;function&lt;/span&gt; &lt;span class="nf"&gt;setNewYearPreset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextYear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;targetDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// January 1, next year&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The "Aha" Moment: Single File Architecture
&lt;/h2&gt;

&lt;p&gt;One of my best decisions was keeping everything in a single HTML file. Here's why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No build step&lt;/strong&gt;: Edit, save, refresh. That's the entire development loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy deployment&lt;/strong&gt;: Just upload one file to any static host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant sharing&lt;/strong&gt;: Users can save the file locally and use it offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: No network requests for assets — everything is inline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off? The file gets large (around 15KB for this project), but for a utility tool, that's perfectly acceptable. Users get instant load times with zero requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Progress Bar Problem
&lt;/h2&gt;

&lt;p&gt;The progress bar was trickier than expected. If the user sets a start date, we can show progress. But what if they don't? I needed to handle three states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No start date set → Show no progress bar&lt;/li&gt;
&lt;li&gt;Start date in the past → Calculate progress as &lt;code&gt;(now - start) / (target - start)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Target date passed → Show 100% (or switch to "expired" visual)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI kept overcomplicating this. It wanted to create a full state machine. I simplified it to:&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;function&lt;/span&gt; &lt;span class="nf"&gt;updateProgress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// Update progress bar width&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes the simplest solution is the right one, even if it doesn't handle every edge case perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. AI is a multiplier, not a replacement&lt;/strong&gt;: It made me 3x faster at writing boilerplate code, but I still needed to understand the domain deeply to catch its mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Time is always more complex than you think&lt;/strong&gt;: Timezones, daylight saving, leap years — these aren't edge cases, they're the norm. Always test with real-world dates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Single-file apps have a place&lt;/strong&gt;: For utility tools, the simplicity of deployment and usage outweighs the benefits of a build system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Performance matters, even for simple tools&lt;/strong&gt;: The difference between updating the DOM every 100ms vs. every 1000ms might not seem like much, but it affects CPU usage and battery life on mobile devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Dark mode isn't optional anymore&lt;/strong&gt;: Users expect it. The &lt;code&gt;prefers-color-scheme&lt;/code&gt; media query is your friend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After about three days of work (two of which were debugging timezone issues), I had a working countdown timer that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works entirely in the browser&lt;/li&gt;
&lt;li&gt;Supports English and Chinese&lt;/li&gt;
&lt;li&gt;Has dark mode&lt;/li&gt;
&lt;li&gt;Is fully responsive down to 320px&lt;/li&gt;
&lt;li&gt;Updates in real-time&lt;/li&gt;
&lt;li&gt;Handles expired states gracefully&lt;/li&gt;
&lt;li&gt;Weighs less than 20KB total&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. You can try it here: &lt;a href="https://craftvo.app/en/tool/countdown-timer" rel="noopener noreferrer"&gt;Craftvo Countdown Timer&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building a countdown timer taught me that "simple" tools often have hidden complexity. It also showed me that the right tool for the job might be simpler than you think — sometimes a single HTML file is all you need.&lt;/p&gt;

&lt;p&gt;The AI-assisted development experience was genuinely positive, but it's not magic. It's like having a fast, enthusiastic pair programmer who sometimes needs guidance. The key is knowing enough to catch its mistakes and guide it in the right direction.&lt;/p&gt;

&lt;p&gt;And remember: it's always a timezone issue. It's always a timezone issue.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building an ASCII Art Generator with AI: The Good, The Bad, and The Figlet</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:36:02 +0000</pubDate>
      <link>https://dev.to/ggwork/building-an-ascii-art-generator-with-ai-the-good-the-bad-and-the-figlet-3d4h</link>
      <guid>https://dev.to/ggwork/building-an-ascii-art-generator-with-ai-the-good-the-bad-and-the-figlet-3d4h</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I was staring at my terminal during a deploy, waiting for the build to finish, when I realized something: I'd been typing &lt;code&gt;figlet "Hello World"&lt;/code&gt; into my terminal for years to generate ASCII art for commit messages and README files. But every time I wanted to share that art with someone who wasn't a developer, I hit a wall.&lt;/p&gt;

&lt;p&gt;"Just install figlet," I'd say.&lt;/p&gt;

&lt;p&gt;"Install what now?" they'd reply.&lt;/p&gt;

&lt;p&gt;The problem wasn't that ASCII art tools don't exist online. The problem was that the ones I found were either bloated with ads, required JavaScript frameworks that made the page take forever to load, or couldn't handle non-Latin characters gracefully. I wanted something that just worked in a browser tab, no installation, no server, no fuss.&lt;/p&gt;

&lt;p&gt;So I decided to build my own. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-Assisted Development Journey
&lt;/h2&gt;

&lt;p&gt;Here's where things get interesting. I've been using AI pair programming for a while now, and this project felt like the perfect test case: it's well-defined, has clear requirements, and involves a lot of repetitive font data that would be tedious to type manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Initial Prompt
&lt;/h3&gt;

&lt;p&gt;I started by describing the requirements to an AI assistant in pretty specific terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a single-file HTML tool that converts text to ASCII art. 
Must have multiple fonts (Block, Slant, Small, Standard, Mini). 
Real-time preview. Copy to clipboard. Download as .txt. 
Support dark mode. Chinese/English i18n. Vanilla JS only.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI came back with something surprisingly decent. It had the basic structure right, the font data was embedded, and the rendering logic was clean. But there were issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where AI Got It Wrong
&lt;/h3&gt;

&lt;p&gt;The first problem was &lt;strong&gt;character handling&lt;/strong&gt;. The AI assumed that all input would be uppercase English letters. When I tested with lowercase, numbers, and special characters, it just... broke. Not crashed, but silently dropped characters.&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="c1"&gt;// What the AI initially wrote (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Silent failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem? This returns empty strings for any character not in the font data. A user typing "Hello World" would get "HELLO WORLD" in ASCII art, which isn't terrible, but a user typing "Café" would lose the "é" entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My fix:&lt;/strong&gt; I had to explicitly handle non-ASCII characters by falling back to the original character output, so the user sees something rather than nothing.&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="c1"&gt;// The fix&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// Show original char&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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;The second issue was &lt;strong&gt;line wrapping&lt;/strong&gt;. The AI's initial implementation would create a massive horizontal scroll for long text. I wanted automatic wrapping at a configurable width.&lt;/p&gt;

&lt;p&gt;The AI's approach was to just let it overflow. My approach was to split the input into chunks of N characters before passing to the renderer:&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;function&lt;/span&gt; &lt;span class="nf"&gt;wrapText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lines&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;This is simple, but it breaks words. For an ASCII art tool, that's actually fine — you're not reading prose, you're looking at a banner.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI Got Right
&lt;/h3&gt;

&lt;p&gt;Honestly, the AI handled the &lt;strong&gt;font data&lt;/strong&gt; brilliantly. Typing out 5 different fonts × 36 characters × 5-7 lines each would have taken me an hour. The AI generated all of it in seconds, and the quality was surprisingly good.&lt;/p&gt;

&lt;p&gt;It also nailed the &lt;strong&gt;i18n pattern&lt;/strong&gt; on the first try. I asked for a simple language toggle, and it implemented a clean dictionary pattern:&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;translations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASCII Art 生成器&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;输入文本&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASCII Art Generator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Input Text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&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;h3&gt;
  
  
  The Iteration Loop
&lt;/h3&gt;

&lt;p&gt;Here's what the real workflow looked like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prompt&lt;/strong&gt;: "Add a character set selector" → AI adds a dropdown with "Standard", "Wide", "Narrow" options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt;: I type "Hello" and switch to "Wide" → Nothing changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug&lt;/strong&gt;: I look at the code and realize the font data doesn't have a "Wide" variant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New prompt&lt;/strong&gt;: "The character set selector doesn't work because there's no wide font data. Generate a wide variant of the Standard font"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This back-and-forth happened maybe 5-6 times for various features. Each iteration was faster than if I'd written the code from scratch, but I had to know what to ask for. The AI couldn't anticipate that the character set selector needed actual font data behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Technical Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Vanilla JS?
&lt;/h3&gt;

&lt;p&gt;I could have used React or Vue, but for a single-file tool, that's overkill. Vanilla JS keeps the file self-contained — no build step, no dependencies, just open the HTML and it works. This is important for a tool that people might want to save locally or use offline.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Font Data Structure
&lt;/h3&gt;

&lt;p&gt;The core of the tool is the font data. Each font is an object where each key is a character and each value is an array of strings (one string per line of the character):&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;fonts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  ██  &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; ████ &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;██  ██&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;██████&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;██  ██&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;█████ &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;██  ██&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;█████ &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;██  ██&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;█████ &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="c1"&gt;// ... 36 more characters&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;This structure makes rendering trivial:&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;function&lt;/span&gt; &lt;span class="nf"&gt;renderLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&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;h3&gt;
  
  
  Copy to Clipboard
&lt;/h3&gt;

&lt;p&gt;The hardest part of the whole project was clipboard functionality. The &lt;code&gt;navigator.clipboard.writeText()&lt;/code&gt; API requires a secure context (HTTPS or localhost), and it returns a Promise that can reject.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;copyToClipboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;showFeedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copied!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Fallback for older browsers&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;textarea&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;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;showFeedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copied!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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;The fallback is ugly but necessary — you never know what browser someone's using.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Wait, That's a CSS Issue" Moment
&lt;/h2&gt;

&lt;p&gt;Spoiler: it was a CSS issue. It's always a CSS issue.&lt;/p&gt;

&lt;p&gt;The ASCII art looked perfect in the &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; tag, but when I copied it to the clipboard and pasted it into a terminal or Discord, the spacing was off. The culprit? The &lt;code&gt;font-family&lt;/code&gt; in the &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; tag used system fonts that rendered wider than standard monospace fonts.&lt;/p&gt;

&lt;p&gt;The fix was to force a specific monospace font stack and set a fixed &lt;code&gt;font-size&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.result-box&lt;/span&gt; &lt;span class="nt"&gt;pre&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Courier New'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Courier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;monospace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;This ensures that what you see is what you get when you paste elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AI Can't Read Your Mind
&lt;/h3&gt;

&lt;p&gt;The AI didn't know that "character set selector" meant I wanted different font widths. I had to be specific about what each feature should do and what data it needed. The more context I gave, the better the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Test Edge Cases Early
&lt;/h3&gt;

&lt;p&gt;The AI-generated code handled common cases well but fell apart on edge cases: empty input, very long text, special characters, and mixed language input. I should have tested these before asking for new features.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Font Data is the Hard Part
&lt;/h3&gt;

&lt;p&gt;Writing the rendering logic is easy. Getting 5 fonts × 36 characters of perfectly aligned ASCII art is the real work. AI made this feasible — I would have given up on the third font otherwise.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep It Simple
&lt;/h3&gt;

&lt;p&gt;Despite all the features I listed, the core functionality is just: take text, look up characters in a font object, render as a string. Everything else is sugar on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file with no dependencies, works offline, and handles the basics well. You can check it out at &lt;a href="https://craftvo.app/en/tool/ascii-art-generator" rel="noopener noreferrer"&gt;Craftvo's ASCII Art Generator&lt;/a&gt; if you're curious.&lt;/p&gt;

&lt;p&gt;The tool supports 5 fonts, real-time preview, copy/download functionality, and automatic wrapping for long text. It handles non-ASCII characters by falling back to the original character, so "Café" renders as "Café" rather than "CAF" with a missing character.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;AI-assisted development for this project was a net win. The AI handled the tedious parts (font data generation, boilerplate CSS) and I handled the architectural decisions and edge cases. But it wasn't magic — I still had to know what to ask for and how to test the results.&lt;/p&gt;

&lt;p&gt;The best workflow was iterative: prompt, test, debug, refine. Each cycle took minutes rather than hours, and the end result was better than anything I would have written alone in the same timeframe.&lt;/p&gt;

&lt;p&gt;If you're thinking about building a similar tool, my advice is: let the AI do the heavy lifting, but don't trust it blindly. Test everything, especially the edge cases, and be prepared to fix the things it gets wrong.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I need to generate an ASCII art banner for my next commit message.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>software</category>
      <category>tools</category>
    </item>
    <item>
      <title>Building a Morse Code Translator: When Retro Communication Meets Modern Web APIs</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Mon, 24 Aug 2026 06:47:26 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-morse-code-translator-when-retro-communication-meets-modern-web-apis-3o5a</link>
      <guid>https://dev.to/ggwork/building-a-morse-code-translator-when-retro-communication-meets-modern-web-apis-3o5a</guid>
      <description>&lt;p&gt;While working on a collection of browser-based tools recently, I found myself needing a Morse code translator. Not because I'm secretly a ham radio enthusiast or planning to send distress signals from my basement, but because I kept running into situations where I needed to decode or encode Morse code quickly.&lt;/p&gt;

&lt;p&gt;The existing solutions were... fine, I guess. But they were mostly mobile apps with more ads than functionality, or websites that felt like they were designed in 1998 and never updated. I wanted something that worked entirely in the browser, had a clean interface, and could actually play the audio tones — not just show the dots and dashes. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Simple" Problem That Wasn't
&lt;/h2&gt;

&lt;p&gt;Here's the thing about Morse code translation: it sounds trivial. Map letters to dots and dashes, reverse the mapping, done. But once you start thinking about the details, it gets interesting.&lt;/p&gt;

&lt;p&gt;First, there's the timing. Morse code isn't just about the symbols — it's about the spaces between them. A dit is one time unit, a dah is three units, the gap between letters is three units, and between words it's seven units. Get the timing wrong and your "translation" sounds like someone mashing a telegraph key with no rhythm.&lt;/p&gt;

&lt;p&gt;Then there's the audio playback. The Web Audio API gives you incredible control, but it also has this tendency to make you feel like you're writing assembly language when all you want is a simple beep.&lt;/p&gt;

&lt;p&gt;And finally, there's the bidirectional nature of the UI. Type text, get Morse. Type Morse, get text. Both happening in real-time, with the ability to swap directions. It's the kind of problem that seems simple until you actually try to build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Decisions
&lt;/h2&gt;

&lt;p&gt;Let me walk you through the key decisions I made, starting with the most important one: keeping everything in a single HTML file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why single-file?&lt;/strong&gt; For a tool like this, a single-file architecture makes sense. There's no build step, no server required, and users can literally save the file and run it offline. It's the ultimate in portability. The trade-off is that you can't use any external libraries — everything has to be vanilla JavaScript and CSS. Which, honestly, is fine for this scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Morse mapping.&lt;/strong&gt; The core data structure is straightforward:&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;MORSE_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;C&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-.-.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;D&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;F&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..-.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;G&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;H&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;....&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;J&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ... rest of alphabet and numbers&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reverse mapping is derived programmatically — no need to maintain two separate objects that could drift out of sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bidirectional conversion.&lt;/strong&gt; This was trickier than I expected. When you're converting text to Morse, it's straightforward: uppercase everything, map each character, join with spaces. But going the other direction requires parsing the Morse code with its spaces and slashes:&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;function&lt;/span&gt; &lt;span class="nf"&gt;morseToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;morse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;morse&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;REVERSE_MORSE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&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;Simple, but effective. The slash separates words, spaces separate letters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Audio Playback Challenge
&lt;/h2&gt;

&lt;p&gt;This was where things got interesting. I wanted to play the Morse code as actual sound — the characteristic beeps you hear in old war movies. The Web Audio API can do this, but it requires careful timing management.&lt;/p&gt;

&lt;p&gt;The first approach I tried was scheduling all the tones upfront using &lt;code&gt;AudioContext&lt;/code&gt;'s time-based scheduling. This worked, but it had a problem: if the user wanted to stop playback mid-way, I'd have to track every scheduled oscillator and disconnect them all. It got messy fast.&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;function&lt;/span&gt; &lt;span class="nf"&gt;playMorse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;morseCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wpm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ditDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;wpm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// milliseconds&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Schedule all tones upfront&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;morseCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;playTone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ditDuration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ditDuration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;playTone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ditDuration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ditDuration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ditDuration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// gap between symbols&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;The better approach was to use &lt;code&gt;setTimeout&lt;/code&gt; to schedule each tone individually, making it easy to cancel with &lt;code&gt;clearTimeout&lt;/code&gt; when the user hits stop. The trade-off is less precise timing, but for a tool like this, that's acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When AI Actually Helped (And When It Didn't)
&lt;/h2&gt;

&lt;p&gt;I'll be honest: I used AI assistance for parts of this project, and the experience was mixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where AI excelled:&lt;/strong&gt; The initial scaffolding. Describing the UI layout, the i18n system, and the basic conversion logic to Claude got me a solid foundation in minutes. The AI was particularly good at generating the complete Morse code mapping table — all 36 characters plus punctuation — without any errors. That's the kind of tedious, error-prone work that AI handles perfectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where AI struggled:&lt;/strong&gt; The audio playback timing. My first prompt to Claude produced code that technically worked but sounded wrong. The timing between letters was too short, making everything sound like one continuous tone. It took several iterations of "no, the gap between letters needs to be longer" before the AI finally got the timing right.&lt;/p&gt;

&lt;p&gt;The real lesson? AI is great for generating boilerplate and well-known logic patterns, but you still need to understand the domain well enough to verify the output. The AI didn't know Morse code timing standards — I had to tell it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Approach
&lt;/h2&gt;

&lt;p&gt;Given that I'm building a collection of tools for a global audience, internationalization was non-negotiable. But I didn't want to pull in a full i18n library for what's essentially a two-language tool.&lt;/p&gt;

&lt;p&gt;The solution was a lightweight dictionary-based approach:&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;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Morse Code Translator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Text Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;morse-output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Morse Code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;摩斯电码翻译器&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;文本输入&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;morse-output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;摩斯电码&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&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;The language detection follows a specific priority: URL parameter &amp;gt; browser language &amp;gt; default. It's not perfect — there's no way to handle languages beyond English and Chinese without adding more dictionaries — but it covers the main use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CSS That Made Me Question My Life Choices
&lt;/h2&gt;

&lt;p&gt;Let me talk about the CSS for a moment. I wanted the tool to look modern and clean, with proper dark mode support. The approach was CSS variables with &lt;code&gt;prefers-color-scheme&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111827&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3b82f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a2e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e2e8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#60a5fa&lt;/span&gt;&lt;span class="p"&gt;;&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;This worked perfectly... until I realized the &lt;code&gt;accent-color&lt;/code&gt; property for range inputs doesn't respect CSS variables in all browsers. Classic "works on my machine" situation. The fix was to explicitly set the accent color using the variable, which works in modern browsers but required a fallback for older ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Timing precision matters more than you think.&lt;/strong&gt; When I first built the audio playback, I was using &lt;code&gt;setTimeout&lt;/code&gt; with the calculated durations. But the actual timing was off because &lt;code&gt;setTimeout&lt;/code&gt; isn't precise — it fires when it can, not when you ask it to. For a tool that needs to generate Morse code at a specific WPM, this is a real problem. The solution was to use the Web Audio API's built-in scheduling whenever possible, falling back to &lt;code&gt;setTimeout&lt;/code&gt; only when necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Don't trust AI with domain-specific knowledge.&lt;/strong&gt; The AI was confident when it generated the audio timing code, but it was wrong about the standard timings. Morse code has specific rules: dit = 1 unit, dah = 3 units, gap between symbols = 1 unit, gap between letters = 3 units, gap between words = 7 units. The AI didn't know these standards and made up its own timing scheme.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Single-file architecture is underrated.&lt;/strong&gt; For small tools, the simplicity of a single HTML file is unmatched. No build process, no dependencies, no deployment headaches. Just open the file and it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It handles bidirectional translation, plays audio with adjustable speed and tone, includes a complete reference chart, and works entirely offline. You can find it here: &lt;a href="https://craftvo.app/en/tool/morse-code-translator" rel="noopener noreferrer"&gt;Craftvo's Morse Code Translator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is about 400 lines of JavaScript, 200 lines of CSS, and one HTML file. It's not the most impressive engineering feat, but it solves a real problem in a clean way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building tools like this reminds me that sometimes the most interesting engineering problems come from the simplest requirements. Morse code translation sounds trivial, but it touches on character encoding, audio synthesis, timing precision, and internationalization. Each of these presents its own challenges, and solving them together makes for a satisfying engineering exercise.&lt;/p&gt;

&lt;p&gt;And yes, I did test it by sending "SOS" in Morse code. Because I'm a responsible engineer who tests thoroughly. That's my story, and I'm sticking to it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;webdev&lt;/code&gt;, &lt;code&gt;javascript&lt;/code&gt;, &lt;code&gt;webaudio&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, &lt;code&gt;i18n&lt;/code&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Parse TOML in the Browser Without a Backend</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Mon, 24 Aug 2026 01:49:44 +0000</pubDate>
      <link>https://dev.to/ggwork/how-to-parse-toml-in-the-browser-without-a-backend-1ckb</link>
      <guid>https://dev.to/ggwork/how-to-parse-toml-in-the-browser-without-a-backend-1ckb</guid>
      <description>&lt;p&gt;While working on a configuration-heavy project recently, I kept running into the same annoying problem. I'd receive TOML config files from teammates or clients, and I needed to quickly verify the structure, check for syntax errors, or convert them to JSON for debugging. Opening a terminal, installing a parser, and writing a script for a one-off check felt like overkill.&lt;/p&gt;

&lt;p&gt;I tried using online tools, but most were either cluttered with ads, required uploading my config to a server, or only supported JSON/YAML. For a format that's supposed to be "obvious and minimal," the tooling around it felt anything but.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable developer would do: I built my own browser-based TOML parser. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Existing Solutions
&lt;/h2&gt;

&lt;p&gt;Before diving into code, let me explain why existing tools weren't cutting it.&lt;/p&gt;

&lt;p&gt;Most online parsers have a fundamental flaw: they send your data to a server. For config files containing API keys, database credentials, or internal service endpoints, that's a non-starter. Even for non-sensitive data, the round-trip adds latency and breaks the flow of quick debugging.&lt;/p&gt;

&lt;p&gt;The other issue was format support. Many "universal" config converters handle JSON and YAML well but treat TOML as an afterthought. When they do support it, the error messages are often cryptic — just "parse error" without line numbers or context.&lt;/p&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs entirely in the browser (no uploads)&lt;/li&gt;
&lt;li&gt;Parses TOML 1.0.0 spec correctly&lt;/li&gt;
&lt;li&gt;Gives helpful error messages with line numbers&lt;/li&gt;
&lt;li&gt;Shows results in both JSON and tree view&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing the Right Parser Library
&lt;/h2&gt;

&lt;p&gt;The first decision was which TOML library to use. I evaluated two options:&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="c1"&gt;// Option 1: smol-toml (ESM, modern)&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;parse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://esm.sh/smol-toml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Option 2: @iarna/toml (UMD, battle-tested)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iarna/toml&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;I went with &lt;code&gt;smol-toml&lt;/code&gt; for a few reasons. It's actively maintained, follows the TOML 1.0.0 spec strictly, and provides detailed error messages with line/column info. The ESM format works cleanly with modern browser imports.&lt;/p&gt;

&lt;p&gt;But there's a catch: if the CDN fails or the user is offline, the tool breaks. I needed a fallback strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling CDN Failures Gracefully
&lt;/h2&gt;

&lt;p&gt;This was one of those "it worked on my machine" situations that almost bit me in production. I initially loaded the library with a simple import statement:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://esm.sh/smol-toml&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;The problem? If that CDN request fails, the entire script crashes. No error message, no graceful degradation — just a blank page.&lt;/p&gt;

&lt;p&gt;The fix was wrapping the import in a dynamic import with error handling. If the library fails to load, I show a clear message and fall back to a minimal built-in parser for basic cases:&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;let&lt;/span&gt; &lt;span class="nx"&gt;tomlParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadTomlParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://esm.sh/smol-toml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;tomlParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to load TOML parser library. Check your connection.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;tomlParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fallbackParser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// minimal built-in&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;The fallback parser handles basic key-value pairs and simple tables, but for complex nested structures, it honestly struggles. The lesson here: always plan for dependency failures, even with CDNs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Tree View
&lt;/h2&gt;

&lt;p&gt;The most interesting part was the tree visualization. JSON output is straightforward — just stringify the parsed object with indentation. But a collapsible tree view requires recursive rendering.&lt;/p&gt;

&lt;p&gt;Here's the core of the tree renderer:&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;function&lt;/span&gt; &lt;span class="nf"&gt;renderTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;renderTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
      &amp;lt;div&amp;gt;
        &amp;lt;span class="tree-toggle" onclick="toggleNode(this)"&amp;gt;▸&amp;lt;/span&amp;gt;
        &amp;lt;span class="tree-key"&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;/span&amp;gt;: 
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; 
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;div class="tree-children"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;renderTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;span class="tree-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;formatValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;/div&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;span class="tree-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;formatValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;`&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;The recursive approach works well for most cases, but I hit a performance wall with deeply nested structures. A config with 5+ levels of nesting and large arrays would freeze the browser momentarily. The fix was adding a depth limit — if the tree goes beyond 10 levels, I collapse it by default and let users expand manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-Assisted Development Process
&lt;/h2&gt;

&lt;p&gt;Full disclosure: I used Claude to help build this. Here's how the collaboration actually went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First attempt&lt;/strong&gt;: I described the tool requirements in detail — the layout, the features, the i18n support, the dark mode. The AI generated a complete HTML file in one shot. It looked impressive at first glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problems emerged immediately&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The tree view didn't handle arrays properly (showed them as objects)&lt;/li&gt;
&lt;li&gt;Error messages from the parser weren't being captured with line numbers&lt;/li&gt;
&lt;li&gt;The i18n implementation was incomplete — some strings were hardcoded in Chinese&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The iteration loop&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I'd test a specific interaction&lt;/li&gt;
&lt;li&gt;Report the bug to the AI with a screenshot or error message&lt;/li&gt;
&lt;li&gt;Get a fix, but sometimes a new bug would appear elsewhere&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most frustrating part was the AI's tendency to over-engineer. It kept adding features I didn't ask for — like a settings panel and localStorage persistence — while missing the core error handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the AI nailed&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CSS design system with variables for dark mode&lt;/li&gt;
&lt;li&gt;The responsive layout (grid to single column below 640px)&lt;/li&gt;
&lt;li&gt;The basic parsing logic with debounced input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where I had to step in&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error handling edge cases (empty input, malformed TOML, circular references)&lt;/li&gt;
&lt;li&gt;The tree view's array rendering&lt;/li&gt;
&lt;li&gt;Making error messages actually helpful with line numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Key Lesson: Debouncing Matters
&lt;/h2&gt;

&lt;p&gt;When I first wired up the input to parse on every keystroke, it was laggy. Not because parsing is expensive, but because rendering the tree view on every change was wasteful.&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;let&lt;/span&gt; &lt;span class="nx"&gt;debounceTimer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debounceTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;debounceTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parseAndRender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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;A simple 300ms debounce made the tool feel instant. This is one of those "obvious in hindsight" optimizations that makes a huge difference in perceived performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were building this again, I'd spend more time on the error messages. The TOML spec has specific error cases — unclosed strings, invalid dates, duplicate keys — and each deserves a clear, actionable message. The library gives me the line number, but converting that into "you forgot to close a quote on line 12" requires more work than I initially thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that handles TOML parsing, JSON conversion, tree visualization, and error reporting — all locally in the browser. If you're dealing with TOML configs regularly, you can check it out at &lt;a href="https://craftvo.app/en/tool/toml-parser" rel="noopener noreferrer"&gt;Craftvo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The real takeaway here isn't the tool itself — it's the process. Building browser-based dev tools that respect user privacy (no server uploads) is increasingly important. And with AI assistance, what used to take days can now take hours. The catch is that AI gives you a working solution, not a correct one. The debugging and edge-case handling still require human judgment.&lt;/p&gt;

&lt;p&gt;That's the honest truth about AI-assisted development: it's a great rubber duck, but it's not a senior engineer. Yet.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
