Automatic Language Detection by GeoIP for Website
Auto-detecting language and region lets you show content in user's language without explicit choice. Combines GeoIP (country detection by IP) and browser Accept-Language header for accurate preferred language detection.
Data Sources
GeoIP databases:
- MaxMind GeoLite2 — free, requires registration, updated weekly
- MaxMind GeoIP2 — paid, more accurate
- ip-api.com — API, free up to 45 req/min
- ipinfo.io — API with plans
Accept-Language header — browser itself sends preferred languages list:
Accept-Language: en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7
MaxMind GeoLite2 Installation
# Download database
mkdir -p /usr/share/GeoIP
wget "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_KEY&suffix=tar.gz" \
-O GeoLite2-Country.tar.gz
tar -xzf GeoLite2-Country.tar.gz -C /usr/share/GeoIP/
// composer require maxmind-db/reader
use MaxMind\Db\Reader;
class GeoIpService
{
private Reader $reader;
public function __construct()
{
$this->reader = new Reader('/usr/share/GeoIP/GeoLite2-Country.mmdb');
}
public function getCountryCode(string $ip): ?string
{
try {
$record = $this->reader->get($ip);
return $record['country']['iso_code'] ?? null;
} catch (\Exception) {
return null;
}
}
}
Language Detection (Combined Logic)
class LanguageDetectionService
{
// Country → language mapping
private array $countryToLanguage = [
'RU' => 'ru', 'BY' => 'ru', 'KZ' => 'ru',
'UA' => 'uk',
'DE' => 'de', 'AT' => 'de', 'CH' => 'de',
'US' => 'en', 'GB' => 'en', 'AU' => 'en', 'CA' => 'en',
];
private array $supportedLanguages = ['ru', 'en', 'de', 'uk'];
public function detect(Request $request): string
{
// 1. Explicit user choice (highest priority)
if ($lang = $request->cookie('locale')) {
if (in_array($lang, $this->supportedLanguages)) {
return $lang;
}
}
// 2. Accept-Language header
$acceptLanguage = $request->header('Accept-Language', '');
$preferred = $this->parseAcceptLanguage($acceptLanguage);
foreach ($preferred as $lang) {
$short = substr($lang, 0, 2);
if (in_array($short, $this->supportedLanguages)) {
return $short;
}
}
// 3. GeoIP
$ip = $request->ip();
$countryCode = app(GeoIpService::class)->getCountryCode($ip);
if ($countryCode && isset($this->countryToLanguage[$countryCode])) {
return $this->countryToLanguage[$countryCode];
}
// 4. Default
return config('app.locale', 'en');
}
private function parseAcceptLanguage(string $header): array
{
preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})*)(?:;q=([0-9.]+))?/i', $header, $matches);
$langs = array_combine($matches[1], array_map(
fn($q) => $q === '' ? 1.0 : (float) $q,
$matches[2]
));
arsort($langs);
return array_keys($langs);
}
}
Middleware for Language Application
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$lang = app(LanguageDetectionService::class)->detect($request);
App::setLocale($lang);
$response = $next($request);
// Save to cookie (without changing Accept-Language)
if (!$request->cookie('locale')) {
$response->withCookie(cookie('locale', $lang, 60 * 24 * 365));
}
return $response;
}
}
Redirect to Language Subdomain or Subdirectory
// Redirect on first visit
if (!$request->cookie('locale') && !$request->is('*/')) {
$lang = $this->detect($request);
$localizedUrl = url("/{$lang}" . $request->getPathInfo());
return redirect($localizedUrl)->withCookie(cookie('locale', $lang, 525600));
}
GeoIP Caching
GeoIP request is fast (file read), but under high load cache in Redis:
$country = Cache::remember("geoip:{$ip}", 3600, fn() =>
app(GeoIpService::class)->getCountryCode($ip)
);
Automatic GeoLite2 Database Update
# Cron: update database weekly
0 4 * * 2 /usr/local/bin/update-geoip.sh
# update-geoip.sh
wget -qO /tmp/GeoLite2.tar.gz "https://download.maxmind.com/..."
tar -xzf /tmp/GeoLite2.tar.gz -C /tmp/
mv /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /usr/share/GeoIP/
Implementation Timeline
2–3 days: MaxMind setup + middleware + priority logic + testing with VPN from different countries.







