Adding Arabic to a product is not a localisation project. It is an architecture project wearing a translation costume, and treating it as the former is the single most expensive misjudgement I see GCC B2B SaaS teams make.
The clearest version of that I have watched involved a Next.js platform built in about six weeks with an AI builder, sold successfully to three UAE clients, and then pitched to a Saudi ministry who asked for Arabic. The team estimated four days. Translation files existed, the framework supported locales, the plan looked sound. It took eleven weeks — not because Arabic is hard, but because roughly 80% of the components in the generated design system had been written with margin-left and text-align: left, and every one of them had to be opened. What that taught me is that direction is a property of your layout system, decided on the first day, and translation is the last five per cent of the work rather than the first ninety-five.

Why the Gulf breaks a generated Next.js codebase in two places at once
Most B2B SaaS prototypes are built for a single tenant in a single language, and both assumptions are invisible while they hold.
The GCC removes both at the same moment. Your second customer wants their own branded subdomain and cannot see the first customer's data; your third customer is a government entity that requires an Arabic interface and in-Kingdom hosting. Neither requirement is unusual here. Arriving together, six weeks after launch, they compound.
Fair objection, and I hear it from good engineers: you should not build multi-tenancy or internationalisation before you have customers, because most prototypes die and that work is wasted. I agree with the general principle. My disagreement is about which decisions are cheap to defer — and there are exactly two that are not, because they are structural rather than additive.
The three-axis model for a GCC-ready platform
What nobody tells you is that these three axes are usually conflated into one "settings" concept early on, and separating them later is the refactor everyone dreads.
Axis one: tenant
Which organisation does this request belong to, and where is that decided? The only safe answer is: on the server, from a source the client cannot forge — the host header, a signed session claim, or a middleware lookup. A tenant read from a client-side context object is not a boundary, it is a suggestion.
Axis two: locale and direction
Locale carries more than language. It sets text direction, numeral system, calendar, currency position, date order, sort order and the shape of a person's name. In Next.js this belongs in the route segment — app/[locale]/layout.tsx — so the server renders <html lang="ar" dir="rtl"> rather than a client effect flipping the page after paint.
Axis three: region
This is the axis nobody outside this market plans for, and it is the one that reshapes a schema. A Saudi enterprise customer's personal data must be processed inside the Kingdom under the framework administered by SDAIA, which means tenant is not only a column — it is a routing key that decides which database, in which country, a request reaches.
| Do not model region as a tenant attribute. Treat it as the outermost routing dimension, above tenant, so a single connection string never spans two countries. Teams that discover this after signing their first Saudi client end up migrating live data mid-contract. |
How do you make a Next.js app properly right-to-left?
Making a Next.js application properly right-to-left requires four changes: set dir="rtl" on the server-rendered <html> element from the locale segment, replace every physical CSS property with its logical equivalent, mirror directional icons while leaving non-directional ones alone, and load an Arabic typeface with real weights. Translating strings is the last step, not the first.
The logical-property swap is the bulk of the work and the least interesting part of it. margin-left becomes margin-inline-start, padding-right becomes padding-inline-end, left: 0 becomes inset-inline-start: 0, and text-align: left becomes text-align: start. The CSS logical properties specification exists precisely so one stylesheet serves both directions. Teams on Tailwind CSS get the ms-, me-, ps-, pe-, start- and end- utilities plus rtl: and ltr: variants for the handful of cases that genuinely need a direction-specific rule.
Icons need judgement rather than a rule. Mirror anything that implies forward motion or reading order — back arrows, chevrons, progress bars, undo, indent, the send arrow. Leave alone anything that maps to the physical world or to a universal convention: clocks, the media play triangle, checkmarks, magnifying glasses, and logos. The guidance published with Material Design is the clearest short reference I know for the boundary cases.
Then the details that only show up in front of a real Arabic reader. Set letter-spacing: 0, because Arabic letterforms join and any tracking visibly breaks the connections. Raise line height — we typically run Arabic about 15% looser than the Latin equivalent at the same font size, even though the translated string is often shorter in character count. And pick a typeface with genuine Arabic weights rather than relying on a system fallback that will render your bold headings in an unrelated face. The W3C Internationalization Activity keeps the most reliable public notes on these typographic requirements.
Numbers are where I have seen the most confusion. Intl.NumberFormat('ar-EG') renders Arabic-Indic digits; Intl.NumberFormat('ar-AE') commonly renders Latin ones; and appending the -u-nu-latn extension forces Latin digits regardless of locale. Dates have the same problem in a sharper form, since Intl.DateTimeFormat('ar-SA-u-ca-islamic') produces a Hijri date that a Gulf user may expect on some screens and not others. The Unicode CLDR project is the source of truth underneath all of it, and worth reading once rather than guessing repeatedly.
| A product is not bilingual because both languages appear. It is bilingual when a customer using the second language never notices which one you built first. |
Which multi-tenant isolation model fits a GCC B2B SaaS?
Most GCC B2B SaaS platforms should use shared tables with a tenant column and PostgreSQL row-level security, moving to a database per country for data-residency reasons rather than per tenant. Schema-per-tenant looks tidy and becomes painful past a few hundred tenants; database-per-tenant is justified only when a single customer's contract demands physical separation.
The trap inside the recommended option is operational rather than architectural, and it has cost more teams a weekend than any design flaw. PostgreSQL row-level security policies usually read the current tenant from a session variable. Put PgBouncer in transaction pooling mode in front of that database — which every serverless deployment ends up doing — and session state is discarded between statements, so the variable is empty and the policy filters against nothing. The fix is small: set it with set_config('app.tenant_id', $1, true) inside the transaction, where the third argument makes it transaction-local. The failure mode, if you do not, is a query that returns no rows rather than an error, which is exactly the kind of bug that reaches production.
A worked refactor, in the order that actually works
The eleven-week Arabic project I mentioned came back to us the following year for its Saudi expansion, and we ran the second refactor differently. Six weeks, and the sequence was the reason.
- Week one: make direction structural. Move the locale into the route segment, render
diron the server, and add a build check that fails if any stylesheet contains a physical property. That check catches regressions forever afterwards, which is worth more than the initial sweep. - Week two: sweep the component library once. Physical to logical properties, then icon mirroring, then typography. Do it in one pass across every component rather than page by page, or you will do it twice.
- Week three: move tenant resolution to the edge. Middleware reads the host, resolves the tenant, and attaches it to the request. Nothing downstream trusts a client-supplied tenant identifier again.
- Week four: put the boundary in the database. Row-level security on every tenant-scoped table, tested with two real accounts rather than a mocked client, and the pooling fix above applied from the start.
- Weeks five and six: split by region and test with humans. Route Saudi tenants to an in-Kingdom database, then put the Arabic interface in front of native Arabic speakers doing their real jobs — not reading a checklist.
That last half-day found things no automated test would have. A date picker that opened with the week starting on Monday, which no Gulf user expects. A phone field that stripped the leading zero from a Saudi mobile number. A table of invoices sorted correctly by value and nonsensically by customer name, because the sort ran on byte order rather than through a locale-aware collator. None of those were bugs in the sense a test suite understands. All three would have been the first thing a customer mentioned.
The other concession worth making: everything above assumes you are keeping the generated codebase, and sometimes that is wrong. If a prototype has no component library at all — inline styles scattered across forty pages — the sweep costs more than rebuilding the front end on a system that was logical from the start. We have made that call twice, and both times the rebuild finished sooner than the estimate for the sweep.
Where to start this week
Two things, and the first takes twenty minutes. Open your codebase and search for margin-left, padding-right, text-align: left and left:. The count that comes back is a fair estimate of your Arabic readiness, and most teams are surprised by it in the wrong direction.
The second is a conversation rather than a task. Ask your team which country each customer's data physically sits in today, and whether anything in the code would stop you routing a new customer somewhere else. If the answer involves editing a connection string by hand, that is the work to schedule next.
Where I would look after that is the tenant-aware caching layer, which is the next thing to break in a multi-tenant Next.js application and gets far less attention than it deserves — a cached page keyed without the tenant will happily serve one customer's dashboard to another. Worth an hour with your team and the Next.js internationalisation documentation open, before it becomes an incident report.
Frequently asked questions
Is adding Arabic to a Next.js app just a translation task?
No. Translation is the smallest part. Arabic requires right-to-left layout throughout, logical CSS properties in place of physical ones, mirrored directional icons, a typeface with real Arabic weights, zero letter-spacing, increased line height, and locale-aware numbers and dates. Most of that work sits in the component library rather than in language files.
Should each tenant get its own database in a GCC SaaS platform?
Usually not. Shared tables with a tenant column and PostgreSQL row-level security handle large tenant counts well and keep migrations manageable. Separate databases are justified by data-residency law — one per country you sell into — or by an individual enterprise contract that requires physical separation, rather than as a default design.
Why do row-level security policies stop working behind a connection pooler?
Because transaction-mode pooling discards session state between statements. A policy that reads the tenant from a session variable set with a plain SET statement finds it empty and filters against nothing. Setting it with set_config and the transaction-local flag inside the same transaction keeps the value attached to the query that needs it.