Implement Shopify Hydrogen Remix i18n Translations
By
John Phung

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.ts23import { RemixI18Next } from "remix-i18next/server";4import i18n from "~/lib/i18n"; // your i18n configuration file5import enCommon from "public/locales/en.json"6import zhCommon from "public/locales/zh.json"78let i18next = new RemixI18Next({9 detection: {10 supportedLanguages: i18n.supportedLngs,11 fallbackLanguage: i18n.fallbackLng,12 },13 // This is the configuration for i18next used14 // when translating messages server-side only15 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 system24 // Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here25 plugins: [],26});2728export 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.tsx2import {I18nextProvider, initReactI18next} from 'react-i18next';3import type {EntryContext, AppLoadContext} from '@shopify/remix-oxygen';45import enCommon from 'public/locales/en.json';678export default async function handleRequest(9 request: Request,10 responseStatusCode: number,11 responseHeaders: Headers,12 remixContext: EntryContext,13 context: AppLoadContext,14) {1516 // the default logic already existing1718 // logic to add i18next1920 let instance = createInstance();21 let lng = localeFromUrl; // lang extracted from helper function22 let ns = i18next.getRouteNamespaces(remixContext);2324 await instance25 .use(initReactI18next) // Tell our instance to use react-i18next2627 .init({28 // This is the list of languages your application supports29 supportedLngs: ["en", "zh"],30 // This is the language you want to use in case31 // if the user language is not in the supportedLngs32 fallbackLng: "en",33 // The default namespace of i18next is "translation", but you can customize it here34 defaultNS: "common",35 lng, // The locale we detected above36 ns, // The namespaces the routes about to render wants to use37 resources: {38 en: {common: enCommon},39 //zh: {common: zhCommon}, // add other languages as needed40 },41 });4243 // .. other logic44 // wrap NonceProvider with I18NextProvider45 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-console56 console.error(error);57 responseStatusCode = 500;58 },59 },60 );61}
Now modify the entry.client.tsx.
1// entry.client.tsx23import {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';1112if (!window.location.origin.includes('webcache.googleusercontent.com')) {13 i18next14 .use(initReactI18next) // Tell i18next to use the react-i18next plugin15 .init({16 supportedLngs: ["en", "zh"],17 // This is the language you want to use in case18 // if the user language is not in the supportedLngs19 fallbackLng: "en",20 // The default namespace of i18next is "translation", but you can customize it here21 defaultNS: "common",22 // This function detects the namespaces your routes rendered while SSR use23 ns: getInitialNamespaces(),24 // backend: {loadPath: '../locales/{{lng}}/{{ns}}.json'},2526 resources: {27 en: {common: enCommon},28 zh: {common: zhCommon},29 },30 detection: {31 // Here only enable htmlTag detection, we'll detect the language only32 // server-side with remix-i18next, by using the `` attribute33 // we can communicate to the client the language detected server-side34 order: ['htmlTag'],35 // Because we only use htmlTag, there's no reason to cache the language36 // on the browser, so we disable it37 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';23const Index = () => {4 const {t} = useTranslation();56 return (7 <div>8 <h1>{t('welcome')}</h1>9 </div>10 )1112}1314export 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 }1112 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.