From dab573d6d73bb6722174296b4443c8bb03c79c5b Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 13 May 2026 22:44:22 +0200 Subject: [PATCH] Add LocalizationService for API-based string loading Introduces a LocalizationService that loads all localized strings from the API and exposes them via an indexer, emulating IStringLocalizer behavior. The service supports language switching, formatting, and notifies consumers on language changes. Supported languages are provided as a static list for UI use. Designed for scoped DI in Blazor components. --- .../Services/LocalizationService.cs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Services/LocalizationService.cs diff --git a/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Services/LocalizationService.cs b/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Services/LocalizationService.cs new file mode 100644 index 00000000..58a7fed7 --- /dev/null +++ b/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Services/LocalizationService.cs @@ -0,0 +1,119 @@ +using EnvelopeGenerator.ReceiverUI.Web.Client.Api; + +namespace EnvelopeGenerator.ReceiverUI.Web.Client.Services; + +/// +/// Pulls all localized strings from the API once and exposes them +/// via an indexer mimicking IStringLocalizer<Resource>["Key"] +/// in the legacy MVC Web project. +/// +/// Missing keys fall back to the key itself (matching the previous behavior +/// where _localizer["X"].Value returned "X" if not found). +/// +/// Components consume this service as a scoped dependency and may call +/// in OnInitializedAsync. +/// +public class LocalizationService +{ + private readonly ReceiverApiClient _api; + private readonly ILogger _logger; + private Dictionary _strings = new(); + private Task? _loadTask; + private readonly object _gate = new(); + + public LocalizationService(ReceiverApiClient api, ILogger logger) + { + _api = api; + _logger = logger; + } + + /// + /// Languages exposed in the footer's language switcher. Kept as a + /// small in-memory list so the receiver UI does not need an extra + /// API roundtrip just to populate a dropdown. Mirrors the cultures + /// configured server-side (de-DE, en-US, tr-TR). + /// + public static readonly IReadOnlyList<(string Code, string Native, string Flag)> SupportedLanguages = + new[] + { + ("de", "Deutsch", "fi-de"), + ("en", "English", "fi-gb"), + ("tr", "Türkçe", "fi-tr"), + }; + + /// Currently active language code (best effort, set after a switch). + public string? CurrentLanguage { get; private set; } + + /// Fires whenever the language changes and strings are reloaded. + public event Action? Changed; + + /// + /// Get a localized string by key. Returns the key itself if not found + /// (compatible with the legacy _localizer["..."].Value behavior). + /// + public string this[string key] + { + get => _strings.TryGetValue(key, out var v) ? v : key; + } + + /// + /// Format a localized template with positional arguments (e.g. "{0}", "{1}"). + /// + public string Format(string key, params object?[] args) + { + var template = this[key]; + try + { + return string.Format(template, args); + } + catch (FormatException) + { + return template; + } + } + + public IReadOnlyDictionary All => _strings; + + /// + /// Loads localization strings from the API. Safe to call multiple times; + /// concurrent callers share the same in-flight request. + /// + public Task EnsureLoadedAsync(CancellationToken ct = default) + { + lock (_gate) + { + return _loadTask ??= LoadCoreAsync(ct); + } + } + + /// Forces a reload (e.g. after a language change). + public Task ReloadAsync(CancellationToken ct = default) + { + lock (_gate) + { + _loadTask = LoadCoreAsync(ct); + return _loadTask; + } + } + + private async Task LoadCoreAsync(CancellationToken ct) + { + // Refresh the current language alongside the strings so the + // footer dropdown reflects the cookie value picked up by the API. + var langTask = _api.GetLanguageAsync(ct); + var dict = await _api.GetLocalizationStringsAsync(ct); + if (dict is not null) + _strings = new Dictionary(dict, StringComparer.OrdinalIgnoreCase); + else + _logger.LogWarning("Localization strings could not be loaded; falling back to keys."); + CurrentLanguage = await langTask ?? CurrentLanguage; + Changed?.Invoke(); + } + + public async Task ChangeLanguageAsync(string language, CancellationToken ct = default) + { + await _api.SetLanguageAsync(language, ct); + CurrentLanguage = language; + await ReloadAsync(ct); + } +}