Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.24% covered (success)
90.24%
37 / 41
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocaleDetectionMiddleware
90.00% covered (success)
90.00%
36 / 40
71.43% covered (warning)
71.43%
5 / 7
20.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 resolveLocale
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 resolveQueryLocale
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
5.26
 resolveSessionLocale
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 resolveHeaderOrDefault
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 detectFromAcceptHeader
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Translation\Middleware;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Translation\LocaleContext;
12use App\Core\Translation\LocaleContextInterface;
13use Psr\Http\Message\ResponseInterface;
14use Psr\Http\Message\ServerRequestInterface;
15use Psr\Http\Server\MiddlewareInterface;
16use Psr\Http\Server\RequestHandlerInterface;
17use Yiisoft\Session\SessionInterface;
18use Yiisoft\Translator\TranslatorInterface;
19
20/**
21 * Locale Detection Middleware.
22 *
23 * Resolves user locale from session, authenticated identity, or Accept-Language header.
24 *
25 * @package App\Core\Translation\Middleware
26 */
27final readonly class LocaleDetectionMiddleware implements MiddlewareInterface
28{
29    /** @var string Session storage key for selected locale. */
30    public const string SESSION_KEY = 'app_locale';
31
32    /**
33     * LocaleDetectionMiddleware constructor.
34     *
35     * @param LocaleContextInterface $localeContext Locale context manager.
36     * @param TranslatorInterface $translator Yii3 translator service.
37     * @param SessionInterface $session Session interface.
38     */
39    public function __construct(
40        private LocaleContextInterface $localeContext,
41        private TranslatorInterface $translator,
42        private SessionInterface $session
43    ) {
44    }
45
46    /**
47     * {@inheritdoc}
48     */
49    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
50    {
51        $resolvedLocale = $this->resolveLocale($request);
52        $this->localeContext->setLocale($resolvedLocale);
53        $this->translator->setLocale($this->localeContext->getLocale());
54
55        return $handler->handle($request);
56    }
57
58    /**
59     * Resolves target locale in priority order: Session -> Auth -> Accept-Language -> Default.
60     *
61     * @param ServerRequestInterface $request HTTP server request.
62     * @return string Resolved locale code.
63     */
64    private function resolveLocale(ServerRequestInterface $request): string
65    {
66        $queryLocale = $this->resolveQueryLocale($request);
67        if ($queryLocale !== null) {
68            return $queryLocale;
69        }
70
71        $sessionLocale = $this->resolveSessionLocale();
72        if ($sessionLocale !== null) {
73            return $sessionLocale;
74        }
75
76        return $this->resolveHeaderOrDefault($request);
77    }
78
79    private function resolveQueryLocale(ServerRequestInterface $request): ?string
80    {
81        $queryParams = $request->getQueryParams();
82        $queryLocale = strtolower(trim((string) ($queryParams['lang'] ?? $queryParams['locale'] ?? '')));
83        if ($queryLocale !== '' && $this->localeContext->isValidLocale($queryLocale)) {
84            if ($this->session->isActive()) {
85                $this->session->set(self::SESSION_KEY, $queryLocale);
86            }
87            return $queryLocale;
88        }
89
90        return null;
91    }
92
93    private function resolveSessionLocale(): ?string
94    {
95        if ($this->session->isActive()) {
96            $sessionLocale = (string) $this->session->get(self::SESSION_KEY, '');
97            if ($sessionLocale !== '' && $this->localeContext->isValidLocale($sessionLocale)) {
98                return $sessionLocale;
99            }
100        }
101
102        return null;
103    }
104
105    private function resolveHeaderOrDefault(ServerRequestInterface $request): string
106    {
107        $acceptHeader = $request->getHeaderLine('Accept-Language');
108        if ($acceptHeader !== '') {
109            $detected = $this->detectFromAcceptHeader($acceptHeader);
110            if ($detected !== null) {
111                if ($this->session->isActive()) {
112                    $this->session->set(self::SESSION_KEY, $detected);
113                }
114                return $detected;
115            }
116        }
117
118        return LocaleContext::DEFAULT_LOCALE;
119    }
120
121    /**
122     * Parses and matches Accept-Language header against active languages.
123     *
124     * @param string $header Raw Accept-Language header value.
125     * @return string|null Matching active language code or null.
126     */
127    private function detectFromAcceptHeader(string $header): ?string
128    {
129        $activeLanguages = $this->localeContext->getActiveLanguages();
130        $parts = explode(',', $header);
131
132        foreach ($parts as $part) {
133            $langTag = trim(explode(';', $part)[0]);
134            $primaryLang = strtolower(explode('-', $langTag)[0]);
135
136            if (isset($activeLanguages[$primaryLang])) {
137                return $primaryLang;
138            }
139        }
140
141        return null;
142    }
143}