npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@palamedes/remix

v1.25.0

Published

Full-stack Remix v3 i18n with server and browser macro transforms

Downloads

1,974

Readme

@palamedes/remix

Remix v3 server and browser asset integration for Palamedes.

Installation

pnpm add @palamedes/core @palamedes/core-node @palamedes/remix @palamedes/runtime remix

@palamedes/core must be a direct runtime dependency because generated catalog modules import defineCompiledCatalog() from its compiled entrypoint. @palamedes/core-node is needed when generating serializable browser catalogs with compileCatalogArtifact() as shown below.

Use this package with Remix v3's default Node loader path. Register Remix's TSX loader first, then Palamedes:

node --import remix/node-tsx --import @palamedes/remix/register server.ts

The order is load-bearing. If @palamedes/remix/register is registered before remix/node-tsx, Remix short-circuits the TS/TSX load and Palamedes macros can reach runtime as untransformed stubs.

The register hook composes with remix/node-tsx, receives the JavaScript source that Remix compiled from .ts and .tsx files, and runs the Palamedes macro transform before Node executes the module.

For browser-delivered modules, install the post-compile asset loader and allow the generated runtime import:

import { createPalamedesRemixAssetLoader, PALAMEDES_REMIX_ASSET_PACKAGES } from "@palamedes/remix";
import { createAssetServer } from "remix/assets";

const assetServer = createAssetServer({
  basePath: "/assets",
  allowFiles: ["app/routes.ts", "app/**/public/**"],
  allowPackages: ["remix", ...PALAMEDES_REMIX_ASSET_PACKAGES],
  sourceMaps: process.env.NODE_ENV === "development" ? "external" : undefined,
  scripts: { loaders: [createPalamedesRemixAssetLoader()] },
});

The browser loader transforms ordinary macros after Remix compiles TypeScript and JavaScript. Remix then rewrites the injected @palamedes/runtime import to an asset URL. The loader does not compile .po imports or load Palamedes config; those remain server-hook responsibilities. A custom runtimeModule package must be added to allowPackages in place of the default package constant.

Use the correctly spelled PALAMEDES_REMIX_ASSET_PACKAGES export in new code. The previous PALEMEDES_REMIX_ASSET_PACKAGES spelling remains available as a deprecated alias for compatibility.

Development source maps and invalidation

Enable Remix sourceMaps in development as shown above. The Palamedes loader returns a map from its generated runtime calls to Remix's compiled JavaScript; Remix composes that map with its own TypeScript/JSX map and any later import rewrites or minification. Browser stack traces therefore point to the authored .ts/.tsx call site. Transform errors are remapped through the same incoming map before Remix reports them. Without Remix source maps, the loader can only report positions in the compiled JavaScript it receives.

Remix's asset watcher owns macro-bearing browser source files. Editing one invalidates the cached transformed module and re-runs the Palamedes loader; the normal Remix HMR rules then apply. A module below an accepted HMR boundary is updated in place, while a change without an accepted boundary intentionally causes a full browser reload. Neither case requires a server-process restart. The loader is stateless and safe when Remix invokes it repeatedly for the same module.

PO catalogs and palamedes.yaml are intentionally not dependencies of browser asset modules: browser transforms contain stable message IDs and source fallbacks, while the active catalog comes from the document bootstrap. The server register hook makes imported PO files and the config file dependencies of the Node module graph. With node --watch, changing either restarts the server, clears the per-locale server/bootstrap caches, and requires a full document reload so markup and browser messages change atomically. Custom development runners must provide the equivalent restart. A catalog/config edit is therefore never expected to hot-swap only an already running browser module.

Scope

This integration is tested against [email protected]:

| Area | Status | | ---------------------- | -------------------------------------------------------------------------------------- | | Server macros | t, plural, select, and selectOrdinal through the Node register hook | | Browser macros | The same ordinary macros through createPalamedesRemixAssetLoader() | | Rich Remix UI messages | Trans, Plural, Select, and SelectOrdinal in server and browser modules | | Client catalog | Serializable ICU strings embedded in the inert document bootstrap; no browser .po | | HMR and source maps | Authored TS/TSX mappings plus Remix watch/HMR invalidation for browser source modules | | Remix UI Frames | Server-rendered document and direct frame requests retain independent request scope | | Locale switching | Cookie, route, subdomain, and TLD through intentional full-document navigation | | Public hosting | Repository example and CI browser proof are ready; a public live deployment is pending |

