Автоматическая локализация: получение языка клиента в PHP

Раздел: Веб-разработка на PHP -> Определение параметров клиента

Определение языка браузера позволяет PHP-приложению автоматически подстраивать локализацию интерфейса, выбирать правильные языковые файлы и улучшать пользовательский опыт. Существует несколько подходов к решению этой задачи, от простого чтения HTTP-заголовка до использования JavaScript и внешних баз GeoIP. Каждый метод имеет свои особенности, преимущества и ограничения.

Основные способы определения языка браузера в PHP

Наиболее эффективный и распространённый метод заключается в анализе HTTP-заголовка Accept-Language, который браузер отправляет на сервер. Этот заголовок содержит предпочтения пользователя по языку в порядке убывания приоритета (с указанием коэффициента качества q).

Простой способ извлечь основной язык:


$lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', 0, 2);

язык браузера php (определение языка браузера в php)

Пояснение: оператор ?? '' предотвращает ошибку, если заголовок отсутствует. Функция substr берёт первые два символа (код языка). Такой подход подходит, если нужен только язык без региона (например, 'ru', 'en').

Однако более корректным является полный разбор заголовка с учётом q-факторов. Пример функции:


function getPreferredLanguage($default = 'en') {
    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) return $default;
    $langs = [];
    preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches);
    foreach ($matches[1] as $i => $lang) {
        $q = $matches[4][$i] !== '' ? (float)$matches[4][$i] : 1.0;
        $langs[$lang] = $q;
    }
    arsort($langs);
    return key($langs);
}

Функция извлекает все языки, присваивает коэффициенты качества и возвращает язык с наивысшим приоритетом. Типичные проблемы:

  • Отсутствие заголовка – например, при запросах от некоторых ботов или старых клиентов. Решение: задать язык по умолчанию.
  • Неверное определение региона – если заголовок содержит только 'en-US', а нужен 'en', либо наоборот. Следует нормализовать язык до двухбуквенного кода.
  • Подмена заголовка – пользователь может вручную изменить отправляемые языки. Это не является ошибкой, но стоит учитывать, что приложение может получить непредсказуемый язык.

Как определить язык браузера с помощью JavaScript и AJAX?

Если требуется максимально точное определение без опоры на HTTP-заголовок (например, при работе с SPA или когда сервер не получает Accept-Language), можно использовать JavaScript для получения значения свойства navigator.language или navigator.languages и отправить его на сервер через AJAX.

Пример клиентского кода:


<script>
fetch('/set-language.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({lang: navigator.language || navigator.userLanguage})
});
</script>

На сервере (set-language.php):


$data = json_decode(file_get_contents('php://input'), true);
if ($data && isset($data['lang'])) {
    $_SESSION['lang'] = substr($data['lang'], 0, 2);
}

Проблемы и их решение:

  • Зависимость от JavaScript – если он отключён, язык не определится. Решение: использовать как запасной вариант после проверки Accept-Language.
  • Задержка – требуется дополнительный запрос. Можно совместить со стартовой страницей или сохранить в cookie для последующих запросов.
  • Различия в реализации – в некоторых мобильных браузерах navigator.language может возвращать неполный код. Стоит использовать navigator.languages[0] для более точного результата.

Как использовать GeoIP для определения языка пользователя?

Геолокация по IP-адресу позволяет получить страну, на основании которой можно предположить основной язык. Метод не требует анализа заголовков браузера и работает даже для запросов от API или ботов (при условии передачи IP).

Пример с библиотекой geoip2/geoip2 (необходима база данных GeoLite2):


require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
$reader = new Reader('/path/to/GeoLite2-Country.mmdb');
$record = $reader->country($_SERVER['REMOTE_ADDR']);
$countryCode = $record->country->isoCode; // 'RU', 'DE' и т.д.

Далее сопоставить код страны с языком (например, массив $countryToLang = ['RU' => 'ru', 'DE' => 'de', 'FR' => 'fr'];).

  • Неточность – в мультиязычных странах (Швейцария, Канада, Бельгия) один код страны может соответствовать нескольким языкам. Необходим дополнительный источник.
  • Устаревшая база – если GeoIP-база давно не обновлялась, определение может быть неверным. Рекомендуется регулярно загружать свежие версии.
  • Прокси и VPN – IP может не соответствовать реальному местоположению пользователя.

Как сохранить выбранный язык в сессии или cookie?

После определения языка любым из способов стоит зафиксировать его для дальнейших запросов. Самый простой способ – использовать сессию PHP.


session_start();
if (!isset($_SESSION['lang'])) {
    // Определяем язык через Accept-Language
    $_SESSION['lang'] = getPreferredLanguage();
}
$currentLang = $_SESSION['lang'];

Альтернатива – запись в cookie:


if (!isset($_COOKIE['lang'])) {
    $lang = getPreferredLanguage();
    setcookie('lang', $lang, time() + 365*86400, '/');
    $_COOKIE['lang'] = $lang;
}
$currentLang = $_COOKIE['lang'];
  • Незапущенная сессия – если забыть вызвать session_start(), массив $_SESSION будет пустым, и язык не сохранится.
  • Блокировка cookie – некоторые пользователи отключают куки. Сессия использует cookie по умолчанию, но может работать через URL. В любом случае стоит предусмотреть fallback.
  • Устаревшие данные – если пользователь изменит язык браузера после установки куки/сессии, приложение будет использовать старый язык до очистки. Можно добавить механизм обновления (например, кнопку смены языка).

Расширенные примеры реализации

Ниже приведены полные, готовые к использованию примеры кода с пояснениями и выводами.

