Expanding Globally: How to Internationalize Your E-Commerce Website
Internationalization (i18n) is engineering work you do once, in the architecture: Unicode text handling, locale-aware formatting, currency as integer minor units instead of floats. Localization (l10n) is work that repeats per market: translated strings, region-specific tax rules, local payment methods. Treating i18n as a translation task is the most common way commerce sites end up rebuilding their currency and date handling market by market.
i18n and l10n are different budgets, not one line item
Most commerce roadmaps put "internationalization" and "localization" in the same ticket. They shouldn't be, because they have different owners, different costs, and different failure modes.
- i18n is an architecture decision: can the codebase represent any language, currency, and date format without a code change per market.
- l10n is a content and operations decision: which languages, which currencies, which local payment methods and tax rules actually get turned on for a given market.
Get the architecture right once and each new market is a content and configuration task. Get it wrong and each new market is an engineering project.
Splitting the budget this way also clarifies ownership. Engineering owns i18n architecture and reviews it once per major refactor; content, marketing, and regional operations own l10n and revisit it every time a new market or campaign launches.
The architecture layer: what i18n actually requires
Unicode as the only acceptable text encoding
Every string your system stores, renders, and compares needs to handle the full Unicode range, not just Latin script. The Unicode Locale Data Markup Language (LDML) specification, published as Unicode Technical Standard #35, defines the data model every serious locale library builds on — plural rules, date and number formats, collation, and more, all keyed by locale identifier.
The Common Locale Data Repository (CLDR) is the actual dataset implementing that model: locale-specific formatting patterns maintained collaboratively and consumed by browsers, operating systems, and most locale libraries, including JavaScript's built-in Intl object.
Locale-aware formatting through the platform, not by hand
Date formats, number formats, and currency display all vary by locale in ways that aren't reducible to a simple find-and-replace on separators. JavaScript's Intl.NumberFormat handles this natively, pulling from CLDR data already built into the runtime.
new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(1234.5);
// "1.234,50 €" — decimal comma, thousands period, symbol placement
// all correct for de-DE without any manual string formatting.
Hand-rolled formatting logic reimplements a subset of what CLDR and Intl already give you for free, usually incorrectly for at least one locale nobody tested against.
If your codebase has a function called something like
formatDate(date, country)with a switch statement inside, it's reimplementing CLDR badly. Use the platform'sIntlAPIs and let the locale data live where it's actually maintained.
Money as integer minor units, currency codes from ISO 4217
Store money as an integer count of the currency's minor unit — cents, not a floating-point dollar amount — and identify the currency with a three-letter code from ISO 4217, maintained by SIX Group as the standard's official maintenance agency. Floating-point currency math produces rounding errors that compound across a shopping cart, and they're the kind of bug that surfaces as a support ticket about a total that's off by one cent.
Not every currency has the same number of minor units — most have two decimal places, some (like the Japanese yen) have zero, and a few have three. Reading the ISO 4217 minor-unit value per currency, rather than assuming two everywhere, is the detail most commerce systems get wrong on their first international launch.
The bug pattern is predictable: a price of ¥500 stored assuming two decimal places gets divided by 100 somewhere in the pipeline and displayed as ¥5.00. It passes every test written against USD or EUR test fixtures and fails silently the day a Japan launch goes live.
Pluralization is a category system, not a singular/plural switch
English has two plural forms, which makes it easy to assume every language does too. CLDR's plural rules define up to six categories per language — zero, one, two, few, many, and other — and which categories a given language actually uses varies. Arabic uses all six; Japanese uses only "other."
A commerce string like "3 items in your cart" needs the plural category the target language actually requires, not an if/else that assumes English's two-way split. Intl.PluralRules, built on the same CLDR data, resolves the correct category for a given number and locale without hand-written rules per language.
Flexible layout for text expansion and direction
Translated text doesn't occupy the same space as the source language — German strings commonly run longer than English ones for the same meaning. Layouts built with fixed-width containers around English copy break the moment translation lands.
Right-to-left languages like Arabic and Hebrew need more than mirrored CSS; the W3C's Internationalization Activity documents the full scope of bidirectional text handling, including cases where LTR content (a product SKU, a price) needs to render correctly inside an RTL sentence.
Locale resolution: figuring out which locale to actually serve
Before any formatting or translation happens, the system needs to decide which locale a given request should use. That decision has a defined precedence, not a guess.
The resolver checks signals in priority order and stops at the first match — it doesn't average or merge them.
Language tags follow BCP 47, not a custom scheme
Whatever the resolver outputs should be a valid language tag under IETF BCP 47 — en-US, pt-BR, zh-Hans — not an internal enum that needs its own mapping table to every library you integrate. Every major platform's Intl, CLDR, and locale-routing tooling assumes BCP 47 tags as input.
Two frameworks that build this in
Next.js locale routing
Next.js's internationalization routing documentation covers locale-prefixed paths, locale detection, and how to wire a resolver like the one above into the App Router's middleware layer, rather than building URL-locale handling from scratch.
Shopify Markets for commerce-specific localization
Shopify's Markets platform handles the commerce-specific half of localization — currency conversion, market-specific pricing, and regional domain or subfolder configuration — as a merchant-configurable layer on top of a single store, rather than as separate storefronts to maintain.
Search engines need to be told which locale is which
Locale-routed content is invisible to search engines unless each language and regional variant is explicitly declared. Google's guidance on localized versions documents using hreflang annotations to tell search engines which URL serves which language and region, so a French-Canadian shopper gets pointed at the fr-CA version instead of a generic fr-FR page.
Getting this wrong doesn't just hurt search ranking — it actively serves the wrong regional pricing, currency, or availability to searchers who land on a mismatched locale page.
Comparing localization strategies
| Strategy | What it handles well | Where it falls short |
|---|---|---|
| Separate storefront per market | Full control over region-specific content and layout | Highest maintenance cost; logic and fixes must be replicated per storefront |
| Single storefront, locale-routed (Next.js i18n routing) | One codebase, consistent behavior, easier to maintain | Needs careful content and translation-bundle architecture up front |
| Platform-managed markets (Shopify Markets) | Currency, pricing, and domain configuration handled by the platform | Customization is bounded by what the platform's Markets configuration exposes |
Rolling out a new market without a rebuild
Adding a market should mean configuration and content work, not an engineering sprint, if the architecture layer was built correctly the first time.
- Confirm the codebase already stores money as integer minor units tagged with an ISO 4217 code — retrofitting this after the fact touches every price calculation in the system.
- Confirm the locale resolver's precedence order is documented and tested, not assumed — a signed-in user's saved preference should win over a guessed browser header every time.
- Route the new locale through
Intl-based formatting and CLDR-backed translation bundles rather than adding a one-off formatting function for the new market. - Add the market's payment methods, tax handling, and currency through the commerce platform's market configuration layer, not through custom code forked per region.
The market that breaks your i18n architecture isn't the second one — teams usually get that far by copying what worked. It's the one with a script your string-length assumptions didn't anticipate, or a currency with zero decimal places your rounding logic assumed didn't exist.
FAQ
Should I detect locale automatically from the browser or let users choose explicitly?
Use the browser's Accept-Language header as a starting guess, but always let a signed-in user's saved preference override it, and always give an explicit switcher — automatic detection gets it wrong often enough that silent redirection frustrates users.
Why store currency as integer cents instead of a decimal or float?
Floating-point arithmetic introduces rounding errors that compound across cart totals, taxes, and discounts. Integer minor units avoid the problem entirely, at the cost of remembering to divide by the currency's minor-unit factor when displaying a value.
Do all currencies use two decimal places?
No. ISO 4217 specifies the minor-unit count per currency — the Japanese yen uses zero, some currencies use three. Read this per currency rather than hard-coding two everywhere.
Is Google Translate or a similar automated tool enough for localization?
It's a starting draft at best. Automated translation misses cultural context, idioms, and legal or regulatory phrasing that a market-specific reviewer catches — treat it as a first pass, not a shipped translation.
How is internationalization different from just supporting multiple languages?
Multiple languages is one output of internationalization, but i18n also covers currency, date and number formatting, text direction, and layout flexibility — a site can support ten languages and still fail internationally if currency handling or RTL layout weren't architected in.
What's the minimum i18n work worth doing even for a single-market launch?
Store money as integer minor units with an ISO 4217 code, and use Intl-based formatting instead of hand-rolled date and number strings. Both are nearly free to do correctly from day one and expensive to retrofit later.
Should each market get its own subdomain, subfolder, or country-code domain?
All three are valid URL structures for multi-regional sites; the choice affects hosting and SEO configuration more than the i18n architecture underneath. Whichever you pick, declare it consistently with hreflang annotations so search engines route shoppers to the right regional version.
References
- Unicode — Unicode Locale Data Markup Language (LDML), UTS #35
- Unicode — Common Locale Data Repository (CLDR)
- Unicode CLDR — Language plural rules
- Google Search Central — Managing multi-regional and multilingual sites
- MDN — Intl.NumberFormat
- SIX Group — ISO 4217 currency codes (maintenance agency)
- W3C — Internationalization Activity
- IETF RFC 5646 — Tags for Identifying Languages (BCP 47)
- Next.js — Internationalization routing
- Shopify — About Shopify Markets