GeoIP Redirect

Redirect first-time visitors to their country's language based on IP geolocation.

No geolocation service is bundled. PerfLocale does the routing, caching and consent handling, but it does not know where a visitor is — you tell it, through the perflocale/geo/lookup_country or perflocale/geo/providers filter. Out of the box the feature contacts nothing and no visitor IP leaves the server.

How It Works

  1. A first-time visitor (no language cookie) arrives at the site
  2. PerfLocale detects their IP address (supports proxies, Cloudflare, load balancers)
  3. Your country source returns the visitor's country code — the perflocale/geo/lookup_country filter first, then whichever provider you registered and selected
  4. The country code is mapped to a language via the Country Mapping table
  5. If the mapped language is active and different from the default, a 302 redirect occurs
  6. A cookie is set to prevent future redirects

By default GeoIP redirect runs before Browser Language Redirect, so if both are enabled and GeoIP finds a match, the browser redirect is skipped. The Redirect Priority control on the same settings tab lets you reorder the enabled mechanisms; the first one that redirects wins.

Supplying the country code

Earlier builds shipped a handful of IP-lookup services. They were removed: bundling them meant a plugin update could start sending visitor IPs to a third party, and the choice of vendor — with its terms, its privacy policy and its bill — is properly the site owner’s. Two seams remain.

1. perflocale/geo/lookup_country — the one-liner

Return a two-letter code and PerfLocale uses it directly. If your CDN already resolves the country, this is all you need — and it costs no HTTP request at all:

// Cloudflare sets CF-IPCountry on every request when IP Geolocation is on.
add_filter( 'perflocale/geo/lookup_country', function ( string $country, string $ip ): string {
	return isset( $_SERVER['HTTP_CF_IPCOUNTRY'] )
		? sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_IPCOUNTRY'] ) )
		: $country;
}, 10, 2 );

The same shape works for a local MaxMind database (see the hook reference below) or any other in-process source.

2. perflocale/geo/providers — a registered lookup service

Register a provider with a fetch_callback and it appears in the GeoIP Provider dropdown, with a per-provider circuit breaker in front of the fetch. Use this when the lookup is an outbound API call. See the hook reference for the array shape.

Note that PerfLocale renders no API-key input for a registered provider — those fields went with the bundled services. If your provider declares needs_key and key_setting, populate that setting yourself; simpler still, omit needs_key and read your own constant or environment variable inside the callback.

Settings

GeoIP redirect lives under PerfLocale → Settings → URL & Routing.

Enable GeoIP Redirect

Checkbox to enable/disable the feature. When disabled, no lookup runs at all.

GeoIP Provider

Lists the providers your site registered through perflocale/geo/providers. With none registered — the default — the field is replaced by a note saying so, since there is nothing to choose between. You can leave it that way and still use the feature via perflocale/geo/lookup_country.

Custom IP Header

If your site is behind a CDN or reverse proxy that uses a custom header for the real visitor IP (e.g., Imperva, Sucuri, or a custom load balancer), use the perflocale/geo/visitor_ip filter to read it:

// Example: Imperva (Incapsula) uses X-Incap-Client-IP.
add_filter( 'perflocale/geo/visitor_ip', function ( string $ip ): string {
	if ( ! empty( $_SERVER['HTTP_X_INCAP_CLIENT_IP'] ) ) {
		return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_INCAP_CLIENT_IP'] ) );
	}
	return $ip;
} );

PerfLocale automatically detects Cloudflare (CF-Connecting-IP), standard proxies (X-Forwarded-For), and nginx (X-Real-IP). Use this filter only if your proxy sends a non-standard header.

Cache Duration

GeoIP results are cached via PerfLocale's object cache (the perflocale_geo_lookup group), falling back to a WordPress transient when no external object cache is present. The cache key is a salted, one-way hash of the visitor's anonymized network address (host bits removed) — the IP itself is never stored, and nearby visitors on the same network share one lookup. Default: 24 hours. This prevents repeated API calls and respects provider rate limits.

Country Mapping

Zero-config in the common case. Once a country source is wired up, there is usually nothing to fill in here: the plugin auto-derives country codes from each language's locale (en_USUS, de_DEDE, fr_FRFR, etc.), so a typical multi-language site maps itself.

The Country Mapping table only matters in two cases:

  1. Languages spoken across many countries — Arabic (ar), generic Chinese (zh), Spanish across Latin America. The plugin can't guess your intent here, so map the country codes you want routed to that language (e.g. SA, AE, EG, JO for Arabic).
  2. Custom routing overrides — e.g. you want Australian visitors served the British English translation: enter AU next to English (UK).
LanguageCountry Codes (typical)
English (UK)auto-derived: GB — or override with GB, AU, NZ
Germanauto-derived: DE — or extend to DE, AT, CH
Spanish (Spain)auto-derived: ES — or extend to ES, MX, AR, CO, CL
Arabic (locale ar)Must be set explicitly: e.g. SA, AE, EG, JO, MA

Multiple countries can map to the same language. Each country code can only map to one language — the first match wins.

The default language is intentionally never mappable

Visitors from countries that don't match any mapping stay on the default automatically — the default IS the catch-all. Mapping the default would over-redirect (e.g. visitors who explicitly chose another language). The Country Mapping table therefore hides the default-language row, and the server-side sanitiser drops any default-language entry submitted via a form, so the guarantee holds even against hand-crafted POSTs.

