Author:
    Creation:2026-09-09Last update:2026-09-21

    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 (or remix/router): Lightweight, standard-compliant routing built on the Fetch API (Request and Response).
    • 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: Installs context.render(<Page />) on every request, streaming the JSX tree to an HTML Response.
    • 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

    ide.intlayer.org
    intlayer-remix-3-template.vercel.app

    See the Application Template on GitHub.

    1. Install Dependencies

      Install intlayer, remix-intlayer and remix (version 3) using your preferred package manager:

      bash
      npm install intlayer remix-intlayer remix@next
      
      • intlayer: Core internationalization engine providing configuration management, dictionary declaration (t(), Dictionary), CLI tools, and runtime interpreter.
      • remix-intlayer: The Remix 3 integration: the intlayer() router middleware that resolves the locale of each request, and the useIntlayer, useDictionary and useLocale hooks that read it anywhere downstream.
      • remix: The unified Remix 3 framework package exporting remix/router, remix/routes, remix/ui, remix/middleware/render, and remix/node-fetch-server.
    2. Configure Intlayer

      Architecture

      In this architecture, the intlayer() middleware of remix-intlayer is registered in createRouter() ahead of the render() middleware. It strips the locale prefix before the router matches, so routes are declared once in src/routes.ts without a :locale segment, and it runs the rest of the request inside an AsyncLocalStorage scope, which is what lets useIntlayer / useLocale read the locale with no argument in route handlers and remix/ui views. Content declarations are placed alongside your views in src/:

      bash
      .
      ├── src
      │   ├── home.content.ts               # Home page content declaration
      │   ├── router.tsx                    # createRouter() with the intlayer() and render() middleware
      │   ├── routes.ts                     # Type-safe routes, declared once without locale segment
      │   ├── server.ts                     # fetch handler (Node.js, Bun, Deno, Cloudflare Workers)
      │   └── views
      │       ├── document.tsx              # HTML shell setting <html lang dir> from the locale
      │       └── home.tsx                  # Localized page using useIntlayer / useLocale
      ├── intlayer.config.ts
      ├── package.json
      └── tsconfig.json
      

      Configuration

      Create an intlayer.config.ts in the root of your project to declare your supported languages and internationalization settings:

      intlayer.config.ts
      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.
    3. Declare Your Multilingual Content

      Declare your localized content in a .content.ts file:

      src/home.content.ts
      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.
    4. Build Intlayer Dictionaries

      Compile the dictionary definitions to generate TypeScript types and runtime registries:

      bash
      npx intlayer build
      

      This compiles your content into the .intlayer artifact directory, enabling full TypeScript autocompletion and rapid dictionary lookup.

    5. Add the Intlayer Middleware

      Remix 3 provides a composable middleware pipeline via createRouter({ middleware: [...] }).

      remix-intlayer ships the intlayer() middleware, the Remix counterpart of the next-intlayer and vite-intlayer proxies. For each incoming request it:

      1. Routes the locale, following routing.mode (prefix-no-default by default). A URL without a locale prefix is redirected to the localized URL of the detected locale (the storage cookie INTLAYER_LOCALE or custom header x-intlayer-locale, then standard Accept-Language negotiation, then your defaultLocale) unless that locale needs no prefix. A prefixed URL such as /fr/about is served from the /about route as French, /en/about is redirected to /about, and routing.rewrite rules are applied both ways (/fr/about → /fr/a-propos). Static assets and, in routing.domains setups, locale domains are handled the same way as in the other integrations.
      2. Resolves the locale into the Remix request context, as context.intlayer (or context.get(Intlayer)), with locale, defaultLocale and availableLocales.
      3. 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 :locale segment, whatever the routing mode. The middleware then runs the rest of the request inside an AsyncLocalStorage scope bound to the request context, which is what lets the hooks of the package read the locale with no argument, in route handlers, views and remix/ui components alike:

      typescript
      import { useIntlayer, useLocale } from "remix-intlayer";
      
      // Anywhere downstream of the middleware
      const { locale, availableLocales } = useLocale();
      const { title } = useIntlayer("home");
      

      useIntlayer("home", "fr") or useIntlayer("faq", { item: 2 }) override the request locale for one call, and useDictionary(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 build does not leave the registry empty.
      Set routing.enableProxy: false in intlayer.config.ts to keep the locale resolution only and handle the routing yourself. intlayer({ ignore }) leaves the matching requests untouched (an API prefix, for instance), and intlayer({ isDevServer }) controls whether the stored locale drives redirects in the default enableProxy auto mode.
    6. Define Type-Safe Routes

      Define your application routes using route() from remix/routes. Declare them once, without a locale segment. The middleware serves every locale from them:

      src/routes.ts
      import { route } from "remix/routes";
      
      export const routes = route({
        home: "/",
      });
      

      Using route() gives you type-safe URL generation across your application, and getLocalizedPath applies the routing mode to it:

      typescript
      import { getLocalizedPath } from "intlayer";
      
      routes.home.href(); // "/"
      getLocalizedPath(routes.home.href(), "fr"); // "/fr"
      getLocalizedPath(routes.home.href(), "en"); // "/" (default locale)
      
    7. Render Localized Pages with JSX

      Remix 3 renders UI with JSX components from remix/ui. A component is a setup function that receives a Handle and returns a render function. Setup runs once per instance, render runs on every update, and props are read through handle.props.

      Start with a shared Document shell that sets the <html lang="..." dir="..."> attributes from the locale resolved by the middleware:

      src/views/document.tsx
      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 useIntlayer and renders a language switcher:

      src/views/home.tsx
      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: class is written as-is (className is also accepted), and re-renders are triggered explicitly with handle.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.
    8. Wire Up the Router and Server

      Add the render() middleware from remix/middleware/render next to the Intlayer middleware. It installs context.render(node, init) on every request, which streams the JSX tree into an HTML Response (prepending <!DOCTYPE html> and setting the Content-Type header):

      src/router.tsx
      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.render accepts an optional ResponseInit as second argument, e.g. context.render(<NotFoundPage />, { status: 404 }). The resolved locale stays reachable from the handler as context.intlayer.locale, for instance to build a Response.json payload.

      Finally, expose the router through a standard fetch handler. The same router runs on Node.js, Bun, Deno, and Cloudflare Workers:

      src/server.ts
      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);
        },
      };
      
    9. Audit and Auto-Fill Translations

      Intlayer provides a CLI to audit for missing translations and automatically fill them using AI:

      bash
      # Audit missing translations
      npx intlayer test
      
      # Fill missing translations using AI
      npx intlayer fill
      

    TypeScript Configuration

    Point JSX at the remix/ui runtime and ensure that your tsconfig.json includes the generated .intlayer types:

    tsconfig.json
    {
      "compilerOptions": {
        "moduleResolution": "Bundler",
        "module": "ESNext",
        "target": "ESNext",
        "jsx": "react-jsx",
        "jsxImportSource": "remix/ui",
        "skipLibCheck": true,
        "strict": true
      },
      "include": ["src/**/*", ".intlayer/**/*.ts"]
    }
    
    jsxImportSource: "remix/ui" is what makes <HomePage /> resolve to Remix's createElement instead 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.