How to Add Multiple Languages in Shopify Hydrogen

Sat Dec 23 2023 - Jaxson Luu

How to Add Multiple Languages in Shopify Hydrogen

Table of content

  • Step 1: Installation
  • Step 2: i18next configuration
  • Step 3: Client-side configuration
  • Step 4: Server-side configuration
  • Step 5: Update CountrySelector component
  • Step 6: Modify app/root.tsx file
  • Step 7: Usage

This article will show you the way to add multiple languages in Shopify Hydrogen. We'll use the remix-i18next package to implement i18n in the Shopify Hydrogen and add German language for customer login page as an example.

Follow the steps below to implement multiple languages in Shopify Hydrogen:

Step 1: Installation

Open terminal from root folder of Shopify Hydrogen then run the command below:

npm install remix-i18next i18next react-i18next i18next-browser-languagedetector

You will need to configure an i18next backend and language detector, in that case you can install them too, for the rest of the setup guide we'll use the http and fs backends:

npm install i18next-http-backend i18next-fs-backend

Step 2: i18next configuration

First let's create two translation files (or more) in public/locales folder:

public/locales/en/common.json

{
  "account": {
    "login": "Sign in",
    "create": "Create an account",
    "forgot": "Forgot password"
  },
  "fields": {
    "email": "Email address",
    "password": "Password"
  }
}

public/locales/de/common.json

{
  "account": {
    "login": "Anmelden",
    "create": "Ein Konto erstellen",
    "forgot": "Passwort vergessen"
  },
  "fields": {
    "email": "E-Mail-Adresse",
    "password": "Passwort"
  }
}

Next, set your i18next configuration.

These two files can go somewhere in your app folder. For this example, we will create app/i18n.ts:

export default {
 supportedLngs: ['en', 'de'],
 fallbackLng: 'en',
 defaultNS: 'common',
 react: {useSuspense: false},
};

And then create app/i18next.server.ts with the following code:

import {RemixI18Next} from 'remix-i18next';
import {createCookie} from '@shopify/remix-oxygen';

import i18n from '~/i18n';

import enCommon from '../public/locales/en/common.json';
import deCommon from '../public/locales/de/common.json';

export const localeCookie = createCookie('locale', {
 path: '/',
 httpOnly: true,
});
const i18next = new RemixI18Next({
 detection: {
   supportedLanguages: i18n.supportedLngs,
   fallbackLanguage: i18n.fallbackLng,
   cookie: localeCookie,
 },
 // This is the configuration for i18next used
 // when translating messages server-side only
 i18next: {
   ...i18n,
   supportedLngs: ['de', 'en'],
   resources: {
     de: {common: deCommon},
     en: {common: enCommon},
   },
 },
 // The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
 // E.g. The Backend plugin for loading translations from the file system
 // Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
});

export default i18next;

Step 3: Client-side configuration

Now in your app/entry.client.tsx replace the default code with this:

import {RemixBrowser} from '@remix-run/react';
import {startTransition, StrictMode} from 'react';
import {hydrateRoot} from 'react-dom/client';
import i18next from 'i18next';
import {I18nextProvider, initReactI18next} from 'react-i18next';
import Backend from 'i18next-http-backend';
import {getInitialNamespaces} from 'remix-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import i18n from '~/i18n';

import deCommon from '../public/locales/de/common.json';
import enCommon from '../public/locales/en/common.json';

async function hydrate() {
 await i18next
   .use(initReactI18next) // Tell i18next to use the react-i18next plugin
   .use(LanguageDetector) // Setup a client-side language detector
   .use(Backend) // Setup your backend
   .init({
     ...i18n, // spread the configuration
     // This function detects the namespaces your routes rendered while SSR use
     ns: getInitialNamespaces(),
     // backend: {loadPath: '../locales/{{lng}}/{{ns}}.json'},
     resources: {
       en: {common: enCommon},
       de: {common: deCommon},
     },
     detection: {
       // Here only enable htmlTag detection, we'll detect the language only
       // server-side with remix-i18next, by using the `` attribute
       // we can communicate to the client the language detected server-side
       order: ['htmlTag'],
       // Because we only use htmlTag, there's no reason to cache the language
       // on the browser, so we disable it
       caches: [],
     },
   });

 startTransition(() => {
   hydrateRoot(
     document,
     <I18nextProvider i18n={i18next}>
       <StrictMode>
         <RemixBrowser />
       </StrictMode>
     </I18nextProvider>,
   );
 });
}

