diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/LanguageControls.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/LanguageControls.razor new file mode 100644 index 00000000..fbab5db4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/LanguageControls.razor @@ -0,0 +1,84 @@ +@using EnvelopeGenerator.Server.Client.Services +@rendermode InteractiveWebAssembly + +@inject LocalizationService LocalizationService +@inject NavigationManager Navigation + +
+ + + @if (_isOpen) + { + + +
+ @foreach (var option in _options) + { + + } +
+ } +
+ +@code { + private readonly IReadOnlyList _options = + [ + new("de-DE", "fi-de", "DE"), + new("en-US", "fi-us", "EN"), + new("fr-FR", "fi-fr", "FR") + ]; + + private LanguageOption _selected = new("de-DE", "fi-de", "DE"); + private bool _isOpen; + + protected override async Task OnInitializedAsync() + { + var currentLanguage = await LocalizationService.GetLanguageAsync(); + _selected = _options.FirstOrDefault(x => x.Code.Equals(currentLanguage, StringComparison.OrdinalIgnoreCase)) + ?? _options[0]; + } + + private void ToggleMenu() + { + _isOpen = !_isOpen; + } + + private void CloseMenu() + { + _isOpen = false; + } + + private async Task SelectLanguageAsync(LanguageOption option) + { + if (_selected.Code.Equals(option.Code, StringComparison.OrdinalIgnoreCase)) + { + _isOpen = false; + return; + } + + _selected = option; + _isOpen = false; + await LocalizationService.SetLanguageAsync(option.Code); + Navigation.NavigateTo(Navigation.Uri, forceLoad: true); + } + + private sealed record LanguageOption(string Code, string FlagClass, string ShortName); +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor index b43c4b99..b6a72a36 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor @@ -21,7 +21,10 @@ Datenschutz - + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs index c6be87e1..88c85d23 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs @@ -33,6 +33,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // UI Preferences & Theme builder.Services.AddScoped(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/LocalizationService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/LocalizationService.cs new file mode 100644 index 00000000..9bc83f5c --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/LocalizationService.cs @@ -0,0 +1,30 @@ +using System.Net; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class LocalizationService(IHttpClientFactory httpClientFactory) +{ + private HttpClient CreateDefaultClient() => httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + + public async Task GetLanguageAsync(CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var response = await http.GetAsync("/api/Localization/lang", cancel); + + if (response.StatusCode == HttpStatusCode.NotFound) + return "de-DE"; + + if (!response.IsSuccessStatusCode) + return "de-DE"; + + var language = await response.Content.ReadAsStringAsync(cancel); + return string.IsNullOrWhiteSpace(language) ? "de-DE" : language.Trim('"'); + } + + public async Task SetLanguageAsync(string language, CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var response = await http.PostAsync($"/api/Localization/lang?language={Uri.EscapeDataString(language)}", null, cancel); + response.EnsureSuccessStatusCode(); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/App.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/App.razor index b43f7aa6..6d5ebdc4 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/App.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/App.razor @@ -6,6 +6,7 @@ + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor index cc5c2817..7b4e6cfb 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor @@ -527,6 +527,7 @@ } + @* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/LocalizationController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/LocalizationController.cs index 0fbb72cb..50972987 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/LocalizationController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/LocalizationController.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Localization; -using EnvelopeGenerator.Application.Resources; namespace EnvelopeGenerator.Server.Controllers; @@ -20,7 +19,6 @@ public class LocalizationController : ControllerBase private readonly ILogger _logger; private readonly IStringLocalizer _mLocalizer; - private readonly IStringLocalizer _localizer; private readonly IMemoryCache _cache; /// @@ -32,14 +30,12 @@ public class LocalizationController : ControllerBase /// Lokalisierungsdienst für Modelle. public LocalizationController( ILogger logger, - IStringLocalizer localizer, IMemoryCache memoryCache, - IStringLocalizer _modelLocalizer) + IStringLocalizer modelLocalizer) { _logger = logger; - _localizer = localizer; _cache = memoryCache; - _mLocalizer = _modelLocalizer; + _mLocalizer = modelLocalizer; } /// diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs index 782ad06d..635970cd 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs @@ -337,6 +337,7 @@ try builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // Business Services (Server specific) builder.Services.AddScoped(); @@ -410,9 +411,14 @@ try // Set CORS policy app.UseCors("AllowSpecificOriginsPolicy"); - // Localization (fixed to German for now; persisted via cookie) + // Localization (default German; can be switched via cookie) var defaultCulture = new CultureInfo("de-DE"); - IList list = [defaultCulture]; + IList list = + [ + defaultCulture, + new CultureInfo("en-US"), + new CultureInfo("fr-FR") + ]; var requestLocalizationOptions = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(defaultCulture), @@ -420,6 +426,7 @@ try SupportedUICultures = list, RequestCultureProviders = [new CookieRequestCultureProvider()] }; + requestLocalizationOptions.ApplyCurrentCultureToResponseHeaders = true; app.UseRequestLocalization(requestLocalizationOptions); app.UseHttpsRedirection(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/app.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/app.css index 36e4645a..bb082d1e 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/app.css +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/app.css @@ -404,6 +404,89 @@ article { opacity: 0.4; } +.receiver-footer__right { + display: inline-flex; + align-items: center; + gap: 12px; +} + +.sf-lang-combo { + position: relative; + display: inline-flex; + align-items: center; +} + +.sf-lang-trigger { + width: 28px; + height: 20px; + border: 1px solid rgba(255, 255, 255, 0.28); + border-radius: 5px; + background: rgba(255, 255, 255, 0.08); + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.sf-lang-trigger:hover { + background: rgba(255, 255, 255, 0.16); +} + +.sf-lang-menu { + position: absolute; + right: 0; + bottom: 28px; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 64px; + padding: 6px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(29, 38, 55, 0.98); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); + z-index: 110; +} + +.sf-lang-backdrop { + position: fixed; + inset: 0; + border: 0; + background: transparent; + padding: 0; + margin: 0; + cursor: default; + z-index: 105; +} + +.sf-lang-option { + display: inline-flex; + align-items: center; + gap: 6px; + border: 0; + border-radius: 6px; + background: transparent; + color: rgba(255, 255, 255, 0.88); + font-size: 0.68rem; + font-weight: 600; + padding: 4px 6px; + cursor: pointer; +} + +.sf-lang-option:hover, +.sf-lang-option--active { + background: rgba(255, 255, 255, 0.14); +} + +.sf-lang-trigger .fi, +.sf-lang-option .fi { + width: 16px; + height: 12px; + border-radius: 2px; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25) inset; +} + /* ── Footer: Theme Slider ────────────────────────────────────────────────── */ /* 🌙 blazing-dark ──◇──·──◇── purple ☀️ */ /* Left group: copyright | datenschutz → Right group: slider */ @@ -613,4 +696,4 @@ article { [data-sf-theme="blazing-dark"] .login-card .text-muted { color: rgba(255, 255, 255, 0.50) !important; -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css index e727bd23..e121770a 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css @@ -238,6 +238,7 @@ overflow: auto; } + .sender-grid-container { background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(20px); @@ -690,6 +691,7 @@ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.06); } + [data-sf-theme="blazing-dark"] .sender-grid-container::before { background: linear-gradient(90deg, #6366f1 0%, #4338ca 100%); }