Locale detection with Remix

January 18, 2022 ・ 4 min read

Lately, I’ve been fooling around with remix.run and server rendering apps instead of client-side only stuff. However, whenever I use things like toLocaleString from the Date API, we get shifting date formats on the frontend, and Text content did not match errors from React. This warning often happens in either Remix or Next.js, or any SSR (server-side rendering) React-based solution. Let’s see why it happens and how we can fix it.

Why does this happen when using SSR?

This warning is common for devs getting started with SSR and using Javascript APIs that access the locale, like toLocaleString on the Date API. Often, the locale of the Node.js runtime on the server will not match your browser’s locale, so when your app server renders an HTML page, it will use the default locale of that particular server, usually en-us. When your browser re-hydrates the page, it will use your default locale. If it doesn’t match the en-us locale, your toLocaleString call will most likely be different from the server, and you will get that warning, and you may sometimes even notice the date flashing back from one format to another.

How can we fix it then?

Well, the quick approach is to hardcode en-us everywhere. Most locale-based APIs accept a locale or an array of locales as an argument. If your website or app only is available in one language, it might even be a good idea just to show it with a single locale to avoid UI confusion. (...).toLocaleString("en-us") would fix it by using that locale always on the server and the client.

However, most SSR frameworks can access the headers of an HTTP request, which means you can access the Accept-Language HTTP header! You can check out in the MDN docs what the header is and its format. Essentially it tells servers the list of languages the user accepts.

Why a list, though? Because most browsers allow you to set a list of preferred languages ordered by your preference. Take a look at mine (you can access the page via chrome://settings/languages).

My list of preferred languages: UK English, US English, English, Portuguese

My solution is simple. We get the header value from the request, then pass it into a Provider. Then we will create a hook to get the locale value whenever we need it. We will only extract the user’s first language (the favorite one).

We will use the accept-language-parser package to parse the header value. Let’s install it

# or npm if you are into that
yarn add accept-language-parser

# if you are using typescript, this might be useful
yarn add --dev @types/accept-language-parser

Now, let’s create our provider/context/hook combo. I’ll put it under app/hooks/useLocale.tsx. Our default export is the hook, but we also export the provider to use it on the root file of remix.

app/hooks/useLocale.tsx
import { createContext, ReactNode, useContext } from "react";

const LocaleContext = createContext<string>("");

export function LocaleProvider({
  children,
  locale,
}: {
  children: ReactNode;
  locale: string;
}) {
  return (
    <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
  );
}

export default function useLocale() {
  return useContext(LocaleContext);
}

Then create a new loader in your root file. Mine is in the app/root.tsx file. If you already have a loader, augment it with this one.

app/root.tsx
import type { LoaderFunction } from "remix";
import acceptLanguage from "accept-language-parser";

export const loader: LoaderFunction = async ({ request }) => {
  const languages = acceptLanguage.parse(
    request.headers.get("Accept-Language") as string
  );

  // If somehow the header is empty, return a default locale
  if (languages?.length < 1) return "en-us";

  // If there is no region for this locale, just return the code
  if (!languages[0].region) return languages[0].code;

  return `${languages[0].code}-${languages[0].region.toLowerCase()}`;
};

Then, wrap it with our provider in your default export of the root file.

app/root.tsx
import { useLoaderData } from "remix";
import { LocaleProvider } from "~/hooks/useLocale";

export default function App() {
  const locale = useLoaderData();

  return (
    <LocaleProvider locale={locale}>
      {/* Whatever your already had here */}
    </LocaleProvider>
  );
}

And that’s it. Any component on your remix routes can now use useLocale to get the current locale! An example:

app/routes/index.tsx
import useLocale from "~/hooks/useLocale";

export default function HomePage() {
  const locale = useLocale();

  return (
    <div>
      {1_000_000.toLocaleString(locale)}
    </div>
  );
}

For Next.js the solution is very similar! Just play around with this concept using getInitialProps on the _app.js component.

I hope this solution is of use to you! You can see it in action on my remix starter repo with prisma support and auth via cookies.

Stay safe!

Want to talk about this? Feel free to reach me on the web: