@@ -123,12 +123,16 @@ public function encoding(array $supported = []): string
123123 * types the application says it supports, and the types requested
124124 * by the client.
125125 *
126- * If no match is found, the first, highest-ranking client requested
126+ * If loose locale negotiation is enabled and no match is found, the first, highest-ranking client requested
127127 * type is returned.
128128 */
129129 public function language (array $ supported ): string
130130 {
131- return $ this ->getBestMatch ($ supported , $ this ->request ->getHeaderLine ('accept-language ' ), false , false , config (Feature::class)->simpleNegotiateLocale );
131+ if (config (Feature::class)->looseLocaleNegotiation ) {
132+ return $ this ->getBestMatch ($ supported , $ this ->request ->getHeaderLine ('accept-language ' ), false , false , true );
133+ }
134+
135+ return $ this ->getBestLocaleMatch ($ supported , $ this ->request ->getHeaderLine ('accept-language ' ));
132136 }
133137
134138 // --------------------------------------------------------------------
@@ -190,6 +194,62 @@ protected function getBestMatch(
190194 return $ strictMatch ? '' : $ supported [0 ];
191195 }
192196
197+ /**
198+ * Strict locale search, including territories (en-*)
199+ *
200+ * @param list<string> $supported App-supported values
201+ * @param ?string $header Compatible 'Accept-Language' header string
202+ */
203+ protected function getBestLocaleMatch (array $ supported , ?string $ header ): string
204+ {
205+ if ($ supported === []) {
206+ throw HTTPException::forEmptySupportedNegotiations ();
207+ }
208+
209+ if ($ header === null || $ header === '' ) {
210+ return $ supported [0 ];
211+ }
212+
213+ $ acceptable = $ this ->parseHeader ($ header );
214+ $ fallbackLocales = [];
215+
216+ foreach ($ acceptable as $ accept ) {
217+ // if acceptable quality is zero, skip it.
218+ if ($ accept ['q ' ] === 0.0 ) {
219+ continue ;
220+ }
221+
222+ // if acceptable value is "anything", return the first available
223+ if ($ accept ['value ' ] === '* ' || $ accept ['value ' ] === '*/* ' ) {
224+ return $ supported [0 ];
225+ }
226+
227+ // look for exact match
228+ if (in_array ($ accept ['value ' ], $ supported , true )) {
229+ return $ accept ['value ' ];
230+ }
231+
232+ // set a fallback locale
233+ $ fallbackLocales [] = strtok ($ accept ['value ' ], '- ' );
234+ }
235+
236+ foreach ($ fallbackLocales as $ fallbackLocale ) {
237+ // look for exact match
238+ if (in_array ($ fallbackLocale , $ supported , true )) {
239+ return $ fallbackLocale ;
240+ }
241+
242+ // look for locale variants match
243+ foreach ($ supported as $ locale ) {
244+ if (str_starts_with ($ locale , $ fallbackLocale . '- ' )) {
245+ return $ locale ;
246+ }
247+ }
248+ }
249+
250+ return $ supported [0 ];
251+ }
252+
193253 /**
194254 * Parses an Accept* header into it's multiple values.
195255 *
0 commit comments