What Arabic Language Taught Us About Rebuilding CranL [English]

meta
updates
engineering
In this post, we discuss the effect of Arabic language on CranL’s rebuilding process.
Author
Published

September 18, 2026

Hello from the CranL team. Arabic support across the whole product and a rebuilt interface are both live today. This post is about how we built them, and about the places where what we assumed going in turned out to be wrong.

See the Arabic version here :

Why Arabic, and why not the quick version

There is a fast way to do this. Extract the strings, hand them to a translator, set dir=“rtl” on the root element, mirror the margins, ship. It produces something that looks like Arabic at a glance and comes apart on the second glance.

We wanted a product that is written in Arabic for the people reading Arabic, not an English product wearing Arabic words. That choice set the cost. Roughly 700 engineering hours went into Arabic and the redesign together, and most of the Arabic half went into things a translation layer never touches.

Two decisions carried a lot of the weight. First, the locale resolves on the server, so the first byte of HTML already carries the right lang and dir. Nothing renders left to right and then snaps around. Second, the English dictionary exports its type and the Arabic dictionary declares against it. A missing translation is a build failure. It cannot reach production and wait there to be discovered.

What Arabic taught us

English has two plural forms. Arabic has six: zero, one, two, few, many, other. Every count in the product resolves through Intl.PluralRules with all six categories filled in, and filling them in is where the education happened.

The dual is a real grammatical number, not a rounding error on the plural. 2 دقيقة is wrong the way “2 minute” is wrong, except the repair is not a suffix. It is a different word: دقيقتين (“two minutes”).

Case changes with position, too. After a preposition the dual is genitive: قبل دقيقتين (“two minutes ago”). Standing on its own it is nominative: تطبيقان (“two applications”). Same number, same noun, two forms, and a naive interpolation has no idea which sentence it landed in unless the sentence tells it.

Above ten it changes again. Eleven through ninety-nine take an accusative singular tamyīz, so eleven applications is 11 تطبيقًا, with a singular noun. A counter we had treated as one branch turns out to change shape as the number climbs: the dual تطبيقان, then the small plural for three to ten (3 تطبيقات, please check this one against your own ear), then 11 تطبيقًا above that.

This is also why we stopped building sentences out of fragments. Every sentence is stored as a whole template with placeholders, because Arabic orders the parts differently and because the conjunction و attaches directly to the word after it with no space. Concatenation cannot survive either fact.

Before any dictionary work started we wrote a house style guide, and most of it is a list of bans on عرنجية, the calqued register that pours English structure into Arabic words. No تم plus a مصدر to fake a passive. No قم بـ. No الخاص بك where a possessive suffix belongs. No يرجى on routine instructions. No بشكل plus an adjective where a حال is the correct form. One rule applies everywhere: مصدر for buttons and labels, imperative for helper text and errors.

Some Latin text stayed on purpose. Brand names, code samples, HTTP status names, keyboard shortcuts, and git’s main are not translation gaps. They are the words.

The redesign, and what we found under it

The interface overhaul shipped alongside Arabic because both jobs required opening every screen. More than 7,500 people wrote in to tell us what was and was not working in the dashboard, and what they told us was consistent enough that it stopped being a list of requests and started being one sentence: the dashboard was hard to work in.

The core of it was that CranL had grown faster than its navigation. A project can hold applications, managed databases, object storage, CDN zones, sandboxes, email domains, cron schedules and custom domains, and each of those had arrived with its own screens, its own layout conventions and its own idea of where the important controls live. Learning one told you very little about the next. Routine work suffered most. Getting from an application to the database it talks to meant leaving the thing you were looking at and finding your way back in from the top. Connection details, environment variables and logs sat at different depths depending on which resource you had opened. Deploying and then checking whether the deploy worked were two different places.

None of that is a bug you can file. It is a tax, paid in clicks and short-term memory, every time someone does ordinary work. So the redesign was not a reskin. We rebuilt the shell around the project, gave every resource type the same anatomy so that overview, settings, logs and connection information are in the same position no matter what you opened, and put the actions people take most often where they are looking when they need them.

Reading every page twice, once for direction and once for design, is also how we found problems that had nothing to do with either. These ones were live. Users had them.

Our logo was invisible in light mode. Application status was stale everywhere except one page, including in the public API that customers script against, which means anyone automating against us was reading a number that had stopped being true. The deploy button kept insisting a deployment was still running after it had finished, until you reloaded the page. And the login form, the single screen every user passes through, had no autocomplete attributes, no announced errors for screen readers, and a country picker you could not operate from the keyboard.

None of these were RTL bugs. They were bugs we had been shipping in English, and we only saw them because we were finally looking at every screen instead of the screen we were working on.

The part where our tooling told us we were finished

Our first coverage checker searched the codebase for quoted strings and reported that we were done. It was wrong in a specific, embarrassing way: text written directly into markup, with no quotes around it, was invisible to it. We rewrote the checker to walk every file with the TypeScript compiler’s own parser instead of matching text. It found 462 user-visible English strings across 78 files, including entire paragraphs on the signup and billing screens.

The bidirectional algorithm caught us in the same register. A slash sitting between two runs of digits has no direction of its own, so it inherits the paragraph direction, and the pair around it reverses. A storage quota reading “2 GB / 10 GB” renders as “10 GB / 2 GB”. Nothing throws. Nothing logs. The layout is mechanically correct and it tells the reader the opposite of the truth. In the same sweep we found the password reveal button sitting directly on top of the password it was supposed to reveal.

We found all three of these during development, which is the only reason we can write about them this calmly. No user saw them. But they are the clearest thing we learned all year: the failures that matter most in this work do not announce themselves. They pass the build, pass the tests, and read as fine to anyone who is not reading the language.

If you are starting this work

Resolve the locale on the server and let the type system make completeness mandatory, because both decisions remove a class of bug rather than a bug. Store sentences as templates. Write the style guide before the dictionaries, not after, or you will be reconciling registers at the end. Do not trust a checker that matches text; parse the source. And budget real time for a fluent speaker to read every screen with their own eyes, because the algorithm will happily lay out a lie.

try cranl

Ship your app in seconds, not weekends.

Flat pricing, MENA-first infra, GitHub deploys, CDN, DDoS, and SSL — all included.