Add multilingual support with language selector

Introduced a `LanguageControls` component for switching between
languages (`de-DE`, `en-US`, `fr-FR`) and integrated a new
`LocalizationService` to manage language preferences. Updated
`LocalizationController` to handle language persistence via
cookies and support multiple cultures. Enhanced the footer
layout to include the language selector alongside theme
controls.

Added `flag-icons` library for displaying country flags and
configured request localization in `Program.cs`. Styled the
language selector dropdown and improved sender page visuals
with updated CSS. Defaulted language handling to `de-DE` for
fallback scenarios. Cleaned up redundant code in
`LocalizationController`.
This commit is contained in:
2026-09-28 09:19:46 +02:00
parent 5aaa37b8ec
commit 0d2e43b2d6
10 changed files with 218 additions and 10 deletions

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/envelope-viewer.css" />
<link rel="stylesheet" href="EnvelopeGenerator.Server.styles.css" />

View File

@@ -527,6 +527,7 @@
</div>
}
</div>
</div>
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@

View File

@@ -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<LocalizationController> _logger;
private readonly IStringLocalizer<Resource> _mLocalizer;
private readonly IStringLocalizer<Resource> _localizer;
private readonly IMemoryCache _cache;
/// <summary>
@@ -32,14 +30,12 @@ public class LocalizationController : ControllerBase
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
public LocalizationController(
ILogger<LocalizationController> logger,
IStringLocalizer<Resource> localizer,
IMemoryCache memoryCache,
IStringLocalizer<Resource> _modelLocalizer)
IStringLocalizer<Resource> modelLocalizer)
{
_logger = logger;
_localizer = localizer;
_cache = memoryCache;
_mLocalizer = _modelLocalizer;
_mLocalizer = modelLocalizer;
}
/// <summary>

View File

@@ -337,6 +337,7 @@ try
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.UserPreferencesService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ThemeService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.CustomImagesService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.LocalizationService>();
// Business Services (Server specific)
builder.Services.AddScoped<DocumentService>();
@@ -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<CultureInfo> list = [defaultCulture];
IList<CultureInfo> 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();

View File

@@ -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;
}
}

View File

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