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)
+ {
+
+
+
+ }
+
+
+@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%);
}