Initial commit: nick docs
This commit is contained in:
276
05 - Design System/Internationalization & RTL.md
Normal file
276
05 - Design System/Internationalization & RTL.md
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user