Tutorials

How to Add i18n to Shopify Hydrogen (Complete Guide)

Ali Osman Delismen
Ali Osman Delismen
·12 min read
Share
How to Add i18n to Shopify Hydrogen (Complete Guide)

Shopify Hydrogen is the modern way to build custom storefronts — but adding internationalization (i18n) can be challenging. You need locale-aware routing, CDN-delivered translations, and seamless integration with Shopify's Storefront API for localized product data.

Better i18n solves this with a single package: @better-i18n/remix. It handles UI translations via CDN while Shopify handles product content — giving you the best of both worlds.

Free to get started — no credit card required. Set up your Hydrogen store in minutes.


Why Hydrogen Needs a Dedicated i18n Solution

Shopify Hydrogen runs on Cloudflare Workers with React Router v7. This creates unique constraints:

  • remix-i18next**** doesn't work — it requires React Router v7 middleware, which Hydrogen's Worker fetch() handler doesn't support
  • Shopify Storefront API handles product data but not UI strings (buttons, navigation, forms, error messages)
  • Edge runtime means no filesystem access — translations must come from a CDN or be bundled

Better i18n bridges this gap with a CDN-first architecture that's built for edge runtimes.

Blog post image

Architecture: Dual-Source Localization

URL: /tr/products/hat
  ├─ Better i18n CDN → Turkish UI translations (buttons, nav, forms)
  └─ Shopify Storefront API @inContext(language: TR) → Turkish product data

This architecture means:

  • UI strings (navigation, buttons, footer, error messages) → managed in Better i18n dashboard, delivered via CDN

  • Product data (titles, descriptions, prices, variants) → managed in Shopify, delivered via Storefront API with @inContext directive

  • URL locale drives both systems simultaneously

    Blog post image

Step 1: Install the Package

npm install @better-i18n/remix i18next react-i18next

The @better-i18n/remix package provides:

  • createRemixI18n() — server-side i18n singleton
  • getLocaleFromRequest() — locale detection from URL segments
  • CDN-powered translation loading with built-in caching
  • React components (LocaleDropdown, LanguageSwitcher)

Step 2: Create Your Better i18n Project

  1. Go to better-i18n.com and create a free account
  2. Create a new project (e.g., my-org/hydrogen-store)
  3. Add your target languages
  4. Add translation keys for your UI strings

You can organize keys by namespace — for example:

  • common — shared strings (navigation, footer, buttons)
  • home — homepage-specific copy
  • products — product page labels
  • cart — cart and checkout strings

Step 3: Create the i18n Singleton

Create app/i18n.server.ts:

import { createRemixI18n } from "@better-i18n/remix";

export const i18n = createRemixI18n({
  project: "my-org/hydrogen-store",
  defaultLocale: "en",
});

This singleton connects to the Better i18n CDN and handles:

  • Translation fetching with TTL caching
  • Locale validation against your project's language list
  • URL prefix management

Step 4: Wire Up the Server Entry

In your server.ts, detect the locale and pass it to the Hydrogen context:

import { i18n } from "./app/i18n.server";

// Inside your fetch handler:
const locale = await i18n.getLocaleFromRequest(request);
const isDefaultLocale = locale === "en";
const languages = await i18n.getLanguages();

// Derive Shopify locale from Better i18n locale
const shopifyI18n = deriveShopifyLocale(locale, isDefaultLocale);

// Pass to createStorefrontClient
const { storefront } = createStorefrontClient({
  i18n: shopifyI18n,
  // ... other config
});

The deriveShopifyLocale helper maps Better i18n locale codes to Shopify's LanguageCode and CountryCode:

function deriveShopifyLocale(locale: string, isDefault: boolean) {
  const parts = locale.split("-");
  const language = parts[0].toUpperCase(); // "tr" → "TR"
  const country = parts[1]?.toUpperCase() ?? language; // "en-gb" → "GB"

  return {
    language,
    country: isDefault ? "US" : country,
    pathPrefix: isDefault ? "" : `/${locale}`,
  };
}

Step 5: Set Up Locale Routes

All your routes should use the ($locale) optional segment pattern:

app/routes/
  ($locale)._index.tsx          → /  and  /tr/
  ($locale).products._index.tsx → /products  and  /tr/products
  ($locale).products.$handle.tsx → /products/hat  and  /tr/products/hat
  ($locale).cart.tsx             → /cart  and  /tr/cart

This gives you clean URLs:

  • Default locale: /products/cozy-hat (no prefix)
  • Other locales: /tr/products/cozy-hat, /fr/products/cozy-hat

Step 6: Load Translations in Route Loaders

Each route loader fetches both UI translations and Shopify data in parallel:

export async function loader({ params, context }: LoaderFunctionArgs) {
  const locale = params.locale ?? "en";

  const [messages, { products }] = await Promise.all([
    i18n.getMessages(locale),
    context.storefront.query(PRODUCTS_QUERY),
  ]);

  return { locale, messages, products };
}

Step 7: Set Up the Root Provider

In app/root.tsx, create a per-request i18next instance:

import { I18nextProvider } from "react-i18next";
import i18next from "i18next";

function createI18nextInstance(locale: string, messages: Record<string, any>) {
  const instance = i18next.createInstance();
  instance.init({
    lng: locale,
    resources: { [locale]: messages },
    interpolation: { escapeValue: false },
    initImmediate: false, // Critical for SSR
  });
  return instance;
}

