A health and wellness DTC brand came to me with a simple problem: their international customers were seeing wrong prices. The store used Shopify Markets to sell across 236 regions, but prices on promotional sections, announcement bars, and comparison tables showed USD regardless of the visitor’s country.
The root cause: 38 hardcoded USD prices scattered across their theme code. Every one of them had been written as static text ("$29.99") instead of using Shopify’s Liquid money filters.
A previous developer had tried to fix this with an async JavaScript price rewriter that detected the visitor’s currency and swapped the text after page load. The script was 90 lines of JavaScript that ran on every page. It worked sometimes. But it caused a visible flash where prices loaded in USD, then jumped to the local currency 200-400ms later.
That flash registered as layout shift. CLS went from passing (0.00) to failing (0.11). Core Web Vitals broke. And international customers saw prices change in front of their eyes, which is a trust killer.
The fix was straightforward: find every hardcoded price, replace it with a Liquid money filter, and delete the JavaScript rewriter entirely.
Why Hardcoded Prices Break International Stores
Shopify’s multi-currency system works at the Liquid rendering level. When a visitor from Germany views your store, Shopify’s server converts prices before the HTML is sent to the browser. But this only applies to prices output through Liquid objects:
{{ product.price | money }} <!-- Converts correctly -->
{{ cart.total_price | money }} <!-- Converts correctly -->
{{ variant.compare_at_price | money }} <!-- Converts correctly -->
Anything written as static text is invisible to the conversion system:
<p>Starting at $29.99</p> <!-- Always shows USD -->
<span>Save $10 on your first order</span> <!-- Always shows USD -->
<td>$49.99/month</td> <!-- Always shows USD -->
The German visitor sees a mix of EUR prices (on product cards) and USD prices (on banners and promotional sections). This looks broken and erodes trust.
Where Hardcoded Prices Hide
After auditing dozens of Shopify themes for multi-currency issues, these are the most common locations:
Announcement Bars
<!-- BAD: Hardcoded -->
Free shipping on orders over $50
<!-- GOOD: Dynamic -->
Free shipping on orders over {{ 5000 | money }}
Note: 5000 is the amount in cents (Shopify stores all prices in the smallest currency unit). For USD, $50.00 = 5000 cents.
Section Schema Defaults
Many themes use text settings in section schemas that include prices:
{
"type": "text",
"id": "promo_text",
"label": "Promotional text",
"default": "Starting at $29.99"
}
These values get stored in settings_data.json and are output as plain text. They never pass through currency conversion.
Fix: Use a separate price type setting or a number setting that you pipe through | money in the Liquid template:
{
"type": "text",
"id": "promo_price_cents",
"label": "Promotional price (in cents)",
"default": "2999",
"info": "Enter price in cents. 2999 = $29.99"
}
Starting at {{ section.settings.promo_price_cents | times: 1 | money }}
The | times: 1 ensures the string is treated as a number before the money filter processes it.
Comparison Tables and Pricing Grids
Subscription tiers, bundle pricing, and “What’s Included” tables frequently use hardcoded prices. Every cell needs to pull from a dynamic source or use the cents-to-money pattern above.
Metafield Values
If you store prices in metafields as text strings ("$49.99"), those will not convert either. Use Shopify’s money metafield type, or store the value in cents and pipe through | money when rendering.
Rich Text Content Blocks
Theme editor rich text blocks, blog post content, and page content with prices are all static text. If you mention a price in a blog post (“Our kits start at $399”), that will show as USD globally.
For blog and page content, there is no clean Liquid solution since the content is free-form text. The practical approach is to avoid mentioning specific prices in content, or use relative language (“starting under $400” becomes “starting at an affordable price point”).
The JavaScript Rewriter Anti-Pattern
When developers discover the hardcoded price problem, the instinct is to write JavaScript that finds price strings in the DOM and replaces them:
// DON'T DO THIS
document.querySelectorAll('[data-currency-convert]').forEach(el => {
const usd = parseFloat(el.textContent.replace('$', ''));
const converted = usd * exchangeRate;
el.textContent = currencySymbol + converted.toFixed(2);
});
This approach has three problems:
1. Layout shift. The page renders with USD prices, then JavaScript rewrites them. The text changes size (EUR prices are formatted differently than USD), containers resize, and CLS spikes. On the health brand I mentioned, this caused CLS of 0.11 against a 0.10 threshold.
2. Flash of wrong prices. For 200-400ms, every visitor sees USD prices before the script runs. International visitors see the price change in front of their eyes. This looks like a pricing error and destroys trust.
3. Fragility. The script depends on finding specific DOM patterns. Theme updates, section reordering, or new content blocks can break the selector logic silently. Prices stop converting with no visible error.
The fix on the health brand: I deleted the entire 90-line JavaScript rewriter and replaced all 38 hardcoded prices with Liquid money filters. CLS dropped from 0.11 to 0.00. No more price flashing. Currency conversion works server-side before the page reaches the browser.
Step-by-Step Fix Process
Step 1: Find All Hardcoded Prices
Search your theme code for your currency symbol. For USD stores:
grep -rn '\$[0-9]' sections/ snippets/ templates/ layout/
grep -rn '\$[0-9]' config/settings_data.json
Also search for common price patterns in the theme editor: open each section in the customizer and look for text fields containing prices.
Step 2: Categorize Each Instance
For each hardcoded price, determine the source:
- Liquid template code - Replace with
{{ amount_in_cents | money }} - Section schema default - Change to a cents input field + money filter
- settings_data.json stored value - Update in theme customizer after changing the schema
- Metafield value - Change metafield type or add money filter to the template
- Blog/page content - Remove specific prices or accept they will not convert
Step 3: Replace Using the Money Filter
The core pattern:
<!-- Static amount you control -->
{{ 5000 | money }}
<!-- Amount from a setting (stored as cents string) -->
{{ section.settings.price_cents | times: 1 | money }}
<!-- Product price (already in cents) -->
{{ product.price | money }}
<!-- Formatted with currency code -->
{{ product.price | money_with_currency }}
<!-- Without currency symbol -->
{{ product.price | money_without_currency }}
Step 4: Delete Any JavaScript Price Rewriters
Search for scripts that manipulate price text in the DOM. Common patterns:
grep -rn 'currency' assets/*.js snippets/*.liquid
grep -rn 'money\|exchange\|convert' assets/*.js
If you find an async price conversion script, delete it after confirming all prices now use Liquid money filters.
Step 5: Test Every Market
After deploying, test your store in at least 3 different currencies:
- Change your market preview in Shopify Admin > Settings > Markets > Preview
- Or use a VPN to visit from different countries
- Check: announcement bar, hero section, product cards, PDP, cart, and any promotional sections
Every price should display in the local currency with correct formatting. No flashing. No layout shift.
Common Money Filter Mistakes
Using | money on a string instead of a number. If a setting stores “29.99” as text, | money may produce unexpected results. Multiply by 100 first to convert to cents: {{ section.settings.price | times: 100 | money }}.
Confusing dollars and cents. Shopify stores prices in the smallest currency unit (cents for USD, pence for GBP). {{ 50 | money }} outputs $0.50, not $50.00. For $50.00, use {{ 5000 | money }}.
Hardcoding currency symbols in Liquid. Never write ${{ product.price | money_without_currency }}. The | money filter already includes the correct symbol for the visitor’s currency. Adding a dollar sign manually breaks every non-USD market.
Forgetting compare-at prices. If your product cards show both the current price and the original price, both need money filters. A common pattern is dynamic current price but hardcoded compare-at in a custom badge or savings calculation.
The Liquid | money filter is the single most important filter for international Shopify stores. Use it on every price, everywhere, without exception. For more Liquid best practices, see my Shopify Liquid Development Guide. For the complete CRO audit framework including international pricing checks, read the Shopify CRO Audit Checklist.