DEV Community

ggwork
ggwork

Posted on

How to Build a Text Case Converter That Actually Handles Edge Cases

While working on a collection of browser-based developer tools recently, I kept running into the same problem: I needed to convert text between different case formats constantly. camelCase for JavaScript variables, snake_case for database columns, kebab-case for CSS classes — you name it.

The existing solutions? Most were either bloated web apps with more ads than functionality, or required sending my text to a server somewhere. That felt wrong for something so simple. I wanted a tool that worked entirely in the browser, fast and private.

So I decided to build my own. Spoiler: it was harder than I thought. Here's what happened.

The "Simple" Problem That Wasn't

My initial mental model was straightforward. Take text, apply a bunch of string methods, display the results. How hard could it be?

// My first naive attempt
const upper = text.toUpperCase();
const lower = text.toLowerCase();
const camel = text.replace(/(?:\s|_|-)(\w)/g, (m, c) => c.toUpperCase());
Enter fullscreen mode Exit fullscreen mode

This worked great for "hello world". Then I tried it on real-world input:

  • parseXMLDoc became parse X M L Doc — terrible
  • münchen lost its umlaut
  • Hello123World split in the wrong places
  • Chinese text like 你好世界 got mangled by the word-splitting logic

That's when I realized case conversion is deceptively tricky. The "simple" regex approach breaks the moment you have anything beyond basic ASCII text.

The Unicode Awakening

Here's what I learned: JavaScript's regular expressions have a secret weapon — Unicode property escapes. They've been around since ES2018, but most developers don't use them.

// Split words properly, keeping Unicode characters intact
function splitWords(s) {
  return String(s)
    .replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1 $2')     // helloWorld -> hello World
    .replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1 $2')      // parseXMLDoc -> parse XML Doc
    .replace(/[_\-\s]+/gu, ' ');                         // snake_case -> snake case
}
Enter fullscreen mode Exit fullscreen mode

The \p{L} and \p{Lu} patterns match any Unicode letter (lowercase and uppercase respectively), not just ASCII. The u flag enables Unicode mode. This single change fixed most of my edge cases.

The AI Collaboration

I built this tool with heavy AI assistance, and it was a mix of impressive and frustrating. Here's how it went:

Where AI excelled: The initial structure, i18n setup, and basic conversion functions came together quickly. I described what I needed and got a solid foundation in minutes.

Where AI struggled: Edge cases. My first prompt produced code that handled "hello world" perfectly but fell apart on real-world input. The AI didn't think about Unicode, didn't consider how parseXMLDoc should split, and assumed all text was ASCII.

The back-and-forth was revealing. I'd test with weird input, find a bug, describe it to the AI, and get a fix. But the fixes were often band-aids — patching one edge case without considering the broader pattern.

My honest take: AI is excellent for scaffolding and common patterns, but you still need to think critically about your domain. The AI didn't know that case conversion involves Unicode normalization, acronym handling, and locale-specific rules. I had to bring that knowledge.

The i18n Decision

One thing I insisted on from the start: full internationalization. The tool targets both Chinese and English speakers, and I wanted it done right.

The approach was simple but effective:

const I18N = {
  zh: { title: "文本大小写转换", copy: "复制" },
  en: { title: "Case Converter", copy: "Copy" }
};

function detectLang() {
  const p = new URLSearchParams(location.search);
  if (p.get('lang') === 'en') return 'en';
  if (p.get('lang') === 'zh') return 'zh';
  return navigator.language.startsWith('zh') ? 'zh' : 'en';
}
Enter fullscreen mode Exit fullscreen mode

Language detection follows a sensible priority: URL parameter overrides everything, then browser language, then defaults to Chinese. It's not perfect — some users might prefer English even with a Chinese browser — but it covers most cases.

The Copy Button Problem

Here's something I didn't expect: the copy button was the hardest part to get right.

async function copyText(text) {
  try {
    await navigator.clipboard.writeText(text);
    showFeedback('copied');
  } catch {
    // Fallback for older browsers
    const textarea = document.createElement('textarea');
    textarea.value = text;
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    document.body.removeChild(textarea);
  }
}
Enter fullscreen mode Exit fullscreen mode

The navigator.clipboard API is clean but requires HTTPS and secure context. The fallback uses the old execCommand approach, which works everywhere but feels hacky. And in both cases, you need to handle the user feedback — showing "Copied!" vs "Copy failed" — which adds UI complexity.

Performance Considerations

With real-time preview, performance matters. Every keystroke triggers ten different conversions. For normal text, this is instant. But what about pasting a 10MB document?

The solution was surprisingly simple: debounce the input.

let timeout;
input.addEventListener('input', () => {
  clearTimeout(timeout);
  timeout = setTimeout(updateResults, 100);
});
Enter fullscreen mode Exit fullscreen mode

A 100ms debounce means the UI stays responsive even with large inputs. The conversions themselves are O(n) string operations, so they're fast — it's the DOM updates that slow things down.

What I'd Do Differently

Looking back, there are a few things I'd change:

  1. Test with real-world data from the start. I should have created a test suite with edge cases before writing the conversion logic.

  2. Consider locale-specific rules. Turkish has a dotted/dotless I that behaves differently from English. German has ß which converts to SS in uppercase. These are niche but real.

  3. Build the i18n system first. I added it later, which meant retrofitting text throughout the codebase.

The Result

The final tool does what I needed: instant case conversion in the browser, no data leaving the device, supporting ten different formats. It handles Unicode gracefully, works in dark mode, and adapts to both Chinese and English users.

The full implementation lives at craftvo.app if you want to see it in action.

Key Takeaways

  1. Unicode property escapes are your friend. \p{L} and friends make JavaScript regex way more powerful for international text.

  2. AI assistance has limits. It's great for getting started, but you need domain knowledge to catch the edge cases it misses.

  3. Real-time features need debouncing. Even fast operations can lag with large inputs.

  4. Internationalization isn't optional. Your users speak more languages than you think.

Building this tool taught me that even "simple" text operations have hidden complexity. The next time you reach for text.toUpperCase(), remember: there's a whole world of edge cases lurking beneath that simple method call.

Top comments (0)