Themes
Markets and localization
Markets and localization
A store can sell into several markets, for example Pakistan in PKR and the UAE in AED. The platform picks a market for every request and prices the catalogue for it before your template runs. A theme's job is to show the result, let the shopper switch, and keep its strings in locale files.
Merchants set markets up under Markets in the admin sidebar. See Shipping for how per-market shipping zones work on the merchant side.
How the market is chosen
The platform resolves one market per request, trying these in order and stopping at the first match:
- The URL. A market with a domain or subdomain matches when the request host equals its domain or custom domain. A subpath market, such as
/ae, matches when the path is that prefix or starts with it. - The
market_idcookie, set by/set-market. - The
?market=query parameter, which takes a market ID or handle. - The visitor's country, from the
CF-IPCountry,X-Country-CodeorX-Vercel-IP-Countryheader. The first active market that includes that country wins, checking the primary market first and the rest by name. - The primary market.
The URL comes first, so the cookie cannot move a shopper off a market's domain or subpath. To switch a shopper between URL-based markets, link to the other market's URL. If none of the steps matches and the store has no active primary market, there is no market. localization.market is then empty and prices are the base prices.
Prices are already resolved
Product and variant prices in Liquid are final for the resolved market. Don't convert them in the theme.
| Step | Rule |
|---|---|
| Fixed price | A price the merchant set for this product or variant in this market wins outright, with rounding applied |
| Exchange rate | Otherwise the base price is converted from the primary market's currency, when the market uses automatic exchange pricing |
| Adjustment | A percentage or fixed adjustment is applied |
| Rounding | 0.99, 0.95 or whole-number rounding, as configured |
shop.currency, the money format and Shopify.currency.active all follow the market's currency, so {{ product.price | money }} is correct as it is.
The AJAX endpoints apply only the adjustment and rounding steps (AJAX Cart API). For a market in another currency, take display prices from Liquid.
The localization object
| Property | Value |
|---|---|
localization.market | The resolved market, or empty |
localization.market.id, .handle, .name, .primary | Market identity |
localization.market.currency.iso_code, .currency.name | Market currency |
localization.market.domain_type | subpath, subdomain or domain |
localization.market.domain_value, .custom_domain | The market's subpath or domain |
localization.country | The first country in the resolved market: iso_code, name, currency.iso_code. It is not necessarily the visitor's own country |
localization.language | The first available language: iso_code, name, primary |
localization.available_markets | Every active market, primary first, then by name, with the same fields as market |
localization.available_countries | Every country across active markets: iso_code, name, currency.iso_code and its market |
localization.available_languages | Published languages across active markets. It falls back to English only |
{% if localization.available_markets.size > 1 %}
<p>
{{ localization.country.iso_code | country_flag }}
Shipping to {{ localization.country.name | default: localization.market.name }}
· {{ localization.market.currency.iso_code }}
</p>
{% endif %}Switching markets
POST /set-market stores the shopper's choice in the market_id cookie. The cookie lasts 30 days, uses path / and is readable from JavaScript.
| Part | Detail |
|---|---|
| Body | market_id: the market's UUID, as JSON or form-encoded. A handle is not accepted here |
| JSON response | Sent when Accept is exactly application/json, or X-Requested-With is XMLHttpRequest: { "success": true, "market_id", "currency_code", "currency_name" } |
| Other requests | Redirect to the referring page on the same store, or to / |
| Errors | 400 when market_id is missing, 404 when it isn't an active market of this store |
Prices are rendered on the server, so reload the page after switching. {% form 'localization' %} has no handler behind it. Use this endpoint instead.
async function switchMarket(marketId) {
const res = await fetch("/set-market", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ market_id: marketId }),
});
if (!res.ok) throw new Error((await res.json()).error);
window.location.reload();
}The country selector
Loom ships a ready-made selector in snippets/country-selector.liquid. It is part of the theme, not the platform, so copy it into your own snippets/ before rendering it.
It renders only when the store has two or more active markets. It shows a button with the current country or market and currency. The button opens a dialog listing every market with its flag (using country_flag) and currency, and selecting one calls /set-market and reloads. The styles use Loom's CSS variables (--border, --foreground, --background, --surface, --text-secondary), so map them to your own.
<footer class="site-footer">
{% render 'country-selector' %}
</footer>Translations
Theme strings belong in locales/ and are read with the t filter (Locales). Keys use dot notation, and values must be strings.
How t behaves today:
- English only. It reads
locales/en.default.json, thenlocales/en.json. Other locale files, such asur.json, are accepted in the theme buttnever reads them. The store language and the shopper's market do not change which file is used. - Interpolation is positional. Pass pairs of name and value:
t: 'count', cart_count, 'name', customer_name. Each{{ name }}or{{name}}in the string is replaced. The Shopify formt: count: cart_countis a syntax error that stops the template rendering. - No pluralisation. A value that is an object, such as
{ "one": …, "other": … }, prints as raw data. Keep plural variants as separate keys and choose between them withpluralizeorif. - Missing keys don't error. The last segment of the key is shown with each word capitalised, so
products.add_to_cartrenders asAdd To Cart. Compare your locale file with your templates before submitting.
{
"cart": {
"title": "Your cart",
"count": "{{ count }} items"
}
}Store language
request.locale.iso_code and shop.language hold the language code from the store's settings, and request.locale.endonym_name holds its display name. Use them for the document language:
<html lang="{{ request.locale.iso_code }}" dir="{% if request.locale.iso_code == 'ur' or request.locale.iso_code == 'ar' %}rtl{% else %}ltr{% endif %}">Updated 15 September 2026