Internationalising a SaaS: i18n, RTL and Locale Data Without a Rewrite
Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
Internationalise a SaaS by building for it before you have a second language: keep every user-facing string out of the code and in message files; express plurals, gender and interpolation as rules a translator can honour; format dates, numbers and currency with the locale, never by hand; and make the layout direction-aware so it mirrors for right-to-left languages. Retrofitting these into a shipped product is the expensive path.
This is engineering guidance for SaaS on a modern web stack. RTL is treated as an engineering skill, not a translation service. If you would rather have it built and tested for you, see how RAITHub would build this below.
What is the difference between i18n and localisation?
Internationalisation (i18n) is the engineering that makes a product able to adapt: no hard-coded text, no assumptions that a day is "MM/DD/YYYY" or that text flows left to right. Localisation (l10n) is adapting it for one locale: the translations, the regional formats, the cultural choices. i18n is yours to build once; localisation is added per market. The order matters, because a product that was not internationalised cannot be localised without touching every screen.
| Concern | Naive version | Internationalised version |
|---|---|---|
| Text | Strings in the code | Keys resolved from per-locale message files |
| Plurals | "1 item(s)" or an if/else | Locale plural rules (some languages have up to six forms) |
| Dates and numbers | Hand-formatted | Intl APIs that format by locale |
| Layout | Left-to-right only | Direction-aware, mirrors for RTL |
| Money | "$" prepended | Currency formatted per locale, stored in minor units |
How should translatable text be structured?
Move every string the user sees into message files keyed by a stable identifier, and resolve the key at render time for the active locale. A translator then edits text, never code, and a missing translation can fall back to the default language instead of showing a blank.
// en.json { "cart.title": "Your cart", "cart.items": "{count, plural, one {# item} other {# items}}" }
// ar.json { "cart.title": "سلتك", "cart.items": "{count, plural, zero {لا عناصر} one {عنصر} ... other {# عنصر}}" }
import { IntlMessageFormat } from 'intl-messageformat'
export function t(messages: Record<string, string>, key: string, locale: string, vars?: object) {
const template = messages[key] ?? key // fall back to the key, never a blank
return new IntlMessageFormat(template, locale).format(vars)
}
// t(ar, 'cart.items', 'ar', { count: 3 }) applies Arabic plural rules automatically.
Use the ICU MessageFormat syntax for plurals, gender and interpolation, rather than concatenating fragments. Gluing "You have " + count + " items" assumes English word order and a single plural form; Arabic has up to six plural categories and Polish three, and the Unicode plural rules define which forms each language needs. ICU keeps that logic in the message, where the translator controls it.
How do you format dates, numbers and currency by locale?
Never build a date or number string by hand. The browser's Intl APIs format for a locale, including the calendar, separators, currency symbol and decimal places.
new Intl.DateTimeFormat('ar-EG', { dateStyle: 'long' }).format(date) // Arabic, Egyptian conventions
new Intl.NumberFormat('de-DE').format(1234.5) // "1.234,5" (dot groups, comma decimal)
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(1000) // "¥1,000", no decimals
Store timestamps in UTC and money in integer minor units, and let the formatter decide presentation; the money rules are in the multi-currency support guide. Keep locale (how to format) separate from currency (what the money is) and from time zone (when to bucket), because a user in Germany might pay in dollars and read your dashboard in Berlin time.
What does supporting right-to-left actually take?
More than translating into Arabic or Hebrew. The whole layout mirrors: text aligns right, the sidebar moves, icons that imply direction flip, and progress flows the other way. Build it with logical CSS properties so the browser mirrors automatically when the document direction changes.
/* Physical properties do NOT mirror; logical properties do. */
.panel { margin-inline-start: 1rem; padding-inline: 1.5rem; text-align: start; }
/* Set direction once, high up; children inherit it. */
html[dir="rtl"] { direction: rtl; }
Logical properties (margin-inline-start, padding-inline, text-align: start) resolve against the writing direction, so one stylesheet serves both. Mixed content, such as an English product code inside an Arabic sentence, needs the Unicode bidirectional algorithm to order it correctly; test it rather than assuming. RAITHub treats RTL as a QA discipline, with a dedicated checklist in the Arabic RTL QA checklist.
Where do locale, currency and time zone each belong?
Store the user's preferred language, their time zone, and the currency context separately, and never infer one from another. A browser's language header is a hint for a first guess, not the user's settled choice, so let them pick and persist it. The active locale drives text and formatting; the time zone drives when a "day" starts, as in the SaaS dashboard guide; the currency drives money, above.
Do-it-yourself estimate: extracting strings, wiring a message framework and formatting with Intl is 1–2 weeks on a small product; full RTL support and the QA pass add one to two more. The main risk is hidden hard-coded strings and concatenated sentences that only surface when a translator sees them.
Buy, build or hire?
| Option | Examples | Choose this when | Watch out for |
|---|---|---|---|
| A translation management platform | A hosted service for managing message files and translators | You have many strings and several languages to keep in sync | It manages translations, not your code structure or RTL layout |
| Machine translation only | An automated translation API | A first draft, or low-stakes internal text | Plural, gender and tone errors reach users; weak for a product UI |
| Custom i18n build | An i18n library plus ICU messages, as here | The product is going multi-market and must mirror for RTL | You own the extraction, plural rules and the RTL QA |
| Hire a team to build it | RAITHub or another studio | You want it internationalised once, correctly, with RTL tested | Get the pseudo-localisation and RTL tests in the handover |
How do you test internationalisation?
- Pseudo-localisation: render the UI with accented, lengthened placeholder text to expose hard-coded strings and layouts that break when text grows.
- Plurals: assert counts of 0, 1, 2 and many render correctly in a language with more than two plural forms.
- Formatting: assert dates, numbers and currency match the locale's conventions, including a zero-decimal currency.
- RTL: switch direction and assert the layout mirrors, alignment flips, and mixed LTR/RTL content orders correctly.
- Fallback: remove a translation and assert the default language shows, never a blank or the raw key in production.
How RAITHub would build this
- Message architecture: all user-facing text extracted to per-locale files with ICU plural, gender and interpolation, and a safe fallback.
- Formatting: dates, numbers and currency formatted with Intl by locale, with locale, time zone and currency kept as separate settings.
- RTL: a direction-aware layout built on logical CSS properties, with bidirectional content handled and checked.
- Tests: pseudo-localisation, plural, formatting and RTL tests in CI.
Timeline: internationalising a product sits in the 4–6 week fixed scope for a focused build, longer where RTL and many locales are in scope. You receive: i18n and RTL tests in CI, handover docs and runbooks, and full IP under NDA.
Next step: a free 15-minute technical audit, then a written fixed quote. See the SaaS development service, pair it with the Arabic RTL QA checklist, and book the audit.
Frequently asked questions
What is the difference between i18n and localisation?
Internationalisation is the engineering that makes a product able to adapt: no hard-coded text, no layout or format assumptions. Localisation is adapting it for one locale with translations and regional formats. You build i18n once; you add localisation per market.
Why shouldn't I just concatenate strings for plurals?
Because word order and plural forms differ by language. Arabic has up to six plural categories, Polish three, and some languages reorder the sentence. ICU MessageFormat keeps plural and interpolation logic in the message, where the translator controls it.
How much work is right-to-left (RTL) support?
More than translating text: the layout mirrors, alignment and directional icons flip, and mixed-direction content must order correctly. Logical CSS properties let one stylesheet serve both directions, but RTL still needs its own QA pass.
Should I auto-detect the user's language?
Use the browser's language header only as a first guess, then let the user choose and persist their language, time zone and currency separately. Inferring one from another, such as currency from language, produces wrong results for many users.
Can I add internationalisation after launch?
Yes, but it is more expensive, because every hard-coded string and left-to-right assumption must be found and changed across the product. Building the structure early, even with one language, makes adding markets a content task rather than a rewrite.
Related posts
Building File Storage and Sharing in a SaaS: Safe Uploads, Done Right
9 min readGenerating PDF Reports and Invoices in a SaaS Without Breaking Production
8 min readSafe User Impersonation for SaaS Support Teams: Consent, Scope and Audit
8 min readReady to discuss your project?
Book a free 15-minute technical audit with our engineering team.