Shopify Hydrogen
इस पेज पर
Shopify Hydrogen is a React-based framework for building custom Shopify storefronts. @better-i18n/remix provides first-class support for Hydrogen with Storefront API locale bridging.
How It Works #
Better i18n handles your UI translations (button labels, headings, static text), while Shopify's Storefront API handles product content (titles, descriptions, prices). The locale detected from the URL drives both systems:
URL: /tr/products/hat
└─> Better i18n: loads Turkish UI translations from CDN
└─> Storefront API: queries product data in Turkish (language: TR)npm install @better-i18n/remix i18next react-i18nextCreate the i18n singleton #
import { createRemixI18n } from "@better-i18n/remix";
export const i18n = createRemixI18n({
projectId: "my-company/hydrogen-store", // [!code highlight]
defaultLocale: "en", // [!code highlight]
});Define locale mapping #
getLocaleFromRequest derives Shopify LanguageCode and CountryCode enums automatically from the CDN language list — no static map needed:
import type {
LanguageCode,
CountryCode,
} from "@shopify/hydrogen/storefront-api-types";
import type { LanguageOption } from "@better-i18n/remix";
export interface I18nLocale {
language: LanguageCode;
country: CountryCode;
pathPrefix: string;
}
/**
* Derives Shopify Storefront API locale enums from a Better i18n locale code.
* Better i18n uses lowercase ("en", "tr"), Shopify uses uppercase enums (EN, TR).
*
* Supports both simple ("tr") and compound ("en-gb") locale formats.
* "en" without a region defaults to country "US" (most common Shopify use case).
*/
function deriveShopifyLocale(code: string, isDefault: boolean): I18nLocale {
const [lang, country] = code.toLowerCase().split("-");
return {
language: lang.toUpperCase() as LanguageCode,
country: (country ?? (lang === "en" ? "us" : lang)).toUpperCase() as CountryCode,
pathPrefix: isDefault ? "" : `/${code}`,
};
}
/**
* Extract locale from URL path prefix, validated against CDN language list.
*
* /tr/products/hat → "tr"
* /products/hat → defaultLocale
*/
export function getLocaleFromRequest(
request: Request,
languages: LanguageOption[],
defaultLocale = "en",
): { locale: string; i18n: I18nLocale } {
const url = new URL(request.url);
const firstSegment = url.pathname.split("/")[1]?.toLowerCase();
if (firstSegment && firstSegment !== defaultLocale && languages.some((l) => l.code === firstSegment)) {
return {
locale: firstSegment,
i18n: deriveShopifyLocale(firstSegment, false),
};
}
return { locale: defaultLocale, i18n: deriveShopifyLocale(defaultLocale, true) };
}Configure server.ts #
Wire everything together in your Hydrogen server entry:
import { createStorefrontClient } from "@shopify/hydrogen";
import { createRequestHandler } from "@shopify/remix-oxygen";
import { getLocaleFromRequest } from "~/lib/i18n";
import { i18n } from "~/i18n.server";
export default {
async fetch(
request: Request,
env: Env,
executionContext: ExecutionContext,
): Promise<Response> {
const cache = await caches.open("hydrogen");
// 1. Fetch CDN language list (TtlCache'd — instant after first request)
const languages = await i18n.getLanguages(); // [!code highlight]
// 2. Detect locale from URL path, validated against CDN language list
const { locale, i18n: shopifyI18n } = getLocaleFromRequest(request, languages); // [!code highlight]
// 3. Load Better i18n translations from CDN
const messages = await i18n.getMessages(locale); // [!code highlight]
// 4. Create Shopify Storefront client with locale
const { storefront } = createStorefrontClient({
cache,
waitUntil: (p: Promise<unknown>) => executionContext.waitUntil(p),
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
storeDomain: env.PUBLIC_STORE_DOMAIN,
storefrontApiVersion: env.PUBLIC_STOREFRONT_API_VERSION || "2026-01",
i18n: shopifyI18n, // [!code highlight]
});
const handleRequest = createRequestHandler({
build: remixBuild,
mode: process.env.NODE_ENV,
getLoadContext() {
return {
storefront,
env,
locale, // [!code highlight]
messages, // [!code highlight]
languages, // [!code highlight]
cart: {} as never,
};
},
});
return handleRequest(request);
},
};Augment AppLoadContext #
Add type declarations so context.locale, context.messages, and context.languages are typed in your loaders:
import type { Storefront, HydrogenCart } from "@shopify/hydrogen";
import type { Messages, LanguageOption } from "@better-i18n/remix"; // [!code highlight]
declare module "@shopify/remix-oxygen" {
interface AppLoadContext {
storefront: Storefront;
cart: HydrogenCart;
env: Env;
locale: string; // [!code highlight]
messages: Messages; // [!code highlight]
languages: LanguageOption[]; // [!code highlight]
}
}Add CSP entry #
Hydrogen uses Content Security Policy by default. Add cdn.better-i18n.com to connectSrc:
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_STORE_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
connectSrc: [
"'self'",
"cdn.better-i18n.com", // [!code highlight]
],
});Create the i18next client helper #
This helper creates a per-request i18next instance from the CDN translations already loaded in the server.
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import type { Messages } from "@better-i18n/remix";
export function createI18nextInstance(locale: string, messages: Messages) {
const instance = i18next.createInstance();
instance.use(initReactI18next).init({
lng: locale,
resources: { [locale]: messages }, // [!code highlight]
initImmediate: false, // sync init — required for SSR // [!code highlight]
lowerCaseLng: true,
defaultNS: "common",
fallbackNS: "common",
interpolation: { escapeValue: false },
});
return instance;
}Wrap your app with I18nextProvider #
import { useMemo } from "react";
import { I18nextProvider } from "react-i18next";
import { createI18nextInstance } from "~/lib/i18n-client";
export default function App() {
const { locale, messages, languages, githubStars } =
useLoaderData<typeof loader>();
const i18nInstance = useMemo( // [!code highlight]
() => createI18nextInstance(locale, messages), // [!code highlight]
[locale, messages], // [!code highlight]
); // [!code highlight]
return (
<html lang={locale} dir="ltr">
<body>
<I18nextProvider i18n={i18nInstance}> {/* [!code highlight] */}
<Layout locale={locale} languages={languages}>
<Outlet />
</Layout>
</I18nextProvider> {/* [!code highlight] */}
</body>
</html>
);
}Using Translations in Routes #
Use the useTranslation() hook from react-i18next in any component. Translations are pre-loaded server-side via the loader — no client-side fetching needed.
import { useLoaderData } from "react-router";
import type { LoaderFunctionArgs } from "@shopify/remix-oxygen";
import { useTranslation } from "react-i18next";
export async function loader({ context }: LoaderFunctionArgs) {
const { storefront, locale, messages } = context;
const { products } = await storefront.query(PRODUCTS_QUERY);
return { locale, messages, products: products.nodes };
}
export default function Homepage() {
const { locale, products } = useLoaderData<typeof loader>();
const { t: tc } = useTranslation("common");
const { t: tp } = useTranslation("products");
return (
<div>
<h1>{tc("welcome")}</h1>
{products.map((product) => (
<div key={product.id}>
{/* Product title comes from Storefront API (already localized) */}
<h2>{product.title}</h2>
{/* UI labels come from Better i18n via useTranslation */}
<span>{tc("view_details")}</span>
<span>{tp("from")}</span>
</div>
))}
</div>
);
}Storefront API Language Support #
When you pass the i18n option to createStorefrontClient, Shopify automatically adds the @inContext directive to GraphQL queries. Product titles, descriptions, and collection names are returned in the requested language — no extra work needed.
// The Storefront client handles localization automatically:
const { products } = await storefront.query(PRODUCTS_QUERY);
// When locale is "tr", product.title returns Turkish content
// (if available in Shopify admin)Full Example #
For a complete working implementation, see the hydrogen-demo in the Better i18n repository.
Better I18N