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