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:
parent
d71e8717ec
commit
53b29bd0e6
15 changed files with 89 additions and 27 deletions
|
|
@ -1 +1 @@
|
||||||
1.0.0
|
1.0.1
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,17 @@ class InstallerController extends Controller
|
||||||
private InstallLockManager $lockManager,
|
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 ---
|
// --- Step 1: Welcome ---
|
||||||
|
|
||||||
public function showWelcome(Request $request): View
|
public function showWelcome(Request $request): View
|
||||||
|
|
|
||||||
36
schneespur/app/Http/Middleware/SetInstallerLocale.php
Normal file
36
schneespur/app/Http/Middleware/SetInstallerLocale.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,17 +2,24 @@
|
||||||
|
|
||||||
use App\Models\Setting;
|
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
|
function brand(): string
|
||||||
{
|
{
|
||||||
$slug = Setting::get('app_brand', 'schneespur');
|
return match (brand_slug()) {
|
||||||
|
|
||||||
return match ($slug) {
|
|
||||||
'wintertrace' => 'Wintertrace',
|
'wintertrace' => 'Wintertrace',
|
||||||
default => 'Schneespur',
|
default => 'Schneespur',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function brand_slug(): string
|
|
||||||
{
|
|
||||||
return Setting::get('app_brand', 'schneespur');
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use App\Http\Middleware\EnsureDriver;
|
||||||
use App\Http\Middleware\EnsureDsgvoInformed;
|
use App\Http\Middleware\EnsureDsgvoInformed;
|
||||||
use App\Http\Middleware\InstallerGuard;
|
use App\Http\Middleware\InstallerGuard;
|
||||||
use App\Http\Middleware\RedirectToInstaller;
|
use App\Http\Middleware\RedirectToInstaller;
|
||||||
|
use App\Http\Middleware\SetInstallerLocale;
|
||||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
|
|
@ -57,6 +58,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
AddQueuedCookiesToResponse::class,
|
AddQueuedCookiesToResponse::class,
|
||||||
StartSession::class,
|
StartSession::class,
|
||||||
ShareErrorsFromSession::class,
|
ShareErrorsFromSession::class,
|
||||||
|
SetInstallerLocale::class,
|
||||||
ValidateCsrfToken::class,
|
ValidateCsrfToken::class,
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -12,7 +12,7 @@
|
||||||
"src": "node_modules/leaflet/dist/images/marker-icon.png"
|
"src": "node_modules/leaflet/dist/images/marker-icon.png"
|
||||||
},
|
},
|
||||||
"resources/css/app.css": {
|
"resources/css/app.css": {
|
||||||
"file": "assets/app-CQAECC6q.css",
|
"file": "assets/app-CPqZi6LM.css",
|
||||||
"src": "resources/css/app.css",
|
"src": "resources/css/app.css",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"name": "app",
|
"name": "app",
|
||||||
|
|
|
||||||
|
|
@ -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} didn’t 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} didn’t 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")});
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,17 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans text-gray-900 antialiased">
|
<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="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>
|
<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>
|
</div>
|
||||||
|
|
||||||
@include('installer._stepper', ['currentStep' => $currentStep ?? 1])
|
@include('installer._stepper', ['currentStep' => $currentStep ?? 1])
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
<form method="POST" action="{{ route('install.config.store') }}" x-data="{
|
<form method="POST" action="{{ route('install.config.store') }}" x-data="{
|
||||||
timezone: '{{ old('timezone', $timezone) }}',
|
timezone: '{{ old('timezone', $timezone) }}',
|
||||||
locale: '{{ old('locale', 'de') }}',
|
locale: '{{ old('locale', app()->getLocale()) }}',
|
||||||
get brandName() {
|
get brandName() {
|
||||||
return this.locale === 'de' ? 'Schneespur' : 'Wintertrace';
|
return this.locale === 'de' ? 'Schneespur' : 'Wintertrace';
|
||||||
},
|
},
|
||||||
|
|
@ -27,17 +27,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} 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
|
@csrf
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::middleware('installer')->prefix('install')->name('install.')->group(function () {
|
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::get('/', [InstallerController::class, 'showWelcome'])->name('welcome');
|
||||||
Route::post('/welcome', [InstallerController::class, 'processWelcome'])->name('welcome.process')->middleware('throttle:10,1');
|
Route::post('/welcome', [InstallerController::class, 'processWelcome'])->name('welcome.process')->middleware('throttle:10,1');
|
||||||
|
|
||||||
|
|
|
||||||
1
schneespur/storage/app/schneespur_update_state.json
Normal file
1
schneespur/storage/app/schneespur_update_state.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"current_version":"1.0.1","last_counter":1}
|
||||||
|
|
@ -85,6 +85,7 @@ return array(
|
||||||
'App\\Http\\Middleware\\EnsureDsgvoInformed' => $baseDir . '/app/Http/Middleware/EnsureDsgvoInformed.php',
|
'App\\Http\\Middleware\\EnsureDsgvoInformed' => $baseDir . '/app/Http/Middleware/EnsureDsgvoInformed.php',
|
||||||
'App\\Http\\Middleware\\InstallerGuard' => $baseDir . '/app/Http/Middleware/InstallerGuard.php',
|
'App\\Http\\Middleware\\InstallerGuard' => $baseDir . '/app/Http/Middleware/InstallerGuard.php',
|
||||||
'App\\Http\\Middleware\\RedirectToInstaller' => $baseDir . '/app/Http/Middleware/RedirectToInstaller.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\\AnonymizeDriverRequest' => $baseDir . '/app/Http/Requests/Admin/AnonymizeDriverRequest.php',
|
||||||
'App\\Http\\Requests\\Admin\\StoreCustomerObjectRequest' => $baseDir . '/app/Http/Requests/Admin/StoreCustomerObjectRequest.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',
|
'App\\Http\\Requests\\Admin\\StoreCustomerRequest' => $baseDir . '/app/Http/Requests/Admin/StoreCustomerRequest.php',
|
||||||
|
|
|
||||||
|
|
@ -688,6 +688,7 @@ class ComposerStaticInitfc2407b1a509d7fcbbc5146f46a2c921
|
||||||
'App\\Http\\Middleware\\EnsureDsgvoInformed' => __DIR__ . '/../..' . '/app/Http/Middleware/EnsureDsgvoInformed.php',
|
'App\\Http\\Middleware\\EnsureDsgvoInformed' => __DIR__ . '/../..' . '/app/Http/Middleware/EnsureDsgvoInformed.php',
|
||||||
'App\\Http\\Middleware\\InstallerGuard' => __DIR__ . '/../..' . '/app/Http/Middleware/InstallerGuard.php',
|
'App\\Http\\Middleware\\InstallerGuard' => __DIR__ . '/../..' . '/app/Http/Middleware/InstallerGuard.php',
|
||||||
'App\\Http\\Middleware\\RedirectToInstaller' => __DIR__ . '/../..' . '/app/Http/Middleware/RedirectToInstaller.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\\AnonymizeDriverRequest' => __DIR__ . '/../..' . '/app/Http/Requests/Admin/AnonymizeDriverRequest.php',
|
||||||
'App\\Http\\Requests\\Admin\\StoreCustomerObjectRequest' => __DIR__ . '/../..' . '/app/Http/Requests/Admin/StoreCustomerObjectRequest.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',
|
'App\\Http\\Requests\\Admin\\StoreCustomerRequest' => __DIR__ . '/../..' . '/app/Http/Requests/Admin/StoreCustomerRequest.php',
|
||||||
|
|
|
||||||
4
schneespur/vendor/composer/installed.php
vendored
4
schneespur/vendor/composer/installed.php
vendored
|
|
@ -3,7 +3,7 @@
|
||||||
'name' => 'laravel/laravel',
|
'name' => 'laravel/laravel',
|
||||||
'pretty_version' => 'dev-master',
|
'pretty_version' => 'dev-master',
|
||||||
'version' => 'dev-master',
|
'version' => 'dev-master',
|
||||||
'reference' => 'ca5069bf81288542f9fd3acd2ab2a5e42a0c5d80',
|
'reference' => '2b36cc8a8edf1403b5796fbe5db3e8168c103c29',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|
@ -418,7 +418,7 @@
|
||||||
'laravel/laravel' => array(
|
'laravel/laravel' => array(
|
||||||
'pretty_version' => 'dev-master',
|
'pretty_version' => 'dev-master',
|
||||||
'version' => 'dev-master',
|
'version' => 'dev-master',
|
||||||
'reference' => 'ca5069bf81288542f9fd3acd2ab2a5e42a0c5d80',
|
'reference' => '2b36cc8a8edf1403b5796fbe5db3e8168c103c29',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue