eekaam.docs

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:

  1. 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.
  2. The market_id cookie, set by /set-market.
  3. The ?market= query parameter, which takes a market ID or handle.
  4. The visitor's country, from the CF-IPCountry, X-Country-Code or X-Vercel-IP-Country header. The first active market that includes that country wins, checking the primary market first and the rest by name.
  5. 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.

StepRule
Fixed priceA price the merchant set for this product or variant in this market wins outright, with rounding applied
Exchange rateOtherwise the base price is converted from the primary market's currency, when the market uses automatic exchange pricing
AdjustmentA percentage or fixed adjustment is applied
Rounding0.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

PropertyValue
localization.marketThe resolved market, or empty
localization.market.id, .handle, .name, .primaryMarket identity
localization.market.currency.iso_code, .currency.nameMarket currency
localization.market.domain_typesubpath, subdomain or domain
localization.market.domain_value, .custom_domainThe market's subpath or domain
localization.countryThe first country in the resolved market: iso_code, name, currency.iso_code. It is not necessarily the visitor's own country
localization.languageThe first available language: iso_code, name, primary
localization.available_marketsEvery active market, primary first, then by name, with the same fields as market
localization.available_countriesEvery country across active markets: iso_code, name, currency.iso_code and its market
localization.available_languagesPublished 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.

PartDetail
Bodymarket_id: the market's UUID, as JSON or form-encoded. A handle is not accepted here
JSON responseSent when Accept is exactly application/json, or X-Requested-With is XMLHttpRequest: { "success": true, "market_id", "currency_code", "currency_name" }
Other requestsRedirect to the referring page on the same store, or to /
Errors400 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, then locales/en.json. Other locale files, such as ur.json, are accepted in the theme but t never 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 form t: count: cart_count is 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 with pluralize or if.
  • Missing keys don't error. The last segment of the key is shown with each word capitalised, so products.add_to_cart renders as Add 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