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.
How it works
Section titled “How it works”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 pricesCountry detection
Section titled “Country detection”Priority order (first non-null wins):
_lk_countrycookie (set on every response, 1-day TTL)?__country=XXquery param (dev/testing escape hatch)CF-IPCountryheader (Cloudflare’s MaxMind geolocation)"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.”
Seed pipeline
Section titled “Seed pipeline”Three steps run during pnpm seed:kv:
Step 1: Fetch markets
Section titled “Step 1: Fetch markets”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.
Step 2: Fetch contextual pricing
Section titled “Step 2: Fetch contextual pricing”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}.
Step 3: Fetch localization metadata
Section titled “Step 3: Fetch localization metadata”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.
Render-time overlay
Section titled “Render-time overlay”Product pages
Section titled “Product pages”After loading the product from KV, if the visitor is in a non-primary market:
- Load
{slug}:prices:{marketHandle}:{productHandle}from KV - Call
overlayPrices(product, priceOverlay)— mutates the product in-place - Converts Admin API decimal dollars → cents (e.g.
"13.50"→1350) - Updates:
price,price_min,price_max,price_varies,compare_at_price*, each variant’sprice/compare_at_price, andselected_or_first_available_variant
Block products (V2)
Section titled “Block products (V2)”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.
Collection pages
Section titled “Collection pages”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.
Currency threading
Section titled “Currency threading”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.
Section cache scoping
Section titled “Section cache scoping”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.
Currency switcher
Section titled “Currency switcher”Shopify’s currency switcher form POSTs to /localization with country_code and return_to. Our handler:
- Reads
country_codefrom the form body - Sets
_lk_countrycookie (1-day TTL) - 302 redirects to
return_to(or/)
Next page load picks up the cookie and renders with the new market’s pricing.
KV key reference
Section titled “KV key reference”| Key pattern | Contents |
|---|---|
{slug}:markets | Market config + country→market mapping |
{slug}:prices:{market}:{handle} | Price overlay for one product in one market |
{slug}:localization | Available countries, languages, currencies |
Currency support
Section titled “Currency support”Both currencyToLocale() and currencySymbol() throw on unknown currencies. When expanding to a new market with an unsupported currency, add entries to:
CURRENCY_LOCALE_MAPinpackages/cf-client-site/src/data/localization.tsCURRENCY_SYMBOL_MAPin 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.
Testing locally
Section titled “Testing locally”# Seed markets + pricing + localizationpnpm seed:kv --manifest='{"products":{"metafields":[]}}'
# Start dev serverpnpm dev --skip-seed
# Test different countriescurl http://localhost:8787/products/some-product?__country=CAcurl http://localhost:8787/products/some-product?__country=GBcurl http://localhost:8787/products/some-product?__country=TH
# Verify cookiecurl -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 switchercurl -X POST http://localhost:8787/localization \ -d 'country_code=GB&return_to=/products/some-product'# → 302 redirect + _lk_country=GB cookie| File | Role |
|---|---|
packages/cf-client-site/src/data/localization.ts | Market resolution, price overlay, money formats, localization builder |
packages/cf-client-site/src/data/localization.test.ts | 42 unit tests |
packages/cf-client-site/src/worker.ts | Country detection, KV loads, overlay calls, /localization handler |
packages/cf-client-site/src/runtime/index.ts | Currency threading into formatMoney |
packages/cf-client-site/src/render/section-cache.ts | Market handle in cache hash |
packages/cf-client-site/src/render/page-renderer.ts | Market handle through render options |
packages/cf-client-site/src/data/page-data.ts | @inContext on collection queries |
scripts/seed-kv.ts | Market, pricing, localization fetch at seed time |