From aefd3cc3dd745f1558d8b690e556411b3d02928e Mon Sep 17 00:00:00 2001 From: Bertware Date: Sun, 15 Jan 2017 16:13:03 +0100 Subject: [PATCH] Improved detection in accept-language #242 Added tests for language, special cases. Fixed broken tests #253 --- app/Http/Controllers/Welcome.php | 2 +- app/Http/Middleware/Language.php | 59 +++++++++++++++++-- .../Middleware/LanguageMiddlewareTest.php | 33 +++++++++++ tests/VariousTest.php | 3 +- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Welcome.php b/app/Http/Controllers/Welcome.php index 4da4ef1c..f351f2dd 100644 --- a/app/Http/Controllers/Welcome.php +++ b/app/Http/Controllers/Welcome.php @@ -14,7 +14,7 @@ public function __construct() public function index() { - if (! Session::get('lang')) { + if (Session::get('lang') == null || empty(Session::get('lang'))) { return View('language'); } else { return Redirect::to('route'); diff --git a/app/Http/Middleware/Language.php b/app/Http/Middleware/Language.php index 28882757..3d1c8e2e 100644 --- a/app/Http/Middleware/Language.php +++ b/app/Http/Middleware/Language.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Session; +use Locale; class Language { @@ -29,13 +30,16 @@ class Language */ public function handle($request, Closure $next) { - $browserLanguage = \Locale::lookup(['nl', 'fr', 'en'], $request->server('HTTP_ACCEPT_LANGUAGE')); + $browserLanguage = $this->matchAcceptLanguage($request->server('HTTP_ACCEPT_LANGUAGE'), ['nl', 'fr', 'en']); - // if the user didn't set a language in the cookie, and a browser language is available, use browser language. - if (empty(Session::get('lang') && ! empty($browserLanguage))) { + if (!empty(Input::get('lang'))) { + // if a language is set in the browser, we need to update the language. User may have requested a switch. + $language = Input::get('lang'); + } elseif (empty(Session::get('lang')) && !empty($browserLanguage)) { + // if the user didn't set a language in the cookie, and a browser language is available, use browser language. $language = $browserLanguage; } else { - $language = (Input::get('lang')) ?: Session::get('lang'); + $language = Session::get('lang'); } $this->setSupportedLanguage($language); @@ -63,4 +67,51 @@ private function setSupportedLanguage($lang) Session::put('lang', $lang); } } + + /** + * Search a HTTP accept-header for a supported language. Take order (weight, q=) into account. Skip unsupported + * languages. + * + * This method has a big avantage over locale_lookup, as locale_lookup will only look at the first language in accept-language. + * This method will skip unsupported languages and keep searching for a supported, meaning that if only NL and EN are supported, + * da, nl;q=0.5, en;q=0.2 will result in nl being chosen. (vs null return from locale_lookup) + * + * @param string $header the HTTP accept-language header to search + * @param array $supported the list of supported languages + * @return string|null if a language matches, a value from the supported array is returned. If not, null is + * returned. + */ + private function matchAcceptLanguage($header, $supported) + { + + // step 1: transform header into array + // source for step 1: http://stackoverflow.com/questions/6168519/transform-accept-language-output-in-array + + $values = explode(',', $header); + + $accept_language = array(); + + foreach ($values AS $lang) { + $cnt = preg_match('/([-a-zA-Z]+)\s*;\s*q=([0-9\.]+)/', $lang, $matches); + if ($cnt === 0) { + $accept_language[$lang] = 1; + } else { + $accept_language[$matches[1]] = $matches[2]; + } + } + + // step 2: match array with supported languages + + foreach ($accept_language as $accept_lang => $accept_lang_q) { + foreach ($supported as $supported_lang) { + // use filterMatches to ensure nl-be, nl, nl-nl all match nl, etc.. + if (Locale::filterMatches($accept_lang, $supported_lang)) { + return $supported_lang; + }; + } + } + + // no match found + return null; + } } diff --git a/tests/App/Http/Middleware/LanguageMiddlewareTest.php b/tests/App/Http/Middleware/LanguageMiddlewareTest.php index 775255a6..870ced0f 100644 --- a/tests/App/Http/Middleware/LanguageMiddlewareTest.php +++ b/tests/App/Http/Middleware/LanguageMiddlewareTest.php @@ -17,6 +17,39 @@ public function testSetLanguageToFrench() $this->setAssertLanguageTest('fr', 'fr'); } + public function testSetLanguageToFrenchWhileBrowserDutch() + { + // GET parameters should have priority over browser language. + $response = $this->get('/?lang=fr', ['accept-language' => 'nl-be']); + $this->assertEquals('fr', $this->getApplicationLanguage()); + } + + public function testSetLanguageFromBrowser() + { + $response = $this->get('/', ['accept-language' => 'nl-be']); + $this->assertEquals('nl', $this->getApplicationLanguage()); + } + + public function testSetLanguageFromBrowser2() + { + $response = $this->get('/', ['accept-language' => 'nl']); + $this->assertEquals('nl', $this->getApplicationLanguage()); + } + + public function testSetLanguageFromBrowser3() + { + // unsupported languages mixed with supported. Should pick first supported. + $response = $this->get('/', ['accept-language' => 'da, nl;q=0.9, en;q=0.5']); + $this->assertEquals('nl', $this->getApplicationLanguage()); + } + + public function testSetLanguageFromBrowser4() + { + // unsupported languages. Should fall back to en. + $response = $this->get('/', ['accept-language' => 'da, se;q=0.9, no;q=0.5']); + $this->assertEquals('en', $this->getApplicationLanguage()); + } + public function testFallbackToDefaultLanguageWhenLanguageDoesNotExist() { $this->setAssertLanguageTest('en', 'NotALanguage'); diff --git a/tests/VariousTest.php b/tests/VariousTest.php index 02edbca7..5982cc1a 100644 --- a/tests/VariousTest.php +++ b/tests/VariousTest.php @@ -5,8 +5,9 @@ class VariousTest extends TestCase { public function testHome() { + // expect redirect, since call includes "accept-language" header $response = $this->call('GET', '/'); - $this->assertEquals(200, $response->status()); + $this->assertEquals(302, $response->status()); } public function testContributors()