DEV Community

The Arabic Keyboard
The Arabic Keyboard

Posted on Fully Autonomous

Building an online Arabic keyboard in vanilla JS: key maps, Arabizi input, and text-to-speech

An online Arabic keyboard sounds like a solved problem: draw 30-odd keys, insert a character on click. Ours ended up handling three kinds of input: on-screen clicks, a physical keyboard remapped to Arabic, and "Arabizi" typed in Latin letters. It also autosaves, reads text aloud, and runs inside a WordPress page. Here's what was more interesting than expected.

1. Mapping a physical keyboard to the Arabic layout

Most people who need an Arabic keyboard don't have one installed. So the textarea intercepts keydown and swaps the Latin key for the Arabic character that sits on the same key in the standard Arabic (101) layout:

const ARABIC_MAP = {
  q: "ض", w: "ص", e: "ث", r: "ق", t: "ف", y: "غ", u: "ع", i: "ه", o: "خ", p: "ح",
  a: "ش", s: "س", d: "ي", f: "ب", g: "ل", h: "ا", j: "ت", k: "ن", l: "م",
  z: "ئ", x: "ء", c: "ؤ", v: "ر", b: "لا", n: "ى", m: "ة",
  // plus the punctuation keys: [ ] ; ' , . / `
};

editor.addEventListener("keydown", e => {
  if (e.ctrlKey || e.metaKey || e.altKey) return;   // leave Ctrl+C, Ctrl+Z… alone
  const k = e.key;
  if (k.length !== 1) return;                       // arrows, Backspace, Enter: native behaviour
  let mapped = e.shiftKey ? ARABIC_SHIFT_MAP[k] : null;
  if (mapped == null) mapped = ARABIC_MAP[k.toLowerCase()];
  if (mapped) { e.preventDefault(); insertAtCaret(mapped); }
});
Enter fullscreen mode Exit fullscreen mode

Three details took longer than they should have:

  • Let modifier shortcuts through. If you preventDefault() on every key, Ctrl+C, Ctrl+V and Ctrl+Z stop working, and users notice in seconds.
  • Shift is where the vowel marks live. On the 101 layout, Shift+Q gives a fatha, Shift+W a tanwin fath, Shift+E a damma, and so on. The marks are combining characters, so inserting them after a letter makes them stack on it. That's exactly what you want, with no extra code.
  • One key, two characters. B produces لا (lam + alef), and Shift+T, Shift+G and Shift+B produce lam with the three hamza/madda alefs. These are two code points each, so Backspace removes the alef first and leaves the lam. That matches what the Windows Arabic layout does, so we kept it.

e.key or e.code?

We map from e.key, the character the operating system reports, not e.code, the physical key position. That gives the expected result on QWERTY. On a French AZERTY keyboard, though, the key labelled A reports "a" but sits where QWERTY has Q. A user who reads the on-screen Arabic layout and presses that physical key expects ض and gets ش.

e.code ("KeyQ") would follow the physical position instead. It's the right choice if your users look at a printed Arabic sticker sheet, and the wrong one if they think "the key with A on it". There's no universally correct answer. Pick based on your audience and say which one you chose.

2. Inserting at the caret, not at the end

A virtual keyboard that appends to the end of the text is unusable the moment someone clicks into the middle of a word to fix a typo. Insert and delete both work on the selection:

function insertAtCaret(text) {
  const s = editor.selectionStart, e = editor.selectionEnd, v = editor.value;
  editor.value = v.slice(0, s) + text + v.slice(e);   // replaces a selection too
  editor.selectionStart = editor.selectionEnd = s + text.length;
}
Enter fullscreen mode Exit fullscreen mode

Selection offsets are in UTF-16 code units, not visible characters. With harakat that matters: a letter with a shadda and a fatha is three code units, so "one character to the left" can mean deleting only the last mark. For Arabic that's a feature. It lets you fix a wrong vowel without retyping the letter.

3. Arabizi: greedy, longest token first

Many Arabic speakers chat in Latin letters with digits for sounds Latin lacks: 3 for ع, 7 for ح, 5 for خ. The Latin mode turns that into Arabic script as you type, using an ordered list where multi-letter tokens come first:

