$availableLanguages * @param array $inactiveLanguages */ public function __construct( private readonly array $availableLanguages, private readonly array $inactiveLanguages, string $preferredLanguage, string $defaultLanguage, ) { $this->defaultLanguage = $this->normalize($defaultLanguage); $this->preferredLanguage = $this->normalize($preferredLanguage); } /** * @return array{string, string} */ public function chooseCode( string|array|null $langParam, string $requestUri, ?string $acceptLanguageHeader, ): array { // Default values for languages $explicitly_specified = ''; // Specified for the request (GET/POST parameter) if (is_string($langParam)) { $langCode = $this->normalize(htmlspecialchars($langParam, ENT_QUOTES, 'UTF-8')); $explicitly_specified = $langCode; if ($this->isAvailableLanguage($langCode)) { return [$langCode, $explicitly_specified]; } } // Specified in a shortcut URL (eg. /en/echo or /pt_br/echo) if (preg_match("!^/(\\w{2}(_\\w{2})?)/!", htmlspecialchars($requestUri,ENT_QUOTES, 'UTF-8'), $flang)) { // Put language into preference list $rlang = $this->normalize($flang[1]); // Set explicitly specified language if (empty($explicitly_specified)) { $explicitly_specified = $rlang; } // Drop out language specification from URL, as this is already handled $_SERVER['STRIPPED_URI'] = preg_replace( "!^/$flang[1]/!", "/", htmlspecialchars($requestUri, ENT_QUOTES, 'UTF-8'), ); if ($this->isAvailableLanguage($rlang)) { return [$rlang, $explicitly_specified]; } } // Specified in a manual URL (eg. manual/en/ or manual/pt_br/) if (preg_match("!^/manual/(\\w{2}(_\\w{2})?)(/|$)!", htmlspecialchars($requestUri, ENT_QUOTES, 'UTF-8'), $flang)) { $flang = $this->normalize($flang[1]); // Set explicitly specified language if (empty($explicitly_specified)) { $explicitly_specified = $flang; } if ($this->isAvailableLanguage($flang)) { return [$flang, $explicitly_specified]; } } // Honor the users own language setting (if available) if ($this->isAvailableLanguage($this->preferredLanguage)) { return [$this->preferredLanguage, $explicitly_specified]; } // Specified by the user via the browser's Accept Language setting // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" $browser_langs = []; // Check if we have $_SERVER['HTTP_ACCEPT_LANGUAGE'] set and // it no longer breaks if you only have one language set :) if (isset($acceptLanguageHeader)) { $browser_accept = explode(",", $acceptLanguageHeader); // Go through all language preference specs foreach ($browser_accept as $value) { // The language part is either a code or a code with a quality // We cannot do anything with a * code, so it is skipped // If the quality is missing, it is assumed to be 1 according to the RFC if (preg_match("!([a-z-]+)(;q=([0-9\\.]+))?!", trim($value), $found)) { $quality = (isset($found[3]) ? (float) $found[3] : 1.0); $browser_langs[] = [$found[1], $quality]; } unset($found); } } // Order the codes by quality usort($browser_langs, fn ($a, $b) => $b[1] <=> $a[1]); // For all languages found in the accept-language foreach ($browser_langs as $langdata) { // Translation table for accept-language codes and phpdoc codes switch ($langdata[0]) { case "pt-br": $langdata[0] = 'pt_br'; break; case "zh-cn": $langdata[0] = 'zh'; break; case "zh-hk": $langdata[0] = 'hk'; break; case "zh-tw": $langdata[0] = 'tw'; break; } // We do not support flavors of languages (except the ones above) // This is not in conformance to the RFC, but it here for user // convenience reasons if (preg_match("!^(.+)-!", $langdata[0], $match)) { $langdata[0] = $match[1]; } $lang = $this->normalize($langdata[0]); if ($this->isAvailableLanguage($lang)) { return [$lang, $explicitly_specified]; } } // Language preferred by this mirror site if ($this->isAvailableLanguage($this->defaultLanguage)) { return [$this->defaultLanguage, $explicitly_specified]; } // Last default language is English return ["en", $explicitly_specified]; } private function normalize(string $langCode): string { // Make language code lowercase, html encode special chars and remove slashes $langCode = strtolower(htmlspecialchars($langCode)); // The Brazilian Portuguese code needs special attention if ($langCode == 'pt_br') { return 'pt_BR'; } return $langCode; } private function isAvailableLanguage(string $langCode): bool { return isset($this->availableLanguages[$langCode]) && !isset($this->inactiveLanguages[$langCode]); } }