I Built a Shakespeare Translator in Vanilla JavaScript — Here's How the Engine Works
A Shakespeare translator sounds like a machine learning problem. It is not — or at least, the useful version of it is not. I built ShakespeareanTranslator.com as a fully client-side tool: no API calls, no server, no mode
A Shakespeare translator sounds like a machine learning problem. It is not — or at least, the useful version of it is not. I built ShakespeareanTranslator.com as a fully client-side tool: no API calls, no server, no model. Just dictionaries, phrase tables, and careful string processing, all running in the browser with instant results.
I am a teacher, not an NLP researcher, and I built this because my students needed it. This post is a technical walkthrough of the architecture: why I skipped ML, how the multi-dictionary replacement engine works, the edge cases that almost broke it, and the performance tricks that keep translation instant even on long texts.
Why Not Machine Learning?
The obvious modern approach is a seq2seq model or an LLM API call. I rejected both, for practical reasons.
First, latency. A classroom tool that waits two seconds per translation is a dead tool. Students translate sentence by sentence while reading; the feedback loop must feel instant. Client-side dictionary replacement translates a page of text in under 50 milliseconds. No API can touch that.
Second, cost and reliability. An API-backed translator needs keys, quotas, billing, and error handling for every outage. A static dictionary ships with the page and works offline, forever, for free. For a free educational tool, that is not a minor advantage — it is the whole business model.
Third, and most interestingly: determinism. A neural model paraphrases creatively, which means the same input can produce different outputs. For a learning tool, that is a bug, not a feature. Students need to see that "thou" always becomes "you" — the consistency is the lesson. Dictionary translation is deterministic by construction.
The trade-off is real: dictionary systems cannot handle genuinely novel constructions the way a model can. But Shakespeare translation is a constrained problem — the vocabulary is finite, the grammar patterns are enumerable — and for constrained problems, boring technology wins.
The Core Architecture: Layered Dictionaries
The engine's heart is a set of ordered dictionaries, applied in sequence. Each dictionary maps Early Modern English forms to modern equivalents, and the ordering matters enormously.
Dictionary layers
The layers, in application order:
Famous phrases — complete known quotations mapped to polished modern renderings. "Shall I compare thee to a summer's day" gets a hand-written translation, not a word-by-word grind. This layer exists because famous lines deserve better than mechanical conversion.
Multi-word phrases — idioms and collocations like "good sir," "by my troth," "prithee." These must be replaced before single words, or the word layer will mangle them.
Pronouns and grammar words — thou/thee/thy/thine, hath/doth, verb endings. High-frequency, fully regular.
General vocabulary — the big word list: wherefore→why, anon→soon, hither→here, and hundreds more.
Reverse dictionaries — for the modern-to-Shakespearean direction, inverted mappings with their own ordering.
Why ordering matters
Consider "good sir." If the word layer runs first, "good" stays "good" (fine) but the phrase layer never sees the intact phrase. Worse: some phrases contain words that individually map to something else. The rule is iron: longest match first, phrases before words, and every layer operates on the output of the previous one.
The Placeholder Chaining Trick
The nastiest bug in the engine's history came from cross-dictionary collisions. Here is the problem: dictionary A maps "X" to "Y," and dictionary B maps "Y" to "Z." If both run in sequence, input "X" becomes "Z" — wrong.
The concrete case: identity phrases. Some entries map a phrase to itself ("good sir" → "good sir") as a way of protecting it from later mangling. But if placeholders do not survive across all dictionary passes, a later layer re-processes the "protected" text.
The fix is placeholder chaining: when a layer matches a phrase, it substitutes a unique placeholder token, and placeholders persist through every subsequent layer untouched. Only at the very end are placeholders resolved to their final text. This is the single most important implementation detail in the whole engine — without it, multi-dict replacement silently corrupts output in ways that are extremely hard to debug.
Unicode-Aware Matching
Shakespeare-adjacent historical content sometimes includes much older English text with characters like þ (thorn), ð (eth), and æ (ash). JavaScript's \b word boundary does not understand these as word characters — it treats þ as a non-word character, so boundary matching fails on such text.
The fix: replace \b with explicit lookarounds using Unicode-aware character classes. The u flag with \p{L} treats þðæ as letters, so boundaries work. This is a general lesson for anyone doing dictionary replacement on non-ASCII text: never trust \b.
Case Preservation
Dictionary keys are lowercase, but input text has real capitalization. "Thou" at a sentence start must become "You," not "you." The engine handles this with a case-transfer function applied to every replacement: ALL CAPS input → ALL CAPS output, capitalized input → capitalized output, lowercase → lowercase. It handles 99% of cases, and the remaining 1% are not worth the complexity.
The Reverse Direction
Modern-to-Shakespearean translation is not just the forward dictionaries inverted. The mapping is asymmetric: many modern words map to one Shakespearean word (you → thou/you/ye depending on context), and style matters — a "classic" rendering differs from a "dramatic" one.
The engine handles this with four style presets, each a different dictionary stack: Natural (light touch), Classic (fuller conversion), Dramatic (rhetorical inversions), Poetic (maximum flourish). Each style is just a different ordered list of the same dictionary modules. This composability is the payoff of the layered architecture: new styles are configuration, not code.
Performance: Keeping It Instant
Three decisions keep it fast: (1) Pre-sorted keys — dictionary keys are sorted by length once at load, not per translation. (2) Single-pass regex per layer — five layers means five passes, linear in text length. (3) No DOM thrashing — translation writes to a single output node.
Measured: a 2,000-character passage translates in under 30ms on a mid-range phone.
The Takeaway for Builders
If you are building any kind of constrained translation or text-transformation tool — dialect converters, jargon explainers, reading-level adjusters — consider the boring approach first. Ordered dictionaries with placeholder chaining, Unicode-aware boundaries, and case transfer will take you shockingly far, run instantly, cost nothing, and behave deterministically.
The tool described here is ShakespeareanTranslator.com — free, no signup, client-side.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.