Reactive in-document locale replacement is intentionally not supported. A locale change must create a new document so SSR markup, <html lang>, the bootstrap catalog, and browser runtime always agree.

Browser Catalog Bootstrap

Deliver the server-selected locale and its serializable ICU string catalog in the document, then initialize Palamedes before importing or rendering translated browser modules:

// Server setup
export const remixI18n = createRemixI18nServer({
  locales,
  strategy: "cookie",
  loadMessages, // May be an executable server catalog.
  loadClientMessages(locale) {
    return browserCatalogs[locale]; // Serializable Record<string, string>.
  },
});

// While rendering inside remixI18n.run(...)
const catalog = remixI18n.renderClientBootstrap(locale);

Place catalog inside the rendered <body> before the external browser entry. It is an inert <template id="palamedes-i18n-bootstrap">, not executable inline script. In the browser entry:

import { createI18n } from "@palamedes/core";
import { initializeRemixClientI18n } from "@palamedes/remix/client";

initializeRemixClientI18n({ createI18n });
await import("./translated-app.js");

The server payload uses ICU strings deliberately. Produce them at build or server startup with compileCatalogArtifact(...).messages from @palamedes/core-node; do not serialize executable .po module exports. initializeRemixClientI18n() runs only in a browser environment. It uses the parser-capable @palamedes/core runtime, validates the payload and exact <html lang> match, installs the catalog, and only then exposes it to transformed browser code. Render <html lang={locale}>; if the attribute is missing, initialization reports that it cannot verify the document locale. Missing, malformed, executable, or locale-mismatched payloads fail with an actionable error instead of mixing locales silently.

Locale changes require a full document navigation. A new request resolves the cookie, route, host, or language header again and emits a matching document and catalog. There is no browser .po request and no separate catalog HTTP cache: the payload follows the document's cache policy. Vary shared document caches by the locale inputs they use (for cookie selection, use Vary: Cookie or a private response) and invalidate them when the returned catalogVersion changes. The default version is a stable SHA-256 content digest; a deployment version can be supplied with catalogVersion.

Server and client catalogs are cached per locale for the life of the server instance. Development watch processes should restart when PO/config inputs change. The inert template works with a CSP that disallows inline scripts; keep the bootstrap entry in an allowed external module. Advanced renderers may pass an already parsed bootstrap object or custom document/elementId to the client initializer.

Remix UI, Frames, and Rich Messages

Remix UI Frames are supported on the server. Render both the document and the frame endpoint inside remixI18n.run() so a streamed frame and a later, client-initiated frame reload independently resolve the same request locale:

import { Frame } from "remix/ui";
import { renderToStream } from "remix/ui/server";

function renderDocument(request: Request, locale: string) {
  return renderToStream(
    <html lang={locale}>
      <body>
        <Frame name="locale-summary" src="/frames/locale-summary" fallback={<p>Loading…</p>} />
      </body>
    </html>,
    {
      frameSrc: request.url,
      signal: request.signal,
      resolveFrame: () => renderLocaleSummary(),
    },
  );
}

The cookie example exercises both /frames and /frames/locale-summary with German translations. Use ordinary JavaScript macros such as t inside Remix UI components; those calls remain visible to the server loader after JSX lowering.

Import rich-message macros from the Remix-specific entry:

import { Plural, Select, SelectOrdinal, Trans } from "@palamedes/remix/macro";
import type { Handle } from "remix/ui";

export function Greeting(handle: Handle<{ name: string; count: number }>) {
  return () => (
    <p>
      <Trans>
        Hello <strong>{handle.props.name}</strong>
      </Trans>
      <Plural value={handle.props.count} one="# message" other="# messages" />
    </p>
  );
}

The transform rewrites Trans to @palamedes/remix/compiled and lowers the choice macros to the active Palamedes runtime. The compiled entry uses Remix UI elements and component handles directly; it has no React runtime or type dependency. Named tags preserve the supplied element's props and receive the translated children. Values may contain Remix elements and nested node arrays.

