Locale resolution
URL → header → cookie → Accept-Language → tenant default. Edge-cached.
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.
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.
| Locale | Direction | Calendar | Numerals | Currency | First day |
|---|---|---|---|---|---|
| en-US | LTR | Gregorian | Latin | USD | Sun |
| en-GB | LTR | Gregorian | Latin | GBP | Mon |
| ar-SA | RTL | Hijri Umm-al-Qura | Arab (٠–٩) | SAR | Sat |
| ar-AE | RTL | Gregorian | Arab (٠–٩) | AED | Sat |
| fa-IR | RTL | Jalali | Arab-extended (۰–۹) | IRR | Sat |
| en-XA | LTR | Gregorian | Latin | USD | Mon |
URL → header → cookie → Accept-Language → tenant default. Edge-cached.
Plurals, gender, select. Catalog parity verified by pnpm i18n:check.
Gregorian (date-fns), Hijri (@umalqura/core), Jalali (jalaali-js). 8 reference dates pass.
Latin, Arab, Arab-extended. Round-trip in NumberInput / CurrencyInput.
Tailwind tailwindcss-rtl; ESLint blocks physical properties.
en-XA auto-generated. Catches layout breaks before Arabic does.
Every error code maps to an i18n key; user sees errors in their language.
Noto Sans Arabic for Arabic; Vazirmatn for Persian. Subset to glyphs in use.
/* 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 */Carbone formatters consume the same locale data; PDF dates, numbers, and currencies are correct.
FeaturePer-jurisdiction questionnaires render in the client's preferred locale and calendar.
FeatureOutput guardrails reject locale-mismatched generation; en-US inputs cannot leak Persian outputs.
We run your screens through pseudo-locale, RTL flips, and three numeral systems. Output: a layout-bug report.