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.
The Problem Nobody Asked Me to Solve
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.
Here's the thing: most "Braille translators" online are either:
- Flash relics from 2005 that barely work on modern browsers
- Server-side tools that require uploading your text somewhere
- Overly complex accessibility suites when all you need is "text → Braille → text"
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.
The Moment I Realized Braille is Just Binary
Here's what blew my mind: Braille isn't some mystical code. It's literally binary.
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.
The mapping is beautifully simple:
// Dot 1 is bit 0, dot 2 is bit 1, dot 3 is bit 2, etc.
const brailleCodePoint = 0x2800 + dotPatternNumber;
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 ⠃.
Wait, did I just say "the letter 'a' is dot 1"? Because that's where the fun begins.
The Alphabet Is Not What You Think
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:
- a = dot 1
- b = dots 1,2
- c = dots 1,4
- d = dots 1,4,5
- e = dots 1,5
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.
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.
The Number Problem: A Trap for the Unwary
Just when I thought I had it figured out, I hit the numbers issue.
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 "⠁⠃⠉".
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.
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.
function textToBraille(text) {
let result = '';
let inNumber = false;
for (const char of text.toLowerCase()) {
if (char >= '0' && char <= '9') {
if (!inNumber) {
result += BRAILLE_NUMBER_SIGN;
inNumber = true;
}
result += BRAILLE_MAP[char];
} else {
inNumber = false;
result += BRAILLE_MAP[char] || char;
}
}
return result;
}
This was one of those moments where I realized: "I'm not just building a translator, I'm building a state machine."
The Dot Pattern Visualization
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.
The visualization is straightforward once you have the dot pattern number:
function getDots(codePoint) {
const value = codePoint - 0x2800;
const dots = [];
for (let i = 1; i <= 6; i++) {
if (value & (1 << (i - 1))) {
dots.push(i);
}
}
return dots;
}
Bit manipulation. Again. It's like binary is following me around.
AI-Assisted Development: The Good, The Bad, The Ugly
Now for the part I'm most honest about: I used AI to build much of this. And it was a mixed experience.
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."
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.
But then came the bugs.
Bug #1: The Case Sensitivity Trap
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.
Bug #2: The Number Sign Placement
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.
Bug #3: The Reverse Translation Mapping
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.
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.
Where I Had to Step In
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.
I had to:
- Explain the number sign rule explicitly, multiple times
- Provide the actual Braille mapping table (the AI kept hallucinating wrong patterns)
- Design the dot visualization myself — the AI's first attempt was a mess of nested divs that looked like a CSS nightmare
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.
The i18n Challenge
Building bilingual support (Chinese/English) added another layer of complexity. The tool needed to work for both Chinese and English speakers, which meant:
- All UI strings needed translation
- The language switcher needed to persist (I used localStorage)
- The Braille reference chart needed to be language-agnostic (it is — Braille is Braille)
The i18n system itself was straightforward:
const i18n = {
zh: {
title: '盲文翻译器',
textInput: '文本输入',
brailleOutput: '盲文输出',
// ...
},
en: {
title: 'Braille Translator',
textInput: 'Text Input',
brailleOutput: 'Braille Output',
// ...
}
};
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.
Performance Considerations
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.
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 auto-fill 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).
The "Why" Behind the Architecture
I chose vanilla JavaScript over React or Vue for a few reasons:
- Zero dependencies — this tool needs to work forever, even if npm packages disappear
- Single file — easy to embed, easy to share, easy to deploy
- No build step — I can just open the HTML file and it works
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 <textarea> with a mapping function.
What I Learned
- Bit manipulation is everywhere — even in something as human as Braille
- Domain knowledge beats clever code — you can't algorithmically derive the Braille alphabet; you need the actual mapping
- AI is a junior developer — great at scaffolding, bad at domain-specific edge cases
- Simple tools are underrated — sometimes the best solution is a single HTML file that just works
The Final Product
The result is a browser-based Braille translator that:
- Converts text to Braille and back
- Shows dot patterns for each character
- Handles numbers correctly (with the number sign)
- Works in both Chinese and English
- Has a complete reference chart
During this process, I built a small browser-based tool to make this workflow easier. You can find it here if you're curious.
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.
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.
Top comments (0)