Release v1.0.1: installer locale detection from Step 1

User-facing improvements
- Installer detects browser language from the Accept-Language header
  on the very first request. de-* → Schneespur/DE, everything else
  (en-*, zh, ja, ar, it, fr, …) → Wintertrace/EN.
- DE/EN switcher in the installer layout header for manual override.
  The choice persists for the browser session.
- Step 5 (Config) now reflects the already-resolved locale instead of
  detecting again client-side, eliminating a brief "wrong language"
  flash between steps.

Under the hood
- New SetInstallerLocale middleware (Symfony's getPreferredLanguage
  with strict top-preference, list order ['en','de'] so non-matching
  browsers fall back to en/Wintertrace, not de/Schneespur).
- brand() helper resolves from the runtime locale when the app_brand
  setting is absent (pre-install). Post-install behaviour unchanged.
- Composer now declares ext-sodium explicitly (required by the Ed25519
  signature verification in the auto-update flow).

No DB migrations, no breaking changes, no config rewrites needed.
This commit is contained in:
Michael 2026-05-17 17:35:02 +00:00
parent d71e8717ec
commit 53b29bd0e6
15 changed files with 89 additions and 27 deletions

View file

@ -1 +1 @@
1.0.0
1.0.1

View file

@ -27,6 +27,17 @@ class InstallerController extends Controller
private InstallLockManager $lockManager,
) {}
// --- Locale switcher (works on any installer step) ---
public function switchLocale(Request $request, string $locale): RedirectResponse
{
if (in_array($locale, ['de', 'en'], true)) {
$request->session()->put('installer_locale', $locale);
}
return redirect($request->headers->get('referer') ?: route('install.welcome'));
}
// --- Step 1: Welcome ---
public function showWelcome(Request $request): View

View file

@ -0,0 +1,36 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\Response;
class SetInstallerLocale
{
// Order matters: Symfony's getPreferredLanguage falls back to the FIRST list
// entry when no browser language matches, so 'en' must lead — that makes
// zh-CN, ja, ar, etc. resolve to en/Wintertrace instead of de/Schneespur.
private const SUPPORTED = ['en', 'de'];
private const FALLBACK = 'en';
public function handle(Request $request, Closure $next): Response
{
$session = $request->session()->get('installer_locale');
if (in_array($session, self::SUPPORTED, true)) {
$locale = $session;
} else {
$locale = $request->getPreferredLanguage(self::SUPPORTED) ?: self::FALLBACK;
$request->session()->put('installer_locale', $locale);
}
App::setLocale($locale);
View::share('installerLocale', $locale);
return $next($request);
}
}

View file

@ -2,17 +2,24 @@
use App\Models\Setting;
function brand_slug(): string
{
try {
$slug = Setting::get('app_brand');
if ($slug !== null) {
return $slug;
}
} catch (\Throwable) {
// Settings table not yet migrated (early installer steps) — fall through to locale-based default.
}
return app()->getLocale() === 'de' ? 'schneespur' : 'wintertrace';
}
function brand(): string
{
$slug = Setting::get('app_brand', 'schneespur');
return match ($slug) {
return match (brand_slug()) {
'wintertrace' => 'Wintertrace',
default => 'Schneespur',
};
}
function brand_slug(): string
{
return Setting::get('app_brand', 'schneespur');
}

View file

@ -7,6 +7,7 @@ use App\Http\Middleware\EnsureDriver;
use App\Http\Middleware\EnsureDsgvoInformed;
use App\Http\Middleware\InstallerGuard;
use App\Http\Middleware\RedirectToInstaller;
use App\Http\Middleware\SetInstallerLocale;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Console\Scheduling\Schedule;
@ -57,6 +58,7 @@ return Application::configure(basePath: dirname(__DIR__))
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
SetInstallerLocale::class,
ValidateCsrfToken::class,
]);
})

View file

