7.7 KiB
title, tags, created
| title | tags | created | ||||
|---|---|---|---|---|---|---|
| Internationalization & RTL |
|
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)
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):
{
"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
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>:
<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
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
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:
<Box sx={{ direction: 'ltr' }}>
<Iconify icon="solar:play-bold" />
</Box>
7. Date & time formatting
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
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:
<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:
i18n.changeLanguage(code)settings.onUpdate('direction', dir)← persists into settings context- Page does NOT reload — the cache + theme + provider all react reactively.
11. Adding a new language
- Create
src/locales/langs/<code>/common.jsonwith the same key set asen/common.json(usei18next-parserto identify missing keys). - Register in
allLangsarray inlocales-config.ts. - Add
dayjslocale import if it differs from the existing set. - Verify number formatter outputs on a sample page.
- 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-parsercan 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)