---
title: Setup multilingual and multi-regional storefronts with URL paths
description: Learn how to setup multiregion and multilingual storefront with URL paths to get the right language and currency.
source_url:
html: https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths
md: https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths.md
---
ExpandOn this page
* [Requirements](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#requirements)
* [Step 1: Create a utility that checks the requested URL paths locale](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-1-create-a-utility-that-checks-the-requested-url-paths-locale)
* [Step 2: Match routes that contain language in the URL](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-2-match-routes-that-contain-language-in-the-url)
* [Step 3: Add i18n to the storefront client](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-3-add-i18n-to-the-storefront-client)
* [Step 4: Add @inContext directive to your GraphQL queries](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-4-add-incontext-directive-to-your-graphql-queries)
* [Step 5: Match non-existent pages](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-5-match-non-existent-pages)
* [Step 6: Handle invalid URL lang parameters](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-6-handle-invalid-url-lang-parameters)
* [Step 7: Create a utility function to add a language path prefix](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-7-create-a-utility-function-to-add-a-language-path-prefix)
* [Step 8: Create Link component with locale path prefix](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-8-create-link-component-with-locale-path-prefix)
* [Step 9: Make sure redirects are properly url encoded](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-9-make-sure-redirects-are-properly-url-encoded)
* [Next steps](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#next-steps)
# Setup multilingual and multi-regional storefronts with URL paths
Note
This guide might not be compatible with features introduced in Hydrogen version 2025-05 and above. Check the latest [documentation](https://shopify.dev/docs/api/hydrogen) if you encounter any issues.
In this guide you will learn how to setup your Hydrogen project for supporting multi-region and multilingual storefronts by using URL paths.
For example, say you have a storefront that should work in English (EN) and in non-regional French (FR) for different customers.
You will setup the project to handle requests as following:
| Language | URL path |
| - | - |
| English | `ca.hydrogen.shop` |
| French | `ca.hydrogen.shop/fr` |
***
## Requirements
* You have a working Hydrogen project. For more information, refer to the [getting started guide](https://shopify.dev/docs/storefronts/headless/hydrogen/getting-started).
* You have setup the regions and languages you chose for your store with [Shopify Markets](https://help.shopify.com/en/manual/markets).
* You're familiar with [using the Storefront API with Shopify Markets](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/markets).
***
## Step 1: Create a utility that checks the requested URL paths locale
Create a utility function that reads the requested host and directory path which return the right `Locale` object using the Storefronts API's supported [language](https://shopify.dev/docs/api/storefront/latest/enums/LanguageCode) and [country](https://shopify.dev/docs/api/storefront/latest/enums/CountryCode) codes.
Tip
You can use the `/app/lib/utils.js` in the Hydrogen demo store as a reference.
The following is an example utility function with the following locales `en_CA`, `fr_CA` and `en_US`.
## utils
## /app/lib/utils.js
```js
export function getLocaleFromRequest(request) {
// Get the user request URL
const url = new URL(request.url);
// Match the URL host
switch (url.host) {
case 'ca.hydrogen.shop':
// This regex matches `/fr/` paths in the request
if (/^\/fr($|\/)/.test(url.pathname)) {
return {
language: 'FR',
country: 'CA',
};
} else {
return {
language: 'EN',
country: 'CA',
};
}
break;
default:
return {
language: 'EN',
country: 'US',
};
}
}
```
```ts
export function getLocaleFromRequest(request: Request): Locale {
// Get the user request URL
const url = new URL(request.url);
// Match the URL host
switch (url.host) {
case 'ca.hydrogen.shop':
// This regex matches `/fr/` paths in the request
if (/^\/fr($|\/)/.test(url.pathname)) {
return {
language: 'FR',
country: 'CA',
};
} else {
return {
language: 'EN',
country: 'CA',
};
}
break;
default:
return {
language: 'EN',
country: 'US',
};
}
}
```
The `Locale` object returned should resemble the following example, which is using the Storefont API's supported [language](https://shopify.dev/docs/api/storefront/latest/enums/LanguageCode) and [country](https://shopify.dev/docs/api/storefront/latest/enums/CountryCode) codes.
## TypeScript
```ts
import {
CountryCode,
LanguageCode,
} from '@shopify/storefront-kit-react/storefront-api-types';
export type Locale = {
language: LanguageCode;
country: CountryCode;
};
```
***
## Step 2: Match routes that contain language in the URL
Using React Router's optional segments, add `($locale)` in front of your routes.
This ensures that routes such as `/products/123` and `/fr/products/123` matches to the same product route file in Remix, so that the correct page is rendered.
The following is an example of files and folders before file rename with `($locale)`:
```markdown
├── app
│ ├── routes
│ │ ├── _index.tsx
│ │ ├── products.$productHandle.tsx
...
```
After renaming the routes with `($locale)`, your new file structure should look like the following example:
```markdown
├── app
│ ├── routes
│ │ ├── ($locale)._index.tsx
│ │ ├── ($locale).products.$productHandle.tsx
...
```
At this point, you should see your pages render when you make requests to `/fr/` URL paths.
***
## Step 3: Add i18n to the storefront client
In your `server.js`, update `i18n` to the result of the utility function when creating the Hydrogen storefront client.
By doing this, you now have the locale available throughout the app for every storefront query.
## /server.js
```js
const {storefront} = createStorefrontClient({
...
i18n: getLocaleFromRequest(request),
...
});
```
```ts
const {storefront} = createStorefrontClient({
...
i18n: getLocaleFromRequest(request),
...
});
```
***
## Step 4: Add @inContext directive to your GraphQL queries
To [support international pricing and languages in Storefront API](https://shopify.dev/custom-storefronts/building-with-the-storefront-api/markets/international-pricing), you need to pass the `$country` and `$language` with an `@inContext` directive within any requests.
Update your GraphQL queries with `inContext` directives to include `$country` and `$language`. Hydrogen automatically injects these parameters.
For example, this is a Storefront API query that returns featured collections from the homepage. This updates it to include the `inContext` directive.
## GraphQL queries
## Before
```jsx
const FEATURED_QUERY = `#graphql
query homepage {
collections(first: 3, sortKey: UPDATED_AT) {
nodes {
id
title
handle
image {
altText
width
height
url
}
}
}
}
`;
```
## After
```jsx
const FEATURED_QUERY = `#graphql
query homepage($country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
collections(first: 3, sortKey: UPDATED_AT) {
nodes {
id
title
handle
image {
altText
width
height
url
}
}
}
}
`;
```
You don't need to manually provide query variables for `country` and `language`. You can make the query with `storefront.query` in the data loader and see the right language and currencies for each request.
```js
export async function loader({
context: {storefront},
}) {
return json({
featureCollections: await storefront.query<{
collections;
}>(FEATURED_COLLECTIONS_QUERY),
});
}
```
```ts
export async function loader({
context: {storefront},
}: LoaderArgs) {
return json({
featureCollections: await storefront.query<{
collections: CollectionConnection;
}>(FEATURED_COLLECTIONS_QUERY),
});
}
```
Hydrogen automatically injects the locale parameters to `storefront.query` based on what was defined in `i18n` when you created the client.
For example, if a request came from `hydrogen.fr`, then the country `CA` and language `FR` are used as defined in the utilities function.
The Storefront API returns the correct currency and language if the store was set up in the Shopify admin.
If you want to override the locale determined by your utility option, then you can supply the query variables to the `storefront.query`:
```js
export async function loader({
context: {storefront},
}) {
return json({
featureCollection: await storefront.query(FEATURED_COLLECTIONS_QUERY, {
variables: {
country: 'CA', // Always query back in CA currency
language: 'FR', // Always query back in FR language
}
}),
});
}
```
```ts
export async function loader({
context: {storefront},
}: LoaderArgs) {
return json({
featureCollection: await storefront.query<{
collections: CollectionConnection;
}>(FEATURED_COLLECTIONS_QUERY, {
variables: {
country: 'CA', // Always query back in CA currency
language: 'FR', // Always query back in FR language
}
}),
});
}
```
***
## Step 5: Match non-existent pages
A request to `/this-route-does-not-exist` should return a `404` not found page.
To achieve this, create a `$.(tsx|jsx)` file in the \`/app/routes/\`\` folder. This [Remix splat route](https://remix.run/docs/en/main/file-conventions/route-files-v2#splat-routes) will handle all the non-matching routes.
## Catch-all route
## /app/routes/$.jsx
```js
export async function loader() {
throw new Response('Not found', {status: 404});
}
export default function Component() {
return null;
}
```
```ts
export async function loader() {
throw new Response('Not found', {status: 404});
}
export default function Component() {
return null;
}
```
***
## Step 6: Handle invalid URL lang parameters
In the `/app/routes/index.jsx`, set up handling of invalid URL parameters localization. For example, any request with lang parameter `au` when you don't handle this language, should return a `404`.
## Index route
## /app/routes/index.jsx
```js
export async function loader({
request,
params,
context,
}) {
const {language, country} = context.storefront.i18n;
if (
params.locale &&
params.locale.toLowerCase() !== `${'{'}language{'}'}-${'{'}country{'}'}`.toLowerCase()
) {
// If the locale URL param is defined, yet we still are on `EN-US`
// the the locale param must be invalid, send to the 404 page
throw new Response(null, {status: 404});
}
...
}
```
```ts
export async function loader({
request,
params,
context,
}: LoaderArgs) {
const {language, country} = context.storefront.i18n;
if (
params.locale &&
params.locale.toLowerCase() !== `${'{'}language{'}'}-${'{'}country{'}'}`.toLowerCase()
) {
// If the locale URL param is defined, yet we still are on `EN-US`
// the the locale param must be invalid, send to the 404 page
throw new Response(null, {status: 404});
}
...
}
```
***
## Step 7: Create a utility function to add a language path prefix
Create a utility function that adds the locale path prefix to any URL path. For example, if the path is `/products` and the buyer prefers the locale `fr_CA`, then the utility function converts it to `/fr/products`.
Use this utility function anywhere you need to define a localized path. For example, form actions should have the localized path.
## utils
## /server.js
```js
export function usePrefixPathWithLocale(path) {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
return selectedLocale
? `${selectedLocale.pathPrefix}${
path.startsWith('/') ? path : '/' + path
}`
: path;
}
```
```ts
export function usePrefixPathWithLocale(path: string) {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
return selectedLocale
? `${selectedLocale.pathPrefix}${
path.startsWith('/') ? path : '/' + path
}`
: path;
}
```
***
## Step 8: Create Link component with locale path prefix
Create a `` wrapper component that adds the locale path prefix. You can create this file in any components folder. In the case of the Hydrogen demo store, `components` folder was created for base components.
Caution
Make sure your project is using this `Link` component for all inbound navigation. This ensures the prefix locale gets appended for every link. For example navigating from `fr/products` to `fr/collections` without this `Link` component loses the `fr` path.
## Link
## /app/components/Link.js
```js
import {
Link as RemixLink,
NavLink as RemixNavLink,
useMatches,
} from '@remix-run/react';
import {usePrefixPathWithLocale} from '~/lib/utils';
export function Link(props) {
const {to, className, ...resOfProps} = props;
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
let toWithLocale = to;
if (typeof to === 'string') {
toWithLocale = selectedLocale ? `${selectedLocale.pathPrefix}${'{'}to{'}'}` : to;
}
if (typeof className === 'function') {
return (
);
}
return (
);
}
export function usePrefixPathWithLocale(path) {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
return selectedLocale
? `${selectedLocale.pathPrefix}${
path.startsWith('/') ? path : '/' + path
}`
: path;
}
```
```ts
import {
Link as RemixLink,
NavLink as RemixNavLink,
useMatches,
} from '@remix-run/react';
import {usePrefixPathWithLocale} from '~/lib/utils';
export function Link(props) {
const {to, className, ...resOfProps} = props;
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
let toWithLocale = to;
if (typeof to === 'string') {
toWithLocale = selectedLocale ? `${selectedLocale.pathPrefix}${'{'}to{'}'}` : to;
}
if (typeof className === 'function') {
return (
);
}
return (
);
}
export function usePrefixPathWithLocale(path: string) {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
return selectedLocale
? `${selectedLocale.pathPrefix}${
path.startsWith('/') ? path : '/' + path
}`
: path;
}
```
***
## Step 9: Make sure redirects are properly url encoded
If you have multilingual handles for your product or collection, for example, `products/スノーボード`, make sure to encode url when making redirects.
## Link
## /app/routes/($locale).products.$productHandle.js
```js
export async function loader({params, request, context}) {
const {productHandle} = params; // productHandle = 'スノーボード'
...
if (noSelectedProductVariant) {
// Use URL to prevent accidental double url encoding
const newUrl = new URL(
`/products/${'{'}productHandle{'}'}?${firstVariantSearchParams.toString()}`,
'http://example.com' // Any domain to satisfy the URL api
);
// Redirect to '/products/%E3%82%B9%E3%83%8E%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89?Size=154cm&Color=Syntax'
throw redirect(newUrl.pathname + newUrl.search, 302);
}
...
```
```ts
export async function loader({params, request, context}: LoaderArgs) {
const {productHandle} = params; // productHandle = 'スノーボード'
...
if (noSelectedProductVariant) {
// Use URL to prevent accidental double url encoding
const newUrl = new URL(
`/products/${'{'}productHandle{'}'}?${firstVariantSearchParams.toString()}`,
'http://example.com' // Any domain to satisfy the URL api
);
// Redirect to '/products/%E3%82%B9%E3%83%8E%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89?Size=154cm&Color=Syntax'
throw redirect(newUrl.pathname + newUrl.search, 302);
}
...
```
***
## Next steps
* [Create a country selector](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/country-selector): Learn how to setup a country selector to allow users to choose their own country.
***
* [Requirements](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#requirements)
* [Step 1: Create a utility that checks the requested URL paths locale](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-1-create-a-utility-that-checks-the-requested-url-paths-locale)
* [Step 2: Match routes that contain language in the URL](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-2-match-routes-that-contain-language-in-the-url)
* [Step 3: Add i18n to the storefront client](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-3-add-i18n-to-the-storefront-client)
* [Step 4: Add @inContext directive to your GraphQL queries](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-4-add-incontext-directive-to-your-graphql-queries)
* [Step 5: Match non-existent pages](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-5-match-non-existent-pages)
* [Step 6: Handle invalid URL lang parameters](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-6-handle-invalid-url-lang-parameters)
* [Step 7: Create a utility function to add a language path prefix](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-7-create-a-utility-function-to-add-a-language-path-prefix)
* [Step 8: Create Link component with locale path prefix](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-8-create-link-component-with-locale-path-prefix)
* [Step 9: Make sure redirects are properly url encoded](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#step-9-make-sure-redirects-are-properly-url-encoded)
* [Next steps](https://shopify.dev/docs/storefronts/headless/hydrogen/markets/multiple-languages-url-paths#next-steps)