Пример 1. Функция разбора Accept-Language с приоритетом

Функция возвращает массив языков, отсортированный по убыванию q-фактора, или строку с наилучшим языком.

Пример

function parseAcceptLanguage($acceptLanguage = null, $asArray = false) {
    $acceptLanguage = $acceptLanguage ?? $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
    $langs = [];
    // Разбиваем по запятой, удаляем пробелы
    $items = explode(',', $acceptLanguage);
    foreach ($items as $item) {
        $item = trim($item);
        if (preg_match('/([a-z]{1,8}(-[a-z]{1,8})?)(?:;q=([0-9\.]+))?/i', $item, $m)) {
            $lang = $m[1];
            $q = isset($m[3]) && $m[3] !== '' ? (float)$m[3] : 1.0;
            $langs[$lang] = $q;
        }
    }
    arsort($langs);
    return $asArray ? $langs : array_keys($langs)[0] ?? 'en';
}
// Пример использования
$accept = 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5';
echo 'Лучший язык: ' . parseAcceptLanguage($accept) . "\n";
echo "Массив приоритетов:\n";
print_r(parseAcceptLanguage($accept, true));

Результат выполнения:

Лучший язык: fr-CH
Массив приоритетов:
Array
(
    [fr-CH] => 1
    [fr] => 0.9
    [en] => 0.8
    [de] => 0.7
    [*] => 0.5
)

Пример 2. Использование библиотеки willdurand/negotiation

Установка: composer require willdurand/negotiation. Библиотека предоставляет удобный парсер HTTP-заголовков.

Пример

use Negotiation\LanguageNegotiator;

$negotiator = new LanguageNegotiator();
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
$priorities = ['en', 'ru', 'de', 'fr'];
$bestLang = $negotiator->getBest($acceptLanguage, $priorities);
if ($bestLang) {
    echo 'Выбранный язык: ' . $bestLang->getValue() . "\n";
    echo 'Качество: ' . $bestLang->getQuality() . "\n";
} else {
    echo 'Язык не определён, используется en';
}

Результат (для заголовка 'ru,en;q=0.9'):

Выбранный язык: ru
Качество: 1

Пример 3. JavaScript + PHP: определение языка и сохранение в cookie

Полный пример с отправкой языка при загрузке страницы.

Пример

<!-- HTML -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    if (!document.cookie.includes('client_lang')) {
        var lang = navigator.language || navigator.userLanguage || 'en';
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/save-language.php', true);
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.send('lang=' + encodeURIComponent(lang));
    }
});
</script>
Пример

<?php // save-language.php
if (isset($_POST['lang'])) {
    $lang = substr($_POST['lang'], 0, 5); // например 'en-US'
    setcookie('client_lang', $lang, time() + 86400 * 30, '/');
    $_COOKIE['client_lang'] = $lang;
    echo 'OK';
}
?>

После выполнения язык сохранится в cookie. При следующем запросе PHP может читать его:

Пример

$userLang = $_COOKIE['client_lang'] ?? 'en';
echo 'Язык из cookie: ' . $userLang;

Пример 4. Геолокация через MaxMind GeoLite2 (Composer)

Установка: composer require geoip2/geoip2. Скачайте базу GeoLite2-Country.mmdb с сайта MaxMind.

Пример

require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;

$countryLangMap = [
    'RU' => 'ru',
    'UA' => 'uk',
    'BY' => 'be',
    'DE' => 'de',
    'FR' => 'fr',
    'GB' => 'en',
    'US' => 'en',
    'IT' => 'it',
    'ES' => 'es',
];

try {
    $reader = new Reader('/var/data/GeoLite2-Country.mmdb');
    $record = $reader->country($_SERVER['REMOTE_ADDR']);
    $countryCode = $record->country->isoCode; // 'RU', 'DE' etc.
    $language = $countryLangMap[$countryCode] ?? 'en';
    echo "Страна: $countryCode, предполагаемый язык: $language";
} catch (Exception $e) {
    echo 'Ошибка GeoIP: ' . $e->getMessage();
}

Пример вывода для пользователя из России:

Страна: RU, предполагаемый язык: ru

Пример 5. Комбинированная функция определения языка

Функция проверяет в порядке приоритета: сессию, cookie, Accept-Language, GeoIP, язык по умолчанию.

Пример

function detectLanguage() {
    $default = 'en';
    // 1. Сессия
    if (isset($_SESSION['lang'])) {
        return $_SESSION['lang'];
    }
    // 2. Cookie
    if (isset($_COOKIE['lang'])) {
        $_SESSION['lang'] = $_COOKIE['lang'];
        return $_COOKIE['lang'];
    }
    // 3. Accept-Language
    $acceptLang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
    $parsed = parseAcceptLanguage($acceptLang);
    if ($parsed && $parsed !== '*') {
        $lang2 = substr($parsed, 0, 2);
        if (in_array($lang2, ['ru', 'en', 'de', 'fr'])) {
            $_SESSION['lang'] = $lang2;
            setcookie('lang', $lang2, time()+86400*30, '/');
            return $lang2;
        }
    }
    // 4. GeoIP
    $geoLang = getLanguageByGeoIP(); // воображаемая функция
    if ($geoLang) {
        $_SESSION['lang'] = $geoLang;
        setcookie('lang', $geoLang, time()+86400*30, '/');
        return $geoLang;
    }
    // 5. По умолчанию
    return $default;
}

Результат: для пользователя с Accept-Language 'de,en;q=0.5' функция вернёт 'de' и запишет в сессию и cookie.

Определение языка браузера в PHP - comments

En
язык браузера php (php)