277 lines
7.7 KiB
Markdown
277 lines
7.7 KiB
Markdown
---
|
|
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)
|