Skip to content

Topic · Server components

i18n for React Server Components, without the workaround.

Most JavaScript i18n libraries resolve messages through React Context. Context is a client-tree mechanism, and React Server Components removed the client tree from half your application. That mismatch is why so many App Router i18n guides contain the word 'workaround'.

01 — The problem

Why Context-based i18n breaks in the App Router

A provider wraps a tree and passes values down through it. A server component never enters that tree — it renders on the server, before hydration, outside React's client context entirely. So a library whose lookup depends on a provider has nothing to read from, and the usual fixes are to wrap everything in a client component, duplicate the translation state on both sides, or thread a locale prop through every call site by hand.

02 — The approach

Resolve through request scope, not through a tree

Palamedes never asks React for the active instance. It asks the request.

One accessor, everywhere

Transformed code reaches the active i18n instance through exactly one function: getI18n(). There is no provider to mount, no second hook for server components, and no separate RSC entry point. The same component code runs unchanged in a server component, a client island, or an Express route handler.

Request-local scope on the server

On the server the active locale lives in request-local async context, so concurrent requests in different languages never see each other's state. This is the mechanism Context cannot provide and the reason no bypass is needed — the lookup was never tied to the render tree in the first place.

One locale for the client document

In the browser the same accessor is a plain getter initialized before translated UI hydrates. Locale changes navigate the document, which keeps framework state, module caches, and the server-rendered language aligned.

Not a Next.js feature

The same model runs across 6 server frameworks, including the RSC-capable ones beyond Next.js — Waku and TanStack Start among them. Server-component support here is a property of the architecture, not an integration written for one framework.

03 — In code

The same component, on either side of the boundary.

Server component or client island — identical code

import { t } from "@palamedes/core/macro"

// No "use client". No provider. No locale prop.
export default async function CheckoutHeading({ seats }) {
  return <h1>{t`Buy ${seats} seats`}</h1>
}

Nothing here declares which side of the boundary it runs on, because nothing needs to. The macro compiles to a getI18n() call, and on the server that resolves against request-local scope.

04 — Evidence

Smoke-checked on relevant changes; browser-checked weekly

Server-component rendering is exactly the kind of claim that is easy to assert and tedious to prove, so it is proven mechanically instead.

Example apps
2521 browser-capable examples run Playwright weekly or on manual dispatch; all are built and smoke-checked on relevant PRs and main pushes — no mocked integrations.
Meta-frameworks
6Next.js, TanStack Start, SolidStart, Waku, React Router and Remix v3, each with the same runtime model.
Locale strategies each
4Cookie, route, subdomain and top-level domain, so the server path is exercised under every resolution mode.
What CI checks
SSR + switchThe flow loads each app, asserts server-rendered output, switches locale, and exercises localized server actions.
See the verified framework matrix

05 — Questions

Questions people actually ask

Answered here rather than buried three pages into the documentation.

Does Palamedes work with the Next.js App Router?
Yes, and without a bypass. Server components call the same getI18n() that client components do, backed by request-local async context. The Next.js plugin requires Next 16.
Do I need a provider component?
No. There is no provider to mount anywhere in the tree. This is the design decision that makes server components work, so it is not something that can be opted out of.
How do client components get their translations?
Through the same getI18n() accessor. In the browser it is backed by an external store in React and a signal in Solid, so activating a different catalog re-renders the components that use it, without a page reload.
Does this work outside Next.js?
Yes. The same model is verified across 6 server frameworks in CI, including the RSC-capable ones beyond Next.js. Palamedes ships React and Solid packages; there is no Vue or React Native support.
Why can't React Intl support server components?
Its runtime resolves messages through React Context, which is a client-tree mechanism. That is an architectural premise rather than a missing feature, which is why the request to use React Intl without Context has stayed open rather than being implemented.

06 — Keep reading

Related pages

Every claim on this page is checked into the repository.