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