//----------------------------------------------------------------------- // // Copyright (c) aliasvault. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasVault.Client.Services; using System.Globalization; using AliasVault.Client.Services.Database; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.JSInterop; /// /// Service for managing application language settings and culture switching. /// public class LanguageService( ILocalStorageService localStorage, IJSRuntime jsRuntime, AuthenticationStateProvider authenticationStateProvider, DbService dbService) { private const string AppLanguageKey = "AppLanguage"; /// /// Language configuration containing all supported languages. /// To add a new language, simply add a new entry to this list. /// private static readonly List SupportedLanguages = new() { new LanguageConfig("de", "Deutsch", "🇩🇪"), new LanguageConfig("en", "English", "🇺🇸"), new LanguageConfig("es", "Español", "🇪🇸"), new LanguageConfig("fi", "Suomi", "🇫🇮"), new LanguageConfig("fr", "Français", "🇫🇷"), new LanguageConfig("he", "עברית", "🇮🇱"), new LanguageConfig("it", "Italiano", "🇮🇹"), new LanguageConfig("nl", "Nederlands", "🇳🇱"), new LanguageConfig("pl", "Polski", "🇵🇱"), new LanguageConfig("pt", "Português Brasileiro", "🇧🇷"), new LanguageConfig("ru", "Русский", "🇷🇺"), new LanguageConfig("uk", "Українська", "🇺🇦"), new LanguageConfig("zh", "简体中文", "🇨🇳"), // Add new languages here: // new LanguageConfig("fr", "Français", "🇫🇷"), // new LanguageConfig("es", "Español", "🇪🇸"), }; private readonly ILocalStorageService _localStorage = localStorage; private readonly IJSRuntime _jsRuntime = jsRuntime; private readonly AuthenticationStateProvider _authenticationStateProvider = authenticationStateProvider; private readonly DbService _dbService = dbService; /// /// Event that is triggered when the language is changed. /// public event Action? LanguageChanged; /// /// Gets the list of supported languages. /// /// Dictionary of language codes and display names. public static Dictionary GetSupportedLanguages() { return SupportedLanguages.ToDictionary(lang => lang.Code, lang => lang.DisplayName); } /// /// Gets the list of supported languages with flag emojis. /// /// Dictionary of language codes and display names with flag emojis. public static Dictionary GetSupportedLanguagesWithFlags() { return SupportedLanguages.ToDictionary(lang => lang.Code, lang => $"{lang.FlagEmoji} {lang.DisplayName}"); } /// /// Gets the flag emoji for a specific language code. /// /// The language code. /// Flag emoji string. public static string GetLanguageFlag(string languageCode) { var language = SupportedLanguages.FirstOrDefault(lang => lang.Code == languageCode); return language?.FlagEmoji ?? "🌐"; } /// /// Gets the display name for a specific language code. /// /// The language code. /// Display name string. public static string GetLanguageDisplayName(string languageCode) { var language = SupportedLanguages.FirstOrDefault(lang => lang.Code == languageCode); return language?.DisplayName ?? languageCode; } /// /// Checks if a language code is supported. /// /// The language code to check. /// True if the language is supported, false otherwise. public static bool IsLanguageSupported(string languageCode) { return SupportedLanguages.Any(lang => lang.Code == languageCode); } /// /// Gets the default language code. /// /// Default language code. public static string GetDefaultLanguage() { return SupportedLanguages.FirstOrDefault()?.Code ?? "en"; } /// /// Gets the current language from the browser. /// /// Browser language code. public async Task GetBrowserLanguageAsync() { try { var browserLanguage = await _jsRuntime.InvokeAsync("navigator.language"); var cultureName = browserLanguage.Split('-')[0]; return GetSupportedLanguages().ContainsKey(cultureName) ? cultureName : "en"; } catch { return "en"; } } /// /// Gets the current language setting. /// /// Current language code. public async Task GetCurrentLanguageAsync() { var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); if (authState.User.Identity?.IsAuthenticated == true) { // User is authenticated, get language from vault settings try { var language = await _dbService.Settings.GetSettingAsync(AppLanguageKey); if (!string.IsNullOrEmpty(language)) { return language; } } catch { // Ignore errors and fall back to local storage first, then browser language } // If no vault setting found, check localStorage to migrate user's pre-auth preference try { var storedLanguage = await _localStorage.GetItemAsync(AppLanguageKey); if (!string.IsNullOrEmpty(storedLanguage)) { // Migrate the localStorage setting to vault and then return it await MigrateLanguageSettingToVault(storedLanguage); return storedLanguage; } } catch { // Ignore errors and fall back to browser language } } else { // User is not authenticated, check local storage try { var storedLanguage = await _localStorage.GetItemAsync(AppLanguageKey); if (!string.IsNullOrEmpty(storedLanguage)) { return storedLanguage; } } catch { // Ignore errors and fall back to browser language } } // Fall back to browser language return await GetBrowserLanguageAsync(); } /// /// Sets the language and updates the culture. /// /// Language code to set. /// Task. public async Task SetLanguageAsync(string languageCode) { if (string.IsNullOrEmpty(languageCode)) { return; } if (!GetSupportedLanguages().ContainsKey(languageCode)) { return; } var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); if (authState.User.Identity?.IsAuthenticated == true) { // User is authenticated, save to vault settings try { await _dbService.Settings.SetSettingAsync(AppLanguageKey, languageCode); } catch { // Ignore errors, still set the culture } } else { // User is not authenticated, save to local storage try { await _localStorage.SetItemAsync(AppLanguageKey, languageCode); } catch { // Ignore errors, still set the culture } } // Set the culture dynamically without page reload var culture = new CultureInfo(languageCode); CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture; CultureInfo.DefaultThreadCurrentCulture = culture; CultureInfo.DefaultThreadCurrentUICulture = culture; // Store in blazorCulture for consistency await _jsRuntime.InvokeVoidAsync("blazorCulture.set", languageCode); // Notify listeners that language has changed LanguageChanged?.Invoke(languageCode); } /// /// Initializes the language service and sets the initial culture. /// /// Task. public async Task InitializeAsync() { var initialLanguage = "en"; // Default fallback try { // Get the initial language preference from JavaScript initialLanguage = await _jsRuntime.InvokeAsync("blazorCulture.get"); } catch { // Fallback if JavaScript is not available yet try { var browserLang = await _jsRuntime.InvokeAsync("eval", "navigator.language"); var cultureName = browserLang.Split('-')[0]; if (GetSupportedLanguages().ContainsKey(cultureName)) { initialLanguage = cultureName; } } catch { // Use default "en" } } // Validate the language if (!GetSupportedLanguages().ContainsKey(initialLanguage)) { initialLanguage = "en"; } // Set the culture var culture = new CultureInfo(initialLanguage); CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture; CultureInfo.DefaultThreadCurrentCulture = culture; CultureInfo.DefaultThreadCurrentUICulture = culture; // Store in blazorCulture for consistency (if available) try { await _jsRuntime.InvokeVoidAsync("blazorCulture.set", initialLanguage); } catch { // Ignore if blazorCulture is not available yet } } /// /// Migrates a language setting from localStorage to vault settings. /// This is called when a user was anonymous, set a language preference, then authenticated. /// /// The language code to migrate. /// Task. private async Task MigrateLanguageSettingToVault(string languageCode) { try { // Save to vault settings await _dbService.Settings.SetSettingAsync(AppLanguageKey, languageCode); // Clear from localStorage since it's now in vault await _localStorage.RemoveItemAsync(AppLanguageKey); } catch { // Ignore migration errors - user can still change language manually } } /// /// Configuration for a supported language. /// private sealed record LanguageConfig(string Code, string DisplayName, string FlagEmoji); }