Robber language generator
Robber language (Swedish: rövarspråket) is said to have been invented by Sture Lindgren — husband of the author Astrid Lindgren — when he played with his friends as a child. Astrid Lindgren's books about Kalle Blomkvist made the language popular in Sweden. Robber language is a spoken code where every consonant is replaced by the consonant + "o" + the consonant again. From that simple rule, building a robber language generator should be dead easy. When it comes to written robber language, however, there are a few more aspects to consider. So this post is really about a written robber language generator.
TL;DR
- Robber language has a simple rule for spoken language, but a few rules need to be added to make the written form match the spoken rules and standard writing conventions.
- A robber language generator can be built in JavaScript in many ways. Two main strategies I walk through in this post are starting from the consonants of the alphabet, and starting from the sentence that should be translated.
Try the finished robber language generator here:
The robber language model
The most basic description of robber language could be:
After every consonant, the letter "o" and the same consonant are added.
So the word "hey" becomes "hohejoj"… actually let's use a Swedish example: "hej" becomes "hohejoj" and "rövarspråket" becomes "rorövovarorsospoproråkoketot". This simple rule is enough for spoken language. For written language, however, some additional rules are needed. How does it work with uppercase and lowercase in writing? Based on the description above, the same consonant should be written again. Does that mean "Hej" becomes "HoHejoj"? I assume uppercase and lowercase rules should work the same as in regular writing — no uppercase in the middle of words. So "Hej" should become "Hohejoj".
Spoken language is the base. Written robber language is built to be correct when spoken. The letter "x" is a consonant and should according to the model become "xox". But since "x" is pronounced "ks", it works better — based on the spoken language — if "x" is translated to "koksos". So "yxa" becomes "ykoksosa" rather than "yxoxa".
The rules can be summarized as follows:
- The letter "x" is replaced with "ks".
- After every consonant, the letter "o" and the same consonant are added.
- For uppercase consonants, only the consonant before "o" is uppercase, the other is lowercase.
Building a robber language generator
Let's think about how a robber language generator could be built in JavaScript. First, a reminder of the simple rule for the spoken language:
After every consonant, the letter "o" and the same consonant are added.
Starting from the consonants
The simplest approach to the problem I can think of is to loop through a variable with all the consonants and in each iteration replace the current consonant in the sentence with the consonant + "o" + the same consonant again. Like this:
Example 1const consonants = "bcdfghjklmnpqrstvwxz"
let sentence = "Min mening"
consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
})
// sentence = "Minon momenoninongog"
If you try typing something in the box above, you'll see the generator actually works — sometimes. Since the consonants in the variable are lowercase, and JavaScript distinguishes between upper- and lowercase, the generator won't do anything with uppercase consonants. Why not add all consonants as uppercase to the variable too?
Example 2const consonants = "bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ"
let sentence = "Min mening"
consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
})
// sentence = "MoMinon momenoninongog"
OK, that handles uppercase too. But it doesn't feel particularly nice to have to specify every consonant twice. The fact that "Min" becomes "MoMinon" also violates rule 3, which says uppercase should only occur for the consonant before the "o" in a translation. How can we improve the generator's code? JavaScript has the built-in functions toUpperCase() and toLowerCase() we can use:
const consonants = "bcdfghjklmnpqrstvwxz"
let sentence = "Min mening"
consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
sentence = sentence.replaceAll(
consonant.toUpperCase(),
consonant.toUpperCase() + "o" + consonant,
)
})
// sentence = "Mominon momenoninongog"
Or like this:
Example 4const consonants = "bcdfghjklmnpqrstvwxz"
const allConsonants = consonants + consonants.toUpperCase()
let sentence = "Min mening"
allConsonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(
consonant,
consonant + "o" + consonant.toLowerCase(),
)
})
// sentence = "Mominon momenoninongog"
Between examples 3 and 4, I think 3 is nicest because it's easier to understand at a glance. So I'll continue from example 3.
But how do we handle rule 1? The rule says "x" should be replaced with "ks". Easiest is to do that replacement before the rest of the translation:
Example 5const consonants = "bcdfghjklmnpqrstvwxz"
let sentence = "Xylofoner är fina"
sentence = sentence.replaceAll("x", "ks").replaceAll("X", "Ks")
consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
sentence = sentence.replaceAll(
consonant.toUpperCase(),
consonant.toUpperCase() + "o" + consonant,
)
})
// sentence = "Koksosylolofofononeror äror fofinona"
Note that replacing "x" with "ks" isn't enough — uppercase "X" also needs to be replaced, becoming "Ks" for the rules to match.
Now all three rules are followed and the generator works. But there's plenty left to wish for in the code. Since it doesn't contain functions with descriptive names, you have to figure out what the code does line by line. Even if the code gets longer, it also becomes easier to understand if we break some things into separate functions:
Example 6const consonants = "bcdfghjklmnpqrstvwxz"
const sentence = "Xylofoner är fina"
const sentenceWithoutX = replaceX(sentence)
const translatedSentence = translate(sentenceWithoutX)
function replaceX(sentence) {
sentence = sentence.replaceAll("x", "ks")
sentence = sentence.replaceAll("X", "Ks")
return sentence
}
function translate(sentence) {
consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
sentence = sentence.replaceAll(
consonant.toUpperCase(),
consonant.toUpperCase() + "o" + consonant,
)
})
return sentence
}
// translatedSentence = "Koksosylolofofononeror äror fofinona"
Now you can follow what the code does by reading it as a sentence:
We take a sentence and replace x so we get a sentence without x. Then we translate that sentence.
Starting from the sentence to translate
Even though example 6 works and has fairly readable code, there's something about the strategy of starting from the consonants rather than the sentence to translate that feels a bit backwards. So let's try to build the same generator from the other end.
Example 7const consonants = "bcdfghjklmnpqrstvwxz"
const sentence = "Xylofoner är fina"
const sentenceWithoutX = sentence.replaceAll("x", "ks").replaceAll("X", "Ks")
let translatedSentence = ""
sentenceWithoutX.split("").forEach((letter, index) => {
if (consonants.split("").includes(letter)) {
translatedSentence += sentenceWithoutX[index] + "o" + letter
} else if (consonants.toUpperCase().split("").includes(letter)) {
translatedSentence += sentenceWithoutX[index] + "o" + letter.toLowerCase()
} else {
translatedSentence += sentenceWithoutX[index]
}
})
// translatedSentence = "Koksosylolofofononeror äror fofinona"
Example 7 generates robber writing according to the rules, but there are a lot of problems with the code. This is a perfect use case for the built-in map function:
const consonants = "bcdfghjklmnpqrstvwxz"
const sentence = "Xylofoner är fina"
const sentenceWithoutX = sentence.replaceAll("x", "ks").replaceAll("X", "Ks")
const translatedSentence = sentenceWithoutX
.split("")
.map((letter) =>
consonants.split("").includes(letter.toLowerCase())
? letter + "o" + letter.toLowerCase()
: letter,
)
.join("")
// translatedSentence = "Koksosylolofofononeror äror fofinona"
I think this is a nice solution. But this code too would benefit from being broken into descriptive functions:
Example 9const consonants = "bcdfghjklmnpqrstvwxz"
const sentence = "Xylofoner är fina"
const translatedSentence = translate(sentence)
function translate(sentence) {
const sentenceWithoutX = replaceX(sentence)
return sentenceWithoutX
.split("")
.map((letter) => handleLetter(letter))
.join("")
}
function replaceX(sentence) {
sentence = sentence.replaceAll("x", "ks")
sentence = sentence.replaceAll("X", "Ks")
return sentence
}
function handleLetter(letter) {
return isConsonant(letter) ? handleConsonant(letter) : letter
}
function isConsonant(letter) {
return consonants.split("").includes(letter.toLowerCase())
}
function handleConsonant(letter) {
return letter + "o" + letter.toLowerCase()
}
If for some reason you wanted to avoid using replaceAll, you could attack the x problem like this instead:
const consonants = "bcdfghjklmnpqrstvwxz"
const sentence = "Xylofoner är fina"
const translatedSentence = translate(sentence)
function translate(sentence) {
return sentence
.split("")
.map((letter) => handleLetter(letter))
.join("")
}
function handleLetter(letter) {
return isConsonant(letter) ? handleConsonant(letter) : letter
}
function isConsonant(letter) {
return consonants.split("").includes(letter.toLowerCase())
}
function handleConsonant(letter) {
return isX(letter) ? handleX(letter) : translateConsonant(letter)
}
function isX(letter) {
return letter.toLowerCase() === "x"
}
function handleX(letter) {
return isUpperCase(letter) ? translateUpperCaseX() : translateLowerCaseX()
}
function translateConsonant(letter) {
return letter + "o" + letter.toLowerCase()
}
function isUpperCase(letter) {
return letter === letter.toUpperCase()
}
function translateUpperCaseX() {
return translateConsonant("K") + translateConsonant("s")
}
function translateLowerCaseX() {
return translateConsonant("k") + translateConsonant("s")
}
At first glance example 10 can look very convoluted. But since each function only has a tiny job, it's easy to give the functions meaningful names, which in turn makes it easier to understand what's happening.
Writing clean code is always a trade-off.