export default function App() {
  const { locale, messages } = useLoaderData<typeof loader>();
  const i18nInstance = useMemo(
    () => createI18nextInstance(locale, messages),
    [locale, messages]
  );

  return (
    <I18nextProvider i18n={i18nInstance}>
      <Outlet />
    </I18nextProvider>
  );
}

Important: initImmediate: false is critical — it ensures synchronous initialization for server-side rendering. Without this, translations won't be available during the first render.

Step 8: Use Translations in Components

import { useTranslation } from "react-i18next";

function HeroSection({ products }) {
  const { t: tc } = useTranslation("common");
  const { t: th } = useTranslation("home");

  return (
    <section>
      <h1>{th("hero_title")}</h1>
      <p>{th("hero_subtitle")}</p>
      <a href="/products">{tc("shop_now")}</a>

      {/* Product titles come from Shopify — already localized */}
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </section>
  );
}

Step 9: SEO Meta Tags with msg()

Hydrogen's meta() functions run outside React — you can't use useTranslation(). Use the msg() helper instead:

import { msg } from "@better-i18n/remix";

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: msg(data?.messages?.common, "meta_title", "My Store") },
    {
      name: "description",
      content: msg(data?.messages?.common, "meta_description", "Shop our products"),
    },
  ];
};

Step 10: Add a Language Switcher

Better i18n provides two built-in components:

// Option 1: Styled dropdown with flags and keyboard navigation
import { LocaleDropdown } from "@better-i18n/remix/react";

<LocaleDropdown
  locale={locale}
  languages={languages}
  currentPath={location.pathname}
/>

// Option 2: Simple select element
import { LanguageSwitcher } from "@better-i18n/remix/react";

<LanguageSwitcher
  locale={locale}
  languages={languages}
  currentPath={location.pathname}
/>

Blog post image

Content Security Policy (CSP)

Hydrogen uses CSP by default. Add the Better i18n CDN to your allowed sources in entry.server.tsx:

const { nonce, header, NonceProvider } = createContentSecurityPolicy({
  connectSrc: [
    "'self'",
    "cdn.better-i18n.com", // Better i18n CDN
  ],
});

AI-Powered Translation Workflow

Once your keys are in Better i18n, translating is simple:

  1. Add keys in your source language (English)
  2. Click "Translate with AI" — Better i18n uses context-aware AI that understands your glossary, brand voice, and product domain
  3. Review and approve translations
  4. Publish — translations are instantly available via CDN, no rebuild needed

<!-- IMAGE_PLACEHOLDER: ai-translation — Screenshot of AI translation in action, showing the translation editor with source English text and AI-generated translations in multiple languages -->

Translation Features

FeatureDescription
AI TranslationContext-aware translation with glossary and brand voice support
CDN DeliveryGlobal edge delivery with ~50ms latency worldwide
Live UpdatesPublish translations without rebuilding or redeploying
GitHub SyncTwo-way sync with your JSON translation files
NamespacesOrganize translations by page or feature
MCP ServerManage translations from AI coding agents (Claude, Cursor, etc.)
CLIScan your codebase for missing keys with npx @better-i18n/cli scan

Common Patterns

Localized Sitemap

Generate a sitemap with hreflang tags for each locale:

const locales = await i18n.getLocales();

const urls = products.flatMap(product =>
  locales.map(locale => ({
    url: `/${locale}/products/${product.handle}`,
    hreflang: locale,
    alternates: locales.map(alt => ({
      href: `/${alt}/products/${product.handle}`,
      hreflang: alt,
    })),
  }))
);

Currency + Language Together

Map locales to both language and currency for a complete localization experience:

const localeConfig = {
  en: { language: "EN", country: "US", currency: "USD" },
  tr: { language: "TR", country: "TR", currency: "TRY" },
  de: { language: "DE", country: "DE", currency: "EUR" },
  ja: { language: "JA", country: "JP", currency: "JPY" },
};

Locale-Aware Cart

Ensure the cart respects the current locale:

const cart = createCartHandler({
  storefront,
  countryCode: shopifyI18n.country,
  languageCode: shopifyI18n.language,
});

Why Better i18n vs. Alternatives

Better i18nDIY with i18nextCrowdinremix-i18next
Hydrogen compatibleYesYes (manual setup)No (no CDN SDK)No (middleware required)
Edge runtimeYes, CDN-firstBundle translationsNoNo
AI translationBuilt-inNoYes (extra cost)No
Live updatesNo rebuildRequires deployPartialNo
MCP for AI agentsYesNoNoNo
Free tierGenerousN/APaid onlyN/A
Setup time~15 minHoursHoursN/A

Live Example

Check out our open-source Hydrogen demo store that uses this exact setup:

  • Demo store: Built with Hydrogen 2026.1, React 19, Cloudflare Workers
  • 10+ languages configured with AI translation
  • Dual-source pattern: Better i18n CDN + Shopify Storefront API
  • Source code: Available in the @better-i18n/remix repository

Blog post image

Getting Started

Ready to add i18n to your Hydrogen store?

  1. Sign up for free — no credit card required
  2. Install @better-i18n/remix in your Hydrogen project
  3. Follow this guide to wire up locale routing + translations
  4. Translate with AI — add your keys and let AI handle the translations
  5. Publish — your store is now multilingual

Need help? Check our developer documentation or reach out through the dashboard chat.


This guide is based on Shopify Hydrogen 2026.1+ with @better-i18n/remix v0.5+. The setup works with any Hydrogen version that uses React Router v7.