feat(users): idioma por defecto con banderas SVG + conmutador coherente

- Campo "Idioma por defecto" al crear/editar usuario (columna locale ya existente),
  como desplegable Alpine con banderas SVG reales (no emoji, que en Windows se ven
  como "ES"/"GB") servidas localmente: public/images/flags/{es,gb}.svg.
- User: locale añadido a fillable. UserForm: propiedad/validación/guardado de locale.
- LanguageSwitcher de la cabecera usa las mismas banderas SVG.
- Regla CSS [x-cloak] en el layout para evitar parpadeo de desplegables Alpine.

Tests: UserLocaleTest (2) — crear/editar persisten el idioma.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 13:10:27 +02:00
parent 8c774d075d
commit 19e1f57983
8 changed files with 150 additions and 3 deletions
+3
View File
@@ -15,6 +15,9 @@
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
{{-- Evita el parpadeo de elementos Alpine (x-cloak) antes de inicializar --}}
<style>[x-cloak]{display:none !important;}</style>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
@@ -1,10 +1,12 @@
<div class="flex items-center gap-1"
x-on:locale-changed.window="window.location.reload()">
@foreach(['en' => '🇬🇧 EN', 'es' => '🇪🇸 ES'] as $code => $label)
@foreach(['es' => ['flag' => 'es.svg', 'label' => 'ES'], 'en' => ['flag' => 'gb.svg', 'label' => 'EN']] as $code => $lang)
<button wire:click="switchLanguage('{{ $code }}')"
class="btn btn-xs {{ $currentLocale === $code ? 'btn-primary' : 'btn-ghost' }}"
class="btn btn-xs gap-1.5 {{ $currentLocale === $code ? 'btn-primary' : 'btn-ghost' }}"
title="{{ __('Language') }}: {{ __($code === 'en' ? 'English' : 'Spanish') }}">
{{ $label }}
<img src="{{ asset('images/flags/' . $lang['flag']) }}"
class="w-4 h-auto rounded-sm border border-base-300" alt="{{ $lang['label'] }}">
{{ $lang['label'] }}
</button>
@endforeach
</div>
@@ -291,6 +291,46 @@
@error('formRole') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
</div>
</div>
<div class="flex items-start gap-4 mt-4">
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
Idioma por defecto <span class="text-error">*</span>
<p class="text-xs text-gray-400 font-normal mt-0.5">Idioma de la interfaz para este usuario</p>
</label>
<div class="flex-1">
<div x-data="{ open: false }" @click.outside="open = false" class="relative w-full max-w-xs">
{{-- Trigger: muestra la bandera + nombre del idioma seleccionado --}}
<button type="button" @click="open = !open"
class="select select-bordered w-full flex items-center gap-2 text-left">
@foreach($languages as $code => $lang)
<span x-show="$wire.locale === '{{ $code }}'" class="flex items-center gap-2">
<img src="{{ asset('images/flags/' . $lang['flag']) }}"
class="w-5 h-auto rounded-sm border border-base-300" alt="{{ $lang['name'] }}">
{{ $lang['name'] }}
</span>
@endforeach
</button>
{{-- Opciones --}}
<ul x-show="open" x-transition x-cloak
class="absolute z-50 mt-1 w-full bg-base-100 border border-base-300 rounded-box shadow-lg p-1">
@foreach($languages as $code => $lang)
<li>
<button type="button"
@click="$wire.set('locale', '{{ $code }}'); open = false"
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-base-200 text-left"
:class="$wire.locale === '{{ $code }}' ? 'bg-base-200 font-medium' : ''">
<img src="{{ asset('images/flags/' . $lang['flag']) }}"
class="w-5 h-auto rounded-sm border border-base-300" alt="{{ $lang['name'] }}">
{{ $lang['name'] }}
</button>
</li>
@endforeach
</ul>
</div>
@error('locale') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
</div>
</div>
</div>
{{-- ══════════════════════════════════════════════════════════