---
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`, `<Trans>`) |
| `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 <Button>{t('marketplace.request.create')}</Button>;
}
```

For sentences with embedded React (link, bold) use `<Trans>`:

```tsx
<Trans i18nKey="legal.terms">
  By signing up you agree to our <Link href="/terms">Terms</Link>.
</Trans>
```

---

## 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] : [];

<AppRouterCacheProvider options={{ key: cacheKey, prepend: true, stylisPlugins }}>
  {children}
</AppRouterCacheProvider>
```

> [!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 `<html dir>`

The root layout sets `<html lang={locale} dir={direction}>`. 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
<Box sx={{ direction: 'ltr' }}>
  <Iconify icon="solar:play-bold" />
</Box>
```

---

## 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
<DataGrid
  localeText={locale === 'fa' ? customFaDataGrid : undefined}
  ...
/>
```

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/<code>/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 `<bdi>` |

---

## 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)
