Practical guide

How to design an online store for multiple countries and currencies

Country, language, and currency are three different dimensions. Model them separately and combine them in a sales market.

30 minutes · E-commerce architecture

In short

One locale is not one country

One country can use several languages and one language can serve several countries. The selected currency is not automatically the shipping or company accounting currency either.

Define a sales market as explicit configuration: country, allowed locales, price lists, currencies, tax, shipping, payment, and domains. An order stores an immutable snapshot.

Prepare

What to learn before designing

Do not begin with flag buttons. List real business differences between markets and an owner for every setting.

  • Markets, languages, domains, currencies, warehouses, shipping, payments, and selling legal entities.
  • Price creation rules: independent price list, exchange-rate conversion, psychological rounding, and validity period.
  • Content that is translated versus content that differs commercially: range, availability, name, description, or terms.
  • Tax and document rules verified with accounting and legal specialists for each country rather than undocumented conditionals.

Steps 1 to 3

Separate the global product from its sales market

Share stable product identity while binding availability, price, and presentation to a channel or market.

1. Create an explicit market model

  1. A global product keeps SKU and shared technical data. A market listing decides whether and how it is sold and presented.
  2. Store locale as language and region such as en-GB or de-AT. Never use a flag alone as a language or business rule.
  3. Select market from a verified domain, user choice, and delivery context. GeoIP can suggest a default but must not silently change a basket.
  4. Every background job, cache key, and search document carries market ID to prevent data leaking between markets.
market = country + allowed_locales + domains + price_lists + tax_profile
Unicode CLDR language and territory data

2. Store multicurrency prices exactly

  1. Model price as an exact amount and currency code. Do not use float or hard-code two decimal places for every currency.
  2. Each currency can have a price list with valid_from and valid_to. An exchange rate informs a new price; it never rewrites an old order.
  3. Clearly mark an indicative secondary currency. Checkout and payment need one binding currency and exact final total.
  4. At confirmation, snapshot line prices, discounts, shipping, tax, currency, and any applied exchange rate into the order.
price_list(market_id, currency, product_id, amount, valid_from, valid_to)
Official PHP number and currency localization documentation

3. Localize content, URLs, and workflows

  1. Store translations by locale with an explicit fallback. Missing content must not silently publish the wrong language without a decision.
  2. Localize money, dates, units, addresses, and phone validation. Keep structured values internally, not their display string.
  3. Set canonical and hreflang for every actual language URL. A switch should lead to matching content, not always the homepage.
  4. Choose shipping, payment, email, and documents from the market and snapshot them into the order. Configuration changes must not rewrite history.
product_translation(product_id, locale, name, slug, description)
W3C Internationalization resources

Step 4

Test combinations, not only each market

Bugs appear when currency, language, or market changes in the middle of basket or checkout.

  1. Build a market, locale, and currency matrix

    Verify allowed and forbidden combinations, prices, availability, shipping, payment, and address format.

    php bin/phpunit --filter MarketCheckout
  2. Change the price list after ordering

    The historical order and document retain original amounts, currency, exchange rate, and tax snapshot.

  3. Switch language in an active basket

    Items remain and labels translate; changing market instead requires explicit repricing and availability validation.

When it goes wrong

Common mistakes

Changing language also changes price and stock

Language is incorrectly used as market. Separate locale from sales market and validate market changes independently.

An order changed after an exchange-rate update

History points at a live price list. Snapshot exact amounts, currency, and applied rate into the order.

A product leaks into another market

Market scope is missing in a repository, cache, index, or background job. Require market ID at every boundary.

A formatted price cannot be calculated again

A localized string entered the domain. Keep amount and currency and use NumberFormatter only at input or output boundaries.

Done

Every market has clear rules without copying the entire store.

Products share identity while the market controls localization, price lists, availability, and checkout. Historical orders remain immutable snapshots.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.