Five Languages on a Serverless Runtime

A consultancy site had to speak five languages — English, French, German, Spanish and Arabic — to a genuinely international audience. Translation was a product requirement from the start rather than something retrofitted, which changes the engineering more than it sounds.
Resolving the locale
Locale is resolved server-side on every request, in a fixed precedence order: an explicit URL parameter first, then a year-long cookie, then a parsed Accept-Language header, then the default. Each step is a weaker signal than the one before it, and the order means an explicit choice always wins over a guess — which is the behaviour a reader expects, even though they will never think about it.
Arabic drives dir="rtl" along with per-locale lang and og:locale tags. Right-to-left is the locale that finds your layout bugs, and it finds them immediately.
The part that actually matters
The obvious way to hold the active dictionary is a module-level variable. Set it when the request comes in, read it from anywhere, no plumbing. On a traditional single-request-per-process server this is mostly fine.
On a serverless runtime it is a race. One isolate handles many concurrent requests, and module scope is shared across all of them. A German request sets the dictionary; a French request sets it a millisecond later; the German request renders and picks up French. This does not fail in development, where you are the only visitor. It fails in production, intermittently, in a way that looks like a caching problem and is not.
So the dictionary is threaded through the router loader into a per-request React context instead. That is more plumbing — every component that needs a string takes it from context rather than importing a singleton — and it is the entire point. Per-request state lives in per-request scope. The decision is documented in the code, because the next person to read it will be tempted by the singleton for exactly the reasons it looks appealing.
Keeping translations reviewable
Translations are typed dictionaries split per page, so a missing key is a compile error rather than a undefined rendered to a reader in Riyadh. Alongside them the repo ships a reviewer-facing document laying every string out side by side in all five languages.
That document exists because of how copy changes actually arrive. A client does not say "update home.hero.subtitle". They say "the German sounds too formal". Being able to hand them a table where the five versions sit next to each other turns a vague note into a specific key, and turns a translation round-trip from a week into an afternoon.
The performance pass
The same project got a repeatable asset pipeline rather than a one-off cleanup — a script that re-encodes photographic PNG and JPG to WebP at a 1920px cap, and every hero MP4 to a 1280px H.264 file with audio stripped and faststart enabled, extracting a WebP poster frame from each.
The video component then makes the poster the LCP element and only mounts the actual <video> once the section is near the viewport and the browser is idle. Under prefers-reduced-motion it skips the video entirely. A WebGL aurora effect is lazy-split into its own chunk so the shaders stay out of the critical bundle.
Recorded before and after: 23 MB of transfer down to 4 MB, Lighthouse performance 55 to 63, total blocking time 310 ms to 130 ms.
The script matters more than the numbers. A manual optimisation pass decays — the next person to add a hero video adds a 12 MB one. A script in the repo is a thing you can run again, and it is the only version of this work that survives you.