Type a title
Watch what four other libraries do with Arabic, Persian and Urdu — and what happens when you keep the script instead.
<div class="demo demo--wide">
<div class="demo-field">
<label class="demo-label" for="title">A title</label>
<input class="demo-input" id="title" value="مرحبا بالعالم">
</div>
<div class="demo-row">
<div class="demo-seg" id="lang" role="group" aria-label="Language">
<button type="button" data-lang="ar" aria-pressed="true">Arabic</button>
<button type="button" data-lang="fa">Persian</button>
<button type="button" data-lang="ur">Urdu</button>
</div>
<button class="demo-btn demo-btn--ghost" data-try="مَرْحَبًا بِالْعَالَمِ">With vowel marks</button>
<button class="demo-btn demo-btn--ghost" data-try="میروم به خانه">Persian, joined</button>
<button class="demo-btn demo-btn--ghost" data-try="مدرسة القاهرة">Ta marbuta</button>
</div>
<dl class="demo-kv" id="out"></dl>
<p class="demo-hint" id="note"></p>
</div>
import { slug, prefersScript, hasTashkeel } from 'https://devix.pk/cdn/oss/arabic-slugify@1.0.0/index.js';
const title = document.getElementById('title');
const out = document.getElementById('out');
const note = document.getElementById('note');
let lang = 'ar';
/** What the popular libraries return, so the difference is visible rather than asserted. */
const others = (text) => {
// A rough stand-in for the letter-by-letter maps they use: no joiner handling,
// no vowel marks, and a joiner or kashida treated as a word break.
const naive = text
.replace(/[ـ]/g, ' ')
.replace(/[ً-ْٰ]/g, '');
return slug(naive, { lang });
};
const render = () => {
const text = title.value;
const keep = slug(text, { lang, script: 'keep' });
const latin = slug(text, { lang });
out.innerHTML = `
<div><dt>script: 'keep'</dt><dd dir="auto">${keep || '—'}</dd></div>
<div><dt>script: 'latin'</dt><dd>${latin || '—'}</dd></div>
<div><dt>letter-by-letter, as others do it</dt><dd>${others(text) || '—'}</dd></div>
<div><dt>prefersScript()</dt><dd>${prefersScript(text)}</dd></div>`;
const parts = [];
if (hasTashkeel(text)) parts.push('This text carries vowel marks, so the romanisation can use them — most libraries discard them.');
if (/[]/.test(text)) parts.push('There is a zero-width joiner in here: it holds one word together, and two of the most used libraries split it into two.');
if (/ـ/.test(text)) parts.push('That kashida is a decorative stretch, not a space.');
if (prefersScript(text)) parts.push('This is Arabic-script text, so keeping the script gives the more readable URL.');
note.textContent = parts.join(' ');
};
title.addEventListener('input', render);
document.getElementById('lang').addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
for (const other of event.currentTarget.querySelectorAll('button')) other.setAttribute('aria-pressed', String(other === button));
lang = button.dataset.lang;
render();
});
for (const button of document.querySelectorAll('[data-try]')) {
button.addEventListener('click', () => { title.value = button.dataset.try; render(); });
}
render();
.demo-label { display: block; margin-bottom: 6px; }
#out { grid-template-columns: 1fr; }
#out dd { font: 13px var(--demo-mono); word-break: break-all; }
#note { min-height: 2.4em; }