🧑‍💻 JP

Implement Shopify Hydrogen Remix i18n Translations

By

John Phung

shopify hydrogen remix i18n translation

The implementation of translations for a Shopify Hydrogen ecommerce storefront is little troublesome due Vite and Oxygen hosting. I followed a few guides out there and had to make slight tweaks to get it working, based on my locale configuration.

My setup is based on using the Route/URL to determine the language to load for example:

I'm only appending the language to the end of the pathname instead of using for example "en-us", although you can do so if you wish. As you can see I'm ignoring the country/region since I wanted multiple languages for one country.

I have created a helper function to determine the lang based on the route, which you need to in order to pass to the i18n config. You will need to create your own helper function to extract the lang from the route.

Packages to install

  • react-i18next
  • remix-i18next/server

We need to create your translation string as a JSON file. What I did was create it in the public folder so we can load them as CDN assets when we deploy it instead of bundling it with the app.

So create /public/locales/en.json file

For example the en.json may look like this

1{
2 "welcome": "hello world"
3}

We are going to create a i18next.server.ts file.

1// i18next.server.ts
2
3import { RemixI18Next } from "remix-i18next/server";
4import i18n from "~/lib/i18n"; // your i18n configuration file
5import enCommon from "public/locales/en.json"
6import zhCommon from "public/locales/zh.json"
7
8let i18next = new RemixI18Next({
9 detection: {
10 supportedLanguages: i18n.supportedLngs,
11 fallbackLanguage: i18n.fallbackLng,
12 },
13 // This is the configuration for i18next used
14 // when translating messages server-side only
15 i18next: {
16 ...i18n,
17 resources: {
18 "en": enCommon,
19 "zh": zhCommon,
20 }
21 },
22 // The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
23 // E.g. The Backend plugin for loading translations from the file system
24 // Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
25 plugins: [],
26});
27
28export default i18next;

Now modify entry.server.tsx to load the i18n config. This step is not necessary as our entry.client.tsx will also load the translations but I found it perform better when switching languages.

1// entry.server.tsx
2import {I18nextProvider, initReactI18next} from 'react-i18next';
3import type {EntryContext, AppLoadContext} from '@shopify/remix-oxygen';
4
5import enCommon from 'public/locales/en.json';
6
7
8export default async function handleRequest(
9 request: Request,
10 responseStatusCode: number,
11 responseHeaders: Headers,
12 remixContext: EntryContext,
13 context: AppLoadContext,
14) {
15
16 // the default logic already existing
17
18 // logic to add i18next
19
20 let instance = createInstance();
21 let lng = localeFromUrl; // lang extracted from helper function
22 let ns = i18next.getRouteNamespaces(remixContext);
23
24 await instance
25 .use(initReactI18next) // Tell our instance to use react-i18next
26
27 .init({
28 // This is the list of languages your application supports
29 supportedLngs: ["en", "zh"],
30 // This is the language you want to use in case
31 // if the user language is not in the supportedLngs
32 fallbackLng: "en",
33 // The default namespace of i18next is "translation", but you can customize it here
34 defaultNS: "common",
35 lng, // The locale we detected above
36 ns, // The namespaces the routes about to render wants to use
37 resources: {
38 en: {common: enCommon},
39 //zh: {common: zhCommon}, // add other languages as needed
40 },
41 });
42
43 // .. other logic
44 // wrap NonceProvider with I18NextProvider
45 const body = await renderToReadableStream(
46 <I18nextProvider i18n={instance}>
47 <NonceProvider>
48 <RemixServer context={remixContext} url={request.url} />
49 </NonceProvider>
50 </I18nextProvider>,
51 {
52 nonce,
53 signal: request.signal,
54 onError(error) {
55 // eslint-disable-next-line no-console
56 console.error(error);
57 responseStatusCode = 500;
58 },
59 },
60 );
61}

Now modify the entry.client.tsx.


1// entry.client.tsx
2
3import {RemixBrowser} from '@remix-run/react';
4import {startTransition, StrictMode} from 'react';
5import {hydrateRoot} from 'react-dom/client';
6import i18next from 'i18next';
7import {I18nextProvider, initReactI18next} from 'react-i18next';
8import {getInitialNamespaces} from 'remix-i18next/client';
9import enCommon from 'public/locales/en.json';
10import zhCommon from 'public/locales/zh.json';
11
12if (!window.location.origin.includes('webcache.googleusercontent.com')) {
13 i18next
14 .use(initReactI18next) // Tell i18next to use the react-i18next plugin
15 .init({
16 supportedLngs: ["en", "zh"],
17 // This is the language you want to use in case
18 // if the user language is not in the supportedLngs
19 fallbackLng: "en",
20 // The default namespace of i18next is "translation", but you can customize it here
21 defaultNS: "common",
22 // This function detects the namespaces your routes rendered while SSR use
23 ns: getInitialNamespaces(),
24 // backend: {loadPath: '../locales/{{lng}}/{{ns}}.json'},
25
26 resources: {
27 en: {common: enCommon},
28 zh: {common: zhCommon},
29 },
30 detection: {
31 // Here only enable htmlTag detection, we'll detect the language only
32 // server-side with remix-i18next, by using the `` attribute
33 // we can communicate to the client the language detected server-side
34 order: ['htmlTag'],
35 // Because we only use htmlTag, there's no reason to cache the language
36 // on the browser, so we disable it
37 caches: [],
38 },
39 })
40 .then(() => {
41 startTransition(() => {
42 hydrateRoot(
43 document,
44 <I18nextProvider i18n={i18next}>
45 <StrictMode>
46 <RemixBrowser />
47 </StrictMode>
48 </I18nextProvider>,
49 );
50 });
51 });
52}
53

To test if this implementation works, you just need to manually change the ending of the URL to /en, /zh or whatever language you decided to support. Oh, and I forgot, you should the useTranslation() hook from "react-i18next" to load the correct translation string.

1import {useTranslation} from 'react-i18next';
2
3const Index = () => {
4 const {t} = useTranslation();
5
6 return (
7 <div>
8 <h1>{t('welcome')}</h1>
9 </div>
10 )
11
12}
13
14export default Index;

You will need to create a language selector which can help you change the URL.

I've found that useNavigate from remix to push a append a new URL with the lang at the end did not update the server to load the new translations.

I haven't tried this solution yet to cause revalidation when params is updated, but probably it will work without force refresh.

1export function shouldRevalidate({
2 currentParams,
3 nextParams,
4 defaultShouldRevalidate,
5}) {
6 const currentId = currentParams.slug.split("--")[1];
7 const nextId = nextParams.slug.split("--")[1];
8 if (currentId === nextId) {
9 return false;
10 }
11
12 return defaultShouldRevalidate;
13}

For now, I just triggered a force refresh whenever they select a new language with window.location.href = "new-url-with-lang-appended", which causes the server to fetch the correct translation file.