Locales
How the platform resolves and threads locale context through every request and data-fetching call.
A locale is an xx-XX language/region tag carried in the URL path segment and passed into
every data-fetching helper. It is dynamic at request time — it is not encoded in the route tree,
and the App Router sees it only after middleware has resolved it. Locale is always a qualifier
under tenant: /[domain]/[locale]/…. No locale segment means middleware runs the fallback
chain before rewriting.
The fallback chain
Middleware consults locale sources in order and terminates at the first match:
- The
xx-XXsegment already present in the request path. - The
Accept-Languageheader matched against the shop's available locales. - The shop's default locale — the single locale persisted on the
Shoprecord.
There is no platform-wide default below the shop's default; the chain always terminates at the tenant level. Locale availability is derived from the commerce provider at request time and is never persisted on the shop document itself.
Two shops served from the same deployment can have completely different locale sets.
One may offer en-GB, fr-FR, and de-DE; another only en-US. Middleware handles
this correctly because it reads locale availability from the resolved shop, not from a
global list.
Threading locale through data fetching
Every call that crosses a package boundary must carry the resolved locale explicitly. The
@nordcom/commerce-cms/api helpers accept { shop, locale } and apply locale-qualified
lookups internally, falling back to the shop default when no locale-specific content exists.
import { getFooter } from '@nordcom/commerce-cms/api';
export async function FooterApi({ shop, locale }: { shop: OnlineShop; locale: Locale }) {
return getFooter({ shop, locale });
}The same contract applies to Shopify API clients. ShopifyApolloApiClient({ shop, locale })
injects @inContext(country: $country, language: $language) into every query automatically via
the Apollo DocumentTransform in @nordcom/commerce-shopify-graphql. Source operations must
not pre-declare @inContext, $country, or $language — the transform owns them and will
throw DuplicateContextDirectiveError / DuplicateContextVariableError if they are already
present.
Passing the wrong locale into getArticle or getPage returns content in the wrong
language rather than throwing. Always resolve the locale from params or middleware
context — never hardcode 'en-US' as a fallback in route code.
Adding a locale to a shop
Locale availability is derived at runtime from the Shopify availableCountries response — the
shop record does not store a list of locales. To add a locale to a storefront, configure the
corresponding market and language in the Shopify admin. Middleware picks it up automatically on
the next request.
The shop's defaultLocale field is the only locale stored on the Shop document. Update it
via the Admin app — shop writes land in the Convex shops table through the
db/shop_write:upsertShop mutation.
Multi-tenancy
One deploy serves many shops. Tenant resolution happens at the edge by hostname, then every downstream call carries explicit { shop, locale }. The App Router never sees an un-tenanted request.
Caching
Tenant-scoped, locale-qualified cache namespaces that ensure shop isolation and precise revalidation.