I'm building a hospital dashboard template with Angular 18 and PrimeNG 17, in English, Arabic and Turkish.
Adding dir="rtl" to <html> is about the first 10% of right-to-left support. Here is the rest, with the code I ended up using.

I did not use an i18n library for this. With around 200 keys and a runtime language switch, a small typed service was enough.
For a bigger app I would look at Transloco.

1. A typed dictionary, so a missing translation breaks the build

English is the master. The other languages must define every key:

// en.ts export const EN = { 'nav.patients': 'Patients' /* ... */ } as const; export type Key = keyof typeof EN; export type Dict = Record<Key, string>; // ar.ts export const AR: Dict = { 'nav.patients': 'المرضى' /* ... */ }; 

If I add a key to en.ts and forget Arabic, ng build fails. You cannot ship half a translation.
(Keys built at runtime, like 'ward.' + p.ward, are plain strings, so those still need a test or a fallback.)

A pipe makes templates short:

@Pipe({ name: 't', standalone: true, pure: false }) export class TranslatePipe implements PipeTransform { private readonly i18n = inject(I18nService); transform(key: string, params?: Record<string, string | number>): string { this.i18n.lang(); // read the signal so views refresh when the language changes return this.i18n.t(key, params); } } 
{{ 'nav.patients' | t }} {{ 'appt.count' | t: { n: 3 } }} 

2. One effect keeps the page, PrimeNG and the direction in sync

effect(() => { const lang = this.lang(); const root = document.documentElement; root.lang = lang; root.dir = lang === 'ar' ? 'rtl' : 'ltr'; this.primeng.setTranslation(this.primeTranslation(lang)); }); 

PrimeNG needs its own texts (month names, weekdays, "no results"). I generate them with Intl instead of typing them:

const days = (weekday: 'long' | 'short' | 'narrow') => Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(locale, { weekday }).format(new Date(2024, 0, 7 + i))); 

(7 January 2024 is a Sunday, which is what PrimeNG expects at index 0.)
For Arabic I use the locale ar-u-nu-latn, which keeps Western digits. For clinical numbers that is what most hospital software does.

3. Use logical CSS properties

This is the change with the best return. Replace left and right with their logical versions and most of the layout flips by itself:

.side { border-inline-end: 1px solid var(--surface-border); } .tile { border-inline-start: 4px solid var(--sev); } .unit { margin-inline-start: .25rem; } .badge { inset-inline-end: 1rem; } 

A CSS grid sidebar also moves to the correct side without any extra code once dir is rtl.

4. The leftovers: what PrimeNG does not flip

This is where the time went. In my case:

[dir='rtl'] { /* arrows are SVG icons in PrimeNG 17, not pi-* classes */ .p-datepicker-prev .p-icon, .p-datepicker-next .p-icon, .p-paginator-prev .p-icon, .p-paginator-next .p-icon { transform: scaleX(-1); } /* table text alignment and sort icon spacing */ .p-datatable .p-datatable-thead > tr > th, .p-datatable .p-datatable-tbody > tr > td { text-align: right; } /* search input with an icon */ .p-input-icon-left > i:first-of-type { left: auto; right: .75rem; } .p-input-icon-left > .p-inputtext { padding-left: .75rem; padding-right: 2.5rem; } } 

My first attempt flipped .pi-chevron-left, and nothing changed. A screenshot showed the calendar's previous button pointing the wrong way. Always check with real screenshots.

5. Things that should stay left-to-right

  • Charts. Time runs left to right in a clinical chart, even in an Arabic UI. I wrap the chart: <div dir="ltr"><p-chart ... /></div>.
  • Values with units. 4–11 ×10³/µL gets scrambled in an RTL paragraph. Wrap it: <bdi dir="ltr">4–11 ×10³/µL</bdi>.
  • Usernames and passwords. Give those inputs dir="ltr".
  • Directional icons, like a "back" arrow, need flipping. Plain icons, like a bell, do not.

My checklist before calling it done

  • Dialogs and dropdown overlays (they are appended to <body>, so check them separately)
  • Calendar header arrows and week start (Saturday for Arabic, Monday for Turkish)
  • Table headers, sort icons and the paginator
  • Mixed text and numbers in the same line
  • Mobile: the bottom navigation bar in RTL

Try it

The result is running here, with a language switch at the top: https://careboard-pro.vercel.app (login: doctor / doctor123).

Disclosure: this comes from a hospital dashboard template I sell (Angular + Spring Boot, three languages with full RTL): https://buy.polar.sh/polar_cl_GfjpPuUI9MVVh75FlpOwUqCb14Sh3T9xjoRkt40qfZG
If you have questions about any part of the RTL work, ask in the comments.