Why Arabic Form Validation Messages Need Their Own Review Pass
Why literally-translated Arabic validation copy reads wrong, how Arabic's six CLDR plural categories break a naive 'you have N errors' string, and RTL-correct code for react-hook-form, Zod and ICU MessageFormat.
A form that validates correctly in Arabic and a form that reads correctly in Arabic are not the same deliverable, and most QA passes only check the first one. The email regex fires, the required-field check fires, a red border appears on the right field — and the message sitting next to it is a string that a native Arabic speaker can read without difficulty but would never actually write: calqued sentence structure borrowed from the English original, a singular/plural distinction that doesn't match how Arabic counts things, an icon glued to the physical left edge of the input because that's where the LTR mockup put it.
None of that shows up in a functional test. It shows up as a product that feels foreign to the audience it was localized for, one string at a time, in exactly the part of the interface where users are already frustrated. This post is about the content and UX layer of Arabic form validation specifically: writing error copy that reads as native prose rather than translated English, handling Arabic's plural grammar correctly when a message includes a count, and positioning inline errors and icons so they resolve correctly in RTL without a second, hand-mirrored stylesheet. It assumes your fields are already forced to the right base direction — our RTL bug-testing guide already covers forcing direction: ltr on email, phone and password inputs inside an Arabic form, so this post doesn't repeat that ground.
Key Takeaways
- Arabic cardinal numbers resolve to six distinct grammatical categories —
zero,one,two,few,many,other— not the two English uses, and MDN's ownIntl.PluralRulesdocumentation uses Arabic specifically to demonstrate all six.- The exact boundaries are documented, not approximate: per Unicode's CLDR plural-rules chart, Arabic's
fewcoversn % 100 = 3..10andmanycoversn % 100 = 11..99— so 6 errors and 106 errors both takefew, while 15 and 115 both takemany.- A ternary written for English (
count === 1 ? 'error' : 'errors') silently collapses four of Arabic's six categories into one wrong string — the bug is invisible in code review because the code compiles and the English fallback still works.- ICU MessageFormat's
pluralargument and libraries built on it — FormatJS, i18next — let translators write one variant per CLDR category instead of forcing a single templated sentence across all six.- Validation libraries don't localize summary counts for you. Zod's error customization and community bridges like
zod-i18n-maptranslate individual field errors; a "you have N errors" summary is application code you write, and it's exactly where plural-category logic needs to live.- WCAG Success Criterion 3.3.1 requires an error to be "described to the user in text," not just an icon or a red border, and W3C's own guidance on inline error notification recommends feedback "at or near the form control" — neither is RTL-specific, so the translation to logical CSS is left to you, and this post does it.
Why the correct translation can still read wrong
Most Arabic validation copy in production started life as an English string passed to a translator, and a fluent, accurate translation of "Please enter a valid email address" can still land as يرجى إدخال عنوان بريد إلكتروني صالح — grammatically flawless and unmistakably a translation. Native error copy in Arabic-first products tends to be shorter, more direct, and less deferential than the English convention of "please": a message like أدخل بريدًا إلكترونيًا صالحًا reads as an instruction a person actually writes, not a sentence assembled by mapping English clause order onto Arabic words.
The tell is usually syntactic before it's lexical. English builds validation messages around a passive, hedged structure — "this field is required," "the password must contain at least 8 characters" — because English UX writing conventions favor indirectness. Carrying that structure into Arabic word-for-word produces something like يجب أن يحتوي هذا الحقل على 8 أحرف على الأقل, which is correct but noticeably more formal and roundabout than how the same constraint gets written by someone drafting Arabic copy from scratch: كلمة المرور يجب أن تتكون من 8 أحرف على الأقل reads more naturally because it leads with the actual subject (the password) instead of an abstract "this field," matching how Arabic sentences are normally built around a clear topic. Neither version is a grammar error. One reads as authored; the other reads as routed through English on its way to the screen.
The fix isn't a better translator brief, though that helps — it's treating Arabic error copy as its own review pass rather than a QA checkbox on the translation deliverable. Have someone read every validation string out loud, in isolation, and ask whether it's a sentence they'd write unprompted. If the answer is "no, but it's technically correct," it needs a rewrite, not a sign-off.
Arabic doesn't have two plural forms — it has six
English pluralization is binary: one form for exactly one, another for everything else, including zero. A validation summary like "You have 1 error" / "You have 2 errors" only ever needs that one branch, which is why a count === 1 ? 'error' : 'errors' ternary is a completely adequate solution in English and an actively broken one once it's reused for Arabic.
Arabic cardinal numbers resolve to six distinct grammatical categories. This isn't an approximation — it's specified. Unicode's CLDR plural-rules documentation defines the six mnemonic tags used across every language CLDR covers — zero, one, two, few, many, other — and Unicode's own language plural-rules chart gives Arabic's exact rule for each:
| Category | Rule | Example counts |
|---|---|---|
| zero | n = 0 | 0 |
| one | n = 1 | 1 |
| two | n = 2 | 2 |
| few | n % 100 = 3..10 | 3–10, 103–110 |
| many | n % 100 = 11..99 | 11–99, 111 |
| other | everything else | 100, 200, 1000 |
The modulo condition is the part most English-first implementations miss: few and many repeat every hundred, so a form with 106 errors takes the same grammatical category as one with 6, and a form with 115 takes the same category as one with 15. A count of exactly 100 falls into other, not many, because 100 % 100 = 0.
The runtime API that exposes this is Intl.PluralRules, and MDN's own documentation reaches for Arabic specifically to demonstrate that a language can use all six categories, where English only ever returns one or other:
const arRules = new Intl.PluralRules('ar-EG');
for (const n of [0, 1, 2, 6, 18, 100]) {
console.log(n, '→', arRules.select(n));
}
// 0 → "zero"
// 1 → "one"
// 2 → "two"
// 6 → "few"
// 18 → "many"
// 100 → "other"
A ternary written for English collapses five of these six branches into one string, and the bug hides well: it compiles, the English build works, and it only surfaces when a native Arabic speaker reads a message that treats 3 errors and 30 errors identically, or renders the dual case (exactly two) as if it were an ordinary plural.
Writing the six variants without sounding like a spec table
Knowing the categories doesn't automatically produce natural sentences — that still takes a deliberate pass per category, because Arabic numeral-noun agreement changes the form of the noun itself, not just which digit is spliced in. Per standard Arabic grammar references, numbers 3–10 take the counted noun in its plural, genitive form, while numbers 11–99 take it in the singular, accusative form instead — a grammatical switch English has no equivalent for. Templating a single sentence and only swapping the number, the way the English original does, produces exactly the kind of technically-parseable-but-off Arabic covered above.
A message map keyed by CLDR category, written by someone drafting each sentence rather than templating one:
const arRules = new Intl.PluralRules('ar-EG');
const errorCountMessage = {
zero: () => 'لا توجد أخطاء في النموذج.',
one: () => 'يوجد خطأ واحد في النموذج.',
two: () => 'يوجد خطآن في النموذج.',
few: (n) => `توجد ${n} أخطاء في النموذج.`,
many: (n) => `يوجد ${n} خطأً في النموذج.`,
other: (n) => `يوجد ${n} خطأ في النموذج.`,
};
function formatErrorCount(n) {
return errorCountMessage[arRules.select(n)](n);
}
The same six variants map directly onto ICU MessageFormat's plural argument, which is the syntax FormatJS and several other i18n toolchains build on. ICU's own guide notes that a translator "need[s] to add one, few etc. if and as necessary per target language" — the categories aren't optional decoration, they're what the format exists to let a translator fill in:
{errorCount, plural,
zero {لا توجد أخطاء في النموذج}
one {يوجد خطأ واحد في النموذج}
two {يوجد خطآن في النموذج}
few {توجد # أخطاء في النموذج}
many {يوجد # خطأً في النموذج}
other {يوجد # خطأ في النموذج}
}
The # is ICU's placeholder for the plural argument's own value, formatted as a number, so it never needs to be re-passed as a separate variable.
If your stack is i18next instead, the same six categories are exposed as key suffixes rather than an inline block, and i18next's own pluralization docs use Arabic by name as the example of a language needing the extended set, calling out that Arabic has "5 plural forms beside the singular":
{
"errorCount_zero": "لا توجد أخطاء في النموذج",
"errorCount_one": "يوجد خطأ واحد في النموذج",
"errorCount_two": "يوجد خطآن في النموذج",
"errorCount_few": "توجد {{count}} أخطاء في النموذج",
"errorCount_many": "يوجد {{count}} خطأً في النموذج",
"errorCount_other": "يوجد {{count}} خطأ في النموذج"
}
Whichever format you use, put a real Arabic-speaking reviewer against all six strings together, not just the other fallback a developer is most likely to eyeball. It's common for zero and two specifically to get skipped in a rushed translation pass, since English has no equivalent branch to remind anyone they exist.
Wiring this into react-hook-form and Zod
Field-level validation messages — "must be at least 8 characters," "enter a valid email" — are a separate problem from the count summary above, and most teams solve it differently: Zod's built-in error customization supports a locale-aware error map, and community packages like zod-i18n-map wire that map to i18next so every built-in Zod error (too_small, invalid_string, and so on) resolves to a translated string automatically, Arabic included:
import { z } from 'zod';
import i18next from 'i18next';
import { zodResolver } from '@hookform/resolvers/zod';
import { makeZodI18nMap } from 'zod-i18n-map';
import ar from 'zod-i18n-map/locales/ar/zod.json';
i18next.init({
lng: 'ar',
resources: { ar: { zod: ar } },
});
z.setErrorMap(makeZodI18nMap());
const signupSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const form = useForm({
resolver: zodResolver(signupSchema),
});
That covers the message attached to each individual field. It does not cover a validation summary that counts how many fields are wrong — "you have 3 errors above" — because that string isn't emitted by Zod at all; it's assembled by your own form component from Object.keys(form.formState.errors).length. This is exactly the count that needs the CLDR-aware formatting from the previous section, and it's easy to miss precisely because it lives in application code, one layer above the validation library that's already been localized:
function ErrorSummary({ errors }: { errors: FieldErrors }) {
const count = Object.keys(errors).length;
if (count === 0) return null;
return <p role="alert">{formatErrorCount(count)}</p>;
}
If your stack renders messages through FormatJS or i18next rather than a hand-rolled function, replace formatErrorCount with a call into whichever ICU-backed formatter your app already uses — the point is that the summary string, unlike the per-field Zod messages, has no library defaulting it to CLDR-correct Arabic on your behalf.
Placing inline errors and icons so they resolve correctly in RTL
Neither of the two most-cited sources on error-message placement is written with RTL in mind, which means their guidance has to be translated into direction-agnostic CSS rather than copied as physical positioning. Nielsen Norman Group's guidelines on form errors recommend showing "an actionable error message below or next to the problem field" and pairing it with "an icon to the left of your error message" for scannability and colorblind users. W3C's own tutorial on inline notification says feedback should appear "at or near the form controls." Both are correct advice, stated in LTR terms by default — "to the left" is a physical instruction that has to become a logical one before it's correct in Arabic.
The mechanical fix is the same one that runs through every RTL layout decision: stop writing left/right and let the browser resolve position from dir. An icon paired with error text inside a flex row does this automatically, because a flex row's start edge already follows the document's inline direction rather than a fixed physical side:
.field-error {
display: flex;
align-items: center;
gap: 0.375rem;
color: var(--color-error);
margin-block-start: 0.25rem;
}
.field-error-icon {
flex-shrink: 0;
/* No margin-left or margin-right — `gap` plus flex's own
start-edge resolution keeps the icon on the correct side
in both directions without a [dir="rtl"] override. */
}
An icon positioned absolutely inside the input itself — a checkmark or error glyph sitting inside the field rather than beside it — is the case that actually breaks in practice, because a value like right: 12px copied straight from an English mockup is a physical instruction that never flips:
/* Wrong: always the input's trailing edge in LTR, always the
wrong edge once the page renders `dir="rtl"`. */
.input-wrapper .status-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
}
/* Correct: resolves to whichever edge is actually the trailing
one, per the CSS Logical Properties and Values specification. */
.input-wrapper .status-icon {
position: absolute;
inset-inline-end: 12px;
top: 50%;
transform: translateY(-50%);
}
inset-inline-end is part of the CSS Logical Properties and Values specification — the same property family our design-system piece covers for layout generally, applied here specifically to the icon-in-input pattern that error states rely on.
Positioning alone doesn't satisfy accessibility, regardless of which direction it resolves in. WCAG Success Criterion 3.3.1 requires that a detected error be "described to the user in text," explicitly noting that "it is perfectly acceptable to indicate the error... through the use of an image, color, or other visual indicator, in addition to the text description" — never instead of it. Practically, that means every error icon needs a text-bearing sibling wired to the field with aria-describedby, and the field itself marked aria-invalid="true", exactly as W3C's notification tutorial describes — a requirement that doesn't change between LTR and RTL, only the CSS that positions it does.
Common mistakes
Templating one Arabic sentence and swapping the number. Arabic's numeral-noun agreement changes the noun's own form across categories, not just the digit — a single template with an interpolated count is the same bug as an English count === 1 ternary, just less visible to a non-Arabic-speaking reviewer.
Skipping zero and two in translation. English has no equivalent branch, so translators working from an English-first spreadsheet routinely fill in one, few, many and other and leave the other two empty or duplicated from other.
Treating a fluent translation as a finished one. Grammatically correct Arabic that mirrors English clause structure reads as translated, not authored — it needs a native-authored review pass independent of the accuracy check.
Hardcoding left/right on error icons. It's the single most common way an otherwise-correct RTL form breaks visibly, because it's invisible in an LTR-only design review and only shows up once someone actually opens the Arabic build.
Assuming your validation library localizes summary counts. Zod's error map and its i18n bridges handle individual field messages; a hand-assembled "you have N errors" string is application code, and it's exactly the string most likely to still be running English pluralization logic on Arabic text.
Getting Arabic form validation right isn't a translation task that happens once and then gets maintained by a spreadsheet. It's copywriting, grammar and layout logic that has to be reviewed together, by someone who reads the six-category message set as prose rather than confirming that a template compiled — the same standard you'd already hold the English version to, applied to a language whose plural system just has more categories to get right.