Context
PR #350 merged the first experimental Remix v3 integration:
@palamedes/remix with a Node registerHooks() loader for server-loaded JS/TS modules
- request-local i18n scope for Remix handlers
examples/remix-cookie with cookie locale switching, Accept-Language negotiation, catalog loading, and matrix smoke coverage
That is a strong server-side candidate. This issue tracks the next step: bring Remix v3 to server-first Palamedes parity where Remix's beta architecture allows it, while making the remaining UI/client boundaries explicit.
Refs #284
Follow-up to #350
Goal
Make Remix v3 a first-class server-side Palamedes integration:
- normal catalog files and extraction workflow
- reusable Remix server API instead of per-example wiring
- standard Palamedes locale strategy examples
- clear documentation for supported macros, unsupported UI/client paths, and Remix beta tracking
Full client-side feature parity is not yet a realistic promise because Remix v3 browser-delivered modules currently go through Remix's asset compiler, not the Node registerHooks() chain. This issue should make that boundary visible and start the upstream/API work needed to remove it.
Spikes Before Implementation
Two risks are cheap to check up front and would change the design if they fail. Run these before the corresponding slice.
Spike 1: .po imports through the real Remix loader chain (gates Slice A)
Claiming .po URLs in the load hook is expected to work — Node's resolver is extension-agnostic, and the default loader never sees the file because the hook returns the module itself. But it has not been proven inside a real remix/node-tsx chain. Half a day, throwaway scaffold:
Spike 2: request scope vs. streamed rendering (gates the Slice B API shape)
The middleware plan wraps next() in scope.run(). That covers everything that executes before the Response is returned. If remix/ui/server renders lazily into the response body stream — and Frames fragments likely use the same path — translated code may run while the body is being consumed, after run() already resolved and potentially outside the AsyncLocalStorage context. If that context is lost, getI18n() silently resolves nothing and the middleware API has to look different.
Known sharp edges (decide, no spike needed)
Slice A: Catalog Workflow And Loader Support
Start here after Spike 1. This is low risk and removes the most fragile part of the current example.
Design decision
The normal Palamedes workflow imports checked-in catalog files:
import { messages } from "../locales/de.po"
Remix v3 has no bundler plugin in its default path, so the natural parity path is for the @palamedes/remix register hook to claim .po URLs and compile them through the existing catalog loading/rendering machinery in @palamedes/transform. Node cannot load .po files by default, so this hook does not need to delegate to nextLoad() for catalog files.
Work
Out of scope for this issue
Changing core fallback behavior when message ids drift is not Remix-specific. pmds audit and reporting are the right tooling surface for that; open a separate core/tooling issue if that behavior needs to change.
Slice B: First-Class Remix Server API, Then Strategy Examples
This must land before adding more Remix examples. Otherwise the route/subdomain/tld examples will copy the current manual wiring and immediately become refactor debt.
The middleware shape depends on the outcome of Spike 2 above — do not finalize the public API before that spike has an answer.
Server API
Locale strategy matrix
After the server API exists, add Remix v3 examples and smoke coverage for the standard Palamedes locale strategies:
Each example should be represented in scripts/example-matrix.mjs with targeted smoke checks.
Route, subdomain, and TLD should be server-side straightforward in Remix v3: route prefix via fetch-router and host-based locale resolution from request headers. The examples should showcase the new server API rather than duplicating manual setup.
Example UI note
The existing shared @palamedes/example-ui package is React-based and not directly usable for Remix v3's component model. Either:
Slice C: UI Adapter, Rich Messages, Client Boundary, And Frames
This is research-heavy and should not block Slice A or Slice B.
Remix UI adapter and rich JSX messages
The transform options from #350 remain valid, but rich messages need a render target. A <Trans> equivalent requires an adapter for Remix v3's own component model.
Acceptance for this area should be a working rich text example equivalent to the existing React/Solid story where Remix's component model allows it. If that is not possible yet, document the unsupported behavior explicitly.
Browser/client asset boundary
Research from #350 indicates Remix v3's asset server currently has no script transform hook. The practical options are wrapping createAssetServer, proxying in front of it, or getting an upstream extension point.
Frames and partial updates
Frames should be cheap once Remix documents and exports the relevant remix/ui Frames API: they are server-rendered fragments, so the request-local i18n scope should apply the same way as full document requests.
Beta Tracking Policy
The example is pinned exactly to the tested Remix beta. Keep that policy.
Documentation And Positioning
Acceptance Criteria
Context
PR #350 merged the first experimental Remix v3 integration:
@palamedes/remixwith a NoderegisterHooks()loader for server-loaded JS/TS modulesexamples/remix-cookiewith cookie locale switching,Accept-Languagenegotiation, catalog loading, and matrix smoke coverageThat is a strong server-side candidate. This issue tracks the next step: bring Remix v3 to server-first Palamedes parity where Remix's beta architecture allows it, while making the remaining UI/client boundaries explicit.
Refs #284
Follow-up to #350
Goal
Make Remix v3 a first-class server-side Palamedes integration:
Full client-side feature parity is not yet a realistic promise because Remix v3 browser-delivered modules currently go through Remix's asset compiler, not the Node
registerHooks()chain. This issue should make that boundary visible and start the upstream/API work needed to remove it.Spikes Before Implementation
Two risks are cheap to check up front and would change the design if they fail. Run these before the corresponding slice.
Spike 1:
.poimports through the real Remix loader chain (gates Slice A)Claiming
.poURLs in the load hook is expected to work — Node's resolver is extension-agnostic, and the default loader never sees the file because the hook returns the module itself. But it has not been proven inside a realremix/node-tsxchain. Half a day, throwaway scaffold:import { messages } from "./locales/de.po"resolves and executes undernode --import remix/node-tsx --import @palamedes/remix/register.node --watch(the dev script) picks up.poedits, i.e. catalog files enter the watched module graph.tsc --noEmitpasses with thepo.d.tsdeclaration pattern the other examples use.Spike 2: request scope vs. streamed rendering (gates the Slice B API shape)
The middleware plan wraps
next()inscope.run(). That covers everything that executes before theResponseis returned. Ifremix/ui/serverrenders lazily into the response body stream — and Frames fragments likely use the same path — translated code may run while the body is being consumed, afterrun()already resolved and potentially outside the AsyncLocalStorage context. If that context is lost,getI18n()silently resolves nothing and the middleware API has to look different.getI18n()resolves correctly inside a streamedremix/ui/serverrender wrapped by the middleware.Response, binding the scope into the stream explicitly (AsyncResource-style capture), or resolving the i18n instance per request and passing it explicitly. This decision defines the public middleware API, which is why it must precede Slice B.Known sharp edges (decide, no spike needed)
includealso matches.cjsand CommonJS-format modules. Either narrow the default or emit a CJS-compatible binding.Slice A: Catalog Workflow And Loader Support
Start here after Spike 1. This is low risk and removes the most fragile part of the current example.
Design decision
The normal Palamedes workflow imports checked-in catalog files:
Remix v3 has no bundler plugin in its default path, so the natural parity path is for the
@palamedes/remixregister hook to claim.poURLs and compile them through the existing catalog loading/rendering machinery in@palamedes/transform. Node cannot load.pofiles by default, so this hook does not need to delegate tonextLoad()for catalog files.Work
.poloading support to@palamedes/remix/register.@palamedes/transform.palamedes.yamltoexamples/remix-cookie.examples/remix-cookiewith checked-in catalog files.pmds extractcan discover Remix example messages.@palamedes/remixdocs.Out of scope for this issue
Changing core fallback behavior when message ids drift is not Remix-specific.
pmds auditand reporting are the right tooling surface for that; open a separate core/tooling issue if that behavior needs to change.Slice B: First-Class Remix Server API, Then Strategy Examples
This must land before adding more Remix examples. Otherwise the route/subdomain/tld examples will copy the current manual wiring and immediately become refactor debt.
The middleware shape depends on the outcome of Spike 2 above — do not finalize the public API before that spike has an answer.
Server API
@palamedes/remix/serverprimitives for locale negotiation fromRequest.AcceptLanguagefromremix/headers@remix-run/async-context-middleware, which uses the same AsyncLocalStorage primitive as Palamedes' server scopenext()in the active i18n request scope.Locale strategy matrix
After the server API exists, add Remix v3 examples and smoke coverage for the standard Palamedes locale strategies:
examples/remix-cookieexamples/remix-routeexamples/remix-subdomainexamples/remix-tldEach example should be represented in
scripts/example-matrix.mjswith targeted smoke checks.Route, subdomain, and TLD should be server-side straightforward in Remix v3: route prefix via fetch-router and host-based locale resolution from request headers. The examples should showcase the new server API rather than duplicating manual setup.
Example UI note
The existing shared
@palamedes/example-uipackage is React-based and not directly usable for Remix v3's component model. Either:examples/README.mdthat Remix examples intentionally do not visually match the React/Solid/etc. matrix until a Remix UI adapter exists.Slice C: UI Adapter, Rich Messages, Client Boundary, And Frames
This is research-heavy and should not block Slice A or Slice B.
Remix UI adapter and rich JSX messages
The transform options from #350 remain valid, but rich messages need a render target. A
<Trans>equivalent requires an adapter for Remix v3's own component model.remix/uiadapter: aTransequivalent in Remix's component model.@remix-run/node-tsxwhile it is still0.1.x.oxc-transformpath if Palamedes owns more of the loader pipeline.node-tsxbehavior for files Palamedes does not touch.Acceptance for this area should be a working rich text example equivalent to the existing React/Solid story where Remix's component model allows it. If that is not possible yet, document the unsupported behavior explicitly.
Browser/client asset boundary
Research from #350 indicates Remix v3's asset server currently has no script transform hook. The practical options are wrapping
createAssetServer, proxying in front of it, or getting an upstream extension point.Frames and partial updates
Frames should be cheap once Remix documents and exports the relevant
remix/uiFrames API: they are server-rendered fragments, so the request-local i18n scope should apply the same way as full document requests.remix/uidocuments and exports Frames.Beta Tracking Policy
The example is pinned exactly to the tested Remix beta. Keep that policy.
beta.6+.remix@nextto detect loader or asset pipeline churn early.Documentation And Positioning
packages/remix/README.mdfrom experimental-first-path language to a clear server-first support matrix.docs/api/remix.mdwith the final setup path.Acceptance Criteria
@palamedes/remix/servermiddleware/helper API exists and is used by the examples.pnpm verify:examples:smoke -- --framework remixpasses.