<?php
namespace Botble\Language;
use Illuminate\Http\Request;
use Locale;
class LanguageNegotiator
{
protected array $supportedLanguages = [];
protected bool $useIntl = false;
public function __construct(protected string $defaultLocale, array $supportedLanguages, protected Request $request)
{
if (extension_loaded('intl') && class_exists('Locale')) {
$this->useIntl = true;
foreach ($supportedLanguages as $key => $supportedLanguage) {
if (! isset($supportedLanguage['lang'])) {
$supportedLanguage['lang'] = Locale::canonicalize($key);
} else {
$supportedLanguage['lang'] = Locale::canonicalize($supportedLanguage['lang']);
}
if (isset($supportedLanguage['regional'])) {
$supportedLanguage['regional'] = Locale::canonicalize($supportedLanguage['regional']);
}
$this->supportedLanguages[$key] = $supportedLanguage;
}
} else {
$this->supportedLanguages = $supportedLanguages;
}
}
/**
* Negotiates language with the user's browser through the Accept-Language
* HTTP header or the user's host address. Language codes are generally in
* the form "ll" for a language spoken in only one country, or "ll-CC" for a
* language spoken in a particular country. For example, U.S. English is
* "en-US", while British English is "en-UK". Portuguese as spoken in
* Portugal is "pt-PT", while Brazilian Portuguese is "pt-BR".
*
* This function is based on negotiateLanguage from Pear HTTP2
* http://pear.php.net/package/HTTP2/
*
* Quality factors in the Accept-Language: header are supported, e.g.:
* Accept-Language: en-UK;q=0.7, en-US;q=0.6, no, dk;q=0.8
*
* @return string The negotiated language result or app.locale.
*/
public function negotiateLanguage(): string
{
$matches = $this->getMatchesFromAcceptedLanguages();
foreach ($matches as $key => $match) {
if (! empty($this->supportedLanguages[$key])) {
return $key;
}
if ($this->useIntl) {
$key = Locale::canonicalize($key);
}
// Search for acceptable locale by 'regional' => 'af_ZA' or 'lang' => 'af-ZA' match.
foreach ($this->supportedLanguages as $keySupported => $locale) {
if ((isset($locale['regional']) && $locale['regional'] == $key) || (isset($locale['lang']) && $locale['lang'] == $key)) {
return $keySupported;
}
}
}
// If any (i.e. "*") is acceptable, return the first supported format
if (isset($matches['*'])) {
reset($this->supportedLanguages);
return key($this->supportedLanguages);
}
if ($this->useIntl && ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$httpAcceptLanguage = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
if (! empty($this->supportedLanguages[$httpAcceptLanguage])) {
return $httpAcceptLanguage;
}
}
if ($this->request->server('REMOTE_HOST')) {
$remoteHost = explode('.', $this->request->server('REMOTE_HOST'));
$lang = strtolower(end($remoteHost));
if (! empty($this->supportedLanguages[$lang])) {
return $lang;
}
}
return $this->defaultLocale;
}
/**
* Return all the accepted languages from the browser.
*
* @return array Matches from the header field Accept-Languages
*/
protected function getMatchesFromAcceptedLanguages(): array
{
$matches = [];
if ($acceptLanguages = $this->request->header('Accept-Language')) {
$acceptLanguages = explode(',', $acceptLanguages);
$genericMatches = [];
foreach ($acceptLanguages as $option) {
$option = array_map('trim', explode(';', $option));
$la = $option[0];
if (isset($option[1])) {
$qa = (float) str_replace('q=', '', $option[1]);
} else {
$qa = null;
// Assign default low weight for generic values
if ($la == '*/*') {
$qa = 0.01;
} elseif (str_ends_with($la, '*')) {
$qa = 0.02;
}
}
// Unweighted values, get high weight by their position in the
// list
$qa = $qa ?? 1000 - count($matches);
$matches[$la] = $qa;
//If for some reason the Accept-Language header only sends language with country
//we should make the language without country an accepted option, with a value
//less than its parent.
$lOps = explode('-', $la);
array_pop($lOps);
while (! empty($lOps)) {
//The new generic option needs to be slightly less important than it's base
$qa -= 0.001;
$op = implode('-', $lOps);
if (empty($genericMatches[$op]) || $genericMatches[$op] > $qa) {
$genericMatches[$op] = $qa;
}
array_pop($lOps);
}
}
$matches = array_merge($genericMatches, $matches);
arsort($matches, SORT_NUMERIC);
}
return $matches;
}
}