Both remix/node-tsx output and browser asset modules are supported. The transformer recognizes the jsx, jsxs, and jsxDEV binding identities emitted by Remix and recovers the same message, placeholders, and tag numbering as authored TSX. Dynamic lowered trees still fail with a source-oriented diagnostic because their message identity cannot be determined statically.

Runtime Cost

Remix v3 intentionally has no build step: remix/node-tsx reads and lowers every .ts, .tsx, and .jsx module through oxc-transform when the process starts, in development and production alike. The Palamedes hook joins that existing pipeline instead of adding a new one, and Palamedes' native macro transform is built on the same OXC infrastructure Remix itself uses.

In practice:

  • Modules without Palamedes macro imports are skipped after a fast substring scan of source that is already in memory.
  • Modules with macros run through the native transform once, at module load time. Macro call sites are patched in place; files are not re-printed.
  • After startup there is no per-request transform work. Requests execute plain runtime calls against compiled catalogs — the same code shape the build-time integrations (@palamedes/vite-plugin, @palamedes/next-plugin) produce.

The register hook preserves source-message fallbacks in both development and production, so deploy skew renders readable source text rather than a compiled hash. Create a custom hook with createPalamedesRemixLoadHook({ keepSourceFallbacks: false }) to opt into smaller hash-only output when source text must not ship. Parser-free runtimes leave retained ICU fallbacks raw; use @palamedes/core when they must format.

Loaded Palamedes configuration is cached between catalog imports, but each hit validates the config file's content digest. Catalog modules also register the config as a dependency, so under node --watch, locale, fallback, or catalog-path edits restart the process without a manual restart.

The transform cost moves from build time to process start, stays proportional to the number of macro-containing modules, and recurs per cold start. That is the same tradeoff Remix makes for its own TypeScript and JSX lowering, so steady-state request performance matches the build-time integrations.

Server Runtime Scope

Use @palamedes/remix/server to bind translated server code to the active request and cache compiled catalog modules at module scope:

import { createRemixI18nServer } from "@palamedes/remix/server"

export const remixI18n = createRemixI18nServer({
  locales,
  strategy: "cookie",
  loadMessages,
})

export default createController(routes, {
  actions: {
    home(context) {
      return remixI18n.run(context, ({ locale }) => context.render(<HomePage locale={locale} />))
    },
  },
})

createRemixI18nRequestScope() remains available for lower-level integrations. Both APIs preserve the active i18n scope while a returned Response.body is streamed, so translated code that executes during body consumption still sees the same request-local i18n instance. Fetch metadata on that response, including url, type, and redirected, is preserved while the body is wrapped.

createRemixI18nServer() also exposes createClientBootstrap(locale) and renderClientBootstrap(locale). Pass loadClientMessages when the server's loadMessages returns executable compiled catalogs; otherwise the existing serializable catalog is reused. catalogVersion accepts a non-empty string or a function of { locale, messages } and defaults to a deterministic digest.

Prerelease Tracking

The package peer range is remix@^3.0.0-rc.1, while the examples pin the exact version they prove: currently [email protected]. A newer prerelease or stable release is called supported only after all four example manifests are updated together and the smoke and focused browser lanes pass:

pnpm verify:examples:smoke -- --framework remix

For early warning, maintainers can run the same smoke command after temporarily overriding the examples to remix@next; failures should be treated as a non-blocking canary signal unless the pinned prerelease also fails.

The repository example is technically ready to become a public demo when the published examples image contains it, the pinned smoke/browser checks are green, and an HTTPS deployment passes a reachability check plus locale switch, hydration, and browser-console verification. Only then should the framework matrix replace its source link with a live URL; hosting remains managed separately from this package.

palamedes is part of the Ferramenta family — Rust-native developer tools that keep the APIs the ecosystem already knows.

Siblings: ferroni · ferriki · ferromark · ferrolex · ferrocat · ferrovia · ferralk · ferrugo.

License

Sebastian Software

MIT OR Apache-2.0 © 2026 Sebastian Software