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

@@ -0,0 +1,84 @@
@using EnvelopeGenerator.Server.Client.Services
@rendermode InteractiveWebAssembly
@inject LocalizationService LocalizationService
@inject NavigationManager Navigation
<div class="sf-lang-combo">
<button type="button"
class="sf-lang-trigger"
@onclick="ToggleMenu"
aria-label="Language"
aria-expanded="@_isOpen">
<span class="fi @_selected.FlagClass"></span>
</button>
@if (_isOpen)
{
<button type="button"
class="sf-lang-backdrop"
aria-label="Close language menu"
@onclick="CloseMenu">
</button>
<div class="sf-lang-menu" role="listbox" aria-label="Languages">
@foreach (var option in _options)
{
<button type="button"
class="sf-lang-option @(_selected.Code == option.Code ? "sf-lang-option--active" : string.Empty)"
role="option"
aria-selected="@(_selected.Code == option.Code)"
@onmousedown:preventDefault="true"
@onclick="() => SelectLanguageAsync(option)">
<span class="fi @option.FlagClass"></span>
<span>@option.ShortName</span>
</button>
}
</div>
}
</div>
@code {
private readonly IReadOnlyList<LanguageOption> _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);
}

View File

@@ -21,7 +21,10 @@
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
</div>
<ThemeControls />
<div class="receiver-footer__right">
<LanguageControls />
<ThemeControls />
</div>
</footer>
</div>