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:
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ builder.Services.AddScoped<SignatureService>();
|
||||
builder.Services.AddScoped<SignatureCacheService>();
|
||||
builder.Services.AddSingleton<AppVersionService>();
|
||||
builder.Services.AddScoped<EnvelopeService>();
|
||||
builder.Services.AddScoped<LocalizationService>();
|
||||
|
||||
// UI Preferences & Theme
|
||||
builder.Services.AddScoped<UserPreferencesService>();
|
||||
|
||||
@@ -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<string> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user