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.
// Simplified: placeholder chaining across dictionary passes
const PLACEHOLDER = '[PH]';
let slot = 0;
const slots = new Map();
function protect(text, replacement) {
const key = `${PLACEHOLDER}${slot++}${PLACEHOLDER}`;
slots.set(key, replacement);
return key;
}
function applyDict(text, dict) {
// longest phrases first
const keys = Object.keys(dict).sort((a, b) => b.length - a.length);
for (const k of keys) {
const re = new RegExp(`\\b${escapeRegExp(k)}\\b`, 'gi');
text = text.replace(re, (m) => protect(m, dict[k.toLowerCase()] ?? dict[k]));
}
return text;
}
Unicode-Aware Matching
Shakespeare-adjacent content includes Old English material (the site also hosts Old English tools), and Old English uses characters like thorn, eth, and ash. JavaScript's \b word boundary does not understand these as word characters. The fix: replace \b with explicit lookarounds using Unicode-aware character classes with the u flag and \p{L}. This is a general lesson for anyone doing dictionary replacement on non-ASCII text: never trust \b.
A related edge case: g-dropping in the pirate-style translator ("going" to "goin'"). The exception list (king, thing, morning) must be checked against the full word, not the stem. Small bug, confusing output, easy fix once spotted.
Case Preservation
Dictionary keys are lowercase, but input text has real capitalization. "Thou" at a sentence start must become "You," not "you." The engine applies case transfer to every replacement: ALL CAPS in → ALL CAPS out, capitalized in → capitalized out, lowercase → lowercase. It handles 99% of cases.
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. The engine handles this with four style presets — Natural, Classic, Dramatic, Poetic — each a different ordered stack of the same dictionary modules. New styles are configuration, not code.
Performance: Keeping It Instant
Three decisions keep translation under 50ms on every keystroke: (1) Pre-sorted keys — sorted once at load, not per translation. (2) Single-pass regex per layer — five passes, linear in text length. (3) No DOM thrashing — one output node write; the word-explanation feature builds HTML in a document fragment and inserts once. Measured: 2,000 characters in under 30ms on a mid-range phone.
What I Would Do Differently
The layered-dictionary design scaled from weekend prototype to production without a rewrite. If starting over, I would use a structured data file (JSON with part of speech, era, confidence metadata) instead of flat key-value pairs, and invest earlier in a test corpus of famous passages with hand-verified outputs run on every dictionary change. Dictionary edits are deceptively dangerous — one innocent mapping can break a famous phrase three layers away.
The Takeaway for Builders
If you are building any 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. If you want to translate Shakespeare to modern English in your own projects, the same dictionary approach ports to any language pair with finite vocabulary.
Top comments (0)