Arabic plurals in production: an engineering playbook
How to map Arabic counts to complete translated messages, test category boundaries, and keep rounding consistent across services.

English gives product teams a misleadingly easy plural model: one item, everything else items. Carry that model into an Arabic interface and the labels may be translated while the grammar is still English.
Arabic cardinal plural selection has six categories in Unicode CLDR: zero, one, two, few, many, and other. That does not mean developers should write six bits of Arabic in application code. It means the message system needs six routes, translators need the whole sentence for each route, and tests need to cover the boundaries where those routes change.
This guide uses JavaScript's Intl.PluralRules, but the design applies to ICU MessageFormat, FormatJS, mobile clients, and backend notification services.
A plural category is not a translated phrase
Intl.PluralRules answers a narrow question: which locale-specific category applies to this number under these formatting options? It returns a label such as few. It does not know whether your product is counting invoices, files, minutes, or people.
That separation matters. The noun, verb, word order, and sometimes the entire sentence can change. The W3C's guidance on composite messages warns against building sentences from reusable fragments because translators may need to reorder or rewrite those parts. Arabic number agreement is one of its examples.
Treat the category as a routing value:
number + precision policy
↓
Intl.PluralRules("ar")
↓
zero | one | two | few | many | other
↓
translator-owned complete message
The last step belongs in the message catalog, not in a chain of string concatenations.
Sources: Unicode CLDR 48, ECMA-402, and a live Chromium probe on 31 August 2026. SultanByte editorial artwork.
What the browser selects
A live Chromium probe with new Intl.PluralRules("ar") returned all six cardinal categories. These examples are useful test fixtures:
| Input | Category |
|---|---|
| 0 | zero |
| 1 | one |
| 2 | two |
| 3, 4, 5, 10 | few |
| 11, 12, 20, 21, 22, 99 | many |
| 100, 101, 102 | other |
| 103 | few |
| 1.5 | other |
The pattern after 100 is where hand-written conditions often fail. A developer may encode 3..10 as few and 11..99 as many, then assume every larger number is other. The category for 103 proves that shortcut is wrong.
Use the runtime rather than copying the table into business logic:
const arabicCardinals = new Intl.PluralRules("ar");
export function pluralCategory(value) {
return arabicCardinals.select(value);
}
The CLDR Arabic chart is the source for the rules and examples. The MDN reference is a useful API guide, while ECMA-402 defines browser behavior.
Do not reuse cardinal logic for ordinals. In the same probe, Arabic ordinal selection returned other for every tested value. Cardinal and ordinal rules are separate datasets and separate constructors:
const cardinal = new Intl.PluralRules("ar", { type: "cardinal" });
const ordinal = new Intl.PluralRules("ar", { type: "ordinal" });
Precision is part of the message contract
The easiest bug to miss is not a boundary such as 10 or 11. It is rounding.
With default options, 1.5 selected other. With { maximumFractionDigits: 0 }, the same input selected two because the value used by plural selection was rounded. In the live probe, 1.1 selected one and 1.5 selected two under that whole-number policy.
new Intl.PluralRules("ar").select(1.5);
// "other"
new Intl.PluralRules("ar", {
maximumFractionDigits: 0,
}).select(1.5);
// "two"
This is not an obscure standards detail. A marketplace may display quantities to two decimal places while a notification service rounds them to integers. If each service creates Intl.PluralRules with different options, the same underlying value can choose different copy.
Define one formatting contract per domain value:
const quantityPolicy = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
};
const number = new Intl.NumberFormat("ar", quantityPolicy);
const plural = new Intl.PluralRules("ar", quantityPolicy);
Use the same policy for display and selection. Store it with the message specification, test it, and change it deliberately.
Keep complete messages together
The ICU MessageFormat guide recommends keeping complex choices in one message pattern. Translators can then see every branch in context. It also requires an other branch, which gives the runtime a safe grammatical route for values that do not match another category.
A FormatJS message can look like this:
{count, plural,
zero {لا توجد عناصر في السلة}
one {عنصر واحد في السلة}
two {عنصران في السلة}
few {# عناصر في السلة}
many {# عنصرًا في السلة}
other {# عنصر في السلة}
}
The Arabic above illustrates catalog structure, not universal product copy. Have an Arabic linguist review the wording for the noun and context you actually use. A shopping cart, a payment warning, and a medical result should not inherit one generic pattern merely because they all contain a count.
FormatJS documents both plural categories and exact-number selectors such as =0. Exact selectors are useful when product copy needs a special branch for a particular number. They should not replace locale categories wholesale.
Avoid this pattern:
`${count} ${count === 1 ? t("item") : t("items")}`
It hard-codes the English two-form assumption, separates the number from its grammatical context, and gives the translator no control over the full sentence.
Build a small message contract
A production message should have enough metadata to survive handoffs between product, engineering, localization, and QA:
type PluralMessageContract = {
id: string;
locale: "ar";
type: "cardinal" | "ordinal";
numberOptions: Intl.PluralRulesOptions;
description: string;
requiredCategories: string[];
};
For Arabic cardinal messages, derive requiredCategories from the runtime rather than maintaining a second list:
const rules = new Intl.PluralRules("ar");
const { pluralCategories } = rules.resolvedOptions();
In the browser probe, pluralCategories was zero, one, two, few, many, and other. Your CI can compare that list with the branches in the compiled catalog. Missing two or many becomes a build failure instead of a production screenshot.
Descriptions matter too. "Items" is weak context. "Number of physical products currently in the shopping cart; shown below the checkout button" gives a translator something they can work with.
Test categories, boundaries, and rendering
A useful test suite has three layers.
First, test representative values and boundaries against Intl.PluralRules. Include at least 0, 1, 2, 3, 10, 11, 99, 100, 102, 103, plus the decimals your product accepts.
Second, test the compiled message catalog. Every category returned by resolvedOptions().pluralCategories needs a branch, and every branch must render without leaking placeholders.
Third, test the interface. Arabic digits, bidirectional text, narrow mobile layouts, screen readers, and dynamic updates can expose problems that unit tests miss. A grammatically correct sentence is still broken if the count is clipped or the reading order is wrong. The same discipline applies to Arabic text normalization and security: locale-aware behavior has to survive the full production path, not only a helper function.
A compact boundary test is enough to catch most accidental regressions:
const cases = new Map([
[0, "zero"],
[1, "one"],
[2, "two"],
[3, "few"],
[10, "few"],
[11, "many"],
[99, "many"],
[100, "other"],
[103, "few"],
[1.5, "other"],
]);
for (const [value, expected] of cases) {
if (plural.select(value) !== expected) {
throw new Error(`Unexpected Arabic plural for ${value}`);
}
}
Keep the test options identical to the display policy. Otherwise the test can pass while the UI chooses another branch.
Ship the grammar as product behavior
Arabic plural handling is not a translation clean-up task at the end of a release. It is observable product logic: a number is rounded under a defined policy, mapped through locale data, and rendered through copy that a translator owns.
The implementation is small. The discipline around it is what prevents bugs. Use the platform's plural engine, keep complete messages together, document precision, and test the values where categories change. That gives Arabic users an interface designed for Arabic rather than an English interface with different labels.