@ -12,7 +12,7 @@
"src": "node_modules/leaflet/dist/images/marker-icon.png"
},
"resources/css/app.css": {
"file": "assets/app-CQAECC6q.css",
"file": "assets/app-CPqZi6LM.css",
"src": "resources/css/app.css",
"isEntry": true,
"name": "app",

View file

@ -1 +1 @@
if(!self.define){let e,s={};const r=(r,n)=>(r=new URL(r+".js",n).href,s[r]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=r,e.onload=s,document.head.appendChild(e)}else e=r,importScripts(r),s()}).then(()=>{let e=s[r];if(!e)throw new Error(`Module ${r} didnt register its module`);return e}));self.define=(n,i)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let t={};const l=e=>r(e,o),c={module:{uri:o},exports:t,require:l};s[o]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(i(...e),t))}}define(["./workbox-466e78f2"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"2d094791c49e920331981a2d203b8cdb"},{url:"assets/marker-icon-hN30_KVU.png",revision:null},{url:"assets/layers-BWBAp2CZ.png",revision:null},{url:"assets/layers-2x-Bpkbi35X.png",revision:null},{url:"assets/app-CQAECC6q.css",revision:null},{url:"assets/app-Bwe1Adxb.js",revision:null}],{}),e.cleanupOutdatedCaches(),e.registerRoute(/\/(login|register|forgot-password|reset-password|verify-email|confirm-password)/,new e.NetworkOnly,"GET"),e.registerRoute(/\.(?:woff2?|ttf|eot|otf)$/,new e.CacheFirst({cacheName:"fonts-cache",plugins:[new e.ExpirationPlugin({maxEntries:30,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/\/driver\/.*/,new e.NetworkFirst({cacheName:"driver-pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET")});
if(!self.define){let e,s={};const r=(r,n)=>(r=new URL(r+".js",n).href,s[r]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=r,e.onload=s,document.head.appendChild(e)}else e=r,importScripts(r),s()}).then(()=>{let e=s[r];if(!e)throw new Error(`Module ${r} didnt register its module`);return e}));self.define=(n,i)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let t={};const l=e=>r(e,o),c={module:{uri:o},exports:t,require:l};s[o]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(i(...e),t))}}define(["./workbox-466e78f2"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"2d094791c49e920331981a2d203b8cdb"},{url:"assets/marker-icon-hN30_KVU.png",revision:null},{url:"assets/layers-BWBAp2CZ.png",revision:null},{url:"assets/layers-2x-Bpkbi35X.png",revision:null},{url:"assets/app-CPqZi6LM.css",revision:null},{url:"assets/app-Bwe1Adxb.js",revision:null}],{}),e.cleanupOutdatedCaches(),e.registerRoute(/\/(login|register|forgot-password|reset-password|verify-email|confirm-password)/,new e.NetworkOnly,"GET"),e.registerRoute(/\.(?:woff2?|ttf|eot|otf)$/,new e.CacheFirst({cacheName:"fonts-cache",plugins:[new e.ExpirationPlugin({maxEntries:30,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/\/driver\/.*/,new e.NetworkFirst({cacheName:"driver-pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET")});

View file

@ -25,8 +25,17 @@
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-12 bg-gray-100 installer-fallback">
<div class="mb-6">
<div class="w-full sm:max-w-2xl px-6 flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-800"> {{ brand() }}</h1>
@php($currentLocale = $installerLocale ?? app()->getLocale())
<div class="inline-flex rounded-md shadow-sm overflow-hidden border border-gray-300" role="group" aria-label="Language">
<a href="{{ route('install.locale.switch', ['locale' => 'de']) }}"
class="px-3 py-1 text-xs font-semibold {{ $currentLocale === 'de' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50' }}"
aria-current="{{ $currentLocale === 'de' ? 'true' : 'false' }}">DE</a>
<a href="{{ route('install.locale.switch', ['locale' => 'en']) }}"
class="px-3 py-1 text-xs font-semibold border-l border-gray-300 {{ $currentLocale === 'en' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50' }}"
aria-current="{{ $currentLocale === 'en' ? 'true' : 'false' }}">EN</a>
</div>
</div>
@include('installer._stepper', ['currentStep' => $currentStep ?? 1])

View file

@ -13,7 +13,7 @@
<form method="POST" action="{{ route('install.config.store') }}" x-data="{
timezone: '{{ old('timezone', $timezone) }}',
locale: '{{ old('locale', 'de') }}',
locale: '{{ old('locale', app()->getLocale()) }}',
get brandName() {
return this.locale === 'de' ? 'Schneespur' : 'Wintertrace';
},
@ -27,17 +27,6 @@
}
}
} catch (e) {}
@if(! old('locale'))
try {
const browserLang = (navigator.language || '').substring(0, 2).toLowerCase();
const supported = ['de', 'en'];
if (supported.includes(browserLang)) {
this.locale = browserLang;
} else {
this.locale = 'en';
}
} catch (e) {}
@endif
}
}">
@csrf

