Finding broken Indic text rendering on the web.
Every Indian consumer app ships Indic text. Almost none of it is tested, because the people doing the testing read English, and these bugs are invisible unless you read the script. A clipped shirorekha reads as the font looks a bit odd. A substituted font reads as the design looks a bit cheap. Nobody files those, and everybody sees them.
Akshara points a headless browser at a page and measures. Below is the most common finding, which is also the whole argument.
Two ways, and the first needs no terminal at all.
Drag this to your bookmarks bar, then click it on any page. The full scan runs on the page you are looking at and the findings appear in a panel — click one to outline the elements it found.
अ Akshara ← drag me up to the bookmarks bar (⌘⇧B shows it)
Dragging not working? ,
then right-click the bookmarks bar → Add page… and paste it as the URL.
Not into the address bar — Chrome silently strips
javascript: when you paste there, which is the usual reason a bookmarklet
does nothing.
If that is still fighting you, paste the code into the console instead. It always works and there is nothing to save.
· open any page · ⌥⌘J (Mac) or Ctrl Shift J · paste · Enter.
Chrome asks you to type allow pasting the first time it sees a paste into
the console. That warning is aimed at scams that talk people into pasting code they have
not read — this code is right here,
and you should read it before trusting it, exactly as that warning intends.
It works because it is not cross-origin: a bookmarklet is first-party script in a page
you have already loaded, so it reads that page's DOM and fonts the way its own code
would — including pages behind a login. A strict script-src policy will
block it on a minority of sites; everything else is fair game. Nothing is sent anywhere.
Scanning someone else's URL drives a real browser, so it runs on your machine rather than in this tab. Node 20 or newer; there is nothing to sign up for.
Ordered by how often each one bites in the wild.
.notdef.slice(0, 20) cutting between a consonant and its vowel sign. .length counts code units; a letter is several of them.Both of these run the real thing, in your browser, in your fonts. The measurements come from the same Canvas call the scanner uses, so the verdict flips exactly where CI would fail the build.
शुभ विवाह की हार्दिक शुभकामनाएँ — आपके नए जीवन के लिए ढेर सारा प्यार
Drag it. slice counts UTF-16 code units and lands mid-letter, leaving a
dangling halant or a vowel sign decorating nothing. The right column counts aksharas.
The rendering checks need a real browser. These do not — truncation, normalisation and numeral mixing are decisions about the text itself, so they run here, instantly, on whatever you paste. Nothing leaves your browser.
Intl.Segmenter cannot tell you. It implements UAX #29 grapheme
clusters, and those split क्षि, which is one letter to anyone who reads it.
So Akshara segments on the orthographic unit instead. A virama binds forward into the consonant that follows it; every combining mark binds backward onto the cluster it decorates.
akshara := Base Nukta? ( Virama Join? Base Nukta? )* Mark*
The trap that catches a naive implementation: every virama carries
General_Category=Mn. Test is this a combining mark before
is this a virama and you swallow the one character that binds the conjunct,
and every cluster quietly splits in two.
Real output from the torture fixture in the repository, evidence included, so the call can be checked rather than trusted.
Run against 23 URLs — national and regional news, plus Wikipedia in three scripts as a control. Twenty had Indic text on the page scanned. Seventeen of those twenty raised at least one error or warning.
Arial, sans-serif meant 220 elements of Hindi rendered in whatever the device happened to have.Three sites came back with nothing above a note, and they are worth naming because the failures are not: mathrubhumi.com, hindutamil.in and maalaimalar.com.
The audit found more defects in the scanner than classes of defect on the web — a wait
strategy that could not load a quarter of the sample, a clipping check fooled by
line-height: 0, and 141 truncation findings that turned out to be correctly
spelled Telugu and Hindi. All three are written up, with the numbers and the caveats, in
AUDIT.md.
A linter is worth only as much as the false positives it declines to raise.
வணக்கம் and தமிழ் both end on a bare pulli, because that is how the language is spelled. Calling a trailing virama truncation would condemn two entire languages, so the rule is per-script: raised in Devanagari and Telugu, silent in Tamil, Malayalam and Sinhala.
Chrome's line breaker turns out to be shaping-aware. It wraps between letters and will
not split an akshara even under word-break: break-all — measured, not
assumed. Shipping a check that can never fire would be dishonest, so
linebreak looks instead for clusters divided across DOM nodes, which is
real, and which Chrome cannot save you from.
// scan a page npx akshara-qc https://example.in --screenshot findings.png // or fix the string at its source, with no browser involved import { truncate, aksharaLength } from "akshara-qc"; truncate("नमस्ते दोस्तों", 3); // "नमस्ते…" never a dangling halant "नमस्ते".slice(0, 4); // "नमस्" half a letter aksharaLength("नमस्ते"); // 3 .length says 6
The exit code is non-zero when anything at or above --fail-on is found, so
it drops straight into CI.