DEV Community

Cover image for Using ParaglideJS with Remix

Using ParaglideJS with Remix

Using ParaglideJS with Remix

In this guide you will lean how to add internationalised routing to your Remix App. We will use ParaglideJS to manage our messages and the Remix-paraglideJS for internationalised routing.

Paraglide is a great fit for the Remix App because it uses a compiler to generate tree-shakeable messages. That way your client bundle only includes the messages that are used in client components on any given page.

This guide assumes that you have a Remix project set up. If you don't, you can follow the Remix Getting Started Guide.

Installing dependencies

The recommended way to install ParaglideJS is via the cli. This will create any files that are required for Paraglide.js to work and install the required dependencies.

In your project root, run the following commands and follow the instructions.

npx @inlang/paraglide-js init
npm i --save remix-paraglidejs @inlang/paraglide-js-adapter-vite
Enter fullscreen mode Exit fullscreen mode

This will have done a few things:

  • Created an inlang project in your project root
  • Added the required devDependencies to your package.json
  • Installed the remix-paraglidejs

Setting Up

In your vite.config.ts, import the @inlang/paraglide-js-adapter-vite and apply it to your config.

You will need to pass it some config. The location of the Inlang Project & the output directory that messages should be compiled to. You should stick with the defaults unless you have a reason not to.

import { vitePlugin as remix } from "@remix-run/dev";
import { installGlobals } from "@remix-run/node";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
/* --- INCLUDE THIS --- */
import { paraglide } from "@inlang/paraglide-js-adapter-vite";
/* ----------------- */

installGlobals();

export default defineConfig({
  plugins: [
    remix(),
    tsconfigPaths(),
    /* --- INCLUDE THIS --- */
    paraglide({
      project: "./project.inlang", //Path to your inlang project 
      outdir: "./paraglide", //Where you want the generated files to be placed
    }),
    /* ----------------- */
  ],
});
Enter fullscreen mode Exit fullscreen mode

This Plugin will make sure to automatically recompile messages whenever they change.

Next, modify the Remix entry files entry.client.tsx and entry.server.tsx. If you don't see these files, use npx remix reveal.

// entry.client.tsx
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
/* --- INCLUDE THIS --- */
import { hydrateLang } from 'remix-paraglidejs/client';
import { availableLanguageTags, setLanguageTag } from "<YOUR_PARAGLIDE_DIR>/runtime";
/* ----------------- */

startTransition(() => {
  /* --- INCLUDE THIS --- */
  const lang = hydrateLang('language-tag', availableLanguageTags);
  setLanguageTag(lang);
  /* ----------------- */
  hydrateRoot(
    document,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>
  );
});
Enter fullscreen mode Exit fullscreen mode
// entry.server.tsx
import { PassThrough } from "node:stream";

import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import {
  createReadableStreamFromReadable,
/* --- INCLUDE THIS --- */
  createCookie,
} from "@remix-run/node";
import { setLangServerCookie, getContextLang } from 'remix-paraglidejs/server';
import { setLanguageTag, availableLanguageTags } from "<YOUR_PARAGLIDE_DIR>/runtime";

// language-tag value the same as the one in the entry.client.tsx
export const setLangCookie = createCookie("language-tag", {});
/* ----------------- */

const ABORT_DELAY = 5_000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
) {
  return isbot(request.headers.get("user-agent") || "")
    ? handleBotRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      )
    : handleBrowserRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      );
}

function handleBotRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    let shellRendered = false;
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
      {
        onAllReady() {
          shellRendered = true;
          const body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          responseStatusCode = 500;
          if (shellRendered) {
            console.error(error);
          }
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    let shellRendered = false;
    /* --- INCLUDE THIS --- */
    const lang = getContextLang(remixContext, {
      defaultValue: availableLanguageTags[0],
      availableLanguages: availableLanguageTags,
      // The URL parameter to look for when determining the language
      // for example ($lang)._index.tsx
      urlParam: 'lang',
    });
    setLanguageTag(lang);
    /* ----------------- */
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
      {
        async onShellReady() {
          shellRendered = true;
          const body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);

          responseHeaders.set("Content-Type", "text/html");
          /* --- INCLUDE THIS --- */
          await setLangServerCookie(lang, responseHeaders, setLangCookie);
          /* ----------------- */

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          responseStatusCode = 500;
          if (shellRendered) {
            console.error(error);
          }
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}
Enter fullscreen mode Exit fullscreen mode

Our first Message

Let's create a messages folder in our project root. This is where we will store our messages. Then add an en.json file to it.

// messages/en.json
{
    "$schema": "https://inlang.com/schema/inlang-message-format",
    "title": "Remix web example",
}
Enter fullscreen mode Exit fullscreen mode

If you now run start your dev server you should see a new folder appear in your src folder called paraglide. This folder contains the compiled messages and any runtime code required by paraglide. Your messages live at ./src/paraglide/messages.js, you will be importing from there.

Let's use the title message on our homepage. Create a route ($lang)._index.tsx, import all messages from paraglide/messages.js and use the title message in the h1 tag.

import { Link } from '@remix-run/react';
import * as m from '<YOUR_PARAGLIDE_DIR>/messages';

export default function Index() {
  return (
    <div>
      <h1>{m.title()}</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

You should now see the message "Remix web example" on your homepage!

Note: If you are using Visual Studio Code, you should install Sherlock (VS Code extension). It will give you inline previews of messages and allow you to edit them right in your source code.

Adding more languages

You can add more languages to your project by adding them to the languageTags array in your Inlang project settings. Let's add spanish to our project.

// project.inlang/settings.json
{
    "$schema": "https://inlang.com/schema/project-settings",
    "sourceLanguageTag": "en",
    "languageTags": ["en", "es"], // <-- Added Spanish
    "...": "..."
}
Enter fullscreen mode Exit fullscreen mode

Then add a messages file for the new language.

// messages/es.json
{
    "$schema": "https://inlang.com/schema/inlang-message-format",
    "title": "Remix web de ejemplo",
}
Enter fullscreen mode Exit fullscreen mode

If you now run the dev server, and visit /en and /es, you should see the Server Components switch languages.

Navigating

You can use Remix <Link> to navigate between /en and /es.

import { Link } from '@remix-run/react';
import * as m from '<YOUR_PARAGLIDE_DIR>/messages';

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <h1>{m.title()}</h1>
      <ul>
        <li>
          <Link
            to="/en"
            reloadDocument
          >
            EN
          </Link>
        </li>
        <li>
          <Link
            to="/es"
            reloadDocument
          >
            ES
          </Link>
        </li>
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

If you don't want to reload the document please check out our dynamic example here.

Finally, you should now have a fully functional multilingual Remix app using ParaglideJS.

You can check out the full source code of this example here.

If you want to learn more about ParaglideJS, check out the ParaglideJS Documentation.

Top comments (0)