Skip to content

Commit b4bc1b4

Browse files
authored
Merge pull request #2872 from codeigniter4/localematching
Better locale matching against broad groups. Fixes #2774
2 parents dc733a9 + cf5bf7c commit b4bc1b4

3 files changed

Lines changed: 80 additions & 12 deletions

File tree

system/HTTP/Negotiate.php

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public function encoding(array $supported = []): string
179179
*/
180180
public function language(array $supported): string
181181
{
182-
return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'));
182+
return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true);
183183
}
184184

185185
//--------------------------------------------------------------------
@@ -198,10 +198,11 @@ public function language(array $supported): string
198198
* @param boolean $enforceTypes If TRUE, will compare media types and sub-types.
199199
* @param boolean $strictMatch If TRUE, will return empty string on no match.
200200
* If FALSE, will return the first supported element.
201+
* @param boolean $matchLocales If TRUE, will match locale sub-types to a broad type (fr-FR = fr)
201202
*
202203
* @return string Best match
203204
*/
204-
protected function getBestMatch(array $supported, string $header = null, bool $enforceTypes = false, bool $strictMatch = false): string
205+
protected function getBestMatch(array $supported, string $header = null, bool $enforceTypes = false, bool $strictMatch = false, bool $matchLocales = false): string
205206
{
206207
if (empty($supported))
207208
{
@@ -232,7 +233,7 @@ protected function getBestMatch(array $supported, string $header = null, bool $e
232233
// If an acceptable value is supported, return it
233234
foreach ($supported as $available)
234235
{
235-
if ($this->match($accept, $available, $enforceTypes))
236+
if ($this->match($accept, $available, $enforceTypes, $matchLocales))
236237
{
237238
return $available;
238239
}
@@ -337,12 +338,14 @@ public function parseHeader(string $header): array
337338
/**
338339
* Match-maker
339340
*
340-
* @param array $acceptable
341-
* @param string $supported
342-
* @param boolean $enforceTypes
341+
* @param array $acceptable
342+
* @param string $supported
343+
* @param boolean $enforceTypes
344+
* @param boolean $matchLocales
345+
*
343346
* @return boolean
344347
*/
345-
protected function match(array $acceptable, string $supported, bool $enforceTypes = false): bool
348+
protected function match(array $acceptable, string $supported, bool $enforceTypes = false, $matchLocales = false): bool
346349
{
347350
$supported = $this->parseHeader($supported);
348351
if (is_array($supported) && count($supported) === 1)
@@ -363,6 +366,12 @@ protected function match(array $acceptable, string $supported, bool $enforceType
363366
return $this->matchTypes($acceptable, $supported);
364367
}
365368

369+
// Do we need to match locales against broader locales?
370+
if ($matchLocales)
371+
{
372+
return $this->matchLocales($acceptable, $supported);
373+
}
374+
366375
return false;
367376
}
368377

@@ -409,8 +418,14 @@ protected function matchParameters(array $acceptable, array $supported): bool
409418
*/
410419
public function matchTypes(array $acceptable, array $supported): bool
411420
{
412-
list($aType, $aSubType) = explode('/', $acceptable['value']);
413-
list($sType, $sSubType) = explode('/', $supported['value']);
421+
[
422+
$aType,
423+
$aSubType,
424+
] = explode('/', $acceptable['value']);
425+
[
426+
$sType,
427+
$sSubType,
428+
] = explode('/', $supported['value']);
414429

415430
// If the types don't match, we're done.
416431
if ($aType !== $sType)
@@ -429,4 +444,25 @@ public function matchTypes(array $acceptable, array $supported): bool
429444
}
430445

431446
//--------------------------------------------------------------------
447+
448+
/**
449+
* Will match locales against their broader pairs, so that fr-FR would
450+
* match a supported localed of fr
451+
*
452+
* @param array $acceptable
453+
* @param array $supported
454+
*
455+
* @return boolean
456+
*/
457+
public function matchLocales(array $acceptable, array $supported): bool
458+
{
459+
$aBroad = mb_strpos($acceptable['value'], '-') > 0
460+
? mb_substr($acceptable['value'], 0, mb_strpos($acceptable['value'], '-'))
461+
: $acceptable['value'];
462+
$sBroad = mb_strpos($supported['value'], '-') > 0
463+
? mb_substr($supported['value'], 0, mb_strpos($supported['value'], '-'))
464+
: $supported['value'];
465+
466+
return strtolower($aBroad) === strtolower($sBroad);
467+
}
432468
}

tests/system/HTTP/IncomingRequestTest.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,22 +202,43 @@ public function testSetBadLocale()
202202

203203
//--------------------------------------------------------------------
204204

205+
/**
206+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/2774
207+
*/
205208
public function testNegotiatesLocale()
206209
{
207-
$_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'es; q=1.0, en; q=0.5';
210+
$_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR; q=1.0, en; q=0.5';
208211

209212
$config = new App();
210213
$config->negotiateLocale = true;
211214
$config->supportedLocales = [
215+
'fr',
212216
'en',
213-
'es',
214217
];
215218
$config->baseURL = 'http://example.com';
216219

217220
$request = new IncomingRequest($config, new URI(), null, new UserAgent());
218221

219222
$this->assertEquals($config->defaultLocale, $request->getDefaultLocale());
220-
$this->assertEquals('es', $request->getLocale());
223+
$this->assertEquals('fr', $request->getLocale());
224+
}
225+
226+
public function testNegotiatesLocaleOnlyBroad()
227+
{
228+
$_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr; q=1.0, en; q=0.5';
229+
230+
$config = new App();
231+
$config->negotiateLocale = true;
232+
$config->supportedLocales = [
233+
'fr',
234+
'en',
235+
];
236+
$config->baseURL = 'http://example.com';
237+
238+
$request = new IncomingRequest($config, new URI(), null, new UserAgent());
239+
240+
$this->assertEquals($config->defaultLocale, $request->getDefaultLocale());
241+
$this->assertEquals('fr', $request->getLocale());
221242
}
222243

223244
// The negotiation tests below are not intended to exercise the HTTP\Negotiate class -

tests/system/HTTP/NegotiateTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ public function testAcceptLanguageBasics()
138138
}
139139

140140
//--------------------------------------------------------------------
141+
142+
/**
143+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/2774
144+
*/
145+
public function testAcceptLanguageMatchesBroadly()
146+
{
147+
$this->request->setHeader('Accept-Language', 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7');
148+
149+
$this->assertEquals('fr', $this->negotiate->language(['fr', 'en']));
150+
}
151+
141152
public function testBestMatchEmpty()
142153
{
143154
$this->expectException(Exceptions\HTTPException::class);

0 commit comments

Comments
 (0)