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