Fix browser language detection (& tests)

This commit is contained in:
Philipp Holzer 2019-10-10 16:39:04 +02:00
parent e72402389d
commit a522cf609b
No known key found for this signature in database
GPG key ID: D8365C3D36B77D90
3 changed files with 173 additions and 38 deletions

View file

@ -53,12 +53,12 @@ class L10n
*/ */
private $logger; private $logger;
public function __construct(Configuration $config, Database $dba, LoggerInterface $logger) public function __construct(Configuration $config, Database $dba, LoggerInterface $logger, array $server, array $get)
{ {
$this->dba = $dba; $this->dba = $dba;
$this->logger = $logger; $this->logger = $logger;
$this->loadTranslationTable(L10n::detectLanguage($config->get('system', 'language', 'en'))); $this->loadTranslationTable(L10n::detectLanguage($server, $get, $config->get('system', 'language', 'en')));
} }
/** /**
@ -140,7 +140,7 @@ class L10n
$this->lang = $this->langSave; $this->lang = $this->langSave;
$this->stringsSave = null; $this->stringsSave = null;
$this->langSave = null; $this->langSave = null;
} }
/** /**
@ -158,6 +158,11 @@ class L10n
{ {
$lang = Strings::sanitizeFilePathItem($lang); $lang = Strings::sanitizeFilePathItem($lang);
// Don't override the language setting with empty languages
if (empty($lang)) {
return;
}
$a = new \stdClass(); $a = new \stdClass();
$a->strings = []; $a->strings = [];
@ -166,12 +171,12 @@ class L10n
while ($p = $this->dba->fetch($addons)) { while ($p = $this->dba->fetch($addons)) {
$name = Strings::sanitizeFilePathItem($p['name']); $name = Strings::sanitizeFilePathItem($p['name']);
if (file_exists("addon/$name/lang/$lang/strings.php")) { if (file_exists("addon/$name/lang/$lang/strings.php")) {
include "addon/$name/lang/$lang/strings.php"; include __DIR__ . "/../../../addon/$name/lang/$lang/strings.php";
} }
} }
if (file_exists("view/lang/$lang/strings.php")) { if (file_exists(__DIR__ . "/../../../view/lang/$lang/strings.php")) {
include "view/lang/$lang/strings.php"; include __DIR__ . "/../../../view/lang/$lang/strings.php";
} }
$this->lang = $lang; $this->lang = $lang;
@ -184,49 +189,78 @@ class L10n
* @brief Returns the preferred language from the HTTP_ACCEPT_LANGUAGE header * @brief Returns the preferred language from the HTTP_ACCEPT_LANGUAGE header
* *
* @param string $sysLang The default fallback language * @param string $sysLang The default fallback language
* @param array $server The $_SERVER array
* @param array $get The $_GET array
* *
* @return string The two-letter language code * @return string The two-letter language code
*/ */
public static function detectLanguage(string $sysLang = 'en') public static function detectLanguage(array $server, array $get, string $sysLang = 'en')
{ {
$lang_list = []; $lang_variable = $server['HTTP_ACCEPT_LANGUAGE'] ?? null;
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $acceptedLanguages = preg_split('/,\s*/', $lang_variable);
// break up string into pieces (languages and q factors)
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'], $lang_parse);
if (count($lang_parse[1])) { if (empty($acceptedLanguages)) {
// go through the list of prefered languages and add a generic language $acceptedLanguages = [];
// for sub-linguas (e.g. de-ch will add de) if not already in array }
for ($i = 0; $i < count($lang_parse[1]); $i++) {
$lang_list[] = strtolower($lang_parse[1][$i]); // Add get as absolute quality accepted language (except this language isn't valid)
if (strlen($lang_parse[1][$i]) > 3) { if (!empty($get['lang'])) {
$dashpos = strpos($lang_parse[1][$i], '-'); $acceptedLanguages[] = $get['lang'];
if (!in_array(substr($lang_parse[1][$i], 0, $dashpos), $lang_list)) { }
$lang_list[] = strtolower(substr($lang_parse[1][$i], 0, $dashpos));
} // return the sys language in case there's nothing to do
if (empty($acceptedLanguages)) {
return $sysLang;
}
// Set the syslang as default fallback
$current_lang = $sysLang;
// start with quality zero (every guessed language is more acceptable ..)
$current_q = 0;
foreach ($acceptedLanguages as $acceptedLanguage) {
$res = preg_match(
'/^([a-z]{1,8}(?:-[a-z]{1,8})*)(?:;\s*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i',
$acceptedLanguage,
$matches
);
// Invalid language? -> skip
if (!$res) {
continue;
}
// split language codes based on it's "-"
$lang_code = explode('-', $matches[1]);
// determine the quality of the guess
if (isset($matches[2])) {
$lang_quality = (float)$matches[2];
} else {
// fallback so without a quality parameter, it's probably the best
$lang_quality = 1;
}
// loop through each part of the code-parts
while (count($lang_code)) {
// try to mix them so we can get double-code parts too
$match_lang = strtolower(join('-', $lang_code));
if (file_exists(__DIR__ . "/../../../view/lang/$match_lang") &&
is_dir(__DIR__ . "/../../../view/lang/$match_lang")) {
if ($lang_quality > $current_q) {
$current_lang = $match_lang;
$current_q = $lang_quality;
break;
} }
} }
// remove the most right code-part
array_pop($lang_code);
} }
} }
if (isset($_GET['lang'])) { return $current_lang;
$lang_list = [$_GET['lang']];
}
// check if we have translations for the preferred languages and pick the 1st that has
foreach ($lang_list as $lang) {
if ($lang === 'en' || (file_exists("view/lang/$lang") && is_dir("view/lang/$lang"))) {
$preferred = $lang;
break;
}
}
if (isset($preferred)) {
return $preferred;
}
// in case none matches, get the system wide configured language, or fall back to English
return $sysLang;
} }
/** /**

View file

@ -4,6 +4,7 @@ use Dice\Dice;
use Friendica\App; use Friendica\App;
use Friendica\Core\Cache; use Friendica\Core\Cache;
use Friendica\Core\Config; use Friendica\Core\Config;
use Friendica\Core\L10n\L10n;
use Friendica\Core\Lock\ILock; use Friendica\Core\Lock\ILock;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Factory; use Friendica\Factory;
@ -173,4 +174,9 @@ return [
['addRoutes', [include __DIR__ . '/routes.config.php'], Dice::CHAIN_CALL], ['addRoutes', [include __DIR__ . '/routes.config.php'], Dice::CHAIN_CALL],
], ],
], ],
L10n::class => [
'constructParams' => [
$_SERVER, $_GET
],
],
]; ];

View file

@ -0,0 +1,95 @@
<?php
namespace src\Core\L10n;
use Friendica\Core\L10n\L10n;
use Friendica\Test\MockedTest;
class L10nTest extends MockedTest
{
public function dataDetectLanguage()
{
return [
'empty' => [
'server' => [],
'get' => [],
'default' => 'en',
'assert' => 'en',
],
'withGet' => [
'server' => [],
'get' => ['lang' => 'de'],
'default' => 'en',
'assert' => 'de',
],
'withPipe' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'en-gb'],
'get' => [],
'default' => 'en',
'assert' => 'en-gb',
],
'withoutPipe' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'fr'],
'get' => [],
'default' => 'en',
'assert' => 'fr',
],
'withQuality1' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'fr;q=0.5,de'],
'get' => [],
'default' => 'en',
'assert' => 'de',
],
'withQuality2' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'fr;q=0.5,de;q=0.2'],
'get' => [],
'default' => 'en',
'assert' => 'fr',
],
'withLangOverride' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'fr;q=0.5,de;q=0.2'],
'get' => ['lang' => 'it'],
'default' => 'en',
'assert' => 'it',
],
'withQualityAndPipe' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'fr;q=0.5,de;q=0.2,nb-no;q=0.7'],
'get' => [],
'default' => 'en',
'assert' => 'nb-no',
],
'withQualityAndInvalid' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'fr;q=0.5,bla;q=0.2,nb-no;q=0.7'],
'get' => [],
'default' => 'en',
'assert' => 'nb-no',
],
'withQualityAndInvalid2' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'blu;q=0.9,bla;q=0.2,nb-no;q=0.7'],
'get' => [],
'default' => 'en',
'assert' => 'nb-no',
],
'withQualityAndInvalidAndAbsolute' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'blu;q=0.9,de,nb-no;q=0.7'],
'get' => [],
'default' => 'en',
'assert' => 'de',
],
'withInvalidGet' => [
'server' => ['HTTP_ACCEPT_LANGUAGE' => 'blu;q=0.9,nb-no;q=0.7'],
'get' => ['lang' => 'blu'],
'default' => 'en',
'assert' => 'nb-no',
],
];
}
/**
* @dataProvider dataDetectLanguage
*/
public function testDetectLanguage(array $server, array $get, string $default, string $assert)
{
$this->assertEquals($assert, L10n::detectLanguage($server, $get, $default));
}
}