Geolocation Versus Language Preferences at the Edge
This guide, part of Locale Detection & Edge Routing, separates two questions that multilingual sites often mix up: which language a reader wants, and which region’s prices, shipping, availability and legal rules apply to them. Language comes from the reader’s explicit choice and browser preferences; region can reasonably come from geolocation. The guide shows how to use each signal for its own question at the edge, and how to keep pages cacheable and shareable while doing so.
Every CDN offers the visitor’s country as a request header, and it is tempting to route by it. The problem is that countries do not map to languages. Switzerland has four national languages, Canada two, Belgium three, India many; millions of people live in countries whose majority language they do not read; travellers carry their language with them; and corporate networks and VPNs place users in the wrong country entirely. A French speaker in Zurich routed to German by IP will find the site annoying at best.
The Problem
An electronics retailer routed every visitor by IP country to a country site with a fixed language: Switzerland to German, Belgium to Dutch, Canada to English. French-speaking Swiss and Belgian customers, and French-speaking Canadians, had to find a small link in the footer to switch, and the site forgot their choice on the next visit. Search engines, crawling mostly from the United States, were always sent to the US English site, so the other country sites were poorly indexed. Prices were correct for each country, which was the only part of the design that worked as intended.
How to Separate Language and Region
Language. Decided by the URL for every page, and at locale-less entry points by the language cookie and Accept-Language, as described in the root redirect guide. Geolocation does not choose the language.
Region. Decided by an explicit region choice when the reader made one, stored in its own cookie, and otherwise defaulted from IP geolocation. Region affects currency, prices, tax display, shipping options, product availability and region-specific legal content.
Regional URLs where content truly differs. When regional differences change the page substantially, give the region its own locale and URLs, such as fr-CH and de-CH, with hreflang. When they only change details like price and currency, keep one page per language and resolve the details separately.
Always overridable. Show the detected region clearly, for example “Prices for Switzerland in CHF”, with a way to change it that is remembered.
Implementation
The edge middleware passes the country from the CDN header into the application as a default region, without redirecting. The page renders in the URL’s language; region-dependent components read the region.
// middleware.ts (excerpt)
import { NextResponse, type NextRequest } from "next/server";
const REGIONS = new Set(["CH", "DE", "AT", "FR", "BE", "CA", "US", "GB"]);
export function middleware(req: NextRequest) {
const res = NextResponse.next();
const chosen = req.cookies.get("REGION")?.value;
const geo = req.headers.get("x-vercel-ip-country") ?? req.headers.get("cf-ipcountry") ?? "";
const region = chosen && REGIONS.has(chosen) ? chosen : REGIONS.has(geo) ? geo : "US";
res.headers.set("x-region", region); // read by server components for prices and shipping
return res;
}
To keep product pages cacheable, render region-independent content, text, images and specifications, into the cached HTML, and load region-dependent details, price, currency and shipping estimate, in a small request that varies by region, or render them at the edge per region with the region in the cache key. Never vary the whole HTML page on the country header.
// components/regional-price.tsx
"use client";
import useSWR from "swr";
export function RegionalPrice({ sku }: { sku: string }) {
const { data } = useSWR(`/api/price/${sku}`, (u: string) => fetch(u).then((r) => r.json()));
return (
<p className="price" aria-live="polite" style={{ minHeight: "1.5em" }}>
{data ? new Intl.NumberFormat(document.documentElement.lang, { style: "currency", currency: data.currency }).format(data.amount) : null}
</p>
);
}
The price endpoint reads the region cookie or country header and returns the price for that region, cached per region at the CDN with the region in the cache key. The number is formatted in the page’s language with the region’s currency, so a French-speaking reader in Switzerland sees “CHF 129.00” formatted for French.
Regional locales for genuinely different content
When a region needs different content, not just different prices, create a regional locale with its own URLs and hreflang, as described in regional variants and x-default. Readers reach it through the switcher, a region selector or a suggestion, never through a forced geolocation redirect, and search engines reach it through hreflang.
Suggesting a region or language
Geolocation is still useful for suggestions. When a visitor from Switzerland lands on the German-language page for Germany, a small banner can offer the Swiss variant with local prices and delivery: “You seem to be in Switzerland. Show prices and delivery for Switzerland?” Accepting sets the region cookie, and, if a regional locale exists, navigates to its equivalent page. Declining is remembered for the session. The banner is computed outside the cached HTML, from the country header in a tiny endpoint or at the edge, so pages remain identical for everyone and cacheable. This captures the benefit of geolocation, helping readers find the right regional offer, without forcing anyone anywhere.
Configuration Reference
| Decision | Signal | Storage | Cache impact |
|---|---|---|---|
| Language | URL, cookie, Accept-Language | URL, language cookie | per locale URL |
| Region | region cookie, IP country default | region cookie | small regional fragments only |
| Prices | region | none | per region at the price endpoint |
| Regional variants | regional locale URL | URL | per locale URL |
| Overrides | visible region selector | region cookie | none |
Gotchas & Edge Cases
- Crawlers and geolocation. Crawlers mostly come from a few countries. Anything decided only by geolocation is invisible to search engines for other regions; that is acceptable for prices, not for content.
- Shared links. A link to a product shows the recipient their own region’s price, which is correct; it must not show them a different language.
- VPNs and corporate networks. Geolocation is wrong for many users. Always make the region visible and changeable.
- Legal requirements. Some regions require specific price or tax displays. Resolve them from the region, and make sure the region can be set explicitly for users whose IP is misleading.
Worked Example
The electronics retailer stopped routing by country. Language came from the URL, with a root redirect by preference cookie and Accept-Language; Switzerland, Belgium and Canada got regional French and German or Dutch locales with hreflang where content differed; prices and shipping came from a region cookie defaulting to the IP country, shown with a “change region” link. French-speaking Swiss visitors now reached French pages directly, and search engines began indexing the Swiss French and Belgian French pages within weeks.
Communicating Region to Readers
Readers should always know which region the site assumes and how to change it, because prices and shipping depend on it. Show the region near prices, “Prices for Switzerland in CHF”, and in the header or footer, with a selector that lists regions by name, not by flag alone. When the detected region differs from the language’s usual region, for example a German-language page viewed from France, show prices for the detected region but offer the switch explicitly. At checkout, confirm the region again before payment, since the delivery address is the definitive signal. Record region changes as explicit choices in the region cookie, just like language choices, so the site respects them on future visits.
Rollout Checklist
- Stop redirecting by IP country; keep language in the URL.
- Default the region from geolocation and let readers override it.
- Render language content into cacheable HTML; load regional details separately.
- Create regional locales only where content genuinely differs.
- Show the active region and a way to change it near prices.
- Check crawler access to every regional locale.
Frequently Asked Questions
Is geolocation ever right for language?
As a weak hint when nothing else is known, for example for a language suggestion when Accept-Language is missing. Never as a forced redirect.
Should currency follow language?
No. Currency follows region; a German speaker in Switzerland pays in CHF.
How accurate is IP geolocation?
Accurate at country level for most users, but wrong for VPN, corporate and mobile roaming traffic. Treat it as a sensible default that readers can always correct.
What about users who are logged in?
Use the region and language stored in their profile, typically the delivery country and chosen language, and treat geolocation as irrelevant for them while they remain signed in on that device.
Does separating region from language complicate caching?
Only for the small regional fragments, which cache per region at the edge. The main HTML caches per language URL as before.