View file

@ -5,6 +5,11 @@ use Illuminate\Support\Facades\Route;
Route::middleware('installer')->prefix('install')->name('install.')->group(function () {
Route::get('/locale/{locale}', [InstallerController::class, 'switchLocale'])
->name('locale.switch')
->where('locale', 'de|en')
->middleware('throttle:20,1');
Route::get('/', [InstallerController::class, 'showWelcome'])->name('welcome');
Route::post('/welcome', [InstallerController::class, 'processWelcome'])->name('welcome.process')->middleware('throttle:10,1');

View file

@ -0,0 +1 @@
{"current_version":"1.0.1","last_counter":1}

View file

@ -85,6 +85,7 @@ return array(
'App\\Http\\Middleware\\EnsureDsgvoInformed' => $baseDir . '/app/Http/Middleware/EnsureDsgvoInformed.php',
'App\\Http\\Middleware\\InstallerGuard' => $baseDir . '/app/Http/Middleware/InstallerGuard.php',
'App\\Http\\Middleware\\RedirectToInstaller' => $baseDir . '/app/Http/Middleware/RedirectToInstaller.php',
'App\\Http\\Middleware\\SetInstallerLocale' => $baseDir . '/app/Http/Middleware/SetInstallerLocale.php',
'App\\Http\\Requests\\Admin\\AnonymizeDriverRequest' => $baseDir . '/app/Http/Requests/Admin/AnonymizeDriverRequest.php',
'App\\Http\\Requests\\Admin\\StoreCustomerObjectRequest' => $baseDir . '/app/Http/Requests/Admin/StoreCustomerObjectRequest.php',
'App\\Http\\Requests\\Admin\\StoreCustomerRequest' => $baseDir . '/app/Http/Requests/Admin/StoreCustomerRequest.php',

View file

@ -688,6 +688,7 @@ class ComposerStaticInitfc2407b1a509d7fcbbc5146f46a2c921
'App\\Http\\Middleware\\EnsureDsgvoInformed' => __DIR__ . '/../..' . '/app/Http/Middleware/EnsureDsgvoInformed.php',
'App\\Http\\Middleware\\InstallerGuard' => __DIR__ . '/../..' . '/app/Http/Middleware/InstallerGuard.php',
'App\\Http\\Middleware\\RedirectToInstaller' => __DIR__ . '/../..' . '/app/Http/Middleware/RedirectToInstaller.php',
'App\\Http\\Middleware\\SetInstallerLocale' => __DIR__ . '/../..' . '/app/Http/Middleware/SetInstallerLocale.php',
'App\\Http\\Requests\\Admin\\AnonymizeDriverRequest' => __DIR__ . '/../..' . '/app/Http/Requests/Admin/AnonymizeDriverRequest.php',
'App\\Http\\Requests\\Admin\\StoreCustomerObjectRequest' => __DIR__ . '/../..' . '/app/Http/Requests/Admin/StoreCustomerObjectRequest.php',
'App\\Http\\Requests\\Admin\\StoreCustomerRequest' => __DIR__ . '/../..' . '/app/Http/Requests/Admin/StoreCustomerRequest.php',

View file

@ -3,7 +3,7 @@
'name' => 'laravel/laravel',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'ca5069bf81288542f9fd3acd2ab2a5e42a0c5d80',
'reference' => '2b36cc8a8edf1403b5796fbe5db3e8168c103c29',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -418,7 +418,7 @@
'laravel/laravel' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'ca5069bf81288542f9fd3acd2ab2a5e42a0c5d80',
'reference' => '2b36cc8a8edf1403b5796fbe5db3e8168c103c29',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),