Designing Locale Preferences Across Cookies, Cloudflare Functions, and KV
Locale resolution: signals have authority levels, and persistence must not promote a weak one
Type the same display name as another user, and this public demo lets you read and overwrite their Cloudflare KV locale profile. That collision makes cross-device preferences easy to demonstrate, and makes the design unacceptable for production identity. Locale resolution combines preference, inference, derived policy, persistence, and identity lookup. Those values do not carry equal authority, and the architecture has to reflect that before anything gets written to durable storage.
TL;DR
Locale resolves from a valid name-mapped KV profile, a saved preference cookie, an optional simulated country, the Cloudflare request country, or a US/en/USD fallback, in that order. With an active name profile, an explicit override updates KV and refreshes the browser cookie. Without one, it updates only the cookie. Production needs an authenticated stable subject ID or a high-entropy possession credential.
Signals, provenance, and precedence
Every locale signal carries implicit metadata: where it came from, how durable it is, and how much trust it deserves. I built the selector so durable, explicit signals have more authority than inferred defaults. The first valid source wins:
- If a valid name cookie maps to a valid KV profile, use that profile.
- Otherwise, use a valid saved locale cookie.
- Otherwise, use the optional simulated country.
- Otherwise, use the country on the Cloudflare request.
- Otherwise, fall back to region
US, languageen, and currencyUSD.
Validation applies at every step. Cookies and KV records can contain obsolete or unsupported values. Invalid data is ignored rather than allowed to affect pricing presentation.
type Region = "US" | "GB" | "FR" | "DE";
type Language = "en" | "fr" | "de";
type Locale = {
region: Region;
language: Language;
currency: "USD" | "GBP" | "EUR";
};
const CURRENCY_BY_REGION: Record<Region, Locale["currency"]> = {
US: "USD",
GB: "GBP",
FR: "EUR",
DE: "EUR",
};
function withCurrency(
preference: { region: Region; language: Language },
): Locale {
return {
...preference,
currency: CURRENCY_BY_REGION[preference.region],
};
}
async function resolveLocale(request: Request, env: Env): Promise<Locale> {
const cookies = parseCookies(request.headers.get("Cookie") ?? "");
const name = validateNameCookie(cookies.profile_name);
if (name) {
const profile = await readDemoProfile(env.PREFERENCES, name);
if (profile) return withCurrency(profile);
}
const saved = validateLocaleCookie(cookies.locale);
if (saved) return withCurrency(saved);
const url = new URL(request.url);
const simulated = validateCountry(url.searchParams.get("country"));
if (simulated) return localeForCountry(simulated);
const cloudflareCountry = validateCountry(request.cf?.country);
if (cloudflareCountry) return localeForCountry(cloudflareCountry);
return { region: "US", language: "en", currency: "USD" };
}The simulated country sits below saved preferences because it is an inferred default. It cannot override a visitor's explicit choice.
Inference remains temporary and policy remains server-side
Country data helps with the first render. It does not prove residence or preference. Travelers, VPN users, and corporate networks often appear somewhere unexpected. A locale derived from the simulated country, Cloudflare country, or final fallback is therefore returned without writing browser state. This trust boundary also matters when preserving the real client IP through a proxy chain that rewrites the evidence.
Currency is derived rather than accepted as an independent client value. The client submits region and language. The server validates both, then CURRENCY_BY_REGION supplies currency. That prevents a request from constructing combinations such as region GB with currency USD unless the application policy changes.
Explicit overrides update the active persistence scope
An explicit override follows the context already established in the browser. When a valid name profile is active, the server writes the validated region and language to that KV profile and refreshes the locale cookie with the same values. KV is the durable source for the named profile; the cookie is its local browser projection. KV has higher precedence, so keeping both copies aligned avoids a stale local preference if the named context later becomes unavailable.
Without an active name profile, the server has no named durable target. The override updates only the locale cookie.
The cookie stores a compact value such as GB:en. It is sent with Path=/, Max-Age=31536000, HttpOnly, Secure, and SameSite=Lax.
function serializeLocaleCookie(
preference: { region: Region; language: Language },
): string {
return [
`locale=${preference.region}:${preference.language}`,
"Path=/",
"Max-Age=31536000",
"HttpOnly",
"Secure",
"SameSite=Lax",
].join("; ");
}The cookie is not signed, so I treat it as untrusted preference input. HttpOnly blocks ordinary browser JavaScript from reading or rewriting it. Server-side validation restricts both components to supported values. Currency is always recomputed.
Distributed-systems reality for KV and the cookie
The authority model meets its hardest test under partial failure. Three scenarios matter in practice.
When the KV read fails (a network error, not a miss), the resolver falls through to the locale cookie rather than returning an error. The failure mode is fall-through, not fail-closed. A visitor with a named profile active will see their last cookie value instead, which may be stale but is not fabricated.
When the KV write succeeds but the response or cookie update fails, the profile and the cookie diverge. On the next request, KV still wins because it has higher precedence, so the divergence self-heals as long as the KV write was the one that succeeded. If the KV write was the one that failed but the cookie update succeeded, the cookie temporarily outranks a KV profile that has not been updated. The next successful override corrects both.
Eventual consistency introduces a third case. A freshly written KV profile may not be visible from every edge location immediately. If a second request arrives at a different location before propagation completes, it may read an older profile that outranks a newer cookie. I do not promise immediate cross-device read-after-write behavior here. Locale profiles are small and propagation delays are short, so this tradeoff is tolerable in a demo. Storage that provides strong consistency is the right choice when that guarantee matters.
The name mapping is intentionally not identity
The cross-device mechanism accepts a display name, normalizes it, hashes the result with SHA-256, and uses the digest in the KV key. Normalization makes equivalent input forms resolve consistently. Hashing keeps the raw name out of the key.
function normalizeDisplayName(value: string): string {
return value.normalize("NFKC").trim().toLocaleLowerCase("en-US");
}
async function demoProfileKey(displayName: string): Promise<string> {
const normalized = normalizeDisplayName(displayName);
if (!normalized || normalized.length > 80) {
throw new Error("Invalid display name");
}
const bytes = new TextEncoder().encode(normalized);
const digest = await crypto.subtle.digest("SHA-256", bytes);
const hex = [...new Uint8Array(digest)]
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
return `profile:${hex}`;
}
async function readDemoProfile(
kv: KVNamespace,
displayName: string,
): Promise<{ region: Region; language: Language } | null> {
const stored = await kv.get(demoProfileKey(displayName), "json");
return validateStoredProfile(stored);
}Hashing does not prove ownership or make a predictable name secret. Anyone who supplies the same normalized name reaches the same profile, including different people with identical names. Production should key profiles by an authenticated stable subject ID. An account-free product should use a high-entropy possession credential instead, keeping the display name as display data rather than authorization.
Limitations
Shared-name ownership is the central identity limitation. The demo provides addressability, not exclusive control: it makes cross-device preference storage easy to inspect, but it cannot determine which person may read or overwrite a profile.
The eventual-consistency and partial-failure tradeoffs are covered above. The short version: failures fall through, the higher-precedence signal self-heals divergence when writes succeed in the right order, and strong-consistency storage is the correct choice when propagation delays are not acceptable.
FAQ
When is the locale cookie written?
Only after an explicit override. Inferred and fallback locales are returned without persistence. A named-profile override also writes KV; an unnamed override has only the browser cookie as its persistence target.
Why is currency absent from the cookie and KV profile?
Region determines currency through server-side policy. Recomputing it prevents contradictory client-controlled combinations and keeps that policy in one place.
