Bilingual Typing Fixer

One-click fixer for mistyped Arabic↔English text in input fields and contentEditable elements.

// ==UserScript==
// @name         Bilingual Typing Fixer
// @namespace    https://greasyfork.org/en/users/1444872-tlbstation
// @version      2.3
// @description  One-click fixer for mistyped Arabic↔English text in input fields and contentEditable elements.
// @icon         https://i.ibb.co/Zpv1gJTj/logo.png
// @author       TLBSTATION
// @match        *://*/*
// @grant        none
// @run-at       document-idle
// @homepage     https://bilingualtypingfixer.netlify.app
// @homepageURL  https://bilingualtypingfixer.netlify.app
// @license MIT 
// ==/UserScript==

(function () {
    'use strict';

    const arToEnMap = {
        ض: "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",
        و: ",", ز: ".", ظ: "/", ذ: "`"
    };

    const enToArMap = Object.fromEntries(
        Object.entries(arToEnMap).flatMap(([ar, en]) => [[en, ar]])
    );

    function detectLanguage(text) {
        const arabicChars = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
        const englishChars = /[a-zA-Z]/;

        let ar = 0, en = 0;
        for (let ch of text) {
            if (arabicChars.test(ch)) ar++;
            else if (englishChars.test(ch)) en++;
        }
        return ar >= en ? "arToEn" : "enToAr";
    }

    function translate(text) {
        const dir = detectLanguage(text);
        let result = "";

        if (dir === "arToEn") {
            text = text.replace(/لا/g, "b");
            for (let ch of text) result += arToEnMap[ch] || ch;
        } else {
            for (let ch of text.toLowerCase()) result += enToArMap[ch] || ch;
        }

        return result;
    }

    let currentButton = null;
    let lastTarget = null;

    function createButton(target) {
        if (currentButton) currentButton.remove();

        const btn = document.createElement("button");
        btn.type = "button";
        btn.innerHTML = `
      <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" style="vertical-align:middle;">
        <path fill="currentColor"
              d="M7.5 5.6L5 7l1.4-2.5L5 2l2.5 1.4L10 2L8.6 4.5L10 7zm12 9.8L22 14l-1.4 2.5L22 19l-2.5-1.4L17 19l1.4-2.5L17 14zM22 2l-1.4 2.5L22 7l-2.5-1.4L17 7l1.4-2.5L17 2l2.5 1.4zm-8.66 10.78l2.44-2.44l-2.12-2.12l-2.44 2.44zm1.03-5.49l2.34 2.34c.39.37.39 1.02 0 1.41L5.04 22.71c-.39.39-1.04.39-1.41 0l-2.34-2.34c-.39-.37-.39-1.02 0-1.41L12.96 7.29c.39-.39 1.04-.39 1.41 0" />
      </svg> Fix Text
    `;
      btn.style.position = "absolute";
      btn.style.fontSize = "12px";
      btn.style.background = "#222";
      btn.style.color = "white";
      btn.style.border = "1px solid #555";
      btn.style.borderRadius = "5px";
      btn.style.padding = "3px 6px";
      btn.style.cursor = "pointer";
      btn.style.zIndex = "9999";
      btn.style.boxShadow = "0 2px 5px rgba(0,0,0,0.5)";
      btn.style.display = "flex";
      btn.style.gap = "5px";
      btn.style.alignItems = "center";

      const rect = target.getBoundingClientRect();
      btn.style.top = `${Math.max(0, window.scrollY + rect.top - 30)}px`;
      btn.style.left = `${Math.min(window.innerWidth - 120, window.scrollX + rect.left + rect.width - 100)}px`;

      btn.addEventListener("mousedown", e => e.preventDefault());
      btn.addEventListener("click", e => {
          e.preventDefault();

          let text = "";
          let lexicalSpans = [];

          if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
              text = target.value;
          } else if (target.isContentEditable) {
              lexicalSpans = target.querySelectorAll('[data-lexical-text]');
              text = lexicalSpans.length
                  ? Array.from(lexicalSpans).map(s => s.textContent).join('')
              : target.innerText || target.textContent || '';
          } else {
              text = target.innerText || target.textContent || "";
          }

          const translated = translate(text);

          if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
              target.value = translated;
              target.focus();
              target.selectionStart = target.selectionEnd = translated.length;
          } else if (target.isContentEditable) {
              if (lexicalSpans.length && translated.length >= lexicalSpans.length) {
                  lexicalSpans.forEach((span, i) => {
                      span.textContent = translated[i] || "";
                  });
                  target.focus();
              } else {
                  target.innerText = translated;
                  const range = document.createRange();
                  range.selectNodeContents(target);
                  range.collapse(false);
                  const sel = window.getSelection();
                  sel.removeAllRanges();
                  sel.addRange(range);
                  target.focus();
              }
          } else {
              target.innerText = translated;
              target.focus();
          }
      });

      document.body.appendChild(btn);
      currentButton = btn;
  }

    document.addEventListener("focusin", e => {
        const el = e.target;
        if (
            (el.tagName === "INPUT" && el.type === "text") ||
            el.tagName === "TEXTAREA" ||
            el.isContentEditable
        ) {
            lastTarget = el;
            createButton(el);
        } else {
            if (currentButton) {
                currentButton.remove();
                currentButton = null;
            }
        }
    });

    window.addEventListener("scroll", () => {
        if (lastTarget && currentButton) {
            createButton(lastTarget);
        }
    });

    const observer = new MutationObserver(() => {
        const active = document.activeElement;
        if (
            (active && active.tagName === "INPUT" && active.type === "text") ||
            active.tagName === "TEXTAREA" ||
            active.isContentEditable
        ) {
            if (active !== lastTarget) {
                lastTarget = active;
                createButton(active);
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    setInterval(() => {
        const active = document.activeElement;
        if (
            (active && (active.tagName === "INPUT" && active.type === "text")) ||
            active.tagName === "TEXTAREA" ||
            active.isContentEditable
        ) {
            if (active !== lastTarget) {
                lastTarget = active;
                createButton(lastTarget);
            }
        }
    }, 1000);
})();