Skip to content

Currency Detection

Shopify shows localized pricing per market. Instead of runtime Storefront API calls on every request, we fetch all market-specific pricing at seed/build time via the Admin API and store it in KV. At render time, it’s a KV lookup — zero API calls, zero latency impact.

SEED TIME RENDER TIME
───────── ───────────
Admin API CF-IPCountry: TH
contextualPricing(country: CA) │
contextualPricing(country: GB) Cookie _lk_country (if set)
contextualPricing(country: TH) │
│ Market lookup (KV)
▼ TH → market "international"
KV: {slug}:prices:{market}:{handle} │
per product, per market ▼
Overlay onto KV product data
Render with localized prices

Priority order (first non-null wins):

  1. _lk_country cookie (set on every response, 1-day TTL)
  2. ?__country=XX query param (dev/testing escape hatch)
  3. CF-IPCountry header (Cloudflare’s MaxMind geolocation)
  4. "US" fallback

The cookie is refreshed on every response so there’s no “detection step” — it’s always “read cookie, or read header and set cookie.”

Three steps run during pnpm seed:kv:

Queries the Admin API for active markets and their country assignments:

query {
markets(first: 50) {
nodes {
id handle name primary enabled
regions(first: 200) {
nodes {
__typename
... on MarketRegionCountry {
code name
currency { currencyCode currencyName }
}
}
}
}
}
}

Stored in KV as {slug}:markets:

{
"markets": [
{ "handle": "us", "primary": true, "countries": ["US"], "currency": "USD", "symbol": "$" },
{ "handle": "canada", "countries": ["CA"], "currency": "CAD", "symbol": "CA$" },
{ "handle": "international", "countries": ["GB", "AU", "TH", "..."], "currency": "EUR", "symbol": "" }
],
"countryToMarket": { "US": "us", "CA": "canada", "GB": "international", "TH": "international" }
}

This collapses ~200 countries into 3–5 markets for cache purposes.

For each non-primary market, paginates through all products with contextualPricing:

query($first: Int!, $after: String) {
products(first: $first, after: $after, sortKey: TITLE) {
nodes {
handle
contextualPricing(context: { country: CA }) {
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
}
variants(first: 100) {
nodes {
id
contextualPricing(context: { country: CA }) {
price { amount currencyCode }
compareAtPrice { amount currencyCode }
}
}
}
}
pageInfo { hasNextPage endCursor }
}
}

One representative country per market is sufficient — all countries in a market get the same pricing.

Stored per-market in KV as {slug}:prices:{marketHandle}:{productHandle}.

Queries the Storefront API for available countries and languages:

query {
localization {
availableCountries {
isoCode name
currency { isoCode name symbol }
}
availableLanguages { isoCode name endonymName }
}
}

Stored in KV as {slug}:localization. Provides the full localization Liquid object.

After loading the product from KV, if the visitor is in a non-primary market:

  1. Load {slug}:prices:{marketHandle}:{productHandle} from KV
  2. Call overlayPrices(product, priceOverlay) — mutates the product in-place
  3. Converts Admin API decimal dollars → cents (e.g. "13.50"1350)
  4. Updates: price, price_min, price_max, price_varies, compare_at_price*, each variant’s price/compare_at_price, and selected_or_first_available_variant

resolveSettingsResources() already collects all product handles from section settings (productRefs, productListRefs). After hydrating products from KV, if non-primary market, batch-fetch price overlays and apply overlayPrices() to each.

This localizes all products on the page — main product, product list swatches, featured products, upsells — with zero API calls.

Collections use the Storefront API at request time. The @inContext(country: XX) directive is added to the existing collection query so product prices come back in the correct currency automatically.

The runtime’s formatMoney() receives currency defaults from CreateRuntimeOptions.currency:

currency: { code: "CAD", locale: "en-CA" }

All transpiled | money filters automatically use the correct currency.

The shop object’s money_format, money_with_currency_format, and enabled_currencies are also updated per-market.

hashSectionInputs() includes marketHandle in the hash. Cache cardinality: sections x markets (e.g. 50 sections x 5 markets = 250 entries). Same section + same settings + different market = different cache entry.

Shopify’s currency switcher form POSTs to /localization with country_code and return_to. Our handler:

  1. Reads country_code from the form body
  2. Sets _lk_country cookie (1-day TTL)
  3. 302 redirects to return_to (or /)

Next page load picks up the cookie and renders with the new market’s pricing.

Key patternContents
{slug}:marketsMarket config + country→market mapping
{slug}:prices:{market}:{handle}Price overlay for one product in one market
{slug}:localizationAvailable countries, languages, currencies

Both currencyToLocale() and currencySymbol() throw on unknown currencies. When expanding to a new market with an unsupported currency, add entries to:

  • CURRENCY_LOCALE_MAP in packages/cf-client-site/src/data/localization.ts
  • CURRENCY_SYMBOL_MAP in the same file

Currently supported: USD, CAD, GBP, EUR, AUD, NZD, JPY, THB, SGD, HKD, INR, MXN, BRL, KRW, SEK, NOK, DKK, CHF, PLN, CZK, ILS, AED, ZAR, MYR, PHP, TWD, IDR, VND.

Terminal window
# Seed markets + pricing + localization
pnpm seed:kv --manifest='{"products":{"metafields":[]}}'
# Start dev server
pnpm dev --skip-seed
# Test different countries
curl http://localhost:8787/products/some-product?__country=CA
curl http://localhost:8787/products/some-product?__country=GB
curl http://localhost:8787/products/some-product?__country=TH
# Verify cookie
curl -D - http://localhost:8787/products/some-product?__country=CA 2>&1 | grep Set-Cookie
# → Set-Cookie: _lk_country=CA; Path=/; Max-Age=86400; SameSite=Lax
# Test currency switcher
curl -X POST http://localhost:8787/localization \
-d 'country_code=GB&return_to=/products/some-product'
# → 302 redirect + _lk_country=GB cookie
FileRole
packages/cf-client-site/src/data/localization.tsMarket resolution, price overlay, money formats, localization builder
packages/cf-client-site/src/data/localization.test.ts42 unit tests
packages/cf-client-site/src/worker.tsCountry detection, KV loads, overlay calls, /localization handler
packages/cf-client-site/src/runtime/index.tsCurrency threading into formatMoney
packages/cf-client-site/src/render/section-cache.tsMarket handle in cache hash
packages/cf-client-site/src/render/page-renderer.tsMarket handle through render options
packages/cf-client-site/src/data/page-data.ts@inContext on collection queries
scripts/seed-kv.tsMarket, pricing, localization fetch at seed time