Files
nick-doc/05 - Design System/Internationalization & RTL.md
2026-05-23 20:35:34 +03:30

7.7 KiB

title, tags, created
title tags created
Internationalization & RTL
design-system
i18n
rtl
localization
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:

  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>