The same date, three calendars.
UTC in Postgres. Calendar conversion at the rendering boundary only. Six locales hand-translated. RTL via logical properties — never ml-*. Adding a seventh locale is a JSON file and a config row.
Hand-translated. Verified. Production-ready.
| Locale | Language | Region | Direction | Default calendar | Default numerals | Currency | First day |
|---|---|---|---|---|---|---|---|
| en-US | English | United States | LTR | Gregorian | Latin | USD | Sun |
| en-GB | English | United Kingdom | LTR | Gregorian | Latin | GBP | Mon |
| ar-SA | Arabic | Saudi Arabia | RTL | Hijri Umm-al-Qura | Arab (٠–٩) | SAR | Sat |
| ar-AE | Arabic | UAE | RTL | Gregorian | Arab (٠–٩) | AED | Sat |
| fa-IR | Farsi | Iran | RTL | Jalali | Arab-extended (۰–۹) | IRR | Sat |
| en-XA | Pseudo | — | LTR | Gregorian | Latin | USD | Mon |
Three locales rendering the same client.
Logical properties only. CI fails on physical ones.
Modir uses Tailwind’s logical-property variants (ms-*, me-*, start-*, end-*) and Tailwind’s tailwindcss-rtl plugin. ESLint blocks any ml-*, mr-*, pl-*, pr-* at PR time. <html dir> is set server-side from middleware. Bidirectional fields use dir="auto". Account numbers and IBANs render LTR via <bdi> tags.
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 before staging.
/* Wrong — physical, breaks RTL */
.card { margin-left: 24px; padding-right: 16px; }
/* Right — logical, flips automatically */
.card { margin-inline-start: 24px; padding-inline-end: 16px; }
/* Tailwind */
<div class="ms-6 pe-4"> ✓
<div class="ml-6 pr-4"> ✗ /* CI fails */Eight reference dates verified.
Same number. Three rendering systems.
Latin numerals
- Portfolio
- $2,418,723.45
- Return
- +12.4%
- Order
- 1,000 sh
أرقام عربية
- المحفظة
- ٢٬٤١٨٬٧٢٣٫٤٥ ر.س
- العائد
- +١٢٫٤٪
- الطلب
- ١٬٠٠٠ سهم
اعداد فارسی
- پرتفوی
- ۱۲۳٬۸۲۴٬۳۲۰٬۰۰۰ ﷼
- بازده
- +۱۲٫۴٪
- سفارش
- ۲۰٬۰۰۰ سهم
Plurals. Gender. Select. The whole standard.
// catalog: en-US.json
"client.account.summary": "{count, plural, one {# account} other {# accounts}} totaling {amount, number, ::currency/USD}"
// catalog: ar-SA.json
"client.account.summary": "{count, plural, zero {لا توجد حسابات} one {حساب واحد} two {حسابان} few {# حسابات} many {# حسابًا} other {# حساب}} بقيمة {amount, number, ::currency/SAR}"
// catalog: fa-IR.json
"client.account.summary": "{count, plural, one {# حساب} other {# حساب}} با مجموع {amount, number, ::currency/IRR}"JSON file. Config row. No code changes.
- 1 · Create the catalog
Copy
packages/i18n/catalogs/en-US.json→fr-FR.json. Translate the keys. Runpnpm i18n:checkuntil green. - 2 · Add a config row
Add
fr-FRtopackages/i18n/src/locales.ts: direction LTR, calendar Gregorian, numerals Latin, currency EUR. - 3 · Pseudo-locale regenerates
The build step regenerates
en-XA.jsonautomatically. New keys appear bracketed and accented. - 4 · Translation review
Native-speaker review against the glossary. PR reviewed by both engineering and locale lead.
- 5 · Ship
Calendar and number formatters work without code changes. The locale is selectable in tenant admin and in user preferences.
Across our reference deployments.
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 that prevents future Arabic and Persian launches from being painful.