From 85e33a9fcdc049a219e1d396eaf879b669361b63 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 22 Nov 2025 06:43:45 +0100 Subject: [PATCH] Use current UI language as default identity generator language (#1383) --- .../background/VaultMessageHandler.ts | 3 +- .../pages/credentials/CredentialAddEdit.tsx | 4 +-- .../src/utils/SqliteClient.ts | 29 +++++++++++++++++- .../app/(tabs)/credentials/add-edit.tsx | 2 +- .../(tabs)/settings/identity-generator.tsx | 2 +- apps/mobile-app/utils/SqliteClient.tsx | 29 +++++++++++++++++- .../Main/Pages/Settings/General.razor | 26 +++++++++++++++- .../Services/CredentialService.cs | 30 ++++++++++++++++++- .../Services/JsInterop/JsInteropService.cs | 30 +++++++++++++++++++ .../Services/SettingsService.cs | 6 ++-- 10 files changed, 150 insertions(+), 11 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts index 634e08197..5a081be45 100644 --- a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts @@ -334,12 +334,13 @@ export function handleGetDefaultEmailDomain(): Promise { /** * Get the default identity settings. + * Returns the effective language (with smart UI language matching if no explicit override is set). */ export async function handleGetDefaultIdentitySettings( ) : Promise { try { const sqliteClient = await createVaultSqliteClient(); - const language = sqliteClient.getDefaultIdentityLanguage(); + const language = await sqliteClient.getEffectiveIdentityLanguage(); const gender = sqliteClient.getDefaultIdentityGender(); return { diff --git a/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx b/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx index 41da7bf72..651cf9460 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx @@ -370,8 +370,8 @@ const CredentialAddEdit: React.FC = () => { * Initialize the identity and password generators with settings from user's vault. */ const initializeGenerators = useCallback(async () => { - // Get default identity language from database - const identityLanguage = dbContext.sqliteClient!.getDefaultIdentityLanguage(); + // Get effective identity language (smart default based on UI language if no explicit override) + const identityLanguage = await dbContext.sqliteClient!.getEffectiveIdentityLanguage(); // Initialize identity generator based on language const identityGenerator = CreateIdentityGenerator(identityLanguage); diff --git a/apps/browser-extension/src/utils/SqliteClient.ts b/apps/browser-extension/src/utils/SqliteClient.ts index 131e6a637..4c33fdc05 100644 --- a/apps/browser-extension/src/utils/SqliteClient.ts +++ b/apps/browser-extension/src/utils/SqliteClient.ts @@ -427,9 +427,36 @@ export class SqliteClient { /** * Get the default identity language from the database. + * Returns the stored override value if set, otherwise returns empty string to indicate no explicit preference. + * Use getEffectiveIdentityLanguage() to get the language with smart defaults based on UI language. */ public getDefaultIdentityLanguage(): string { - return this.getSetting('DefaultIdentityLanguage', 'en'); + return this.getSetting('DefaultIdentityLanguage'); + } + + /** + * Get the effective identity generator language to use. + * If user has explicitly set a language preference, use that. + * Otherwise, intelligently match the UI language to an available identity generator language. + * Falls back to "en" if no match is found. + */ + public async getEffectiveIdentityLanguage(): Promise { + const explicitLanguage = this.getDefaultIdentityLanguage(); + + // If user has explicitly set a language preference, use it + if (explicitLanguage) { + return explicitLanguage; + } + + // Otherwise, try to match UI language to an identity generator language + const { mapUiLanguageToIdentityLanguage } = await import('@/utils/dist/shared/identity-generator'); + const { default: i18n } = await import('@/i18n/i18n'); + + const uiLanguage = i18n.language; + const mappedLanguage = mapUiLanguageToIdentityLanguage(uiLanguage); + + // Return the mapped language, or fall back to "en" if no match found + return mappedLanguage ?? 'en'; } /** diff --git a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx index 898138b0d..9d45b9ab3 100644 --- a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx +++ b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx @@ -96,7 +96,7 @@ export default function AddEditCredentialScreen() : React.ReactNode { * Generate a random identity. */ const generateRandomIdentity = useCallback(async () : Promise => { - const identityLanguage = await dbContext.sqliteClient!.getDefaultIdentityLanguage(); + const identityLanguage = await dbContext.sqliteClient!.getEffectiveIdentityLanguage(); const identityGenerator = CreateIdentityGenerator(identityLanguage); const genderPreference = await dbContext.sqliteClient!.getDefaultIdentityGender(); diff --git a/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx b/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx index 091cedbab..b5c0b899a 100644 --- a/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx +++ b/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx @@ -55,7 +55,7 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode { const loadSettings = async (): Promise => { try { const [currentLanguage, currentGender, currentAgeRange] = await Promise.all([ - dbContext.sqliteClient!.getDefaultIdentityLanguage(), + dbContext.sqliteClient!.getEffectiveIdentityLanguage(), dbContext.sqliteClient!.getDefaultIdentityGender(), dbContext.sqliteClient!.getDefaultIdentityAgeRange() ]); diff --git a/apps/mobile-app/utils/SqliteClient.tsx b/apps/mobile-app/utils/SqliteClient.tsx index 52d5c8cdf..9b4c44a7f 100644 --- a/apps/mobile-app/utils/SqliteClient.tsx +++ b/apps/mobile-app/utils/SqliteClient.tsx @@ -464,9 +464,36 @@ class SqliteClient { /** * Get the default identity language from the database. + * Returns the stored override value if set, otherwise returns empty string to indicate no explicit preference. + * Use getEffectiveIdentityLanguage() to get the language with smart defaults based on UI language. */ public async getDefaultIdentityLanguage(): Promise { - return this.getSetting('DefaultIdentityLanguage', 'en'); + return this.getSetting('DefaultIdentityLanguage'); + } + + /** + * Get the effective identity generator language to use. + * If user has explicitly set a language preference, use that. + * Otherwise, intelligently match the UI language to an available identity generator language. + * Falls back to "en" if no match is found. + */ + public async getEffectiveIdentityLanguage(): Promise { + const explicitLanguage = await this.getDefaultIdentityLanguage(); + + // If user has explicitly set a language preference, use it + if (explicitLanguage) { + return explicitLanguage; + } + + // Otherwise, try to match UI language to an identity generator language + const { mapUiLanguageToIdentityLanguage } = await import('@/utils/dist/shared/identity-generator'); + const { default: i18n } = await import('@/i18n'); + + const uiLanguage = i18n.language; + const mappedLanguage = mapUiLanguageToIdentityLanguage(uiLanguage); + + // Return the mapped language, or fall back to "en" if no match found + return mappedLanguage ?? 'en'; } /** diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor index 33d93ede0..0062457ba 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor @@ -181,13 +181,33 @@ } } AutoEmailRefresh = DbService.Settings.AutoEmailRefresh; - DefaultIdentityLanguage = DbService.Settings.DefaultIdentityLanguage; + + await SetDefaultIdentityLanguageAsync(); DefaultIdentityGender = DbService.Settings.DefaultIdentityGender; DefaultIdentityAgeRange = DbService.Settings.DefaultIdentityAgeRange; AppLanguage = DbService.Settings.AppLanguage; ClipboardClearSeconds = DbService.Settings.ClipboardClearSeconds; } + /// + /// Sets the default identity language. + /// + private async Task SetDefaultIdentityLanguageAsync() + { + var explicitLanguage = DbService.Settings.DefaultIdentityLanguage; + if (!string.IsNullOrWhiteSpace(explicitLanguage)) + { + // User has explicitly set a language, use it + DefaultIdentityLanguage = explicitLanguage; + } + else + { + // No explicit language set, try to match UI language to identity generator language + var mappedLanguage = await JsInteropService.MapUiLanguageToIdentityLanguageAsync(DbService.Settings.AppLanguage); + DefaultIdentityLanguage = mappedLanguage ?? "en"; + } + } + /// /// Updates the default email domain. /// @@ -235,10 +255,14 @@ /// /// Updates the app language setting. + /// Also refreshes the identity generator language to reflect the new UI language if no explicit override is set. /// private async Task UpdateAppLanguage() { await LanguageService.SetLanguageAsync(AppLanguage); + + // After changing UI language, refresh identity generator language if user hasn't set an explicit override + await SetDefaultIdentityLanguageAsync(); StateHasChanged(); } diff --git a/apps/server/AliasVault.Client/Services/CredentialService.cs b/apps/server/AliasVault.Client/Services/CredentialService.cs index 8ffcd6c97..479097f0b 100644 --- a/apps/server/AliasVault.Client/Services/CredentialService.cs +++ b/apps/server/AliasVault.Client/Services/CredentialService.cs @@ -61,8 +61,11 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService // Convert age range to birthdate options using shared JS utility var birthdateOptions = await jsInteropService.ConvertAgeRangeToBirthdateOptionsAsync(dbService.Settings.DefaultIdentityAgeRange); + // Get the effective identity language (smart default based on UI language if no explicit override is set) + var identityLanguage = await GetEffectiveIdentityLanguageAsync(); + // Generate a random identity using the TypeScript library - var identity = await jsInteropService.GenerateRandomIdentityAsync(dbService.Settings.DefaultIdentityLanguage, dbService.Settings.DefaultIdentityGender, birthdateOptions); + var identity = await jsInteropService.GenerateRandomIdentityAsync(identityLanguage, dbService.Settings.DefaultIdentityGender, birthdateOptions); // Generate random values for the Identity properties credential.Username = identity.NickName; @@ -576,4 +579,29 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService } } } + + /// + /// Gets the effective identity generator language to use. + /// If user has explicitly set a language preference, use that. + /// Otherwise, intelligently match the UI language to an available identity generator language. + /// Falls back to "en" if no match is found. + /// + /// The identity generator language code to use. + private async Task GetEffectiveIdentityLanguageAsync() + { + var explicitLanguage = dbService.Settings.DefaultIdentityLanguage; + + // If user has explicitly set a language preference, use it + if (!string.IsNullOrWhiteSpace(explicitLanguage)) + { + return explicitLanguage; + } + + // Otherwise, try to match UI language to an identity generator language + var uiLanguage = dbService.Settings.AppLanguage; + var mappedLanguage = await jsInteropService.MapUiLanguageToIdentityLanguageAsync(uiLanguage); + + // Return the mapped language, or fall back to "en" if no match found + return mappedLanguage ?? "en"; + } } diff --git a/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs b/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs index 98aa41b53..4fcbf54eb 100644 --- a/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs +++ b/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs @@ -406,6 +406,36 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) } } + /// + /// Maps a UI language code to an identity generator language code. + /// If no explicit match is found, returns null to indicate no preference. + /// + /// The UI language code (e.g., "en", "en-US", "nl-NL", "de-DE", "fr"). + /// The matching identity generator language code or null if no match. + public async Task MapUiLanguageToIdentityLanguageAsync(string? uiLanguageCode) + { + try + { + if (string.IsNullOrEmpty(uiLanguageCode)) + { + return null; + } + + if (_identityGeneratorModule == null) + { + await InitializeAsync(); + } + + var result = await _identityGeneratorModule!.InvokeAsync("mapUiLanguageToIdentityLanguage", uiLanguageCode); + return result; + } + catch (JSException ex) + { + await Console.Error.WriteLineAsync($"JavaScript error mapping UI language to identity language: {ex.Message}"); + return null; + } + } + /// /// Converts an age range string to birthdate options using the shared JavaScript utility. /// diff --git a/apps/server/AliasVault.Client/Services/SettingsService.cs b/apps/server/AliasVault.Client/Services/SettingsService.cs index a5f20c90b..34382f306 100644 --- a/apps/server/AliasVault.Client/Services/SettingsService.cs +++ b/apps/server/AliasVault.Client/Services/SettingsService.cs @@ -43,9 +43,11 @@ public sealed class SettingsService /// /// Gets the DefaultIdentityLanguage setting. + /// Returns the stored override value if set, otherwise returns empty string to indicate no explicit preference. + /// Use GetEffectiveIdentityLanguageAsync() to get the language with smart defaults based on UI language. /// - /// Default identity language as two-letter code. - public string DefaultIdentityLanguage => GetSetting("DefaultIdentityLanguage", "en")!; + /// Default identity language as two-letter code, or empty string if not set. + public string DefaultIdentityLanguage => GetSetting("DefaultIdentityLanguage"); /// /// Gets the DefaultIdentityGender setting.