if (window.requestIdleCallback) {
 window.requestIdleCallback(hydrate);
} else {
 // Safari doesn't support requestIdleCallback
 // https://caniuse.com/requestidlecallback
 window.setTimeout(hydrate, 1);
}

Step 4: Server-side configuration

And in your app/entry.server.tsx replace the code with this:

import type {EntryContext} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import isbot from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';
import {createInstance} from 'i18next';
import {I18nextProvider, initReactI18next} from 'react-i18next';

import enCommon from '../public/locales/en/common.json';
import deCommon from '../public/locales/de/common.json';

import i18next from './i18next.server';
import i18n from './i18n'; // your i18n configuration file

export default async function handleRequest(
 request: Request,
 responseStatusCode: number,
 responseHeaders: Headers,
 remixContext: EntryContext,
) {
 const instance = createInstance();
 const url = new URL(request.url);
 const lng = url.pathname.startsWith('/de-de') ? 'de' : 'en';
 const ns = i18next.getRouteNamespaces(remixContext);

 await instance
   .use(initReactI18next) // Tell our instance to use react-i18next
   .init({
     ...i18n, // spread the configuration
     lng, // The locale we detected above
     ns, // The namespaces the routes about to render wants to use
     resources: {
       en: {common: enCommon},
       de: {common: deCommon},
     },
   });

 const {nonce, header, NonceProvider} = createContentSecurityPolicy();
 const body = await renderToReadableStream(
   <I18nextProvider i18n={instance}>
     <NonceProvider>
       <RemixServer context={remixContext} url={request.url} />
     </NonceProvider>
   </I18nextProvider>,
   {
     nonce,
     signal: request.signal,
     onError(error) {
       // eslint-disable-next-line no-console
       console.error(error);
       responseStatusCode = 500;
     },
   },
 );

 if (isbot(request.headers.get('user-agent'))) {
   await body.allReady;
 }

 responseHeaders.set('Content-Type', 'text/html');
 responseHeaders.set('Content-Security-Policy', header);
 return new Response(body, {
   headers: responseHeaders,
   status: responseStatusCode,
 });
}

Step 5: Update CountrySelector component

Open app/components/CountrySelector.tsx then modify the code as below:

  • Change:
import {DEFAULT_LOCALE} from '~/lib/utils';

To:

import {DEFAULT_LOCALE, usePrefixPathWithLocale} from '~/lib/utils';
  • In CountrySelector function, add new variable:
const path = usePrefixPathWithLocale('/api/countries');
  • Change:
useEffect(() => {
   if (!inView || fetcher.data || fetcher.state === 'loading') return;
   fetcher.load('/api/countries');
 }, [inView, fetcher]);

To:

useEffect(() => {
   if (!inView || fetcher.data || fetcher.state === 'loading') return;
   fetcher.load(path);
 }, [inView, fetcher, path]);

Step 6: Modify app/root.tsx file

Now, in your app/root.tsx file import two hooks useChangeLanguage and useTranslation:

import {useChangeLanguage} from 'remix-i18next';
import {useTranslation} from 'react-i18next';

Then add new variable named handle:

export const handle = {
// In the handle export, we can add a i18n key with namespaces our route
// will need to load. This key can be a single string or an array of strings.
// TIP: In most cases, you should set this to your defaultNS from your i18n config
// or if you did not set one, set it to the i18next default namespace "translation"
i18n: 'common',
};

In loader function, return new variable:

locale: storefront.i18n.language.toLowerCase(),

The last: modify the code in App function as below:

  • Add i18n variable and useChangeLanguage:
const {i18n} = useTranslation();
useChangeLanguage(data.locale);
  • Change:
<html lang={locale.language}>

To:

<html lang={data.locale} dir={i18n.dir()}>

Step 7: Usage

Finally, in any route you want to translate, you can use the t() function, as per the i18next documentation and use translations from the default namespace. For example, we'll add the translate for login page:

Open app/routes/($locale).account.login.tsx add import useTranslation and i18next:

import {useTranslation} from 'react-i18next';
import i18next from '~/i18next.server';

In Login function, add t() function:

const {t} = useTranslation();

From now, you can add translation for any text in the Login page. Ex: translate “Sign in.” text:

<h1 className="text-4xl">{t('account.login')}</h1>

Check out the GitHub repo below if you can't add multiple languages in Shopify Hydrogen by following the steps above:

Or check out video tutorial below:

Hopefully, this article provided you with new knowledge and saved your time to implement the multiple languages in Shopify Hydrogen. Building a localized headless store with Shopify Hydrogen has never been easier!

Thanks for reading! We hope you found this article helpful!