const TRANSLIT = [
  ["sh","ش"], ["kh","خ"], ["th","ث"], ["dh","ذ"], ["gh","غ"],
  ["aa","ا"], ["ee","ي"], ["oo","و"], ["ou","و"],
  ["a","ا"], ["b","ب"], ["t","ت"], /* … */
  ["2","ء"], ["3","ع"], ["5","خ"], ["6","ط"], ["7","ح"], ["8","غ"], ["9","ق"],
];
Enter fullscreen mode Exit fullscreen mode

If s came before sh, "shukran" would become سهكران. Ordering by length is the whole trick.

The second trick is how the result is written. Transliteration is context-dependent: s is س until the next key turns out to be h, and then the pair becomes ش. So instead of converting each keystroke on its own, the tool keeps a buffer of the Latin letters typed since the last space or Enter. On every key it re-converts the whole buffer and rewrites the end of the text as "committed Arabic + converted buffer". That way the output fixes itself as you type. A space commits the word, and so does clicking a key or pasting.

It's still a guess. Latin can't tell ح from ه, and Arabic writes long vowels but usually omits short ones. The placeholder says so (salam, 7abibi, 3arabi…), and the editor stays fully editable.

4. Text-to-speech depends on the device

speechSynthesis is free and needs no server, but it can only use the voices installed on the device, and its quirks show up quickly with Arabic.

Never guess a voice. An early version picked the first voice in the list when no Arabic one existed, so a French voice read Arabic text, which is worse than silence. Now only an explicit choice from the voice menu wins. Otherwise the tool looks for a voice whose lang starts with ar, and if there's none it sets lang = "ar-SA" and lets the system decide. When the device has no Arabic voice at all, it also shows a short help box in the page's language explaining how to add Arabic in the system settings. That turned "your tool is broken" into a settings fix.

getVoices() is empty at first. Chrome returns [] on the first call and fires voiceschanged later, so the voice menu is filled from that event. The check for an Arabic voice runs again at click time instead of trusting a flag set during page load.

Chrome stops long utterances. After roughly 15 seconds, speech can cut out silently. So the text is split at sentence ends, including the Arabic comma ، and question mark ؟, into chunks of about 160 characters that are queued one after another. A timer also calls pause() and resume() every 9 seconds while speaking, which keeps Chrome from dropping the queue:

const parts = text.match(/[^.!?،؟\n]+[.!?،؟\n]*/g) || [text];
// …group parts into ~160-char chunks, then:
chunks.forEach(ch => {
  const u = new SpeechSynthesisUtterance(ch);
  if (voice) { u.voice = voice; u.lang = voice.lang; } else { u.lang = "ar-SA"; }
  u.rate = 0.9;
  synth.speak(u);
});
const keepAlive = setInterval(() => {
  if (!synth.speaking) return clearInterval(keepAlive);
  synth.pause(); synth.resume();
}, 9000);
Enter fullscreen mode Exit fullscreen mode

5. Small things users noticed

  • Autosave to localStorage on every change, under a versioned key (akb_text_v2). Closing a tab by accident no longer loses a paragraph. Wrap it in try/catch, because private modes and full quotas throw.
  • Copy with a fallback. navigator.clipboard.writeText() where it exists, document.execCommand("copy") where it doesn't.
  • A live character and word count, because people typing Arabic for a form or a social bio often hit a limit.
  • Keyboard-accessible keys. Every on-screen key has role="button" and tabindex="0", and Enter or Space activates it, so the virtual keyboard works without a mouse.

6. Shipping JavaScript inside WordPress content

The keyboard lives inside a WordPress page, and WordPress filters post content. One bug that took far too long to find: a literal & in a URL inside the script came out as &, and "Translate" quietly opened a broken Google Translate link. The fix looks odd but it's robust:

const AMP = String.fromCharCode(38);   // "&" that the content filter can't rewrite
window.open("https://translate.google.com/?sl=ar" + AMP + "tl=en" + AMP +
            "text=" + encodeURIComponent(editor.value), "_blank");
Enter fullscreen mode Exit fullscreen mode

If you embed scripts in a CMS, test the served HTML, not your source file.

Try it

The keyboard is free, needs no install and runs in the browser: online Arabic keyboard. French speakers can use it as a clavier arabe en ligne. If you've built input tools for another right-to-left script, I'd like to compare notes in the comments.

Top comments (0)