[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #15 · 2026-10-01 · Example · Hard · for [geo testing](https://cdpfleet.com/geo-testing), [web scraping](https://cdpfleet.com/web-scraping)

# Three countries, one script — and reading values the way the site does

Camoufox personas for the US, Germany and Japan: timezone, language, local dates and numbers, geolocation, all consistent with the exit country.

[Camoufox](https://cdpfleet.com/docs/browsers/camoufox)

Run on production on 2026-10-23: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

Prices, search results and availability change by country, so you run the same flow as a visitor from several countries. Each visit has to be consistent — IP, timezone, language and formats from the same country — or the site notices. And when you check that from your script, you have to read the values the way the site does.

## What we used, and why

| What | Why |
| --- | --- |
| `PROXY_URL_US`, `_DE`, `_JP` | Exits in each country (most providers take a country in the proxy username). |
| `locale` | Per country: `en-US`, `de-DE`, `ja-JP` — languages, `Accept-Language`, and number/date formats. |
| `geoip: true` | Camoufox sets the timezone and geolocation from the proxy's exit IP at launch. |
| `main_world_eval: true` + `"mw:"` | Runs the check in the page's own JavaScript world — the world the site's scripts see. |
| `context.grantPermissions(['geolocation'])` | As if the visitor allowed location access. |

## How it works

1.  Launch one Camoufox session per country with that country's exit and locale, all at once.
2.  Look up the exit IP's city, timezone and coordinates.
3.  In the page's main world, read languages, timezone, a fixed UTC instant formatted locally, a number, a price and the geolocation.
4.  For contrast, read the timezone again with a default `page.evaluate`.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/15-three-countries), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`
-   `PROXY_URL_DE` — a proxy exit in Germany
-   `PROXY_URL_US` — a proxy exit in the United States
-   `PROXY_URL_JP` — a proxy exit in Japan

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
import { firefox } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const COUNTRIES = [
  { country: 'United States', locale: 'en-US', proxy: process.env.PROXY_URL_US },
  { country: 'Germany', locale: 'de-DE', proxy: process.env.PROXY_URL_DE },
  { country: 'Japan', locale: 'ja-JP', proxy: process.env.PROXY_URL_JP },
];

// What a localizing site reads in the page. Run in the PAGE's own JavaScript world
// ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
// the same way and can report the server's UTC timezone instead of the persona's.
const LOCAL_VIEW = `(async () => {
  const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
    (p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
  return {
    languages: navigator.languages,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
    number: (1234567.891).toLocaleString(),
    price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
    position,
  };
})()`;

// Great-circle distance in km.
const km = (a, b) => {
  const r = (d) => (d * Math.PI) / 180;
  const h = Math.sin(r(b.lat - a.lat) / 2) ** 2 + Math.cos(r(a.lat)) * Math.cos(r(b.lat)) * Math.sin(r(b.lon - a.lon) / 2) ** 2;
  return Math.round(12742 * Math.asin(Math.sqrt(h)));
};

async function persona({ country, locale, proxy }) {
  const res = await fetch('https://starter.cdpfleet.com/camoufox/session', {
    method: 'POST',
    headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
    // geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
    body: JSON.stringify({ proxy, headless: true, os: 'windows', locale, geoip: true, main_world_eval: true }),
  });
  if (!res.ok) return { country, error: `launch ${res.status} ${await res.text()}` };
  const { wsUrl } = await res.json();
  const browser = await firefox.connect(wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    const context = await browser.newContext();
    await context.grantPermissions(['geolocation']); // as if the visitor clicked "Allow"
    const page = await context.newPage();
    const exit = await (await page.goto('http://ip-api.com/json/?fields=country,city,timezone,lat,lon', { timeout: 60000 })).json();
    await page.goto('https://httpbin.org/html', { timeout: 60000 });
    const seen = await page.evaluate(`mw:${LOCAL_VIEW}`);
    const isolatedTimezone = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
    return {
      country, locale,
      exit: `${exit.city}, ${exit.country}`,
      exit_timezone: exit.timezone,
      ...seen,
      position_km_from_exit: seen.position ? km(seen.position, exit) : null,
      isolated_world_timezone: isolatedTimezone, // what a default page.evaluate would have reported
    };
  } finally {
    await browser.close();
  }
}

console.log(JSON.stringify(await Promise.all(COUNTRIES.map(persona)), null, 2));
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
import json
import math
import os
from concurrent.futures import ThreadPoolExecutor

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]
COUNTRIES = [
    {"country": "United States", "locale": "en-US", "proxy": os.environ["PROXY_URL_US"]},
    {"country": "Germany", "locale": "de-DE", "proxy": os.environ["PROXY_URL_DE"]},
    {"country": "Japan", "locale": "ja-JP", "proxy": os.environ["PROXY_URL_JP"]},
]

# What a localizing site reads in the page. Run in the PAGE's own JavaScript world
# ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
# the same way and can report the server's UTC timezone instead of the persona's.
LOCAL_VIEW = """(async () => {
  const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
    (p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
  return {
    languages: navigator.languages,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
    number: (1234567.891).toLocaleString(),
    price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
    position,
  };
})()"""

def km(a, b):
    """Great-circle distance in km."""
    r = math.radians
    h = math.sin(r(b["lat"] - a["lat"]) / 2) ** 2 + math.cos(r(a["lat"])) * math.cos(r(b["lat"])) * math.sin(r(b["lon"] - a["lon"]) / 2) ** 2
    return round(12742 * math.asin(math.sqrt(h)))

def persona(c):
    # geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
    res = requests.post("https://starter.cdpfleet.com/camoufox/session", headers={"x-api-key": KEY}, timeout=60,
                        json={"proxy": c["proxy"], "headless": True, "os": "windows", "locale": c["locale"],
                              "geoip": True, "main_world_eval": True})
    if not res.ok:
        return {"country": c["country"], "error": f"launch {res.status_code} {res.text}"}
    with sync_playwright() as p:  # the sync API is per thread
        browser = p.firefox.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
        try:
            context = browser.new_context()
            context.grant_permissions(["geolocation"])  # as if the visitor clicked "Allow"
            page = context.new_page()
            exit_ = page.goto("http://ip-api.com/json/?fields=country,city,timezone,lat,lon", timeout=60000).json()
            page.goto("https://httpbin.org/html", timeout=60000)
            seen = page.evaluate(f"mw:{LOCAL_VIEW}")
            isolated = page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone")
            return {"country": c["country"], "locale": c["locale"], "exit": f"{exit_['city']}, {exit_['country']}",
                    "exit_timezone": exit_["timezone"], **seen,
                    "position_km_from_exit": km(seen["position"], exit_) if seen["position"] else None,
                    "isolated_world_timezone": isolated}  # what a default page.evaluate would have reported
        finally:
            browser.close()

with ThreadPoolExecutor(len(COUNTRIES)) as pool:
    print(json.dumps(list(pool.map(persona, COUNTRIES)), indent=2, ensure_ascii=False))
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");

  // What a localizing site reads in the page. Run in the PAGE's own JavaScript world
  // ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
  // the same way and can report the server's UTC timezone instead of the persona's.
  static final String LOCAL_VIEW = """
      (async () => {
        const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
          (p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
        return {
          languages: navigator.languages,
          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
          date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
          number: (1234567.891).toLocaleString(),
          price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
          position,
        };
      })()""";

  // Great-circle distance in km.
  static long km(double lat1, double lon1, double lat2, double lon2) {
    double h = Math.pow(Math.sin(Math.toRadians(lat2 - lat1) / 2), 2)
        + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.pow(Math.sin(Math.toRadians(lon2 - lon1) / 2), 2);
    return Math.round(12742 * Math.asin(Math.sqrt(h)));
  }

  static JsonObject persona(String country, String locale, String proxy) throws Exception {
    Gson gson = new Gson();
    // geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
    String body = "{\"proxy\": " + gson.toJson(proxy) + ", \"headless\": true, \"os\": \"windows\", \"locale\": " + gson.toJson(locale)
        + ", \"geoip\": true, \"main_world_eval\": true}";
    HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/camoufox/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    JsonObject out = new JsonObject();
    out.addProperty("country", country);
    if (res.statusCode() != 200) { out.addProperty("error", "launch " + res.statusCode() + " " + res.body()); return out; }
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
    try (Playwright playwright = Playwright.create()) { // Playwright objects are per thread
      Browser browser = playwright.firefox().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        BrowserContext context = browser.newContext();
        context.grantPermissions(List.of("geolocation")); // as if the visitor clicked "Allow"
        Page page = context.newPage();
        JsonObject exit = JsonParser.parseString(page.navigate("http://ip-api.com/json/?fields=country,city,timezone,lat,lon",
            new Page.NavigateOptions().setTimeout(60000)).text()).getAsJsonObject();
        page.navigate("https://httpbin.org/html", new Page.NavigateOptions().setTimeout(60000));
        JsonObject seen = gson.toJsonTree(page.evaluate("mw:" + LOCAL_VIEW)).getAsJsonObject();
        Object isolated = page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone");
        out.addProperty("locale", locale);
        out.addProperty("exit", exit.get("city").getAsString() + ", " + exit.get("country").getAsString());
        out.add("exit_timezone", exit.get("timezone"));
        for (String k : seen.keySet()) out.add(k, seen.get(k));
        JsonElement pos = seen.get("position");
        out.add("position_km_from_exit", pos == null || pos.isJsonNull() ? JsonNull.INSTANCE : new JsonPrimitive(km(
            pos.getAsJsonObject().get("lat").getAsDouble(), pos.getAsJsonObject().get("lon").getAsDouble(),
            exit.get("lat").getAsDouble(), exit.get("lon").getAsDouble())));
        out.addProperty("isolated_world_timezone", (String) isolated); // what a default page.evaluate would have reported
        return out;
      } finally {
        browser.close();
      }
    }
  }

  public static void main(String[] args) throws Exception {
    String[][] countries = {
      {"United States", "en-US", System.getenv("PROXY_URL_US")},
      {"Germany", "de-DE", System.getenv("PROXY_URL_DE")},
      {"Japan", "ja-JP", System.getenv("PROXY_URL_JP")},
    };
    ExecutorService pool = Executors.newFixedThreadPool(countries.length);
    List<Future<JsonObject>> jobs = new ArrayList<>();
    for (String[] c : countries) jobs.add(pool.submit(() -> persona(c[0], c[1], c[2])));
    JsonArray rows = new JsonArray();
    for (Future<JsonObject> f : jobs) rows.add(f.get());
    pool.shutdown();
    System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(rows));
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
var countries = new (string Country, string Locale, string Proxy)[]
{
    ("United States", "en-US", Environment.GetEnvironmentVariable("PROXY_URL_US")!),
    ("Germany", "de-DE", Environment.GetEnvironmentVariable("PROXY_URL_DE")!),
    ("Japan", "ja-JP", Environment.GetEnvironmentVariable("PROXY_URL_JP")!),
};

// What a localizing site reads in the page. Run in the PAGE's own JavaScript world
// ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
// the same way and can report the server's UTC timezone instead of the persona's.
const string LocalView = """
    (async () => {
      const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
        (p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
      return {
        languages: navigator.languages,
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
        date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
        number: (1234567.891).toLocaleString(),
        price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
        position,
      };
    })()
    """;

// Great-circle distance in km.
long Km(double lat1, double lon1, double lat2, double lon2)
{
    double R(double d) => d * Math.PI / 180;
    var h = Math.Pow(Math.Sin(R(lat2 - lat1) / 2), 2) + Math.Cos(R(lat1)) * Math.Cos(R(lat2)) * Math.Pow(Math.Sin(R(lon2 - lon1) / 2), 2);
    return (long)Math.Round(12742 * Math.Asin(Math.Sqrt(h)));
}

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();

async Task<JsonObject> Persona(string country, string locale, string proxy)
{
    // geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
    var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/camoufox/session",
        new { proxy, headless = true, os = "windows", locale, geoip = true, main_world_eval = true });
    if (!res.IsSuccessStatusCode) return new JsonObject { ["country"] = country, ["error"] = $"launch {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}" };
    var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
    var browser = await playwright.Firefox.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
    try
    {
        var context = await browser.NewContextAsync();
        await context.GrantPermissionsAsync(new[] { "geolocation" }); // as if the visitor clicked "Allow"
        var page = await context.NewPageAsync();
        var exit = JsonNode.Parse(await (await page.GotoAsync("http://ip-api.com/json/?fields=country,city,timezone,lat,lon", new() { Timeout = 60000 }))!.TextAsync())!;
        await page.GotoAsync("https://httpbin.org/html", new() { Timeout = 60000 });
        var seen = JsonNode.Parse((await page.EvaluateAsync<JsonElement>("mw:" + LocalView)).GetRawText())!.AsObject();
        var isolated = await page.EvaluateAsync<string>("Intl.DateTimeFormat().resolvedOptions().timeZone");
        var outp = new JsonObject
        {
            ["country"] = country, ["locale"] = locale, ["exit"] = $"{exit["city"]}, {exit["country"]}", ["exit_timezone"] = exit["timezone"]!.DeepClone(),
        };
        foreach (var (k, v) in seen) outp[k] = v?.DeepClone();
        var pos = seen["position"];
        outp["position_km_from_exit"] = pos is null ? null
            : Km((double)pos["lat"]!, (double)pos["lon"]!, (double)exit["lat"]!, (double)exit["lon"]!);
        outp["isolated_world_timezone"] = isolated; // what a default page.evaluate would have reported
        return outp;
    }
    finally
    {
        await browser.CloseAsync();
    }
}

var rows = await Task.WhenAll(countries.Select(c => Persona(c.Country, c.Locale, c.Proxy)));
Console.WriteLine(new JsonArray(rows.ToArray<JsonNode?>()).ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math"
	"net/http"
	"os"
	"sync"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

// What a localizing site reads in the page. Run in the PAGE's own JavaScript world
// ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
// the same way and can report the server's UTC timezone instead of the persona's.
const localView = `(async () => {
  const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
    (p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
  return {
    languages: navigator.languages,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
    number: (1234567.891).toLocaleString(),
    price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
    position,
  };
})()`

func launch(name string, options map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(options)
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

func must[T any](v T, err error) T {
	if err != nil {
		log.Fatal(err)
	}
	return v
}

// Great-circle distance in km.
func km(lat1, lon1, lat2, lon2 float64) int64 {
	r := func(d float64) float64 { return d * math.Pi / 180 }
	h := math.Pow(math.Sin(r(lat2-lat1)/2), 2) + math.Cos(r(lat1))*math.Cos(r(lat2))*math.Pow(math.Sin(r(lon2-lon1)/2), 2)
	return int64(math.Round(12742 * math.Asin(math.Sqrt(h))))
}

// Evaluate returns whole numbers as int and others as float64.
func num(v any) float64 {
	switch n := v.(type) {
	case float64:
		return n
	case int:
		return float64(n)
	case int64:
		return float64(n)
	}
	return 0
}

func persona(pw *playwright.Playwright, country, locale, proxy string) map[string]any {
	fail := func(err error) map[string]any { return map[string]any{"country": country, "error": err.Error()} }
	// geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
	session, err := launch("camoufox", map[string]any{"proxy": proxy, "headless": true, "os": "windows", "locale": locale, "geoip": true, "main_world_eval": true})
	if err != nil {
		return fail(err)
	}
	browser, err := pw.Firefox.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
	if err != nil {
		return fail(err)
	}
	defer browser.Close()
	context, _ := browser.NewContext()
	context.GrantPermissions([]string{"geolocation"}) // as if the visitor clicked "Allow"
	page, _ := context.NewPage()
	res, err := page.Goto("http://ip-api.com/json/?fields=country,city,timezone,lat,lon", playwright.PageGotoOptions{Timeout: playwright.Float(60000)})
	if err != nil {
		return fail(err)
	}
	var exit struct {
		Country, City, Timezone string
		Lat, Lon                float64
	}
	res.JSON(&exit)
	if _, err := page.Goto("https://httpbin.org/html", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}); err != nil {
		return fail(err)
	}
	v, err := page.Evaluate("mw:" + localView)
	if err != nil {
		return fail(err)
	}
	isolated, _ := page.Evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone")
	out := v.(map[string]any)
	out["country"], out["locale"], out["exit"], out["exit_timezone"] = country, locale, exit.City+", "+exit.Country, exit.Timezone
	out["position_km_from_exit"] = nil
	if pos, ok := out["position"].(map[string]any); ok {
		out["position_km_from_exit"] = km(num(pos["lat"]), num(pos["lon"]), exit.Lat, exit.Lon)
	}
	out["isolated_world_timezone"] = isolated // what a default page.evaluate would have reported
	return out
}

func main() {
	pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
	defer pw.Stop()
	countries := [][3]string{
		{"United States", "en-US", os.Getenv("PROXY_URL_US")},
		{"Germany", "de-DE", os.Getenv("PROXY_URL_DE")},
		{"Japan", "ja-JP", os.Getenv("PROXY_URL_JP")},
	}
	rows := make([]map[string]any, len(countries))
	var wg sync.WaitGroup
	for i, c := range countries {
		wg.Add(1)
		go func(i int, c [3]string) { defer wg.Done(); rows[i] = persona(pw, c[0], c[1], c[2]) }(i, c)
	}
	wg.Wait()
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	enc.SetIndent("", "  ")
	enc.Encode(rows)
	fmt.Print(buf.String())
}
```

## What we got

| Persona | Exit | Timezone (page) | Default page.evaluate | 15:30 UTC shown as | Number | Price | Geolocation vs exit (km) |
| --- | --- | --- | --- | --- | --- | --- | --- |
| United States | Lafayette, United States | America/New\_York | America/New\_York | 10/1/2026, 11:30:00 AM | 1,234,567.891 | €49.90 | 1580 |
| Germany | Munich, Germany | Europe/Berlin | Europe/Berlin | 1.10.2026, 17:30:00 | 1.234.567,891 | 49,90 € | 501 |
| Japan | Kobe, Japan | Asia/Tokyo | Asia/Tokyo | 2026/10/2 0:30:00 | 1,234,567.891 | €49.90 | 425 |

From the Node.js run on 2026-10-23. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
[
  {
    "country": "United States",
    "locale": "en-US",
    "exit": "Lafayette, United States",
    "exit_timezone": "America/Chicago",
    "languages": [
      "en-US",
      "en"
    ],
    "timezone": "America/New_York",
    "date": "10/1/2026, 11:30:00 AM",
    "number": "1,234,567.891",
    "price": "€49.90",
    "position": {
      "lat": 41.5433,
      "lon": -81.4838
    },
    "position_km_from_exit": 1580,
    "isolated_world_timezone": "America/New_York"
  },
  {
    "country": "Germany",
    "locale": "de-DE",
    "exit": "Munich, Germany",
    "exit_timezone": "Europe/Berlin",
    "languages": [
      "de-DE",
      "de"
    ],
    "timezone": "Europe/Berlin",
    "date": "1.10.2026, 17:30:00",
    "number": "1.234.567,891",
    "price": "49,90 €",
    "position": {
      "lat": 52.524526,
      "lon": 13.410037
    },
    "position_km_from_exit": 501,
    "isolated_world_timezone": "Europe/Berlin"
  },
  {
    "country": "Japan",
    "locale": "ja-JP",
    "exit": "Kobe, Japan",
    "exit_timezone": "Asia/Tokyo",
    "languages": [
      "ja-JP",
      "ja"
    ],
    "timezone": "Asia/Tokyo",
    "date": "2026/10/2 0:30:00",
    "number": "1,234,567.891",
    "price": "€49.90",
    "position": {
      "lat": 35.689497,
      "lon": 139.692317
    },
    "position_km_from_exit": 425,
    "isolated_world_timezone": "Asia/Tokyo"
  }
]
```

## Takeaways

-   **Timezone and languages are consistent:** the browser timezone matches the exit country, `navigator.languages` and `Accept-Language` follow `locale`, and the same instant (15:30 UTC) shows as local time — morning in New York, afternoon in Berlin, after midnight in Tokyo.
-   **`locale` reaches JavaScript's formats too.** With `locale: "de-DE"` the German persona formats `1.234.567,891` and `1.10.2026, 17:30:00`, and the Japanese one `2026/10/2 0:30:00`, matching their languages. Measured 2026-10-23 on the fleet's current Camoufox 156 build (the official Camoufox library); the first 156 build, live 2026-10-21 to 2026-10-22, kept `Intl` on `en-US` whatever the locale.
-   **The old trap is gone on 156:** on 152 a default `page.evaluate` reported `UTC` for all three (Playwright's isolated world wasn't patched); on 156 it reports the persona's timezone too. `main_world_eval` with the `"mw:"` prefix is still the way to read exactly what the site's scripts see.
-   **Geolocation lands in the right country, not the right city:** geo-IP databases disagree, and a rotating residential proxy may exit from a different IP than the one Camoufox looked up at launch. Use sticky sessions when city-level consistency matters.
-   **Currency is the site's choice, not the browser's:** a euro price is only formatted, never converted. It's `€49.90` for the US and Japanese personas and `49,90 €` for the German one.

---
Source: https://cdpfleet.com/docs/cases/three-countries · Markdown: https://cdpfleet.com/docs/cases/three-countries.md · All docs for AI tools: https://cdpfleet.com/llms.txt
