Feature · Localization

RTL-by-default. Hijri-by-default. Jalali-by-default.

Six locales, three calendars, three numbering systems. UTC in storage; conversion at the rendering boundary only. Adding a locale is a JSON catalog file and a config row.

What it does

Localization as a structural concern.

The i18n package owns locale resolution, ICU formatting, calendar conversion, numeral systems, and the pseudo-locale generator. The resolution chain is explicit: URL prefix → header → cookie → Accept-Language → tenant default. Direction is derived from the locale (LTR / RTL); the <html dir> attribute is set server-side from middleware on every request.

Time storage is UTC, always. Calendar conversion happens only at the rendering boundary: in the UI, in PDFs, in emails. That means every server-side calculation, audit event, hash chain, and database query speaks a single time. There is no “is this Hijri or Gregorian?” debate.

Tailwind’s logical-property variants (ms-*, me-*, start-*, end-*) and the tailwindcss-rtl plugin are mandatory. ESLint blocks ml-*, mr-*, pl-*, pr-* at PR time. Bidirectional fields (e.g., a string with both English and Arabic) use dir="auto"; account numbers and IBANs render LTR via <bdi> tags so they aren’t reversed.

The pseudo-locale en-XA is auto-generated from en-US by the i18n build step. Strings expand 30% with diacritics: "Hello world!!" becomes "[Ĥéĺĺó wóŕĺd!! ~~]". A string that breaks layout in pseudo will break in Arabic too — and we catch it on every PR, not after Riyadh launch.

Six locales

What ships, by configuration.

LocaleDirectionCalendarNumeralsCurrencyFirst day
en-USLTRGregorianLatinUSDSun
en-GBLTRGregorianLatinGBPMon
ar-SARTLHijri Umm-al-QuraArab (٠–٩)SARSat
ar-AERTLGregorianArab (٠–٩)AEDSat
fa-IRRTLJalaliArab-extended (۰–۹)IRRSat
en-XALTRGregorianLatinUSDMon
Capabilities

What this package owns.

Locale resolution

URL → header → cookie → Accept-Language → tenant default. Edge-cached.

ICU MessageFormat

Plurals, gender, select. Catalog parity verified by pnpm i18n:check.

Three calendars

Gregorian (date-fns), Hijri (@umalqura/core), Jalali (jalaali-js). 8 reference dates pass.

Numeral systems

Latin, Arab, Arab-extended. Round-trip in NumberInput / CurrencyInput.

RTL via logical props

Tailwind tailwindcss-rtl; ESLint blocks physical properties.

Pseudo-locale

en-XA auto-generated. Catches layout breaks before Arabic does.

Zod error map

Every error code maps to an i18n key; user sees errors in their language.

Font bundling

Noto Sans Arabic for Arabic; Vazirmatn for Persian. Subset to glyphs in use.

RTL approach

Logical only. Always.

/* Wrong — breaks Arabic */
.card { margin-left: 24px; padding-right: 16px; border-left: 3px solid; }

/* Right — flips automatically */
.card {
  margin-inline-start: 24px;
  padding-inline-end: 16px;
  border-inline-start: 3px solid;
}

/* Tailwind */
<div class="ms-6 pe-4 border-s-3"> ✓
<div class="ml-6 pr-4 border-l-3"> ✗  /* CI fails */
Calendars and scripts comparison infographic
Three calendars, three scripts, six locales — overlapping coverage by design.
Multi-locale side by side
Localization audit

Stress-test your stack against six locales.

We run your screens through pseudo-locale, RTL flips, and three numeral systems. Output: a layout-bug report.