Internationalization (i18n)
Design guidelines for localized treasury interfaces. Covers RTL layout tokens, BiDi text handling, number formatting conventions, icon mirroring rules, and the LocaleSwitcher component. Supports 7 locales with full RTL for Arabic and Hebrew.
Supported Locales
LocaleSwitcher + Live Layout
Currency & Number Conventions
Intl.NumberFormat for all financial numbers. Never manually format. Western numerals (0–9) for all locales in treasury UI — no Eastern Arabic-Indic digit substitution.| Locale | 1234567.89 | Currency formatted | Dec sep | Thou sep | Symbol pos | Negative |
|---|---|---|---|---|---|---|
🇺🇸en-US | 1,234,567.89 | $1,234,567.89 | . | , | before | -$1,234,567.89 |
🇸🇦ar-SA | 1,234,567.89 | 1,234,567.89 ر.س | . | , | after | -1,234,567.89 ر.س |
🇮🇱he-IL | 1,234,567.89 | ₪1,234,567.89 | . | , | before | -₪1,234,567.89 |
🇩🇪de-DE | 1.234.567,89 | 1.234.567,89 € | , | . | after | -1.234.567,89 € |
🇫🇷fr-FR | 1 234 567,89 | 1 234 567,89 € | , | thin sp | after | -1 234 567,89 € |
🇯🇵ja-JP | 1,234,567.89 | ¥1,234,568 | . | , | before | -¥1,234,568 |
🇨🇳zh-CN | 1,234,567.89 | ¥1,234,567.89 | . | , | before | -¥1,234,567.89 |
RTL / Logical CSS Tokens
| Token | Value | Use | CSS Property |
|---|---|---|---|
| --ds-space-inline-start-sm | 16px | Inline-start spacing (left in LTR, right in RTL) | margin-inline-start |
| --ds-space-inline-end-sm | 16px | Inline-end spacing | margin-inline-end |
| --ds-space-inset-inline-md | 16px | Padding in inline direction | padding-inline |
| --ds-border-inline-start | 1px solid var(--ds-border-structure) | Leading border (accent border on cards) | border-inline-start |
| --ds-icon-mirror-rtl | scaleX(-1) | Directional icon flip value | transform on .ds-icon-rtl-mirror |
| --ds-dir-rtl | rtl | Direction token for dir attribute | direction: rtl |
Mixed LTR/RTL Interaction Patterns
layout-mirroringLayout Mirroringmargin-inline-start / padding-inline-end
The entire page layout mirrors horizontally in RTL. Left becomes inline-start; right becomes inline-end. Use logical CSS properties so browsers handle the flip automatically.
- Use padding-inline-start/end instead of padding-left/right
- Use margin-inline-start/end instead of margin-left/right
- Use inset-inline-start/end for absolute positioning
- Use border-inline-start for the DS left-accent pattern
- Never use left: 0 for a leading element — use inset-inline-start: 0
- Never use text-align: left as default — use text-align: start
- Never hardcode flex-direction: row for content that should mirror
- Never use transform: translateX() for RTL offsets — use logical transforms
text-alignmentText Alignmenttext-align: start
Body text, labels, and captions align to the reading start. In LTR this is left; in RTL this is right. Use text-align: start (not left) in all DS components.
- Use text-align: start for body text, labels, descriptions
- Use text-align: end for numeric columns and monetary values
- Use text-align: center only for headings or isolated callouts
- Test mixed content (Arabic label + English bank name) with unicode-bidi: embed
- Never use text-align: left — it won't flip in RTL
- Never center-align a number column — numbers must stack on their decimal point
- Don't override text direction on individual words inside a sentence
number-directionNumber Directiondirection: ltr; unicode-bidi: embed on number spans
Numbers in Arabic and Hebrew are still written LTR (digits read left-to-right) even within RTL text. However, Arabic uses Eastern Arabic-Indic numerals (٠١٢٣٤٥٦٧٨٩) optionally. The DS renders financial numbers in Western Arabic numerals for consistency.
- Wrap numbers in <span dir='ltr'> when inside RTL containers
- Use Intl.NumberFormat for all number formatting — never manual formatting
- Currency symbol position: before in en-US ($125k), after in ar-SA (١٢٥ ر.س)
- Use tabular-nums (font-variant-numeric: tabular-nums) for all financial data
- Don't auto-substitute Eastern Arabic numerals for financial data
- Don't manually format numbers with string concatenation
- Don't place currency symbols without checking locale convention
- Don't use thin-space (U+202F) as thousands separator unless locale requires it
icon-mirroringIcon Mirroring[dir='rtl'] .ds-icon-rtl-mirror { transform: scaleX(-1); }
Icons that represent directionality or movement mirror in RTL. Icons that represent physical objects or state do not mirror. Apply the .ds-icon-rtl-mirror CSS class to eligible icons.
- Mirror: arrows, chevrons, back/forward navigation, undo/redo, send/receive
- Mirror: timeline progression indicators, pagination arrows
- Do NOT mirror: play/pause buttons, loading spinners, checkmarks, X (close)
- Do NOT mirror: flag icons, currency symbols, logos, charts
- Don't mirror icons that represent physical objects (phone, envelope, chart)
- Don't mirror numeric indicators (step numbers stay LTR in RTL layout)
- Don't use CSS rotation instead of scaleX(-1) — rotation changes visual weight
- Don't mirror icons in compact/icon-only buttons without testing with RTL users
mixed-contentMixed LTR/RTL Contentunicode-bidi: isolate on mixed-script spans
Treasury data often mixes RTL labels with LTR values: Arabic bank name with an English IBAN, Hebrew description with a USD amount. Use unicode-bidi and direction carefully to preserve reading order.
- Wrap IBANs, SWIFT codes, BICs, account numbers in dir='ltr'
- Wrap English brand names and product names in unicode-bidi: isolate
- Test all treasury field labels with Arabic content + English values
- Use Intl.DateTimeFormat for dates — calendar systems differ (Hijri, Hebrew)
- Don't rely on browser BiDi heuristics for financial codes — always be explicit
- Don't concatenate RTL and LTR strings without bidi control characters
- Don't use dir='ltr' on the whole page to 'fix' RTL — it breaks everything
- Don't assume all Arabic-script content is RTL — some CSS-encoded Arabic can be LTR
LocaleSwitcher API
import { LocaleSwitcher, DS_LOCALES } from "@/components/ui/LocaleSwitcher";
// Full variant (settings panel, onboarding)
<LocaleSwitcher
value={locale} // BCP 47 string: "en-US", "ar-SA", "he-IL", etc.
onChange={setLocale} // (locale: string) => void
variant="full" // "full" | "compact"
/>
// Compact variant (header, toolbar)
<LocaleSwitcher
value={locale}
onChange={setLocale}
variant="compact" // Shows flag + code only
/>
// Custom locale list
<LocaleSwitcher
value={locale}
onChange={setLocale}
locales={DS_LOCALES.filter(l => l.dir === 'rtl')} // RTL-only
/>
// Props:
// value: string — BCP 47 locale code
// onChange: fn — locale selection callback
// locales: LocaleOption[] — defaults to DS_LOCALES (7 locales)
// variant: "full"|"compact"
// disabled: boolean
// label: string — aria-label for the trigger buttonRTL Layout Pattern
// 1. Set dir attribute on root element based on locale
const { dir } = DS_LOCALES.find(l => l.code === locale) ?? { dir: 'ltr' };
<html dir={dir} lang={locale}>
// 2. Use logical CSS properties in all DS components
// WRONG:
style={{ marginLeft: 16, paddingRight: 12, borderLeft: '3px solid red' }}
// RIGHT:
style={{
marginInlineStart: 16,
paddingInlineEnd: 12,
borderInlineStart: '3px solid var(--ds-color-agency)',
}}
// 3. Wrap financial codes in dir='ltr'
<span dir="ltr" style={{ unicodeBidi: 'isolate' }}>
{ibanCode} // DE89 3704 0044 0532 0130 00
</span>
// 4. RTL icon mirroring
<ArrowRight
className="ds-icon-rtl-mirror" // mirrors under [dir='rtl']
/>
// 5. Number formatting
const fmt = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
maximumFractionDigits: 2,
});
fmt.format(amount); // handles separators, symbol position, sign