|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
declare(strict_types=1); |
|
4
|
|
|
|
|
5
|
|
|
namespace Usox\LanguageNegotiator; |
|
6
|
|
|
|
|
7
|
|
|
use Psr\Http\Message\ResponseInterface; |
|
8
|
|
|
use Psr\Http\Message\ServerRequestInterface; |
|
9
|
|
|
use Psr\Http\Server\RequestHandlerInterface; |
|
10
|
|
|
|
|
11
|
|
|
/** |
|
12
|
|
|
* Negotiate the http client language |
|
13
|
|
|
* |
|
14
|
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language |
|
15
|
|
|
*/ |
|
16
|
|
|
final class LanguageNegotiator implements LanguageNegotiatorInterface |
|
17
|
|
|
{ |
|
18
|
|
|
/** @var string Public name of the psr server request attribute */ |
|
19
|
|
|
public const REQUEST_ATTRIBUTE_NAME = 'negotiated-request-language'; |
|
20
|
|
|
|
|
21
|
|
|
/** @var array<string> List of allowed accept-language header names */ |
|
22
|
|
|
private const ACCEPT_LANGUAGE_HEADERS = ['accept-language', 'http_accept_language']; |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* @param array<string> $supportedLanguages List of language codes you application supports |
|
26
|
|
|
* @param string $fallbackLanguage language code of the fallback language if negotiation fails |
|
27
|
|
|
* @param null|array<string, mixed> $serverRequest Optional server request input ($_SERVER) |
|
28
|
|
|
* @param string $attributeName Optional alternate name of the server request attribute for the negotiated language |
|
29
|
|
|
*/ |
|
30
|
14 |
|
public function __construct( |
|
31
|
|
|
private array $supportedLanguages, |
|
32
|
|
|
private string $fallbackLanguage = 'en', |
|
33
|
|
|
private ?array $serverRequest = null, |
|
34
|
|
|
private string $attributeName = self::REQUEST_ATTRIBUTE_NAME |
|
35
|
|
|
) { |
|
36
|
14 |
|
} |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* Negotiates the client language |
|
40
|
|
|
* If the serverRequest was set by constructor, the parameter can be omitted |
|
41
|
|
|
* |
|
42
|
|
|
* @param array<string, mixed>|null $request Optional dict of server request variables; overwrites serverRequest defined in constructor |
|
43
|
|
|
* |
|
44
|
|
|
* @return string language code of the negotiated client language |
|
45
|
|
|
*/ |
|
46
|
14 |
|
public function negotiate( |
|
47
|
|
|
?array $request = null |
|
48
|
|
|
): string { |
|
49
|
|
|
// return fallback if both, headerLine and serverRequest is not set |
|
50
|
14 |
|
$requestHeaders = $request ?? $this->serverRequest ?? null; |
|
51
|
|
|
|
|
52
|
14 |
|
if ($requestHeaders === null) { |
|
53
|
1 |
|
return $this->fallbackLanguage; |
|
54
|
|
|
} |
|
55
|
|
|
|
|
56
|
13 |
|
$headerValue = $this->normalizeAcceptLanguageString($requestHeaders); |
|
57
|
|
|
|
|
58
|
|
|
// return the fallback if the header is not usable |
|
59
|
13 |
|
if ($headerValue === null) { |
|
60
|
6 |
|
return $this->fallbackLanguage; |
|
61
|
|
|
} |
|
62
|
|
|
|
|
63
|
|
|
// split up the header to determine a list of all accepted languages |
|
64
|
7 |
|
$acceptedLanguages = array_reduce( |
|
65
|
7 |
|
explode(',', $headerValue), |
|
66
|
7 |
|
static function (array $result, string $line): array { |
|
67
|
7 |
|
[$language, $priority] = array_merge(explode(';q=', $line), [1]); |
|
68
|
7 |
|
$result[trim((string) $language)] = (float) $priority; |
|
69
|
7 |
|
return $result; |
|
70
|
7 |
|
}, |
|
71
|
7 |
|
[] |
|
72
|
|
|
); |
|
73
|
|
|
|
|
74
|
|
|
// sort by value which is actually the language priority defined within the client |
|
75
|
7 |
|
arsort($acceptedLanguages); |
|
76
|
|
|
|
|
77
|
|
|
// determine the intersection between supported and available languages |
|
78
|
7 |
|
$result = array_intersect_key( |
|
79
|
7 |
|
$acceptedLanguages, |
|
80
|
7 |
|
array_flip($this->supportedLanguages), |
|
81
|
|
|
); |
|
82
|
|
|
|
|
83
|
|
|
// use the set fallback language if negotiation fails |
|
84
|
7 |
|
if ($result === []) { |
|
85
|
2 |
|
return $this->fallbackLanguage; |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
// returns the first element (the one having the highest priority) |
|
89
|
5 |
|
return key($result); |
|
|
|
|
|
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
/** |
|
93
|
|
|
* Enriches the ServerRequest with the negotiated client language |
|
94
|
|
|
*/ |
|
95
|
1 |
|
public function process( |
|
96
|
|
|
ServerRequestInterface $request, |
|
97
|
|
|
RequestHandlerInterface $handler |
|
98
|
|
|
): ResponseInterface { |
|
99
|
1 |
|
$request = $request->withAttribute( |
|
100
|
1 |
|
$this->attributeName, |
|
101
|
1 |
|
$this->negotiate($request->getHeaders()) |
|
102
|
|
|
); |
|
103
|
|
|
|
|
104
|
1 |
|
return $handler->handle($request); |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
/** |
|
108
|
|
|
* @param array<string, mixed> $headers |
|
109
|
|
|
*/ |
|
110
|
13 |
|
private function normalizeAcceptLanguageString(array $headers): ?string { |
|
111
|
13 |
|
$headerValue = null; |
|
112
|
|
|
|
|
113
|
|
|
// convert all keys to lowercase - just in case |
|
114
|
13 |
|
$headers = array_change_key_case($headers); |
|
115
|
|
|
|
|
116
|
|
|
// lookup all accepted header values |
|
117
|
13 |
|
foreach (self::ACCEPT_LANGUAGE_HEADERS as $headerName) { |
|
118
|
13 |
|
$headerValue = $headers[$headerName] ?? null; |
|
119
|
13 |
|
if ($headerValue !== null) { |
|
120
|
9 |
|
break; |
|
121
|
|
|
} |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
// maybe something goes wrong and we end up with some other scalar values in here - jest ensure, it's a string |
|
125
|
13 |
|
if (!is_string($headerValue)) { |
|
126
|
6 |
|
return null; |
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
7 |
|
return strtolower($headerValue); |
|
130
|
|
|
} |
|
131
|
|
|
} |
|
132
|
|
|
|