GeoIP Redirect
Redirect first-time visitors to their country's language based on IP geolocation.
How It Works
- A first-time visitor (no language cookie) arrives at the site
- PerfLocale detects their IP address (supports proxies, Cloudflare, load balancers)
- The configured GeoIP provider returns the visitor's country code
- The country code is mapped to a language via the Country Mapping table
- If the mapped language is active and different from the default, a 302 redirect occurs
- A cookie is set to prevent future redirects
GeoIP redirect takes priority over Browser Language Redirect. If both are enabled and GeoIP finds a match, the browser redirect is skipped.
Built-in Providers
All providers use HTTPS exclusively. Providers that only offer HTTPS on paid plans require a paid subscription.
| Provider | API Key | Free Tier | Notes |
|---|---|---|---|
| ipinfo.io | Optional | 1,000/day (no token), 50,000/month (with token) | HTTPS, default provider. Also works for paid plans. |
| ipinfo.io Lite | Required | Unlimited | Free signup, country-level data only |
| ipapi.co | Not needed | 1,000 requests/day | HTTPS, non-commercial use only on free tier |
| ipstack.com | Required | N/A | HTTPS requires paid plan |
| ip-api.com | Required | N/A | HTTPS requires Pro plan |
Settings
Enable GeoIP Redirect
Checkbox to enable/disable the feature. When disabled, no API calls are made.
GeoIP Provider
Select which API to use for IP lookups. Provider-specific fields (API keys) are shown/hidden based on selection.
API Keys
GeoIP keys can be supplied as environment variables (recommended for containers / managed hosts), as PHP constants in wp-config.php, or via the admin Settings page. PerfLocale resolves them in the priority order env > constant > database — the first non-empty source wins. The same uppercase canonical name is used for both env and constant:
# As environment variables (Docker, .env, etc.)
PERFLOCALE_IPINFO_TOKEN=your-token-here
PERFLOCALE_IPINFO_LITE_TOKEN=your-lite-token-here
PERFLOCALE_IPSTACK_KEY=your-key-here
PERFLOCALE_IP_API_KEY=your-ip-api-pro-key-here// Or as PHP constants in wp-config.php
define( 'PERFLOCALE_IPINFO_TOKEN', 'your-token-here' );
define( 'PERFLOCALE_IPINFO_LITE_TOKEN', 'your-lite-token-here' );
define( 'PERFLOCALE_IPSTACK_KEY', 'your-key-here' );
define( 'PERFLOCALE_IP_API_KEY', 'your-ip-api-pro-key-here' );When an env var or constant is set, the matching admin field is disabled and shows where the value comes from. See API Keys: Environment Variables & Constants for the full priority logic.
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 per IP address using WordPress transients. Default: 24 hours. This prevents repeated API calls for the same visitor and respects provider rate limits.
Country Mapping
Zero-config in the common case. Enable GeoIP redirect, pick a provider, set the API key — done. The plugin auto-derives country codes from each language's locale (en_US → US, de_DE → DE, fr_FR → FR, etc.), so a typical multi-language site works out of the box without filling in the table.
The Country Mapping table only matters in two cases:
- 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, JOfor Arabic). - Custom routing overrides — e.g. you want Australian visitors served the British English translation: enter
AUnext toEnglish (UK).
| Language | Country Codes (typical) |
|---|---|
| English (UK) | auto-derived: GB — or override with GB, AU, NZ |
| German | auto-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
Bypass all built-in providers by returning a country code from your own source (local database, custom API, etc.).
// 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 back to built-in provider.
}
}, 10, 2 );Parameters:
string $country_code- Empty string (return a 2-letter code to skip built-in providers).string $ip- Visitor IP address.
perflocale/geo/country_code
Modify the country code after it has been looked up by any provider.
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 custom GeoIP providers alongside the built-in ones.
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;
} );Parameters:
array $providers- Associative array of provider definitions.
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 API call is made.
- Bots and crawlers are excluded from redirects (same as browser redirect).
- Caching prevents API abuse - each IP is looked up once per cache duration.
- Empty results are cached too - prevents hammering the API for unresolvable IPs.
- Cookie prevents re-redirect - once a visitor has a language cookie, GeoIP is skipped.
- Priority: URL detection > Cookie > GeoIP redirect > Browser redirect > Default language.