2026-08-05 23:58:53 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Core\Localization;
|
|
|
|
|
|
|
|
|
|
use Closure;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
|
use Illuminate\Support\Facades\App;
|
2026-08-24 14:41:50 +03:00
|
|
|
use Illuminate\Support\Facades\URL;
|
|
|
|
|
use Illuminate\Support\Facades\View;
|
2026-08-05 23:58:53 +03:00
|
|
|
use Lunar\Models\Language;
|
2026-08-26 23:52:33 +03:00
|
|
|
use Modules\Core\Localization\Services\LanguageCache;
|
2026-08-05 23:58:53 +03:00
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
|
|
|
|
|
|
class LocaleMiddleware
|
|
|
|
|
{
|
2026-08-26 23:52:33 +03:00
|
|
|
public function __construct(private readonly LanguageCache $languages) {}
|
2026-08-05 23:58:53 +03:00
|
|
|
|
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
|
|
|
{
|
2026-08-26 23:52:33 +03:00
|
|
|
$languages = $this->languages->all();
|
2026-08-05 23:58:53 +03:00
|
|
|
|
|
|
|
|
if ($languages->isEmpty()) {
|
|
|
|
|
return $next($request);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$segment = (string) $request->segment(1);
|
|
|
|
|
$language = $languages->firstWhere('code', $segment);
|
|
|
|
|
|
|
|
|
|
if (! $language) {
|
|
|
|
|
return $this->redirectToLocalizedUrl($request, $languages);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
App::setLocale($language->code);
|
|
|
|
|
$request->attributes->set('locale', $language->code);
|
|
|
|
|
$request->attributes->set('language', $language);
|
|
|
|
|
|
2026-08-24 14:41:50 +03:00
|
|
|
// Lets route() calls omit {locale} anywhere in the request lifecycle
|
|
|
|
|
// (controllers, views) — without this, every route() call would need
|
|
|
|
|
// locale passed explicitly every time.
|
|
|
|
|
URL::defaults(['locale' => $language->code]);
|
|
|
|
|
|
|
|
|
|
$this->shareLocaleViewData($request, $language, $languages);
|
|
|
|
|
|
2026-08-05 23:58:53 +03:00
|
|
|
return $next($request);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 14:41:50 +03:00
|
|
|
/**
|
2026-08-26 13:26:29 +03:00
|
|
|
* Shares the current locale and every OTHER available locale (each with its
|
|
|
|
|
* own URL for the current page) with all views, so the header language
|
|
|
|
|
* switcher and layout hreflang tags don't have to recompute it.
|
|
|
|
|
*
|
|
|
|
|
* `altLocales` is a collection, not a single value — firstWhere('code', '!=',
|
|
|
|
|
* ...) would only ever surface one alternate, which happens to look correct
|
|
|
|
|
* with exactly 2 configured languages (there's only one "other" to find) but
|
|
|
|
|
* silently drops every locale past the first for a 3+ language store, with no
|
|
|
|
|
* error, just fewer switcher options than actually configured. A view iterates
|
|
|
|
|
* `$altLocales` to render as many links/dropdown entries as there are
|
|
|
|
|
* alternates, whether that's 1 or 10.
|
2026-08-24 14:41:50 +03:00
|
|
|
*/
|
|
|
|
|
private function shareLocaleViewData(Request $request, Language $language, Collection $languages): void
|
|
|
|
|
{
|
|
|
|
|
$route = $request->route();
|
|
|
|
|
$routeName = $route?->getName();
|
|
|
|
|
|
2026-08-26 13:26:29 +03:00
|
|
|
$altLocales = $languages
|
|
|
|
|
->reject(fn (Language $other) => $other->code === $language->code)
|
|
|
|
|
->map(fn (Language $other) => [
|
|
|
|
|
'code' => $other->code,
|
|
|
|
|
'name' => $other->name,
|
|
|
|
|
'url' => $routeName
|
|
|
|
|
? route($routeName, array_merge($route->parameters(), ['locale' => $other->code]))
|
|
|
|
|
: url('/'.$other->code),
|
|
|
|
|
])
|
|
|
|
|
->values();
|
|
|
|
|
|
2026-08-24 14:41:50 +03:00
|
|
|
View::share('currentLocale', $language->code);
|
2026-08-26 13:26:29 +03:00
|
|
|
View::share('altLocales', $altLocales);
|
2026-08-24 14:41:50 +03:00
|
|
|
}
|
2026-08-05 23:58:53 +03:00
|
|
|
|
|
|
|
|
private function redirectToLocalizedUrl(Request $request, Collection $languages): Response
|
|
|
|
|
{
|
|
|
|
|
$locale = $this->negotiateLocale($request, $languages);
|
|
|
|
|
|
|
|
|
|
$path = trim($request->getPathInfo(), '/');
|
|
|
|
|
$target = '/'.$locale.($path !== '' ? '/'.$path : '');
|
|
|
|
|
|
|
|
|
|
$query = $request->getQueryString();
|
|
|
|
|
if ($query) {
|
|
|
|
|
$target .= '?'.$query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return redirect($target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function negotiateLocale(Request $request, Collection $languages): string
|
|
|
|
|
{
|
|
|
|
|
$preferred = $request->getPreferredLanguage($languages->pluck('code')->all());
|
|
|
|
|
|
|
|
|
|
if ($preferred) {
|
|
|
|
|
return $preferred;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $languages->firstWhere('default', true)?->code
|
|
|
|
|
?? $languages->first()->code;
|
|
|
|
|
}
|
|
|
|
|
}
|