This works regardless of what your default language is. If your default is, say, Arabic (locale ar, no specific country), Arabic-speaking visitors from any country simply stay on the default Arabic version — no country code is needed for the default itself.

Developer Hooks

Filters

perflocale/geo/lookup_country

Return a country code from your own source (CDN header, local database, custom API). Runs first; a non-empty return short-circuits the registered provider entirely.

// Use MaxMind GeoLite2 local database instead of API calls.
add_filter( 'perflocale/geo/lookup_country', function ( string $country, string $ip ): string {
	if ( ! class_exists( 'GeoIp2\Database\Reader' ) ) {
		return $country;
	}

	try {
		$reader  = new GeoIp2\Database\Reader( '/path/to/GeoLite2-Country.mmdb' );
		$record  = $reader->country( $ip );
		return $record->country->isoCode;
	} catch ( \Exception $e ) {
		return $country; // Fall through to the registered provider, if any.
	}
}, 10, 2 );

Parameters:

  • string $country_code - Empty string (return a 2-letter code to skip the provider lookup).
  • string $ip - Visitor IP address.

perflocale/geo/country_code

Modify the country code after it has been resolved, whatever the source.

add_filter( 'perflocale/geo/country_code', function ( string $code, string $ip ): string {
	// Custom logic here.
	return $code;
}, 10, 2 );

Parameters:

  • string $country_code - Two-letter country code (uppercase).
  • string $ip - Visitor IP address.

perflocale/geo/redirect_language

Override which language a visitor should be redirected to, after country-to-language mapping.

// Force all South American countries to Spanish.
add_filter( 'perflocale/geo/redirect_language', function ( string $slug, string $country, string $ip ): string {
	$south_america = [ 'AR', 'BO', 'BR', 'CL', 'CO', 'EC', 'GY', 'PY', 'PE', 'SR', 'UY', 'VE' ];

	if ( in_array( $country, $south_america, true ) && $country !== 'BR' ) {
		return 'es';
	}

	return $slug;
}, 10, 3 );

Parameters:

  • string $language_slug - Resolved language slug (or empty if no mapping found).
  • string $country_code - Two-letter country code.
  • string $ip - Visitor IP address.

perflocale/geo/providers

Register the GeoIP providers offered in the settings dropdown. The default list is empty.

add_filter( 'perflocale/geo/providers', function ( array $providers ): array {
	$providers['my_provider'] = [
		'name'           => 'My GeoIP Service',
		'needs_key'      => true,
		'key_setting'    => 'geo_my_provider_key',
		'fetch_callback' => function ( string $ip, $settings ): string {
			$key = $settings->get( 'geo_my_provider_key', '' );
			$response = wp_remote_get( "https://my-api.com/lookup?ip={$ip}&key={$key}" );

			if ( is_wp_error( $response ) ) {
				return '';
			}

			$body = json_decode( wp_remote_retrieve_body( $response ), true );
			return $body['country'] ?? '';
		},
	];

	return $providers;
} );

fetch_callback receives the visitor IP and the Settings instance, and returns a two-letter code (or '' on failure — repeated failures trip a per-provider circuit breaker rather than paying the HTTP timeout on every new visitor). The geo_my_provider_key setting above has no admin input; write it with wp option patch update perflocale_settings geo_my_provider_key '…', or drop needs_key and read your own constant inside the callback.

Parameters:

  • array $providers - Associative array of provider definitions. Empty by default.

perflocale/geo/country_map

Filter the country-to-language mapping array.

// Programmatically add mappings.
add_filter( 'perflocale/geo/country_map', function ( array $map ): array {
	$map['JP'] = 'ja';
	$map['KR'] = 'ko';
	return $map;
} );

Parameters:

  • array $map - [ 'US' => 'en', 'DE' => 'de', ... ]

Actions

perflocale/geo/redirected

Fires after a GeoIP redirect is performed. Useful for analytics or logging.

add_action( 'perflocale/geo/redirected', function ( string $slug, string $country, string $ip ): void {
	error_log( "PerfLocale GeoIP: Redirected {$ip} ({$country}) to {$slug}" );
}, 10, 3 );

Parameters:

  • string $language_slug - Language the visitor was redirected to.
  • string $country_code - Detected country code.
  • string $ip - Visitor IP address.

Behavior Notes

  • Local/private IPs (127.0.0.1, 192.168.x.x, etc.) are skipped - no lookup is made.
  • Bots and crawlers are excluded from redirects (same as browser redirect).
  • Caching keeps lookups down - one entry serves a whole anonymized network for the cache duration.
  • Only successful lookups are cached. An empty result writes nothing, so spoofed X-Forwarded-For headers can't flood wp_options with negative-result rows on sites without an external object cache; a failing lookup also recovers as soon as it starts working again.
  • Cookie prevents re-redirect - once a visitor has a language cookie, GeoIP is skipped.
  • Consent-aware - returning false from perflocale/privacy/consent_given stops the redirect before any lookup happens.
  • Priority (default): URL detection > Cookie > GeoIP redirect > Browser redirect > Default language. The redirect mechanisms among those — geo, browser and edge hint — can be reordered in Settings.