All articles & videos

Arabic, French and MAD: i18n and RTL in enterprise frontends

and for enterprise frontends in Morocco and the Gulf: build-time or runtime translation, dir and , in data, and formatting MAD.

Adil Ben Moussa
08 November 2026· 5 min
Arabic, French and MAD: i18n and RTL in enterprise frontends

The same number, 12450.5 dirhams, comes out three ways from the same Intl API, depending on one string. Then the Arabic version of the screen arrives, every arrow points the wrong way, and a inside an Arabic sentence can show its digit groups in an order nobody typed. In Morocco and the Gulf this is not an edge case. It is the main audience.

Languages in an enterprise frontend are a system design decision because they touch everything at once: the build, the layout, the components, the data, and the . Decided late, RTL touches every screen.

The few concepts that matter

Build-time or runtime translation. 's built-in i18n builds "a separate distributable copy of the application for each locale", with the translations merged in at build time. The CLI writes each locale to its own directory, typically served from its own subdirectory. A runtime library loads translations while the app runs instead. React has no built-in i18n, so on the React side the choice is which library.

dir on the root, logical CSS everywhere. MDN's answer is short: "explicitly set dir on the root element", which "is especially important for RTL scripts", and use the attribute "instead of the related CSS properties when possible". Logical properties are "direction-relative equivalents" of the physical ones, so margin-inline-start follows the language and margin-left does not. Icons need a decision too: a back arrow mirrors, a play button or a logo does not. On tt-tech.io the arrows carry a rtl:-scale-x-100 class for exactly that.

Image: tt-tech.io, adapted from web.dev, Internationalization, CC BY 4.0.

All three frames use the same CSS: border-inline-start, border-inline-end, border-block-start and border-block-end. Only the language changes, and the browser moves the borders.

Bidi in data. The page direction is the easy part. The hard part is a value inside a sentence: a RIB, a phone number, an English product name in Arabic text. In the Unicode bidirectional algorithm, numbers "act as if they were R" for the neutral characters around them, so the space between two digit groups takes the right-to-left direction. Inside Arabic text, the groups of a spaced RIB can come out in reverse order. MDN says dir="auto" "should be used for data with an unknown directionality, like data coming from user input or external data". It describes <bdi> as telling the bidirectional algorithm to treat its text "in isolation from its surrounding text".

Numbers, currency and dates. Intl.NumberFormat "enables language-sensitive number formatting". Language-sensitive also means data-sensitive. I ran this in 24 ( 78):

const mad = (locale: string) =>
  new Intl.NumberFormat(locale, { style: 'currency', currency: 'MAD' }).format(12450.5);
 
mad('fr-FR'); // '12 450,50 MAD' (narrow no-break space)
mad('fr-MA'); // '12.450,50 MAD'
mad('ar-MA'); // '12.450,50 د.م.' (plus two invisible right-to-left marks)

Three outputs, and they depend on the locale data your browser or Node version ships. Fine for display; not fine for a test that asserts an exact string, or a bank that has decided how its statements look.

Pick the money format on purpose, or the locale data picks it for you.

The design system in both directions. Tokens, components and visual tests have to run in RTL as well, or each feature team finds the same bugs on its own.

Fonts and text length. Arabic needs a font that renders it well, not the Latin font's fallback. Translated text grows, and Mozilla's guide notes "Many languages use longer strings than English".

When to choose what

  • Build-time or runtime: build-time when the locale is fixed per user and the texts ship with the code; runtime when users switch language often, or texts come from a .
  • Intl or a fixed formatter: Intl for many locales and display only; a fixed, tested formatter when the format is a business decision.
  • <bdi> or dir="ltr": <bdi> (or dir="auto") for text you do not control, like names; dir="ltr" for values that are always left to right, like a RIB or an .

On Atlas Bank

Atlas Bank's interface is English only today (<html lang="en"> in apps/atlas/src/index.html). Its money formatting is a deliberate choice. formatMad in libs/domain/src/lib/format.ts says it is "Deliberately not Intl": one fixed format, space grouping and comma decimals, "rendered identically in every browser and in Node, so tests can assert the exact string". ICU's own fr-MA data groups with a dot, so this is a choice the code makes on purpose. format.spec.ts asserts '12 450,50 MAD'.

Atlas also shows what RTL would cost. Its styles still use physical properties in places: margin-left: auto in account-detail.ts, text-align: right for the .num utility. Each is a small fix, found late.

A French and Arabic toggle with dir="rtl" would be a good addition to Atlas for this part. It is a suggestion, not in the repo today.

What I left out, and why

  • , , . Real tools and real rules; one line each. For plurals, use ICU messages and never concatenate strings.
  • Locale detection. A product decision; let the user choose and remember it.

On Monday

  • Set dir="rtl" on the html element of your app in a branch and click through the main flow. Write down every screen that breaks.
  • Search your styles for margin-left, padding-right and text-align: left, and count them. That number is the size of your RTL work.
  • Find every place that formats money and check that it goes through one function.

For your coding agent

The rules of this article, written for an agent. Paste them into your AGENTS.md, save them as a skill, or point your agent at the file.

For your coding agent Download .md
# Internationalisation and RTL rules for enterprise apps

Source: https://tt-tech.io/blog/arabic-french-mad-i18n-rtl-enterprise
For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ.

## Rules

1. Choose build-time translation (Angular i18n, one build per locale) when the locale is fixed per user and the texts ship with the code. Choose a runtime library when users switch language often or texts come from a CMS.
2. Set `dir` explicitly on the root `html` element. Use the `dir` attribute instead of the CSS `direction` property where possible.
3. Use logical CSS properties: `margin-inline-start`, `padding-inline-end`, `border-inline-start`, `text-align: start`. Never add `margin-left`, `padding-right` or `text-align: left` in new code.
4. Mirror icons that show direction, such as back and forward arrows, in RTL. Do not mirror a play button or a logo.
5. Wrap text whose direction you do not control (names, user input, external data) in `<bdi>` or give it `dir="auto"`.
6. Mark values that are always left to right, such as a RIB or an IBAN, with `dir="ltr"`.
7. Format money through one function. Use `Intl.NumberFormat` for display across many locales; use a fixed, tested formatter when the format is a business decision.
8. Never assert an exact `Intl` output string in a test. It depends on the locale data the browser or Node version ships.
9. Build plurals and variable messages with ICU messages. Never concatenate translated strings.
10. Run design tokens, components and visual tests in RTL as well as LTR.
11. Use a font that renders Arabic well, not the Latin font's fallback. Leave room for translated text, which is often longer than English.
12. Let the user choose the language and remember the choice.

## Before you say done

- Search the diff for `margin-left`, `margin-right`, `padding-left`, `padding-right`, `text-align: left` and `text-align: right`. Replace each with a logical property or explain it.
- Search the diff for money formatting (`Intl.NumberFormat`, `toFixed`, currency codes) outside the shared formatter.
- Search the diff for string concatenation in translated text, and for names, user input or external values rendered without `<bdi>`, `dir="auto"` or `dir="ltr"`.

Sources

  1. Angular, Merge translations into the app
  2. Angular, Deploy multiple locales
  3. MDN, dir HTML global attribute
  4. MDN, CSS logical properties and values
  5. Unicode, UAX #9: Unicode Bidirectional Algorithm
  6. MDN, <bdi> HTML bidirectional isolate element
  7. MDN, Intl.NumberFormat
  8. Mozilla, Fluent for Firefox Developers
  9. web.dev, Internationalization
Share this article