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