# ADR-001: WithLocalePolicy — Locale/Proxy-Geo Decoupling

## Status: accepted

## Context

Foxhound's identity layer applies anti-detection principle #6: "proxy geo must
match identity locale/timezone". `applyGeoToConfig()` resolves the proxy exit IP
to a country, picks a matching timezone, locale, and language list, and applies
them all. This is correct for human-coherence: a real Russian user behind a
Russian proxy sends `Accept-Language: ru-RU`.

The CloakBrowser audit (Phase 3 + Phase 4) found that when scraping English-
language content (SERP email-enrichment dorks), a locale that matches the proxy
geo (e.g., `ru-RU`) is flagged by Google's anti-bot as a locale-query mismatch:
real Russian users don't send English email-PII dork queries. CloakBrowser, which
defaults to `en-US` regardless of proxy geo, bypassed the same query.

This creates a design tension:
- Principle #6 is correct for HUMAN coherence (real users match locale to geo).
- For SCRAPING English content, the coherent identity is: physical location =
  proxy geo, but language preference = content language.

A real-world analogy: an English-speaking expat in Russia has a Russian IP but
sends `Accept-Language: en-US`. That identity is coherent and not flagged.

## Decision

Add a `LocalePolicy` type and `WithLocalePolicy(policy)` option to the `identity`
package. The policy gates the final locale/language assignment in `Generate()`,
after geo-coordinates and timezone have already been resolved.

### Interface design

```go
// LocalePolicy controls how the Locale and Languages fields are set relative
// to proxy geo resolution.
type LocalePolicy int

const (
    // LocalePolicyProxyGeo (default) — locale and languages are derived from
    // the proxy exit IP or country constraint, matching geo-based anti-detection
    // principle #6. This is the existing behavior; it is not changed.
    LocalePolicyProxyGeo LocalePolicy = iota

    // LocalePolicyEnglishDefault — locale is forced to "en-US" and languages
    // to ["en-US", "en"] regardless of proxy geo. Use this when the scraping
    // target is English-language content (SERP, English news, etc.) and the
    // proxy exit IP is in a non-English-speaking country. Timezone and geo
    // coordinates are still derived from the proxy geo (physical location stays
    // coherent; only language preference is overridden).
    LocalePolicyEnglishDefault
)

// WithLocalePolicy sets the locale assignment policy for the generated identity.
// The default is LocalePolicyProxyGeo (current behavior). Pass
// LocalePolicyEnglishDefault when scraping English-language content through
// non-English proxies.
func WithLocalePolicy(policy LocalePolicy) Option
```

### Precedence rules (in descending priority)

1. Explicit `WithLocale(locale, langs...)` — always wins; policy is ignored.
2. `LocalePolicyEnglishDefault` — forces `en-US`/`["en-US","en"]` when no
   explicit locale is set.
3. `LocalePolicyProxyGeo` (default) — existing geo-based locale resolution.

This preserves backward compatibility: the zero value of `LocalePolicy` is
`LocalePolicyProxyGeo` (iota = 0), so existing `Generate()` calls without
`WithLocalePolicy` behave identically to v0.0.24.

### What the policy does NOT change

- Timezone: still resolved from proxy geo (physical location consistency).
- Geo coordinates (lat/lng): still resolved from proxy geo.
- TLS profile, UA, HTTP/2 fingerprint: unaffected.

### Why NOT a separate `WithLanguage(langs)` option

A standalone `WithLanguage` option would duplicate the existing `WithLocale`
semantics and add confusion about which wins. The policy enum is cleaner: one
knob controls the locale strategy, and explicit `WithLocale` remains the escape
hatch for precise control.

### Why NOT change the default

Changing the default to `en-US` would break the 80% case (non-English-content
scraping with region-matched proxies) to fix the 20% case (English-content
scraping). The opt-in design is correct.

## Consequences

### Positive
- Users scraping English-content sites through non-English proxies can close the
  locale-query mismatch gap without migrating browsers.
- No existing behavior changes (zero-value default).
- The option name and docstring explain the trade-off clearly.

### Negative / Risks
- Users may cargo-cult `LocalePolicyEnglishDefault` for all scraping without
  understanding the trade-off. Mitigated by the docstring and CHANGELOG note.
- A future `LocalePolicyQueryAware` (locale derived from target page language)
  is not implemented now; this enum design makes it trivially addable as a third
  constant without breaking existing code.

## Alternatives considered

### A. Auto-detect from target URL domain
Infer locale from the target domain (`.co.uk` → `en-GB`, `.ru` → `ru-RU`).
Rejected: (1) domain TLD is a weak locale signal; (2) requires a domain lookup
at `Generate()` time which has no URL in scope; (3) overcomplicates `identity`
package which is URL-agnostic.

### B. Expose raw `WithLanguages(langs []string)`
Add a low-level option to override just the language list.
Rejected: this is what `WithLocale` already does. A second path to the same
result increases confusion. The policy enum is clearer and has a named trade-off
documented in its constant name.

### C. CloakBrowser migration
Use CloakBrowser (en-US default) for all English-content scraping.
Rejected per audit recommendation: the audit found only 1 clean fingerprint-
quality win for CloakBrowser (PerimeterX Walmart, separate from the locale
issue). Migrating browsers for a locale mismatch is a sledgehammer when an
opt-in enum fixes it in 15 lines.

## Implementation notes

In `Generate()`, after the existing locale-assignment block:

```go
// Apply locale policy AFTER geo-based locale resolution, BEFORE returning.
// Explicit WithLocale wins over policy (cfg.locale != "" check).
if cfg.localePolicy == LocalePolicyEnglishDefault && cfg.locale == "" {
    p.Locale = "en-US"
    p.Languages = []string{"en-US", "en"}
}
```

This placement ensures:
- `applyGeoToConfig()` has already run (timezone + coords are resolved).
- Explicit `WithLocale(...)` (which sets `cfg.locale`) skips the override.
- The navigator.language / navigator.languages in `BuildCamoufoxConfig()` pick
  up the overridden values from `p.Languages` automatically (no extra change needed
  there).
