Create a country selector
This guide might not be compatible with features introduced in Hydrogen version 2025-05 and above. Check the latest documentation if you encounter any issues.
In this guide you will learn how to create a country selector so that buyers can change the store language and currency.
Anchor to RequirementsRequirements
- You've completed the Hydrogen getting started with a
Hello World
example project. - You've setup the regions and languages for your store with Shopify Markets.
- You've completed the setup multi-region and multilingual storefront with URL paths or setup multi-region and multilingual storefront with domains and subdomains tutorial.
- You're familiar with using the Storefront API with Shopify Markets.
Anchor to Step 1: Provide a list of available countriesStep 1: Provide a list of available countries
Create a JSON file with a list of available countries that will be rendered at every page. You can use the /app/data/countries.js
file in the Hydrogen demo store as a point of reference.
For performance and SEO reasons, Shopify recommends using a static JSON variable for the countries. Optionally, you can create a build script that generates this file on build.
The following is an example of the countries data structure:
/app/data/countries
/app/data/countries.js
export const countries = {
default: {
language: 'EN',
country: 'US',
label: 'United States (USD $)', // Labels to be shown in the country selector
host: 'hydrogen.shop', // The host and pathPrefix are used for linking
},
'en-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (CAD $)',
host: 'ca.hydrogen.shop',
},
'fr-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (Français) (CAD $)',
host: 'ca.hydrogen.shop',
pathPrefix: '/fr',
},
'en-au': {
language: 'EN',
country: 'AU',
label: 'Australia (AUD $)',
host: 'hydrogen.au',
},
};
import type {
CountryCode,
LanguageCode,
} from '@shopify/hydrogen/storefront-api-types';
export type Locale = {
language: LanguageCode;
country: CountryCode;
label: string;
host: string;
pathPrefix?: string
};
export const countries: Record<string, Locale> = {
default: {
language: 'EN',
country: 'US',
label: 'United States (USD $)', // Labels to be shown in the country selector
host: 'hydrogen.shop', // The host and pathPrefix are used for linking
},
'en-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (CAD $)',
host: 'ca.hydrogen.shop',
},
'fr-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (Français) (CAD $)',
host: 'ca.hydrogen.shop',
pathPrefix: '/fr',
},
'en-au': {
language: 'EN',
country: 'AU',
label: 'Australia (AUD $)',
host: 'hydrogen.au',
},
};
Create the getLocaleFromRequest
utility function. This function will read the request and determine the locale to be used throughout the app.
You can use the /app/lib/utils.js
file in the demo Hydrogen demo store as an example.
/app/lib/utils
/app/lib/utils.js
import {countries} from '~/data/countries';
export function getLocaleFromRequest(request) {
const url = new URL(request.url);
switch (url.host) {
case 'ca.hydrogen.shop':
if (/^\/fr($|\/)/.test(url.pathname)) {
return countries['fr-ca'];
} else {
return countries['en-ca'];
}
break;
case 'hydrogen.au':
return countries['en-au'];
break;
default:
return countries['default'];
}
}
import {type Locale, countries} from '~/data/countries';
export function getLocaleFromRequest(request: Request): Locale {
const url = new URL(request.url);
switch (url.host) {
case 'ca.hydrogen.shop':
if (/^\/fr($|\/)/.test(url.pathname)) {
return countries['fr-ca'];
} else {
return countries['en-ca'];
}
break;
case 'hydrogen.au':
return countries['en-au'];
break;
default:
return countries['default'];
}
}
Anchor to Step 3: Add the selected locale in the ,[object Object], loader functionStep 3: Add the selected locale in the root
loader function
root
loader functionThis step gets the user's request and finds the associated locale. You should make the selected locale available throughout the app with the loader
.
/app/root
/app/root.jsx
export async function loader({context, request}) {
...
return defer({
...,
selectedLocale: await getLocaleFromRequest(request),
});
}
export async function loader({context, request}: LoaderArgs) {
...
return defer({
...,
selectedLocale: await getLocaleFromRequest(request),
});
}
Anchor to Step 4: Create a resource route for the available countriesStep 4: Create a resource route for the available countries
A Remix resource route is useful when the UI fetches the available countries to display.
You can use the /routes/($locale).api.countries.js
in the The Hydrogen demo store as an example.
/routes/($locale).api.countries.js
/routes/($locale).api.countries.js
import {json} from '@remix-run/server-runtime';
import {CacheLong, generateCacheControlHeader} from '@shopify/hydrogen';
import {countries} from '~/data/countries';
export async function loader() {
return json(
{...countries},
{headers: {'cache-control': generateCacheControlHeader(CacheLong())}},
);
}
// no-op
export default function CountriesResourceRoute() {
return null;
}
import {json} from '@remix-run/server-runtime';
import {CacheLong, generateCacheControlHeader} from '@shopify/hydrogen';
import {countries} from '~/data/countries';
export async function loader() {
return json(
{...countries},
{headers: {'cache-control': generateCacheControlHeader(CacheLong())}},
);
}
// no-op
export default function CountriesResourceRoute() {
return null;
}
Anchor to Step 5: Render the available countries as a formStep 5: Render the available countries as a form
Create a CountrySelector
component using Remix Forms:
You can use the app/components/CountrySelector.js
file in the the Hydrogen demo store as an example.
/app/components/CountrySelector
/app/components/CountrySelector.jsx
import {Form, useMatches, useLocation, useFetcher} from '@remix-run/react';
import {useEffect, useState} from 'react';
export function CountrySelector() {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
const {pathname, search} = useLocation();
const [countries, setCountries] = useState({});
// Get available countries list
const fetcher = useFetcher();
useEffect(() => {
if (!fetcher.data) {
fetcher.load('/api/countries');
return;
}
setCountries(fetcher.data);
}, [countries, fetcher.data]);
const strippedPathname = pathname.replace(selectedLocale.pathPrefix, '');
return (
<details>
<summary>{selectedLocale.label}</summary>
<div className="overflow-auto border-t py-2 bg-contrast w-full max-h-36">
{countries &&
Object.keys(countries).map((countryKey) => {
const locale = countries[countryKey];
const hreflang = `${locale.language}-${locale.country}`;
return (
<Form method="post" action="/locale" key={hreflang}>
<input type="hidden" name="language" value={locale.language} />
<input type="hidden" name="country" value={locale.country} />
<input
type="hidden"
name="path"
value={`${strippedPathname}${search}`}
/>
<button type="submit">{locale.label}</button>
</Form>
);
})}
</div>
</details>
);
}
import {Form, useMatches, useLocation, useFetcher} from '@remix-run/react';
import {useEffect, useState} from 'react';
import type {Locale} from '~/data/countries';
export function CountrySelector() {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
const {pathname, search} = useLocation();
const [countries, setCountries] = useState<Record<string, Locale>>({});
// Get available countries list
const fetcher = useFetcher();
useEffect(() => {
if (!fetcher.data) {
fetcher.load('/api/countries');
return;
}
setCountries(fetcher.data);
}, [countries, fetcher.data]);
const strippedPathname = pathname.replace(selectedLocale.pathPrefix, '');
return (
<details>
<summary>{selectedLocale.label}</summary>
<div className="overflow-auto border-t py-2 bg-contrast w-full max-h-36">
{countries &&
Object.keys(countries).map((countryKey) => {
const locale = countries[countryKey];
const hreflang = `${locale.language}-${locale.country}`;
return (
<Form method="post" action="/locale" key={hreflang}>
<input type="hidden" name="language" value={locale.language} />
<input type="hidden" name="country" value={locale.country} />
<input
type="hidden"
name="path"
value={`${strippedPathname}${search}`}
/>
<button type="submit">{locale.label}</button>
</Form>
);
})}
</div>
</details>
);
}
Anchor to Step 6: Handle form submitStep 6: Handle form submit
Create the /app/routes/($locale).jsx
route that will handle the form submit action
/app/routes/($locale)
/app/routes/($locale).jsx
import {redirect} from '@shopify/remix-oxygen';
import invariant from 'tiny-invariant';
import {countries} from '~/data/countries';
export const action = async ({request, context}) => {
const {session} = context;
const formData = await request.formData();
// Make sure the form request is valid
const languageCode = formData.get('language');
invariant(languageCode, 'Missing language');
const countryCode = formData.get('country');
invariant(countryCode, 'Missing country');
// determine where to redirect to relative to where user navigated from
// ie. hydrogen.shop/collections -> ca.hydrogen.shop/collections
const path = formData.get('path');
const toLocale = countries[`${languageCode}-${countryCode}`.toLowerCase()];
const cartId = await session.get('cartId');
// Update cart buyer's country code if there is a cart id
if (cartId) {
await updateCartBuyerIdentity(context, {
cartId,
buyerIdentity: {
countryCode,
},
});
}
const redirectUrl = new URL(
`${toLocale.pathPrefix || ''}${path}`,
`https://${toLocale.host}`
);
return redirect(redirectUrl, 302);
};
async function updateCartBuyerIdentity({storefront}, {cartId, buyerIdentity}) {
const data = await storefront.mutate<{
cartBuyerIdentityUpdate: {cart};
}>(UPDATE_CART_BUYER_COUNTRY, {
variables: {
cartId,
buyerIdentity,
},
});
const UPDATE_CART_BUYER_COUNTRY = `#graphql
mutation CartBuyerIdentityUpdate(
$cartId: ID!
$buyerIdentity: CartBuyerIdentityInput!
) {
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
cart {
id
}
}
}
`;
import type {
CountryCode,
LanguageCode,
CartBuyerIdentityInput,
Cart,
} from '@shopify/hydrogen/storefront-api-types';
import {redirect, type AppLoadContext, type ActionFunction} from '@shopify/remix-oxygen';
import invariant from 'tiny-invariant';
import {countries} from '~/data/countries';
export const action: ActionFunction = async ({request, context}) => {
const {session} = context;
const formData = await request.formData();
// Make sure the form request is valid
const languageCode = formData.get('language') as LanguageCode;
invariant(languageCode, 'Missing language');
const countryCode = formData.get('country') as CountryCode;
invariant(countryCode, 'Missing country');
// Determine where to redirect to relative to where user navigated from
// ie. hydrogen.shop/collections -> ca.hydrogen.shop/collections
const path = formData.get('path');
const toLocale = countries[`${languageCode}-${countryCode}`.toLowerCase()];
const cartId = await session.get('cartId');
// Update cart buyer's country code if there is a cart id
if (cartId) {
await updateCartBuyerIdentity(context, {
cartId,
buyerIdentity: {
countryCode,
},
});
}
const redirectUrl = new URL(
`${toLocale.pathPrefix || ''}${path}`,
`https://${toLocale.host}`,
).toString();
return redirect(redirectUrl, 302);
};
async function updateCartBuyerIdentity(
{storefront}: AppLoadContext,
{
cartId,
buyerIdentity,
}: {
cartId: string;
buyerIdentity: CartBuyerIdentityInput;
},
) {
const data = await storefront.mutate<{
cartBuyerIdentityUpdate: {cart: Cart};
}>(UPDATE_CART_BUYER_COUNTRY, {
variables: {
cartId,
buyerIdentity,
},
});
invariant(data, 'No data returned from Shopify API');
return data.cartBuyerIdentityUpdate.cart;
}
const UPDATE_CART_BUYER_COUNTRY = `#graphql
mutation CartBuyerIdentityUpdate(
$cartId: ID!
$buyerIdentity: CartBuyerIdentityInput!
) {
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
cart {
id
}
}
}
`;
Anchor to Step 7: Make sure re-rendering happens at the root HTMLStep 7: Make sure re-rendering happens at the root HTML
Make sure to provide a key
to the components that will change due to localization.
Specially for URL path localization schemes.
Sometimes, React won't know when to re-render a component. To avoid this problem, add localization as key in the App
.
/app/root
/app/root.jsx
export default function App() {
const data = useLoaderData();
const locale = data.selectedLocale;
return (
<html lang={locale.language}>
<head>
<Seo />
<Meta />
<Links />
</head>
<body>
<Layout
layout={data.layout}
key={`${locale.language}-${locale.country}`} . // key by hreflang
>
<Outlet />
</Layout>
<Debugger />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
const data = useLoaderData<typeof loader>();
const locale = data.selectedLocale;
return (
<html lang={locale.language}>
<head>
<Seo />
<Meta />
<Links />
</head>
<body>
<Layout
layout={data.layout as LayoutData}
key={`${locale.language}-${locale.country}`} . // key by hreflang
>
<Outlet />
</Layout>
<Debugger />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}