Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
Version History
- "Use the remix-intlayer middleware and hooks"v9.5.59/19/2026
- "Initial documentation for Remix 3"v9.5.09/9/2026
If you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
Translate your Remix 3 website using Intlayer | Internationalization (i18n)
This guide demonstrates how to integrate Intlayer for seamless internationalization in Remix 3 applications with locale-aware routing, type-safe content declarations, server-rendered JSX components, and cross-runtime support across Node.js, Bun, Deno, and Cloudflare Workers.
What is Remix 3?
Remix 3 represents a fundamental architectural shift towards a composable, runtime-agnostic web framework built entirely on web standards. Rather than being coupled to specific bundlers or proprietary server APIs, Remix 3 is distributed as single-purpose, composable packages:
remix/fetch-router(orremix/router): Lightweight, standard-compliant routing built on the Fetch API (RequestandResponse).remix/ui: A JSX component model (jsxImportSource: "remix/ui"). A component is a setup function that returns a render function, so it looks like React but keeps state in plain JavaScript closures.remix/middleware/render: Installscontext.render(<Page />)on every request, streaming the JSX tree to an HTMLResponse.remix/node-fetch-server: Server adapters for Node.js, while natively supporting Bun, Deno, and edge runtimes.remix/cookie: Cryptographically secure cookie parsing and serialization.
Combined with Intlayer and the remix-intlayer package, a locale middleware plus the same useIntlayer / useDictionary / useLocale hooks as react-intlayer, bound to the Remix request context, you get a complete internationalization system that delivers compile-time safety, automated AI translations, zero-overhead server rendering, and seamless locale routing.
Table of Contents
Why Intlayer over alternatives?
Compared to traditional solutions like i18next or bespoke translation loaders, Intlayer offers an integrated developer experience optimized for modern web architecture:
Intlayer is built to work seamlessly with web standards (Request, Response, Headers, and URL). remix-intlayer plugs into Remix 3's Fetch router as a lightweight middleware, extracting the locale from URL paths, cookies, or Accept-Language headers and exposing it to the rest of the request (handlers, views and remix/ui components) without passing it around or locking you into a specific runtime.
Say goodbye to loose JSON keys and runtime missing-key crashes. Intlayer enforces TypeScript checks across all declared locales, warning you at build time if a translation is missing or invalid.
Remix 3 renders JSX components on the server and streams the HTML to the client. Only the resolved text for the requested locale ends up in the output stream. No client hydration bundles or bulky translation catalogs are needed unless a component is explicitly marked as a clientEntry.
Intlayer co-locates content declarations (.content.ts) with your route logic, reducing the token context required by Large Language Models (LLMs). Built-in CLI commands like intlayer fill and intlayer test let you automate translations in CI/CD pipelines at the cost of your own AI provider.
Beyond code-first workflows, Intlayer provides a self-hosted Visual Editor and a Remote CMS allowing non-technical editors, translators, and copywriters to update content without redeploying code.
Step-by-Step Guide
See the Application Template on GitHub.
Install Dependencies
Install
intlayer,remix-intlayerandremix(version 3) using your preferred package manager:bashCopy codeCopy the code to the clipboard
intlayer: Core internationalization engine providing configuration management, dictionary declaration (t(),Dictionary), CLI tools, and runtime interpreter.remix-intlayer: The Remix 3 integration: theintlayer()router middleware that resolves the locale of each request, and theuseIntlayer,useDictionaryanduseLocalehooks that read it anywhere downstream.remix: The unified Remix 3 framework package exportingremix/router,remix/routes,remix/ui,remix/middleware/render, andremix/node-fetch-server.
Configure Intlayer
Architecture
In this architecture, the
intlayer()middleware ofremix-intlayeris registered increateRouter()ahead of therender()middleware. It strips the locale prefix before the router matches, so routes are declared once insrc/routes.tswithout a:localesegment, and it runs the rest of the request inside anAsyncLocalStoragescope, which is what letsuseIntlayer/useLocaleread the locale with no argument in route handlers andremix/uiviews. Content declarations are placed alongside your views insrc/:bashCopy codeCopy the code to the clipboard
Configuration
Create an
intlayer.config.tsin the root of your project to declare your supported languages and internationalization settings:intlayer.config.tsCopy codeCopy the code to the clipboard
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { internationalization: { locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH], defaultLocale: Locales.ENGLISH, }, }; export default config;For additional configuration settings (such as strict mode or routing storage preferences), refer to the configuration documentation.
Declare Your Multilingual Content
Declare your localized content in a
.content.tsfile:src/home.content.tsCopy codeCopy the code to the clipboard
import { t, type Dictionary } from "intlayer"; const homeContent = { key: "home", content: { title: t({ en: "Welcome to Remix 3", fr: "Bienvenue sur Remix 3", es: "Bienvenido a Remix 3", }), description: t({ en: "A composable, web-standard application with native i18n.", fr: "Une application composable basée sur les standards web avec i18n native.", es: "Una aplicación componible basada en estándares web con i18n nativa.", }), switchLanguage: t({ en: "Switch language:", fr: "Changer de langue :", es: "Cambiar idioma:", }), }, } satisfies Dictionary; export default homeContent;Intlayer also supports JSON, YAML, and CommonJS declaration formats. See the Content Declaration Documentation.
Build Intlayer Dictionaries
Compile the dictionary definitions to generate TypeScript types and runtime registries:
bashCopy codeCopy the code to the clipboard
This compiles your content into the
.intlayerartifact directory, enabling full TypeScript autocompletion and rapid dictionary lookup.Add the Intlayer Middleware
Remix 3 provides a composable middleware pipeline via
createRouter({ middleware: [...] }).remix-intlayerships theintlayer()middleware, the Remix counterpart of thenext-intlayerandvite-intlayerproxies. For each incoming request it:- Routes the locale, following
routing.mode(prefix-no-defaultby default). A URL without a locale prefix is redirected to the localized URL of the detected locale (the storage cookieINTLAYER_LOCALEor custom headerx-intlayer-locale, then standardAccept-Languagenegotiation, then yourdefaultLocale) unless that locale needs no prefix. A prefixed URL such as/fr/aboutis served from the/aboutroute as French,/en/aboutis redirected to/about, androuting.rewriterules are applied both ways (/fr/about→/fr/a-propos). Static assets and, inrouting.domainssetups, locale domains are handled the same way as in the other integrations. - Resolves the locale into the Remix request context, as
context.intlayer(orcontext.get(Intlayer)), withlocale,defaultLocaleandavailableLocales. - Persists the locale through the configured cookie / header, so the follow-up requests resolve the same locale.
Because the locale prefix is stripped before the router matches, your routes are declared once, without a
:localesegment, whatever the routing mode. The middleware then runs the rest of the request inside anAsyncLocalStoragescope bound to the request context, which is what lets the hooks of the package read the locale with no argument, in route handlers, views andremix/uicomponents alike:typescriptCopy codeCopy the code to the clipboard
useIntlayer("home", "fr")oruseIntlayer("faq", { item: 2 })override the request locale for one call, anduseDictionary(homeContent)reads an imported dictionary instead of a key. Outside of a request the hooks fall back to the default locale.The middleware also prepares the Intlayer dictionaries when the server starts, so a missing
intlayer builddoes not leave the registry empty.Set
routing.enableProxy: falseinintlayer.config.tsto keep the locale resolution only and handle the routing yourself.intlayer({ ignore })leaves the matching requests untouched (an API prefix, for instance), andintlayer({ isDevServer })controls whether the stored locale drives redirects in the defaultenableProxyauto mode.- Routes the locale, following
Define Type-Safe Routes
Define your application routes using
route()fromremix/routes. Declare them once, without a locale segment. The middleware serves every locale from them:src/routes.tsCopy codeCopy the code to the clipboard
import { route } from "remix/routes"; export const routes = route({ home: "/", });Using
route()gives you type-safe URL generation across your application, andgetLocalizedPathapplies the routing mode to it:typescriptCopy codeCopy the code to the clipboard
Render Localized Pages with JSX
Remix 3 renders UI with JSX components from
remix/ui. A component is a setup function that receives aHandleand returns a render function. Setup runs once per instance, render runs on every update, and props are read throughhandle.props.Start with a shared
Documentshell that sets the<html lang="..." dir="...">attributes from the locale resolved by the middleware:src/views/document.tsxCopy codeCopy the code to the clipboard
import { getHTMLTextDir } from "intlayer"; import { useLocale } from "remix-intlayer"; import type { Handle, RemixNode } from "remix/ui"; type DocumentProps = { title: string; children?: RemixNode; }; export const Document = (handle: Handle<DocumentProps>) => () => { const { title, children } = handle.props; const { locale } = useLocale(); return ( <html lang={locale} dir={getHTMLTextDir(locale)}> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{title}</title> </head> <body>{children}</body> </html> ); };Then create the home page. It reads the localized dictionary with
useIntlayerand renders a language switcher:src/views/home.tsxCopy codeCopy the code to the clipboard
import { getLocaleName, getLocalizedUrl, getPathWithoutLocale } from "intlayer"; import { useIntlayer, useLocale } from "remix-intlayer"; import { Document } from "./document"; export const HomePage = () => () => { const { locale, availableLocales } = useLocale(); const home = useIntlayer("home"); const pathWithoutLocale = getPathWithoutLocale(); return ( <Document title={home.title}> <header> <nav aria-label="Languages"> <span>{home.switchLanguage}</span> <ul> {availableLocales.map((localeItem) => { const isActive = localeItem === locale; return ( <li key={localeItem} class="p-1"> <a href={getLocalizedUrl(pathWithoutLocale, localeItem)} class={isActive ? "active" : undefined} aria-current={isActive ? "page" : undefined} > {getLocaleName(localeItem, locale)} </a> </li> ); })} </ul> </nav> </header> <main> <h1>{home.title}</h1> <p>{home.description}</p> </main> </Document> ); };Remix JSX is not React:
classis written as-is (classNameis also accepted), and re-renders are triggered explicitly withhandle.update(). Interpolated values are escaped automatically. The Intlayer hooks are plain functions reading the request scope, so they can be called from the setup function or the render function alike.Wire Up the Router and Server
Add the
render()middleware fromremix/middleware/rendernext to the Intlayer middleware. It installscontext.render(node, init)on every request, which streams the JSX tree into an HTMLResponse(prepending<!DOCTYPE html>and setting theContent-Typeheader):src/router.tsxCopy codeCopy the code to the clipboard
import { intlayer } from "remix-intlayer"; import { render } from "remix/middleware/render"; import { createRouter } from "remix/router"; import { routes } from "./routes"; import { HomePage } from "./views/home"; // 1. Initialize router with Intlayer + render middleware export const router = createRouter({ middleware: [intlayer(), render()], }); // 2. Map route handlers: `/`, `/fr`, `/es`… all reach `home` router.map(routes, { actions: { home(context) { return context.render(<HomePage />); }, }, });context.renderaccepts an optionalResponseInitas second argument, e.g.context.render(<NotFoundPage />, { status: 404 }). The resolved locale stays reachable from the handler ascontext.intlayer.locale, for instance to build aResponse.jsonpayload.Finally, expose the router through a standard
fetchhandler. The same router runs on Node.js, Bun, Deno, and Cloudflare Workers:src/server.tsCopy codeCopy the code to the clipboard
import * as http from "node:http"; import { createRequestListener } from "remix/node-fetch-server"; import { router } from "./router"; const PORT = Number(process.env.PORT || 3000); // Node.js const server = http.createServer( createRequestListener((request) => router.fetch(request)) ); server.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); }); // Bun / Deno / Cloudflare Workers export default { port: PORT, fetch(request: Request) { return router.fetch(request); }, };Audit and Auto-Fill Translations
Intlayer provides a CLI to audit for missing translations and automatically fill them using AI:
bashCopy codeCopy the code to the clipboard
TypeScript Configuration
Point JSX at the remix/ui runtime and ensure that your tsconfig.json includes the generated .intlayer types:
Copy the code to the clipboard
jsxImportSource: "remix/ui"is what makes<HomePage />resolve to Remix'screateElementinstead of React's.
Conclusion
With Remix 3 and Intlayer, you have a lean, fully typed, runtime-portable stack that adheres to open web standards. Your application can scale effortlessly from simple localized marketing pages to globally distributed, edge-rendered services.
