Localization showcase

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.

Six supported locales

Hand-translated. Verified. Production-ready.

LocaleLanguageRegionDirectionDefault calendarDefault numeralsCurrencyFirst day
en-USEnglishUnited StatesLTRGregorianLatinUSDSun
en-GBEnglishUnited KingdomLTRGregorianLatinGBPMon
ar-SAArabicSaudi ArabiaRTLHijri Umm-al-QuraArab (٠–٩)SARSat
ar-AEArabicUAERTLGregorianArab (٠–٩)AEDSat
fa-IRFarsiIranRTLJalaliArab-extended (۰–۹)IRRSat
en-XAPseudoLTRGregorianLatinUSDMon
Side-by-side

Three locales rendering the same client.

Multi-locale side-by-side comparison
RTL approach

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 */
Three calendars

Eight reference dates verified.

Three calendars venn-style overlap
The same date in three calendars — Gregorian, Hijri Umm al-Qura, and Jalali.
Gregorian7 July 2024first day of Hijri new year 1446
Hijri Umm al-Qura1 محرم 1446 هـ@umalqura/core 0.0.7
Jalali17 تیر 1403jalaali-js
Numerals

Same number. Three rendering systems.

en-US · Latin

Latin numerals

Portfolio
$2,418,723.45
Return
+12.4%
Order
1,000 sh
ar-SA · Arab

أرقام عربية

المحفظة
٢٬٤١٨٬٧٢٣٫٤٥ ر.س
العائد
+١٢٫٤٪
الطلب
١٬٠٠٠ سهم
fa-IR · Arab-extended

اعداد فارسی

پرتفوی
۱۲۳٬۸۲۴٬۳۲۰٬۰۰۰ ﷼
بازده
+۱۲٫۴٪
سفارش
۲۰٬۰۰۰ سهم
ICU MessageFormat

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}"
Adding a locale

JSON file. Config row. No code changes.

  1. 1 · Create the catalog

    Copy packages/i18n/catalogs/en-US.json fr-FR.json. Translate the keys. Run pnpm i18n:check until green.

  2. 2 · Add a config row

    Add fr-FR to packages/i18n/src/locales.ts: direction LTR, calendar Gregorian, numerals Latin, currency EUR.

  3. 3 · Pseudo-locale regenerates

    The build step regenerates en-XA.json automatically. New keys appear bracketed and accented.

  4. 4 · Translation review

    Native-speaker review against the glossary. PR reviewed by both engineering and locale lead.

  5. 5 · Ship

    Calendar and number formatters work without code changes. The locale is selectable in tenant admin and in user preferences.

Adoption shape

Across our reference deployments.

Locale adoption sunburst
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 that prevents future Arabic and Persian launches from being painful.