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.519/09/2026
- "Initial documentation for Remix 3"v9.5.009/09/2026
The content of this page was translated using an AI.
See the last version of the original content in EnglishIf 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
This guide demonstrates how to integrate Intlayer for seamless internationalisation 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 serialisation.
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 internationalisation 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 optimised 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 catalogues 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 internationalisation 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 internationalisation settings:intlayer.config.tsCopy codeCopy the code to the clipboard
import { Locales, type IntlayerConfig } from "intlayer"; const config: IntlayerConfig = { internationalization: { locales: [ Locales.ENGLISH, Locales.ENGLISH_UNITED_KINGDOM, Locales.FRENCH, Locales.SPANISH, ], defaultLocale: Locales.ENGLISH, }, }; export default config;For additional configuration settings, refer to the configuration documentation.
Declare Your Multilingual Content
Declare your localised 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-GB": "Welcome to Remix 3", en: "Welcome to Remix 3", fr: "Bienvenue sur Remix 3", es: "Bienvenido a Remix 3", }), description: t({ "en-GB": "A composable, web-standard application with native i18n.", 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-GB": "Switch language:", 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. For each incoming request it resolves the locale using:- The URL, in every routing mode but
no-prefix: the path prefix (e.g./en-GBor/fr) or the?locale=search param. - The locale persisted by the client: the storage cookie (
INTLAYER_LOCALE) or custom header (x-intlayer-locale). - Standard
Accept-Languagenegotiation, falling back to your configureddefaultLocale.
The result is stored in the Remix request context as
context.intlayer(orcontext.get(Intlayer)), withlocale,defaultLocaleandavailableLocales. The middleware then runs the rest of the request inside anAsyncLocalStoragescope bound to that 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.- The URL, in every routing mode but
Define Type-Safe Routes
Define your application routes using
route()fromremix/routes:src/routes.tsCopy codeCopy the code to the clipboard
import { route } from "remix/routes"; export const routes = route({ // Default locale route home: "/", // Localized route with dynamic :locale segment localizedHome: "/:locale", });Using
route()gives you type-safe URL generation across your application:typescriptCopy codeCopy the code to the clipboard
Render Localised 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 localised 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 { isDeclaredLocale } from "intlayer"; 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 router.map(routes, { actions: { // Default locale route home(context) { return context.render(<HomePage />); }, // Localized route localizedHome(context) { if (!isDeclaredLocale(context.params.locale)) { return new Response("Not Found", { status: 404 }); } 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 localised marketing pages to globally distributed, edge-rendered services.
