Реализация автоопределения языка по GeoIP для сайта
Автоопределение языка и региона позволяет показывать пользователю контент на его языке без явного выбора. Комбинирует GeoIP (определение страны по IP) и заголовок Accept-Language браузера для точного определения предпочтительного языка.
Источники данных
GeoIP базы:
- MaxMind GeoLite2 — бесплатная, требует регистрации, обновляется еженедельно
- MaxMind GeoIP2 — платная, точнее
- ip-api.com — API, бесплатно до 45 req/min
- ipinfo.io — API с планами
Accept-Language header — браузер сам передаёт список предпочтительных языков:
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Установка MaxMind GeoLite2
# Скачать базу данных
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;
}
}
}
Определение языка (комбинированная логика)
class LanguageDetectionService
{
// Маппинг страна → язык
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. Явный выбор пользователя (приоритет)
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. Дефолт
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 для применения языка
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$lang = app(LanguageDetectionService::class)->detect($request);
App::setLocale($lang);
$response = $next($request);
// Сохранить в cookie (без изменений Accept-Language)
if (!$request->cookie('locale')) {
$response->withCookie(cookie('locale', $lang, 60 * 24 * 365));
}
return $response;
}
}
Редирект на языковой поддомен или субдиректорию
// Редирект при первом визите
if (!$request->cookie('locale') && !$request->is('*/')) {
$lang = $this->detect($request);
$localizedUrl = url("/{$lang}" . $request->getPathInfo());
return redirect($localizedUrl)->withCookie(cookie('locale', $lang, 525600));
}
Кеширование GeoIP
GeoIP-запрос быстрый (чтение файла), но при высокой нагрузке кешируют в Redis:
$country = Cache::remember("geoip:{$ip}", 3600, fn() =>
app(GeoIpService::class)->getCountryCode($ip)
);
Автоматическое обновление базы GeoLite2
# Cron: обновлять базу каждую неделю
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/
Срок реализации
2–3 дня: настройка MaxMind + middleware + логика приоритетов + тестирование с VPN из разных стран.







