Building a Privacy-First Morse Code Encoder
Why Another Morse Tool?
There are plenty of Morse code converters online. Most of them work fine for a quick demonstration, but several recurring problems make them unsuitable for anyone who cares about correctness or privacy. The most common issue is silent character dropping: type a character the tool doesn't recognise and it simply disappears from the output, with no indication that anything was lost. For casual use that's acceptable; for anything where output fidelity matters, it is a quiet source of errors. The second issue is non-standard mappings. Some tools invent Morse representations for characters that have no ITU definition, producing output that no standard decoder would accept. The third issue is architectural: several popular converters send your input to a server. For developers pasting internal documentation, code comments, or anything confidential, that is an unacceptable privacy exposure for a tool that has no business reason to be server-side at all.
Encoding Architecture: Precomputed Map vs Switch/Case
The core of the encoder is a Map<string, string> containing all 54 ITU-standard entries — 26 letters, 10 digits, and 18 punctuation characters. This was a deliberate choice over a switch/case statement or a series of if/else branches. A Map provides O(1) lookup by hash, meaning the cost of encoding any character is constant regardless of how many entries the table has. A switch/case would also be compiled to an efficient jump table in most JavaScript engines, but the Map approach is more readable, easier to audit against the ITU standard, and trivially updatable if a future revision of the standard adds entries. The encoding function itself is a pure function: it accepts the input string and a settings object, performs no mutations, and returns a result object containing the output string, a count of dots and dashes, and a set of any characters it could not encode. Pure functions are easier to reason about, easier to test, and impossible to misuse through shared mutable state.
The / Problem
One subtlety that trips up many implementations is the forward slash character. In Morse code, / (space-slash-space) is the conventional word separator — it signals a pause between words rather than between letters. But / is also a valid input character with its own ITU Morse encoding: -..-.. The encoding logic must treat these two roles as entirely distinct. A / appearing in the input text is a character to be encoded, not a word boundary. Word boundaries are derived exclusively from spaces in the input: a run of one or more space characters between non-space characters constitutes a word boundary, and that boundary is represented as / in the output. The character / typed by the user encodes to -..-. and gets embedded within a word's letter tokens. Getting this wrong produces output where the word separator and the encoded slash are indistinguishable — a subtle correctness failure that is hard to notice in casual testing.
Handling Unsupported Characters Deterministically
Unicode contains over 140,000 assigned characters. The ITU Morse standard covers 54. Every encoder must decide what to do with the gap. Silent dropping — the most common approach — is the worst option: it produces output that looks correct but has silently lost information. I opted for a two-mode approach with transparency as the default. In "Placeholder" mode, any character not in the table emits [?] in the output, making the gap visible and unambiguous. In "Skip" mode, the character is dropped, but the tool simultaneously shows a notice listing every distinct unsupported character it encountered. The key design principle is that the user must always know when information was not encoded. The unsupportedChars set accumulates every unseen character across the full input and is surfaced as an inline notice below the output area. This gives the user the information they need to decide whether the output is acceptable for their purpose, rather than trusting that all input was handled correctly.
Real-Time UX: Debouncing and Screen Reader Support
The encoder runs on every keystroke, but with a 150ms debounce from @vueuse/core. Without debouncing, a fast typist on a long input would trigger the encoding function dozens of times per second, many of them for intermediate states that will immediately be superseded. The 150ms threshold is short enough to feel instantaneous for normal typing, and long enough to eliminate the majority of redundant recomputations. When settings change — switching delimiter or unsupported-character behavior — the encode runs immediately without debouncing, because those changes are deliberate user actions rather than transient keystroke states. The output textarea carries aria-live="polite", which instructs screen readers to announce the updated content after the user pauses interaction. "Polite" rather than "assertive" was the correct choice here: assertive interrupts any current speech immediately, which would be disruptive for every keystroke. Polite queues the update and delivers it at the next natural pause.
What Was Left Out of v1
The most frequently requested feature for a Morse encoder is audio playback — the ability to hear the output as actual dots and dashes. It turns out this is considerably harder to implement well than it appears. The Web Audio API provides everything needed to synthesize tone bursts at the correct ITU timing ratios (dot duration, dash = 3× dot, inter-letter gap = 3× dot, inter-word gap = 7× dot), but doing so correctly requires scheduling audio events with sample-accurate timing rather than using setTimeout, which drifts under load. Handling pause, resume, and cancellation mid-playback requires maintaining a reference to the scheduled audio graph and cancelling future events without cutting off current tones abruptly. There is also the question of speed controls, tone frequency selection, and Farnsworth timing for learning purposes. All of this is well-defined and achievable, but it deserves its own focused implementation effort rather than being bolted onto the encoder as an afterthought. Audio playback will be the subject of a follow-up tool.
Programming is like Cultivating
The similarities between programming and cultivation.
Building a Morse Code Decoder: Line Classification, Greedy Boundaries, and Graceful Errors
How we designed a deterministic Morse decoder that handles mixed input, inconsistent separators, and invalid tokens without silently failing.