--- title: Internationalization & RTL tags: [design-system, i18n, rtl, localization] created: 2026-05-23 --- # Internationalization & RTL The frontend supports six languages with full LTR/RTL bidi handling. Configuration lives in `frontend/src/locales/`. | Code | Language | Direction | Native name | |---|---|---|---| | `en` | English | LTR | English | | `fa` | Persian (Farsi) | **RTL** | فارسی | | `ar` | Arabic | **RTL** | العربية | | `fr` | French | LTR | Français | | `cn` | Chinese (Simplified) | LTR | 简体中文 | | `vi` | Vietnamese | LTR | Tiếng Việt | --- ## 1. Library stack | Package | Role | |---|---| | `i18next` | Core i18n engine | | `react-i18next` | React bindings (`useTranslation`, ``) | | `i18next-browser-languagedetector` | Detect from `navigator.language` / cookie | | `stylis-plugin-rtl` | Auto-flip CSS for RTL | | `dayjs` | Date formatting with locale support | | `@mui/x-date-pickers/AdapterDayjs` | Picker UI honors locale | | `Intl.NumberFormat` (built-in) | Numeric formatting | --- ## 2. Setup (`src/locales/locales-config.ts`) ```ts import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import enCommon from './langs/en/common.json'; import faCommon from './langs/fa/common.json'; import arCommon from './langs/ar/common.json'; // ... fr, cn, vi export const allLangs = [ { value: 'en', label: 'English', dir: 'ltr', icon: 'flagpack:gb-ukm' }, { value: 'fa', label: 'فارسی', dir: 'rtl', icon: 'flagpack:ir' }, { value: 'ar', label: 'العربية', dir: 'rtl', icon: 'flagpack:sa' }, { value: 'fr', label: 'Français', dir: 'ltr', icon: 'flagpack:fr' }, { value: 'cn', label: '简体中文', dir: 'ltr', icon: 'flagpack:cn' }, { value: 'vi', label: 'Tiếng Việt', dir: 'ltr', icon: 'flagpack:vn' }, ]; i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources: { en: { common: enCommon }, fa: { common: faCommon }, ar: { common: arCommon }, fr: { common: frCommon }, cn: { common: cnCommon }, vi: { common: viCommon }, }, fallbackLng: 'en', defaultNS: 'common', interpolation: { escapeValue: false }, }); ``` --- ## 3. Translation files ``` src/locales/langs/ ├── en/common.json ├── fa/common.json ├── ar/common.json ├── fr/common.json ├── cn/common.json └── vi/common.json ``` Convention: one file per **namespace**. `common.json` is the default; split if any namespace exceeds ~500 keys. Examples (key paths): ```jsonc { "actions": { "save": "Save", "cancel": "Cancel", "delete": "Delete" }, "auth": { "signIn": "Sign in", "signOut": "Sign out", "rememberMe": "Remember me" }, "marketplace": { "request": { "create": "Create a request", "openOffers": "{{count}} open offer", "openOffers_plural": "{{count}} open offers" } } } ``` --- ## 4. Usage in components ```tsx import { useTranslation } from 'react-i18next'; function CreateButton() { const { t } = useTranslation(); return ; } ``` For sentences with embedded React (link, bold) use ``: ```tsx By signing up you agree to our Terms. ``` --- ## 5. Direction (LTR / RTL) Triggered by the active locale (`dir: 'rtl'` for `fa` / `ar`). ### 5.1 Cache swap ```tsx const cacheKey = direction === 'rtl' ? 'css-rtl' : 'css'; const stylisPlugins = direction === 'rtl' ? [rtlPlugin] : []; {children} ``` > [!warning] > The cache key **must differ** between modes. Sharing the key causes stale CSS to linger when direction changes at runtime. ### 5.2 Theme ```ts const theme = createTheme({ direction, ... }); ``` ### 5.3 `` The root layout sets ``. Some screen readers and form behaviors (e.g., number input flow) rely on this attribute. --- ## 6. RTL CSS rules | Don't | Do | |---|---| | `margin-left: 8px` | `marginInlineStart: 1` (or `ml: 1` — MUI's `sx` translates) | | `padding-right: 16px` | `paddingInlineEnd: 2` | | `text-align: left` | `textAlign: 'start'` | | `float: left` | `float: 'inline-start'` (or refactor) | | `border-left: 2px` | `borderInlineStart: 2` | | `direction: ltr` hardcoded | inherit from html unless intentional (e.g., code blocks always LTR) | Icons that imply direction (chevrons, arrows) are auto-flipped by `stylis-plugin-rtl`. If an icon is **semantic** (e.g., a play button), wrap it in: ```tsx ``` --- ## 7. Date & time formatting ```tsx import dayjs from 'dayjs'; import 'dayjs/locale/fa'; dayjs.locale('fa'); dayjs('2026-05-23').format('LL'); // → "۲ خرداد ۱۴۰۵" ``` Use the `Format` helpers from `components/...` for consistent display patterns: | Helper | Output | |---|---| | `fDate(d)` | locale-aware short date | | `fDateTime(d)` | date + time | | `fTime(d)` | time only | | `fToNow(d)` | "3 hours ago" | | `fIsBetween(start, end)` | boolean window check | --- ## 8. Number & currency formatting ```ts import { fNumber, fCurrency, fPercent } from '@/locales/utils/number-format-locale'; fNumber(1234567); // "1,234,567" / "۱٬۲۳۴٬۵۶۷" fCurrency(99.5, 'USD'); // "$99.50" / "۹۹٫۵۰ دلار" fPercent(0.123); // "12.3%" ``` Underneath, these wrap `Intl.NumberFormat` with the active locale, falling back to `en-US` when the formatter doesn't support a locale (e.g., older browsers + `fa`). --- ## 9. DataGrid Farsi locale `src/locales/custom-fa-data-grid-locale.ts` exports a Persian translation map for MUI X DataGrid (column menu, pagination labels, filter operators, etc.). Wired in via: ```tsx ``` For Arabic, the built-in MUI `arSD` locale works. --- ## 10. Language switcher UI In the topbar, a chip with the flag + native name opens a popover listing all `allLangs`. On click: 1. `i18n.changeLanguage(code)` 2. `settings.onUpdate('direction', dir)` ← persists into settings context 3. Page does NOT reload — the cache + theme + provider all react reactively. --- ## 11. Adding a new language 1. Create `src/locales/langs//common.json` with the same key set as `en/common.json` (use `i18next-parser` to identify missing keys). 2. Register in `allLangs` array in `locales-config.ts`. 3. Add `dayjs` locale import if it differs from the existing set. 4. Verify number formatter outputs on a sample page. 5. If RTL, add `direction: 'rtl'` and **test bidi** thoroughly. --- ## 12. Translation workflow - Source of truth: `en/common.json` (English). - For new strings, add to `en/` first, then translate to other locales (manual or via Crowdin / Lokalise integration). - Lint check: `i18next-parser` can compare locale files for missing keys. --- ## 13. Common pitfalls | Pitfall | Fix | |---|---| | Hardcoded English in JSX | Move to `common.json` | | `align="left"` on a Box | Use `textAlign="start"` | | `marginLeft` instead of `ml` shorthand | Use shorthand (auto-RTL) | | Forgot to load Persian font | Add to `theme/options/typography.ts` | | Date showing as English digits in fa | Forgot `dayjs.locale('fa')` | | Numbers in RTL appearing reversed | Wrap in `` | --- ## 14. Related - [[Design System Overview]] · [[Theme Configuration]] · [[Typography]] - [[Settings & Theming]] — direction toggle - [[Frontend Architecture]] — provider tree - [[Roles & Personas]] — locale defaults per role (admin defaults English, buyers can be Persian)