1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6/**
7 * This is a port of HttpAcceptLanguage::Parser from the http_accept_language gem
8 * https://github.com/iain/http_accept_language/blob/v2.1.1/lib/http_accept_language/parser.rb.
9 */
10
11namespace App\Libraries\AcceptHttpLanguage;
12
13use InvalidArgumentException;
14
15class Parser
16{
17 public static function parseHeader(?string $header): array
18 {
19 if (!present($header)) {
20 return [];
21 }
22
23 $header = preg_replace('/\s+/', '', $header);
24 $header = explode(',', $header);
25 try {
26 $mappings = array_map(function ($language) {
27 $exploded = explode(';q=', $language);
28 $locale = strtolower($exploded[0]);
29 $quality = (float) ($exploded[1] ?? 1.0);
30 if (!preg_match('/^[a-z\-0-9]+|\*$/i', $locale)) {
31 throw new InvalidArgumentException('Not correctly formatted');
32 }
33
34 if ($locale === '*') {
35 $locale = null; // Ignore wildcards
36 }
37
38 return [$locale, $quality];
39 }, $header);
40 } catch (InvalidArgumentException $_e) {
41 return [];
42 }
43
44 usort($mappings, function ($left, $right) {
45 return $right[1] <=> $left[1];
46 });
47
48 return array_reject_null(array_map(fn ($mapping) => $mapping[0], $mappings));
49 }
50
51 private array $availableLanguages;
52
53 public function __construct(?array $availableLanguages = null)
54 {
55 $this->availableLanguages = $availableLanguages ?? $GLOBALS['cfg']['app']['available_locales'];
56 }
57
58 /**
59 * Returns the first of the user preferred languages that is
60 * also found in available languages. Finds best fit by matching on
61 * primary language first and secondarily on region. If no matching region is
62 * found, return the first language in the group matching that primary language.
63 *
64 * Example:
65 *
66 * request.language_region_compatible($header)
67 */
68 public function languageRegionCompatibleFor(?string $header): ?string
69 {
70 foreach (static::parseHeader($header) as $preferred) {
71 $preferredLanguage = explode('-', $preferred, 2)[0] ?? null;
72
73 $langGroup = array_values(array_filter(
74 $this->availableLanguages,
75 // en
76 fn ($available) => $preferredLanguage === explode('-', $available, 2)[0] ?? null,
77 ));
78
79 foreach ($langGroup as $lang) {
80 if ($lang === $preferred) {
81 return $lang;
82 }
83 }
84
85 if (isset($langGroup[0])) {
86 return $langGroup[0];
87 }
88 }
89
90 return null;
91 }
92}