From 163b71141b859e740b60814dd2884fee43285af2 Mon Sep 17 00:00:00 2001 From: Casparus J Van Zyl Date: Wed, 1 Apr 2026 11:39:18 +0200 Subject: [PATCH 01/15] Change NewGuid() to CreateVersion7() for fragmentation limiting. Regegister TenantProvisioning service, as tenantdetail failed. HealthPage deserialization failed due to missing null check Updated Nuget --- src/BuildingBlocks/Blazor.UI/Blazor.UI.csproj | 3 + .../Theme/FshBrandAssetsPicker.razor | 21 +-- .../Components/Theme/FshThemeCustomizer.razor | 2 +- .../Components/User/FshAccountMenu.razor | 5 +- src/BuildingBlocks/Caching/Extensions.cs | 2 +- src/BuildingBlocks/Core/Domain/DomainEvent.cs | 2 +- .../Core/Domain/IDomainEvent.cs | 7 +- .../Storage/Local/LocalStorageService.cs | 2 +- .../Storage/S3/S3StorageService.cs | 2 +- src/Directory.Packages.props | 28 ++-- .../v1/GetAudits/GetAuditsQueryHandler.cs | 26 +-- .../Persistence/AuditRecordConfiguration.cs | 22 ++- .../Jwt/ConfigureJwtBearerOptions.cs | 2 + .../Domain/Events/PasswordChangedEvent.cs | 2 +- .../Domain/Events/SessionRevokedEvent.cs | 2 +- .../Domain/Events/UserActivatedEvent.cs | 2 +- .../Domain/Events/UserDeactivatedEvent.cs | 2 +- .../Domain/Events/UserRegisteredEvent.cs | 2 +- .../Domain/Events/UserRoleAssignedEvent.cs | 2 +- .../Identity/Modules.Identity/Domain/Group.cs | 2 +- .../Modules.Identity/Domain/UserSession.cs | 2 +- .../GenerateTokenCommandHandler.cs | 4 +- .../Services/IdentityService.cs | 2 +- .../Modules.Identity/Services/TokenService.cs | 2 +- .../Services/UserRegistrationService.cs | 6 +- .../Domain/TenantTheme.cs | 2 +- .../Provisioning/TenantProvisioning.cs | 2 +- .../Provisioning/TenantProvisioningService.cs | 2 +- .../Provisioning/TenantProvisioningStep.cs | 2 +- .../FSH.Playground.AppHost/AppHost.cs | 3 +- .../FSH.Playground.AppHost.csproj | 2 +- ..._AddAuditIndexesForPerformance.Designer.cs | 114 +++++++++++++ ...401091314_AddAuditIndexesForPerformance.cs | 152 ++++++++++++++++++ .../Audit/AuditDbContextModelSnapshot.cs | 28 +++- .../Playground.Blazor/Components/App.razor | 5 +- .../Components/Pages/Health/HealthPage.razor | 1 + .../Components/Pages/ProfileSettings.razor | 7 +- .../Components/Pages/SimpleLogin.razor | 18 ++- .../Pages/Tenants/TenantsPage.razor | 12 +- .../Services/Api/ApiClientRegistration.cs | 3 + .../Services/Api/TokenRefreshService.cs | 4 +- .../Services/SimpleBffAuth.cs | 2 +- .../appsettings.Development.json | 4 + .../Contracts/AuditEnvelopeTests.cs | 6 +- .../Services/CurrentUserServiceTests.cs | 26 +-- .../Services/PasswordExpiryServiceTests.cs | 2 +- .../UpdateGroupCommandValidatorTests.cs | 30 ++-- .../TenantProvisioningStepTests.cs | 32 ++-- .../Provisioning/TenantProvisioningTests.cs | 46 +++--- 49 files changed, 507 insertions(+), 152 deletions(-) create mode 100644 src/Playground/Migrations.PostgreSQL/Audit/20260401091314_AddAuditIndexesForPerformance.Designer.cs create mode 100644 src/Playground/Migrations.PostgreSQL/Audit/20260401091314_AddAuditIndexesForPerformance.cs diff --git a/src/BuildingBlocks/Blazor.UI/Blazor.UI.csproj b/src/BuildingBlocks/Blazor.UI/Blazor.UI.csproj index cccdeac8f8..74824f56d0 100644 --- a/src/BuildingBlocks/Blazor.UI/Blazor.UI.csproj +++ b/src/BuildingBlocks/Blazor.UI/Blazor.UI.csproj @@ -6,6 +6,9 @@ FullStackHero.Framework.Blazor.UI $(NoWarn);MUD0002 + + + true diff --git a/src/BuildingBlocks/Blazor.UI/Components/Theme/FshBrandAssetsPicker.razor b/src/BuildingBlocks/Blazor.UI/Components/Theme/FshBrandAssetsPicker.razor index 0dad490cf9..8168c02160 100644 --- a/src/BuildingBlocks/Blazor.UI/Components/Theme/FshBrandAssetsPicker.razor +++ b/src/BuildingBlocks/Blazor.UI/Components/Theme/FshBrandAssetsPicker.razor @@ -31,15 +31,16 @@ Accept=".png,.jpg,.jpeg,.svg,.webp" FilesChanged="@OnLogoUpload" MaximumFileCount="1"> - + + Elevation="0" + @onclick="@(async (e) => await upload.OpenFilePickerAsync())"> Click to upload logo PNG, JPG, SVG, WebP - + } @@ -72,15 +73,16 @@ Accept=".png,.jpg,.jpeg,.svg,.webp" FilesChanged="@OnLogoDarkUpload" MaximumFileCount="1"> - + + Elevation="0" + @onclick="@(async (e) => await upload.OpenFilePickerAsync())"> Click to upload logo PNG, JPG, SVG, WebP - + } @@ -114,15 +116,16 @@ Accept=".png,.ico,.svg" FilesChanged="@OnFaviconUpload" MaximumFileCount="1"> - + + Elevation="0" + @onclick="@(async (e) => await upload.OpenFilePickerAsync())"> Click to upload favicon 16x16 or 32x32 PNG, ICO - + } diff --git a/src/BuildingBlocks/Blazor.UI/Components/Theme/FshThemeCustomizer.razor b/src/BuildingBlocks/Blazor.UI/Components/Theme/FshThemeCustomizer.razor index 552efdfa82..ecb3dc5ae1 100644 --- a/src/BuildingBlocks/Blazor.UI/Components/Theme/FshThemeCustomizer.razor +++ b/src/BuildingBlocks/Blazor.UI/Components/Theme/FshThemeCustomizer.razor @@ -165,7 +165,7 @@ /// public async Task ResetToDefaultsAsync() { - var confirmed = await DialogService.ShowMessageBox( + var confirmed = await DialogService.ShowMessageBoxAsync( "Reset Theme", "Are you sure you want to reset all theme settings to defaults? This action cannot be undone.", yesText: "Reset", diff --git a/src/BuildingBlocks/Blazor.UI/Components/User/FshAccountMenu.razor b/src/BuildingBlocks/Blazor.UI/Components/User/FshAccountMenu.razor index b917d083ca..e3a6551113 100644 --- a/src/BuildingBlocks/Blazor.UI/Components/User/FshAccountMenu.razor +++ b/src/BuildingBlocks/Blazor.UI/Components/User/FshAccountMenu.razor @@ -6,8 +6,9 @@ TransformOrigin="Origin.TopRight" PopoverClass="fsh-account-popover" Dense="false"> - - } @@ -93,28 +98,28 @@ else - Account Status + @L["Status"] - Account Status + @L["Status"] - @(_profile?.IsActive == true ? "Active" : "Inactive") + @(_profile?.IsActive == true ? L["Active"] : L["Inactive"]) - Email Verified + @L["EmailVerified"] - @(_profile?.EmailConfirmed == true ? "Verified" : "Pending") + @(_profile?.EmailConfirmed == true ? L["Verified"] : L["Pending"]) - Username + @L["Username"] @(_profile?.UserName ?? "-") @@ -126,46 +131,46 @@ else - + + RequiredError="@L["Required"]" /> + RequiredError="@L["Required"]" /> + RequiredError="@L["Required"]" /> @@ -180,7 +185,7 @@ else StartIcon="@Icons.Material.Filled.Refresh" OnClick="ResetProfile" Disabled="_saving"> - Reset + @L["Reset"] - Saving... + @L["ProcessingRequest"] } else { - Save Changes + @L["Save"] } @@ -202,14 +207,14 @@ else - + - Change Password + @PRL["ChangePassword"] - Update your password to keep your account secure + @PRL["PasswordDescription"] @@ -219,40 +224,40 @@ else + RequiredError="@L["Required"]" /> + RequiredError="@L["Required"]" + HelperText="@PRL["PasswordRequirements"]" /> @@ -265,7 +270,7 @@ else StartIcon="@Icons.Material.Filled.Clear" OnClick="ResetPasswordForm" Disabled="_changingPassword"> - Clear + @L["Clear"] - Updating... + @L["ProcessingRequest"] } else { - Update Password + @PRL["ChangePassword"] } @@ -386,7 +391,7 @@ else } catch (Exception ex) { - Snackbar.Add($"Failed to load profile: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -411,14 +416,14 @@ else // Validate file size (2MB max) if (file.Size > 2 * 1024 * 1024) { - Snackbar.Add("File too large. Maximum 2MB allowed.", Severity.Error); + Snackbar.Add(PRL["FileTooLarge"], Severity.Error); return; } // Validate file type if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) { - Snackbar.Add("Only image files are allowed.", Severity.Error); + Snackbar.Add(PRL["OnlyImagesAllowed"], Severity.Error); return; } @@ -438,12 +443,12 @@ else // Set preview URL _avatarPreviewUrl = $"data:{file.ContentType};base64,{Convert.ToBase64String(bytes)}"; - Snackbar.Add("Image ready. Click Save Changes to upload.", Severity.Info); + Snackbar.Add(PRL["ImageReady"], Severity.Info); StateHasChanged(); } catch (Exception ex) { - Snackbar.Add($"Failed to process image: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } } @@ -455,7 +460,7 @@ else _pendingImageContentType = null; _deleteCurrentImage = true; _hasImageChanges = true; - Snackbar.Add("Photo will be removed when you save changes.", Severity.Info); + Snackbar.Add(PRL["PhotoWillBeRemoved"], Severity.Info); StateHasChanged(); } @@ -487,7 +492,7 @@ else await _profileForm.ValidateAsync(); if (!_profileForm.IsValid) { - Snackbar.Add("Please fix the validation errors.", Severity.Warning); + Snackbar.Add(L["Required"], Severity.Warning); return; } } @@ -518,7 +523,7 @@ else await IdentityClient.ProfilePutAsync(command); - Snackbar.Add("Profile updated successfully!", Severity.Success); + Snackbar.Add(PRL["ProfileUpdatedSuccessfully"], Severity.Success); // Reload profile to get updated data await LoadProfileAsync(); @@ -529,11 +534,11 @@ else } catch (FSH.Playground.Blazor.ApiClient.ApiException ex) { - Snackbar.Add($"Failed to update profile: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } catch (Exception ex) { - Snackbar.Add($"An error occurred: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -556,7 +561,7 @@ else return null; // Required validation handles empty if (confirmPassword != _passwordModel.NewPassword) - return "Passwords do not match"; + return L["PasswordsMustMatch"]; return null; } @@ -568,14 +573,14 @@ else await _passwordForm.ValidateAsync(); if (!_passwordForm.IsValid) { - Snackbar.Add("Please fix the validation errors.", Severity.Warning); + Snackbar.Add(L["Required"], Severity.Warning); return; } } if (_passwordModel.NewPassword != _passwordModel.ConfirmPassword) { - Snackbar.Add("Passwords do not match.", Severity.Warning); + Snackbar.Add(L["PasswordsMustMatch"], Severity.Warning); return; } @@ -591,16 +596,16 @@ else await IdentityClient.ChangePasswordAsync(command); - Snackbar.Add("Password changed successfully!", Severity.Success); + Snackbar.Add(PRL["PasswordChangedSuccessfully"], Severity.Success); ResetPasswordForm(); } catch (FSH.Playground.Blazor.ApiClient.ApiException ex) { - Snackbar.Add($"Failed to change password: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } catch (Exception ex) { - Snackbar.Add($"An error occurred: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor index 213b1fac50..aa0f38e382 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor @@ -1,13 +1,18 @@ @using FSH.Playground.Blazor.ApiClient +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Roles +@using Microsoft.Extensions.Localization @inject IIdentityClient IdentityClient @inject ISnackbar Snackbar +@inject IStringLocalizer L +@inject IStringLocalizer RL - @(IsEditMode ? "Edit Role" : "Create New Role") + @(IsEditMode ? RL["EditRole"] : RL["CreateRole"]) - @(IsEditMode ? "Modify role details" : "Define a new role for your organization") + @RL["RoleManagement"] @@ -16,40 +21,31 @@ @* Role Name *@ + Disabled="@(IsEditMode && IsSystemRole(_model.Name))" /> @* Role Description *@ + Lines="3" /> @* Info Alert *@ - @if (IsEditMode) - { - After saving, you can manage permissions for this role from the Roles page. - } - else - { - After creating the role, you can assign permissions to define what users with this role can do. - } + @RL["ManagePermissions"] @@ -61,7 +57,7 @@ Color="Color.Default" OnClick="Cancel" Disabled="_busy"> - Cancel + @L["Cancel"] - @(IsEditMode ? "Saving..." : "Creating...") + @L["ProcessingRequest"] } else { - @(IsEditMode ? "Save Changes" : "Create Role") + @(IsEditMode ? L["Save"] : RL["CreateRole"]) } @@ -139,7 +135,7 @@ if (string.IsNullOrWhiteSpace(_model.Name)) { - Snackbar.Add("Please enter a role name.", Severity.Warning); + Snackbar.Add(L["Required"], Severity.Warning); return; } @@ -147,12 +143,12 @@ try { await IdentityClient.RolesPostAsync(_model); - Snackbar.Add($"Role '{_model.Name}' {(IsEditMode ? "updated" : "created")} successfully!", Severity.Success); + Snackbar.Add(IsEditMode ? RL["RoleUpdatedSuccessfully"] : RL["RoleCreatedSuccessfully"], Severity.Success); MudDialog.Close(DialogResult.Ok(true)); } catch (Exception ex) { - Snackbar.Add($"Failed to {(IsEditMode ? "update" : "create")} role: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor index 8b65223dea..07a8333fbf 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor @@ -1,20 +1,25 @@ @page "/roles/{Id:guid}/permissions" @using FSH.Playground.Blazor.ApiClient @using FSH.Framework.Blazor.UI.Components.Dialogs +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Roles +@using Microsoft.Extensions.Localization @inherits ComponentBase @inject IIdentityClient IdentityClient @inject NavigationManager Navigation @inject ISnackbar Snackbar @inject IDialogService DialogService +@inject IStringLocalizer L +@inject IStringLocalizer RL - + - Back to Roles + @L["Back"] @@ -24,7 +29,7 @@ - Loading role information... + @L["Loading"] } @@ -33,10 +38,10 @@ else if (_role is null) - Role Not Found - The requested role could not be found. + @L["NotFound"] + @L["NotFound"] - Back to Roles + @L["Back"] @@ -49,28 +54,28 @@ else @_role.Name - @(string.IsNullOrEmpty(_role.Description) ? "No description provided" : _role.Description) + @(string.IsNullOrEmpty(_role.Description) ? RL["NoDescription"] : _role.Description) @if (IsAdminRole(_role.Name)) { - Admin (Full Access) + Admin } else if (IsSystemRole(_role.Name)) { - System Role + @RL["System"] } else { - Custom Role + @RL["Custom"] } @@ -87,7 +92,7 @@ else Color="Color.Default" OnClick="ResetChanges" Disabled="_saving"> - Reset + @L["Reset"] } } - Save Changes + @L["Save"] } @@ -110,7 +115,7 @@ else { - Admin role has full access to all permissions. These permissions cannot be modified. + @RL["ManagePermissions"] } @@ -124,7 +129,7 @@ else @_currentPermissions.Count - Assigned + @L["Assigned"] @@ -137,7 +142,7 @@ else @(_allPermissions.Count - _currentPermissions.Count) - Available + @L["Available"] @@ -150,7 +155,7 @@ else @_groupedPermissions.Count - Categories + @L["Categories"] @@ -164,7 +169,7 @@ else Color="@(HasChanges ? Color.Warning : Color.Success)" /> @GetChangedCount() - Changes + @L["Changes"] @@ -176,7 +181,7 @@ else - Deselect All + @L["DeselectAll"] } - Expand + @L["Expand"] - Collapse + @L["Collapse"] @@ -275,7 +280,7 @@ else @if (HasPermissionChanged(perm)) { - @(IsPermissionAssigned(perm) ? "Will be added" : "Will be removed") + @(IsPermissionAssigned(perm) ? L["WillBeAdded"] : L["WillBeRemoved"]) } @@ -300,15 +305,15 @@ else - No permissions found + @L["NoDataAvailable"] @if (!string.IsNullOrEmpty(_searchTerm)) { - No permissions match your search. Try a different term. + @L["NoDataAvailable"] } else { - No permissions available in the system. + @L["NoDataAvailable"] } @@ -474,7 +479,7 @@ else } catch (Exception ex) { - Snackbar.Add($"Failed to load data: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -616,11 +621,11 @@ else _originalPermissions = new HashSet(_currentPermissions); - Snackbar.Add("Permissions updated successfully.", Severity.Success); + Snackbar.Add(RL["PermissionsUpdatedSuccessfully"], Severity.Success); } catch (Exception ex) { - Snackbar.Add($"Failed to save permissions: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -633,9 +638,9 @@ else Navigation.NavigateTo("/roles"); } - private static string FormatCategoryName(string category) + private string FormatCategoryName(string category) { - if (string.IsNullOrEmpty(category)) return "Other"; + if (string.IsNullOrEmpty(category)) return L["Other"]; return char.ToUpper(category[0]) + category.Substring(1); } diff --git a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor index 29a5b4d6f0..9747f4b5e5 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor @@ -2,14 +2,19 @@ @using FSH.Playground.Blazor.ApiClient @using FSH.Framework.Blazor.UI.Components.Dialogs @using FSH.Framework.Blazor.UI.Components.Cards +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Roles +@using Microsoft.Extensions.Localization @inherits ComponentBase @inject IIdentityClient IdentityClient @inject NavigationManager Navigation @inject ISnackbar Snackbar @inject IDialogService DialogService +@inject IStringLocalizer L +@inject IStringLocalizer RL - + @if (_selectedRoles.Any()) { @@ -18,11 +23,11 @@ Color="Color.Error" OnClick="BulkDelete" Disabled="_bulkBusy"> - Delete (@_selectedRoles.Count) + @L["Delete"] (@_selectedRoles.Count) - Clear + @L["Clear"] } @@ -41,40 +46,40 @@ + Label="@RL["TotalRoles"]" + Badge="@L["Total"]" /> + Label="@RL["SystemRoles"]" + Badge="@L["System"]" /> + Label="@RL["CustomRoles"]" + Badge="@RL["Custom"]" /> + Label="@L["Protected"]" + Badge="@L["Protected"]" /> @* Filter Panel *@ - + - Clear Filters + @L["Clear"] - @(string.IsNullOrEmpty(context.Item.Description) ? "No description" : context.Item.Description) + @(string.IsNullOrEmpty(context.Item.Description) ? RL["NoDescription"] : context.Item.Description) - + @if (IsSystemRole(context.Item.Name)) { - System + @L["System"] } else { - Custom + @RL["Custom"] } - + - + - + - No roles found - Try adjusting your filters or create a new role. + @RL["NoRolesFound"] + @RL["TryAdjustingFilters"] @@ -256,7 +261,7 @@ } catch (Exception ex) { - Snackbar.Add($"Failed to load roles: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -335,7 +340,7 @@ CloseButton = true }; - var dialog = await DialogService.ShowAsync("Create Role", options); + var dialog = await DialogService.ShowAsync(RL["CreateRole"], options); var result = await dialog.Result; if (result is not null && !result.Canceled) @@ -348,7 +353,7 @@ { if (IsProtectedRole(role.Name)) { - Snackbar.Add("Protected roles cannot be edited.", Severity.Warning); + Snackbar.Add(RL["SystemRolesCannotBeEdited"], Severity.Warning); return; } @@ -365,7 +370,7 @@ CloseButton = true }; - var dialog = await DialogService.ShowAsync("Edit Role", parameters, options); + var dialog = await DialogService.ShowAsync(RL["EditRole"], parameters, options); var result = await dialog.Result; if (result is not null && !result.Canceled) @@ -380,23 +385,23 @@ if (IsProtectedRole(role.Name)) { - Snackbar.Add("Protected roles cannot be deleted.", Severity.Warning); + Snackbar.Add(RL["SystemRolesCannotBeDeleted"], Severity.Warning); return; } - var confirmed = await DialogService.ShowDeleteConfirmAsync(role.Name ?? "this role"); + var confirmed = await DialogService.ShowDeleteConfirmAsync(role.Name ?? L["Unknown"], L); if (!confirmed) return; _busyRoleId = role.Id; try { await IdentityClient.RolesDeleteAsync(Guid.Parse(role.Id)); - Snackbar.Add("Role deleted successfully.", Severity.Success); + Snackbar.Add(RL["RoleDeletedSuccessfully"], Severity.Success); await LoadData(); } catch (Exception ex) { - Snackbar.Add($"Failed to delete role: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -409,15 +414,15 @@ var roles = _selectedRoles.Where(r => !IsProtectedRole(r.Name)).ToList(); if (!roles.Any()) { - Snackbar.Add("No deletable roles selected. Protected roles cannot be deleted.", Severity.Info); + Snackbar.Add(RL["NoDeletableRolesSelected"], Severity.Info); return; } var confirmed = await DialogService.ShowConfirmAsync( - "Bulk Delete", - $"Delete {roles.Count} role(s)? This action cannot be undone.", - "Delete All", - "Cancel", + RL["BulkDelete"], + string.Format(RL["DeleteRolesConfirm"], roles.Count), + L["Delete"], + L["Cancel"], Color.Error, Icons.Material.Outlined.DeleteForever, Color.Error); @@ -439,7 +444,7 @@ } } _bulkBusy = false; - Snackbar.Add($"Deleted {success} of {roles.Count} roles.", Severity.Success); + Snackbar.Add(string.Format(RL["DeletedRolesCount"], success, roles.Count), Severity.Success); ClearSelection(); await LoadData(); } diff --git a/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor b/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor index 44b3afb133..22121aaa52 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor @@ -1,12 +1,17 @@ @page "/settings/security" @attribute [Microsoft.AspNetCore.Authorization.Authorize] +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Profile +@using Microsoft.Extensions.Localization @inject FSH.Playground.Blazor.ApiClient.IIdentityClient IdentityClient @inject ISnackbar Snackbar +@inject IStringLocalizer L +@inject IStringLocalizer PRL -Security Settings +@PRL["SecuritySettings"] - + @@ -15,14 +20,14 @@ - Change Password + @PRL["ChangePassword"] + ErrorText="@L["PasswordsMustMatch"]" /> @@ -60,11 +65,11 @@ @if (_saving) { - Saving... + @L["ProcessingRequest"] } else { - Update Password + @PRL["ChangePassword"] } @@ -100,18 +105,18 @@ ConfirmNewPassword = _confirmPassword }); - Snackbar.Add("Password updated successfully.", Severity.Success); + Snackbar.Add(PRL["PasswordChangedSuccessfully"], Severity.Success); _currentPassword = string.Empty; _newPassword = string.Empty; _confirmPassword = string.Empty; } catch (FSH.Playground.Blazor.ApiClient.ApiException ex) when (ex.StatusCode == 400) { - Snackbar.Add("Password update failed. Please check your current password.", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } catch (Exception) { - Snackbar.Add("An unexpected error occurred.", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor index e3e9033aee..b291bdab9d 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor @@ -2,6 +2,9 @@ @using FSH.Playground.Blazor.ApiClient @using FSH.Framework.Blazor.UI.Components.Dialogs @using FSH.Framework.Blazor.UI.Components.Cards +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Sessions +@using Microsoft.Extensions.Localization @inherits ComponentBase @using System.Security.Claims @using Microsoft.AspNetCore.Components.Authorization @@ -11,16 +14,18 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService @inject AuthenticationStateProvider AuthenticationStateProvider +@inject IStringLocalizer L +@inject IStringLocalizer SL - + - Logout All Other Devices + @SL["RevokeAllSessions"] @@ -31,29 +36,29 @@ + Label="@SL["ActiveSessions"]" + Badge="@L["Total"]" /> + Label="@SL["DesktopSessions"]" + Badge="@SL["Desktop"]" /> + Label="@SL["MobileSessions"]" + Badge="@SL["Mobile"]" /> + Label="@SL["TabletSessions"]" + Badge="@SL["Tablet"]" /> @@ -108,12 +113,12 @@ - + @context.Item.IpAddress - + @FormatRelativeTime(context.Item.LastActivityAt) @@ -121,7 +126,7 @@ - + @FormatRelativeTime(context.Item.CreatedAt) @@ -129,23 +134,23 @@ - + @if (context.Item.IsActive) { - Active + @L["Active"] } else { - Expired + @L["Expired"] } - + @if (!context.Item.IsCurrentSession) { - + + - No active sessions - You don't have any active sessions at the moment. + @L["NoDataAvailable"] + @SL["NoOtherSessions"] @@ -228,7 +233,7 @@ } catch (Exception ex) { - Snackbar.Add($"Failed to load sessions: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -250,10 +255,10 @@ private async Task RevokeSession(UserSessionDto session) { var confirmed = await DialogService.ShowConfirmAsync( - "Revoke Session", - $"Are you sure you want to log out from this device?\n\n{session.Browser} on {session.OperatingSystem}", - "Revoke", - "Cancel", + SL["RevokeSession"], + string.Format(SL["ConfirmRevokeSession"], session.Browser, session.OperatingSystem), + SL["RevokeSession"], + L["Cancel"], Color.Error); if (!confirmed) return; @@ -262,12 +267,12 @@ try { await IdentityClient.SessionsAsync(session.Id); - Snackbar.Add("Session revoked successfully.", Severity.Success); + Snackbar.Add(SL["SessionRevokedSuccessfully"], Severity.Success); await LoadSessions(); } catch (Exception ex) { - Snackbar.Add($"Failed to revoke session: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -280,15 +285,15 @@ var otherSessions = _sessions.Count(s => !s.IsCurrentSession); if (otherSessions == 0) { - Snackbar.Add("No other sessions to revoke.", Severity.Info); + Snackbar.Add(SL["NoOtherSessions"], Severity.Info); return; } var confirmed = await DialogService.ShowConfirmAsync( - "Logout All Other Devices", - $"This will log you out from {otherSessions} other device(s). Your current session will remain active.", - "Logout All", - "Cancel", + SL["LogoutAllDevices"], + string.Format(SL["LogoutAllConfirm"], otherSessions), + SL["LogoutAll"], + L["Cancel"], Color.Warning, Icons.Material.Outlined.Warning, Color.Warning); @@ -299,12 +304,12 @@ try { await SessionsClient.RevokeAllPostAsync(null); - Snackbar.Add($"Successfully logged out from {otherSessions} device(s).", Severity.Success); + Snackbar.Add(SL["AllSessionsRevoked"], Severity.Success); await LoadSessions(); } catch (Exception ex) { - Snackbar.Add($"Failed to revoke sessions: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -326,21 +331,21 @@ _ => Color.Info }; - private static string FormatRelativeTime(DateTimeOffset dateTime) + private string FormatRelativeTime(DateTimeOffset dateTime) { var timeSpan = DateTimeOffset.UtcNow - dateTime; if (timeSpan.TotalMinutes < 1) - return "Just now"; + return SL["JustNow"]; if (timeSpan.TotalMinutes < 60) - return $"{(int)timeSpan.TotalMinutes} min ago"; + return string.Format(SL["MinutesAgo"], (int)timeSpan.TotalMinutes); if (timeSpan.TotalHours < 24) - return $"{(int)timeSpan.TotalHours} hour(s) ago"; + return string.Format(SL["HoursAgo"], (int)timeSpan.TotalHours); if (timeSpan.TotalDays < 7) - return $"{(int)timeSpan.TotalDays} day(s) ago"; + return string.Format(SL["DaysAgo"], (int)timeSpan.TotalDays); if (timeSpan.TotalDays < 30) - return $"{(int)(timeSpan.TotalDays / 7)} week(s) ago"; + return string.Format(SL["WeeksAgo"], (int)(timeSpan.TotalDays / 7)); - return $"{(int)(timeSpan.TotalDays / 30)} month(s) ago"; + return string.Format(SL["MonthsAgo"], (int)(timeSpan.TotalDays / 30)); } } diff --git a/src/Playground/Playground.Blazor/Components/Pages/SimpleLogin.razor b/src/Playground/Playground.Blazor/Components/Pages/SimpleLogin.razor index 7ef7a4a781..76f668d842 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/SimpleLogin.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/SimpleLogin.razor @@ -2,16 +2,19 @@ @layout EmptyLayout @attribute [AllowAnonymous] @using FSH.Framework.Shared.Multitenancy +@using FSH.Framework.Shared.Localization @using FSH.Playground.Blazor.Services @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.Extensions.Localization @inject NavigationManager Navigation @inject IHttpClientFactory HttpClientFactory @inject AuthenticationStateProvider AuthenticationStateProvider @inject IWebHostEnvironment Environment @inject ISnackbar Snackbar +@inject IStringLocalizer L -Login +@L["Login"] diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor index f20e966b10..18ad730961 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor @@ -1,6 +1,11 @@ @using FSH.Playground.Blazor.ApiClient +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Tenants +@using Microsoft.Extensions.Localization @inject IV1Client V1Client @inject ISnackbar Snackbar +@inject IStringLocalizer L +@inject IStringLocalizer TL @@ -9,8 +14,8 @@ - Create New Tenant - Add a new organization to the platform + @TL["CreateTenant"] + @TL["TenantManagement"] @@ -21,32 +26,30 @@ - Basic Information + @L["Details"] @@ -54,45 +57,45 @@ - Administrator Account + @L["Admin"] @* Advanced Settings Section *@ - + + HelperText="@TL["Issuer"]" /> @@ -104,7 +107,7 @@ - A new tenant will be provisioned with default settings. The admin will receive credentials to access the tenant. + @TL["TenantDetails"] @@ -117,7 +120,7 @@ Color="Color.Default" OnClick="Cancel" Disabled="_busy"> - Cancel + @L["Cancel"] - Creating... + @L["ProcessingRequest"] } else { - Create Tenant + @TL["CreateTenant"] } @@ -171,7 +174,7 @@ string.IsNullOrWhiteSpace(_model.Name) || string.IsNullOrWhiteSpace(_model.AdminEmail)) { - Snackbar.Add("Please fill in all required fields.", Severity.Warning); + Snackbar.Add(L["Required"], Severity.Warning); return; } @@ -179,7 +182,7 @@ var tenantId = _model.Id.Trim().ToLowerInvariant(); if (tenantId.Contains(' ') || tenantId.Contains('\t')) { - Snackbar.Add("Tenant ID cannot contain spaces.", Severity.Error); + Snackbar.Add(L["Required"], Severity.Error); return; } @@ -189,12 +192,12 @@ try { await V1Client.TenantsPostAsync(_model); - Snackbar.Add($"Tenant '{_model.Name}' created successfully!", Severity.Success); + Snackbar.Add(TL["TenantCreatedSuccessfully"], Severity.Success); MudDialog.Close(DialogResult.Ok(true)); } catch (Exception ex) { - Snackbar.Add($"Failed to create tenant: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor index e9780d88a9..198365d1e4 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor @@ -3,6 +3,9 @@ @using FSH.Framework.Blazor.UI.Components.Dialogs @using FSH.Framework.Shared.Constants @using FSH.Framework.Shared.Multitenancy +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Tenants +@using Microsoft.Extensions.Localization @using Microsoft.AspNetCore.Components.Authorization @inherits ComponentBase @inject ITenantsClient TenantsClient @@ -11,46 +14,48 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService @inject AuthenticationStateProvider AuthenticationStateProvider +@inject IStringLocalizer L +@inject IStringLocalizer TL @if (!_isRootTenantAdmin) { - You do not have permission to access this page. Only root tenant administrators can manage tenants. + @L["AccessDenied"] } else if (_loading) { - + - Loading tenant details... + @L["Loading"] } else if (_tenant is null) { - + - Tenant not found. + @L["NotFound"] } else { - + - Back to Tenants + @L["Back"] - Upgrade + @TL["UpgradeTenant"] @if (!IsRootTenant) { @@ -59,7 +64,7 @@ else StartIcon="@(_tenant.IsActive ? Icons.Material.Filled.Block : Icons.Material.Filled.CheckCircle)" OnClick="ToggleStatus" Disabled="_busy"> - @(_tenant.IsActive ? "Deactivate" : "Activate") + @(_tenant.IsActive ? TL["Deactivate"] : TL["Activate"]) } @@ -76,11 +81,11 @@ else @if (_tenant.IsActive) { - Active + @L["Active"] } else { - Inactive + @L["Inactive"] } @@ -91,25 +96,25 @@ else - Tenant Information + @L["Details"] - Tenant ID + @TL["TenantIdentifier"] @_tenant.Id - Name + @L["Name"] @_tenant.Name - Admin Email + @TL["AdminEmail"] @_tenant.AdminEmail - Issuer - @(string.IsNullOrEmpty(_tenant.Issuer) ? "Default" : _tenant.Issuer) + @TL["Issuer"] + @(string.IsNullOrEmpty(_tenant.Issuer) ? L["Default"] : _tenant.Issuer) @@ -120,27 +125,27 @@ else - Subscription Details + @TL["Subscription"] - Status + @L["Status"] @if (_tenant.IsActive) { - Active + @L["Active"] } else { - Inactive + @L["Inactive"] } - Valid Until + @TL["ValidUpto"] @_tenant.ValidUpto.ToString("MMMM dd, yyyy") - Time Remaining + @L["TimeRemaining"] @GetExpiryText(_tenant.ValidUpto) @@ -150,13 +155,13 @@ else @if (IsExpiringSoon(_tenant.ValidUpto)) { - This tenant's subscription is expiring soon. Consider upgrading. + @L["ExpiringSoon"] } else if (IsExpired(_tenant.ValidUpto)) { - This tenant's subscription has expired. + @L["Expired"] } @@ -167,32 +172,32 @@ else - Database Configuration + @L["Database"] - Database Type + @L["Type"] @if (_tenant.HasConnectionString) { - Dedicated + @L["Dedicated"] } else { - Shared + @L["Shared"] } @if (_tenant.HasConnectionString) { - Connection String - + @TL["ConnectionString"] + @@ -207,7 +212,7 @@ else - Provisioning Status + @L["ProvisioningStatus"] - No provisioning information available. + @L["NoDataAvailable"] } else { - Status + @L["Status"] @_provisioningStatus.Status @@ -240,21 +245,21 @@ else @if (!string.IsNullOrEmpty(_provisioningStatus.CurrentStep)) { - Current Step + @L["CurrentStep"] @_provisioningStatus.CurrentStep } @if (_provisioningStatus.StartedUtc.HasValue) { - Started + @L["Started"] @_provisioningStatus.StartedUtc.Value.LocalDateTime.ToString("g") } @if (_provisioningStatus.CompletedUtc.HasValue) { - Completed + @L["Completed"] @_provisioningStatus.CompletedUtc.Value.LocalDateTime.ToString("g") } @@ -276,11 +281,11 @@ else @if (_retrying) { - Retrying... + @L["Loading"] } else { - Retry Provisioning + @TL["RetryProvisioning"] } } @@ -290,7 +295,7 @@ else @if (_provisioningStatus.Steps?.Any() == true) { - Provisioning Steps + @L["ProvisioningStatus"] @foreach (var step in _provisioningStatus.Steps) { @@ -377,7 +382,7 @@ else } catch (Exception ex) { - Snackbar.Add($"Failed to load tenant: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -422,21 +427,21 @@ else return validUpto > now && validUpto <= now.AddDays(30); } - private static string GetExpiryText(DateTimeOffset validUpto) + private string GetExpiryText(DateTimeOffset validUpto) { var now = DateTimeOffset.UtcNow; if (validUpto <= now) - return "Expired"; + return L["Expired"]; var diff = validUpto - now; if (diff.TotalDays < 1) - return "Expires today"; + return L["ExpiringSoon"]; if (diff.TotalDays < 2) - return "Expires tomorrow"; + return L["ExpiringSoon"]; if (diff.TotalDays <= 30) - return $"Expires in {(int)diff.TotalDays} days"; + return $"{(int)diff.TotalDays} {L["Days"]}"; - return $"{(int)diff.TotalDays} days remaining"; + return $"{(int)diff.TotalDays} {L["Days"]}"; } private static Color GetExpiryColor(DateTimeOffset validUpto) @@ -472,10 +477,10 @@ else if (_tenant is null) return; var confirmed = await DialogService.ShowConfirmAsync( - "Retry Provisioning", - $"Are you sure you want to retry provisioning for '{_tenant.Name}'? This will attempt to complete any failed provisioning steps.", - "Retry", - "Cancel", + TL["RetryProvisioning"], + string.Format(TL["ConfirmRetryProvisioning"], _tenant.Name), + L["Retry"], + L["Cancel"], Color.Primary); if (!confirmed) return; @@ -484,11 +489,11 @@ else try { _provisioningStatus = await ProvisioningClient.RetryAsync(_tenant.Id); - Snackbar.Add("Provisioning retry initiated successfully.", Severity.Success); + Snackbar.Add(TL["ProvisioningRetrySuccess"], Severity.Success); } catch (Exception ex) { - Snackbar.Add($"Failed to retry provisioning: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -504,16 +509,15 @@ else if (IsRootTenant) { - Snackbar.Add("Root tenant cannot be deactivated.", Severity.Warning); + Snackbar.Add(TL["RootCannotBeDeactivated"], Severity.Warning); return; } - var action = _tenant.IsActive ? "deactivate" : "activate"; var confirmed = await DialogService.ShowConfirmAsync( - $"{(_tenant.IsActive ? "Deactivate" : "Activate")} Tenant", - $"Are you sure you want to {action} '{_tenant.Name}'?", - _tenant.IsActive ? "Deactivate" : "Activate", - "Cancel", + _tenant.IsActive ? TL["Deactivate"] : TL["Activate"], + string.Format(TL["ConfirmToggleStatus"], _tenant.Name), + _tenant.IsActive ? TL["Deactivate"] : TL["Activate"], + L["Cancel"], _tenant.IsActive ? Color.Warning : Color.Success); if (!confirmed) return; @@ -527,12 +531,12 @@ else IsActive = !_tenant.IsActive }; await TenantsClient.ActivationAsync(_tenant.Id, command); - Snackbar.Add($"Tenant {(_tenant.IsActive ? "deactivated" : "activated")} successfully.", Severity.Success); + Snackbar.Add(TL["TenantUpdatedSuccessfully"], Severity.Success); await LoadTenant(); } catch (Exception ex) { - Snackbar.Add($"Failed to update tenant: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -567,7 +571,7 @@ else CloseButton = true }; - var dialog = await DialogService.ShowAsync("Upgrade Subscription", parameters, options); + var dialog = await DialogService.ShowAsync(TL["UpgradeTenant"], parameters, options); var result = await dialog.Result; if (result is not null && !result.Canceled) diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantSettingsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantSettingsPage.razor index 62789fa24b..8a7287f749 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantSettingsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantSettingsPage.razor @@ -3,22 +3,27 @@ @using FSH.Framework.Blazor.UI.Components.Page @using FSH.Framework.Shared.Constants @using FSH.Framework.Shared.Multitenancy +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Tenants +@using Microsoft.Extensions.Localization @using Microsoft.AspNetCore.Components.Authorization @inherits ComponentBase @inject NavigationManager Navigation @inject ISnackbar Snackbar @inject AuthenticationStateProvider AuthenticationStateProvider +@inject IStringLocalizer L +@inject IStringLocalizer TL @if (!_isRootTenantAdmin) { - You do not have permission to access this page. Only root tenant administrators can manage tenant settings. + @L["AccessDenied"] } else { - + @* Session Management Settings *@ @@ -26,28 +31,28 @@ else - Session Management + @L["Sessions"] - Session management settings will be available in a future update. + @L["ComingSoon"] - Configure session limits, idle timeouts, and concurrent session policies for all tenants. + @TL["TenantManagement"] - Max Sessions per User - Unlimited + @L["MaxSessions"] + @L["Unlimited"] - Session Idle Timeout - None + @L["SessionIdleTimeout"] + @L["None"] - Session Absolute Timeout - 7 days + @L["SessionAbsoluteTimeout"] + 7 @L["Days"] @@ -59,28 +64,28 @@ else - Security Policies + @L["Security"] - Security policy settings will be available in a future update. + @L["ComingSoon"] - Configure password policies, lockout settings, and authentication requirements. + @TL["TenantManagement"] - Password History - Enabled (5) + @L["PasswordHistory"] + @L["Enabled"] (5) - Account Lockout - Default + @L["AccountLockout"] + @L["Default"] - Two-Factor Authentication - Optional + @L["TwoFactorAuthentication"] + @L["Optional"] @@ -92,28 +97,28 @@ else - Usage Quotas + @L["Quotas"] - Quota settings will be available in a future update. + @L["ComingSoon"] - Configure storage limits, API rate limits, and resource quotas per tenant. + @TL["TenantManagement"] - Max Users - Unlimited + @L["MaxUsers"] + @L["Unlimited"] - Storage Limit - Unlimited + @L["StorageLimit"] + @L["Unlimited"] - API Rate Limit - None + @L["APIRateLimit"] + @L["None"] @@ -125,28 +130,28 @@ else - Notifications + @L["Notifications"] - Notification settings will be available in a future update. + @L["ComingSoon"] - Configure email notifications, alerts, and system messages for tenants. + @TL["TenantManagement"] - Email Notifications - Enabled + @L["EmailNotifications"] + @L["Enabled"] - System Alerts - Enabled + @L["SystemAlerts"] + @L["Enabled"] - Subscription Reminders - Enabled + @TL["SubscriptionReminders"] + @L["Enabled"] diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor index e163081797..f8e8b9723a 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor @@ -4,6 +4,9 @@ @using FSH.Framework.Blazor.UI.Components.Cards @using FSH.Framework.Shared.Constants @using FSH.Framework.Shared.Multitenancy +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Tenants +@using Microsoft.Extensions.Localization @using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components.Authorization @inherits ComponentBase @@ -13,23 +16,25 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService @inject AuthenticationStateProvider AuthenticationStateProvider +@inject IStringLocalizer L +@inject IStringLocalizer TL @if (!_isRootTenantAdmin) { - You do not have permission to access this page. Only root tenant administrators can manage tenants. + @TL["NoPermission"] } else { - + - New Tenant + @TL["CreateTenant"] @@ -40,40 +45,40 @@ else + Label="@TL["TotalTenants"]" + Badge="@L["Total"]" /> + Label="@TL["ActiveTenants"]" + Badge="@L["Active"]" /> + Label="@TL["InactiveTenants"]" + Badge="@L["Inactive"]" /> + Label="@L["ExpiringSoon"]" + Badge="@L["Expiring"]" /> @* Filter Panel *@ - + - - All - Active - Inactive + + @L["All"] + @L["Active"] + @L["Inactive"] @@ -92,7 +97,7 @@ else Color="Color.Default" Variant="Variant.Text" OnClick="ClearFilters"> - Clear Filters + @L["Clear"] - - + + @context.Item.ValidUpto.ToString("MMM dd, yyyy") @@ -148,7 +153,7 @@ else } else if (IsExpired(context.Item.ValidUpto)) { - Expired + @L["Expired"] } else { @@ -159,46 +164,46 @@ else - + @if (!string.IsNullOrWhiteSpace(context.Item.ConnectionString)) { - Dedicated + @L["Dedicated"] } else { - Shared + @L["Shared"] } - + @if (context.Item.IsActive) { - Active + @L["Active"] } else { - Inactive + @L["Inactive"] } - + - + - + - No tenants found - Try adjusting your filters or create a new tenant. + @TL["NoTenantsFound"] + @TL["TryAdjustingFilters"] @@ -319,7 +324,7 @@ else } catch (Exception ex) { - Snackbar.Add($"Failed to load tenants: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -404,21 +409,21 @@ else return validUpto > now && validUpto <= now.AddDays(30); } - private static string GetExpiryText(DateTimeOffset validUpto) + private string GetExpiryText(DateTimeOffset validUpto) { var now = DateTimeOffset.UtcNow; if (validUpto <= now) - return "Expired"; + return L["Expired"]; var diff = validUpto - now; if (diff.TotalDays < 1) - return "Expires today"; + return L["ExpiringSoon"]; if (diff.TotalDays < 2) - return "Expires tomorrow"; + return L["ExpiringSoon"]; if (diff.TotalDays <= 30) - return $"Expires in {(int)diff.TotalDays} days"; + return $"{(int)diff.TotalDays} {L["Days"]}"; - return $"{(int)diff.TotalDays} days remaining"; + return $"{(int)diff.TotalDays} {L["Days"]}"; } private bool IsToggleDisabled(TenantDto tenant) => @@ -426,11 +431,11 @@ else _busyTenantId == tenant.Id || IsRootTenant(tenant); - private static string GetToggleTooltip(TenantDto tenant) + private string GetToggleTooltip(TenantDto tenant) { if (IsRootTenant(tenant)) - return "Root tenant cannot be deactivated"; - return tenant.IsActive ? "Deactivate tenant" : "Activate tenant"; + return TL["RootTenantCannotBeDeactivated"]; + return tenant.IsActive ? TL["Deactivate"] : TL["Activate"]; } private void GoToDetail(string? id) @@ -454,16 +459,15 @@ else if (IsRootTenant(tenant)) { - Snackbar.Add("Root tenant cannot be deactivated.", Severity.Warning); + Snackbar.Add(TL["RootTenantCannotBeDeactivated"], Severity.Warning); return; } - var action = tenant.IsActive ? "deactivate" : "activate"; var confirmed = await DialogService.ShowConfirmAsync( - $"{(tenant.IsActive ? "Deactivate" : "Activate")} Tenant", - $"Are you sure you want to {action} '{tenant.Name}'?", - tenant.IsActive ? "Deactivate" : "Activate", - "Cancel", + tenant.IsActive ? TL["Deactivate"] : TL["Activate"], + string.Format(TL["ConfirmToggleStatus"], tenant.Name), + tenant.IsActive ? TL["Deactivate"] : TL["Activate"], + L["Cancel"], tenant.IsActive ? Color.Warning : Color.Success); if (!confirmed) return; @@ -477,12 +481,12 @@ else IsActive = !tenant.IsActive }; await TenantsClient.ActivationAsync(tenant.Id, command); - Snackbar.Add($"Tenant {(tenant.IsActive ? "deactivated" : "activated")} successfully.", Severity.Success); + Snackbar.Add(TL["TenantUpdatedSuccessfully"], Severity.Success); await LoadData(); } catch (Exception ex) { - Snackbar.Add($"Failed to update tenant: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -500,7 +504,7 @@ else CloseButton = true }; - var dialog = await DialogService.ShowAsync("Create Tenant", options); + var dialog = await DialogService.ShowAsync(TL["CreateTenant"], options); var result = await dialog.Result; if (result is not null && !result.Canceled) @@ -524,7 +528,7 @@ else CloseButton = true }; - var dialog = await DialogService.ShowAsync("Upgrade Subscription", parameters, options); + var dialog = await DialogService.ShowAsync(TL["UpgradeTenant"], parameters, options); var result = await dialog.Result; if (result is not null && !result.Canceled) diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor index e09ca42093..ea82e89b3d 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor @@ -1,6 +1,11 @@ @using FSH.Playground.Blazor.ApiClient +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Tenants +@using Microsoft.Extensions.Localization @inject ITenantsClient TenantsClient @inject ISnackbar Snackbar +@inject IStringLocalizer L +@inject IStringLocalizer TL @@ -9,8 +14,8 @@ - Upgrade Subscription - Extend the subscription for @Tenant?.Name + @TL["UpgradeTenant"] + @TL["ExtendValidUpto"] - @Tenant?.Name @@ -21,30 +26,30 @@ @* Current Subscription Info *@ - Current Subscription + @L["Details"] - Tenant + @L["Tenant"] @Tenant.Name - Valid Until + @TL["ValidUpto"] @Tenant.ValidUpto.ToString("MMMM dd, yyyy") - Status + @L["Status"] @if (IsExpired(Tenant.ValidUpto)) { - Expired + @L["Expired"] } else if (IsExpiringSoon(Tenant.ValidUpto)) { - Expiring Soon + @L["ExpiringSoon"] } else { - Active + @L["Active"] } @@ -52,13 +57,13 @@ @* New Expiry Date *@ - New Expiry Date + @TL["ExtendValidUpto"] - Quick Extend + @TL["ExtendValidUpto"] - +30 Days + +30 @L["Days"] - +90 Days + +90 @L["Days"] - +6 Months + +6 @TL["Months"] - +1 Year + +1 @TL["Year"] @@ -124,7 +129,7 @@ Color="Color.Default" OnClick="Cancel" Disabled="_busy"> - Cancel + @L["Cancel"] - Upgrading... + @L["ProcessingRequest"] } else { - Upgrade Subscription + @TL["UpgradeTenant"] } @@ -207,13 +212,13 @@ { if (Tenant is null || !_newExpiryDate.HasValue) { - Snackbar.Add("Please select a new expiry date.", Severity.Warning); + Snackbar.Add(L["Required"], Severity.Warning); return; } if (_newExpiryDate.Value <= DateTime.Today) { - Snackbar.Add("New expiry date must be in the future.", Severity.Error); + Snackbar.Add(L["Required"], Severity.Error); return; } @@ -230,12 +235,12 @@ }; await TenantsClient.UpgradeAsync(Tenant.Id, command); - Snackbar.Add($"Subscription for '{Tenant.Name}' upgraded successfully!", Severity.Success); + Snackbar.Add(TL["TenantUpgradedSuccessfully"], Severity.Success); MudDialog.Close(DialogResult.Ok(true)); } catch (Exception ex) { - Snackbar.Add($"Failed to upgrade subscription: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor b/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor index cc0e37f4dd..6edfdd5c9a 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor @@ -2,21 +2,26 @@ @using FSH.Framework.Blazor.UI.Theme @using FSH.Framework.Blazor.UI.Components.Theme @using FSH.Framework.Blazor.UI.Components.Page +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Profile @using FSH.Playground.Blazor.Services +@using Microsoft.Extensions.Localization @inject ITenantThemeState ThemeState @inject ISnackbar Snackbar +@inject IStringLocalizer L +@inject IStringLocalizer PRL -Theme Settings +@PRL["ThemeSettings"] - + - Reset to Defaults + @L["Reset"] - Saving... + @L["ProcessingRequest"] } else { - Save Changes + @L["Save"] } @@ -64,7 +69,7 @@ { // Reload theme after save to get the actual uploaded URLs await ThemeState.LoadThemeAsync(); - Snackbar.Add("Theme saved successfully.", Severity.Success); + Snackbar.Add(PRL["ThemeSavedSuccessfully"], Severity.Success); } private async Task HandleAssetUpload((IBrowserFile File, string AssetType, Action SetAsset) args) @@ -74,13 +79,13 @@ // Validate file if (args.File.Size > 2 * 1024 * 1024) // 2MB max { - Snackbar.Add("File too large. Maximum 2MB allowed.", Severity.Error); + Snackbar.Add(PRL["FileTooLarge"], Severity.Error); return; } if (!args.File.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) { - Snackbar.Add("Only image files are allowed.", Severity.Error); + Snackbar.Add(PRL["OnlyImagesAllowed"], Severity.Error); return; } @@ -103,11 +108,11 @@ // Set both preview URL and file upload data via callback args.SetAsset(previewUrl, fileUpload); - Snackbar.Add("Image ready. Click Save to upload.", Severity.Info); + Snackbar.Add(PRL["ImageReady"], Severity.Info); } catch (Exception ex) { - Snackbar.Add($"Failed to process file: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } } } diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor index a4912c590f..b140f68660 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor @@ -1,6 +1,11 @@ @using FSH.Playground.Blazor.ApiClient +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Users +@using Microsoft.Extensions.Localization @inject IIdentityClient IdentityClient @inject ISnackbar Snackbar +@inject IStringLocalizer L +@inject IStringLocalizer UL @@ -9,8 +14,8 @@ - Create New User - Add a new team member to the system + @UL["CreateUser"] + @UL["UserManagement"] @@ -22,21 +27,21 @@ - Personal + @L["Profile"] - Account + @L["Details"] - Security + @L["Security"] @@ -45,16 +50,16 @@ - Personal Information + @L["Profile"] - Account Details + @L["Details"] + AdornmentIcon="@Icons.Material.Filled.Phone" /> @* Security Section *@ - Security + @L["Security"] @@ -156,7 +156,7 @@ - The user will receive an email to verify their account. You can manage their roles after creation. + @UL["UserDetails"] @@ -169,7 +169,7 @@ Color="Color.Default" OnClick="Cancel" Disabled="_busy"> - Cancel + @L["Cancel"] - Creating... + @L["ProcessingRequest"] } else { - Create User + @UL["CreateUser"] } @@ -242,13 +242,13 @@ string.IsNullOrWhiteSpace(_model.UserName) || string.IsNullOrWhiteSpace(_model.Password)) { - Snackbar.Add("Please fill in all required fields.", Severity.Warning); + Snackbar.Add(L["Required"], Severity.Warning); return; } if (_model.Password != _model.ConfirmPassword) { - Snackbar.Add("Passwords do not match.", Severity.Error); + Snackbar.Add(L["PasswordsMustMatch"], Severity.Error); return; } @@ -256,12 +256,12 @@ try { await IdentityClient.RegisterAsync(_model); - Snackbar.Add($"User '{_model.FirstName} {_model.LastName}' created successfully!", Severity.Success); + Snackbar.Add(UL["UserCreatedSuccessfully"], Severity.Success); MudDialog.Close(DialogResult.Ok(true)); } catch (Exception ex) { - Snackbar.Add($"Failed to create user: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor index 11d3bf5061..bcf799979c 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor @@ -1,6 +1,9 @@ @page "/users/{Id:guid}" @using FSH.Playground.Blazor.ApiClient @using FSH.Framework.Blazor.UI.Components.Dialogs +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Users +@using Microsoft.Extensions.Localization @inherits ComponentBase @using System.Security.Claims @using FSH.Framework.Shared.Constants @@ -11,15 +14,17 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService @inject AuthenticationStateProvider AuthenticationStateProvider +@inject IStringLocalizer L +@inject IStringLocalizer UL - + - Back to Users + @L["Back"] @if (_user != null) { @@ -27,7 +32,7 @@ Color="Color.Primary" StartIcon="@Icons.Material.Outlined.Security" OnClick="GoToRoles"> - Manage Roles + @UL["ManageRoles"] } @@ -38,7 +43,7 @@ - Loading user... + @L["Loading"] } @@ -47,10 +52,10 @@ else if (_user is null) - User not found - The user you're looking for doesn't exist. + @L["NotFound"] + @L["NotFound"] - Back to Users + @L["Back"] @@ -88,16 +93,16 @@ else - @(_user.IsActive ? "Active" : "Inactive") + @(_user.IsActive ? L["Active"] : L["Inactive"]) - @(_user.EmailConfirmed ? "Verified" : "Unverified") + @(_user.EmailConfirmed ? L["Verified"] : L["Unverified"]) @if (_targetIsAdmin) { - Admin + @L["Admin"] } @@ -107,19 +112,19 @@ else - Email + @L["Email"] @_user.Email - Phone - @(string.IsNullOrEmpty(_user.PhoneNumber) ? "Not provided" : _user.PhoneNumber) + @L["PhoneNumber"] + @(string.IsNullOrEmpty(_user.PhoneNumber) ? L["NotProvided"] : _user.PhoneNumber) - User ID + @L["UserId"] @_user.Id @@ -138,7 +143,7 @@ else @_roles.Count(r => r.Enabled) - Roles + @L["Roles"] @@ -149,8 +154,8 @@ else - @(_user.EmailConfirmed ? "Yes" : "No") - Verified + @(_user.EmailConfirmed ? L["Yes"] : L["No"]) + @L["Verified"] @@ -161,8 +166,8 @@ else - @(_user.IsActive ? "Active" : "Inactive") - Status + @(_user.IsActive ? L["Active"] : L["Inactive"]) + @L["Status"] @@ -173,8 +178,8 @@ else - @(_targetIsAdmin ? "Admin" : "User") - Type + @(_targetIsAdmin ? L["Admin"] : L["User"]) + @L["Type"] @@ -186,18 +191,18 @@ else - Assigned Roles + @L["Roles"] - Manage + @UL["ManageRoles"] @if (!_roles.Any(r => r.Enabled)) { - No roles assigned. Click "Manage" to assign roles. + @L["NoDataAvailable"] } else @@ -213,11 +218,11 @@ else - Group Memberships + @L["Groups"] - View All Groups + @L["ViewAll"] @@ -228,7 +233,7 @@ else else if (!_groups.Any()) { - Not a member of any groups. + @L["NoDataAvailable"] } else @@ -258,7 +263,7 @@ else - Account Actions + @L["Actions"] @@ -268,20 +273,20 @@ else - Confirm Email + @UL["ConfirmEmail"] - Enter the confirmation code sent to the user's email. + @UL["ConfirmEmailDescription"] - Confirm Email + @UL["ConfirmEmail"] @@ -291,26 +296,26 @@ else - Reset Password + @UL["ResetPassword"] - Requires a valid reset token from email flow. + @UL["ResetPasswordDescription"] - Reset Password + @UL["ResetPassword"] @@ -319,15 +324,15 @@ else - Send Password Reset + @UL["SendResetEmail"] - Send a password reset email to the user. + @UL["SendResetEmailDescription"] - Send Reset Email + @UL["SendResetEmail"] @@ -341,17 +346,17 @@ else - Danger Zone + @L["DangerZone"] - @(_user.IsActive ? "Deactivate Account" : "Activate Account") + @(_user.IsActive ? UL["DeactivateAccount"] : UL["ActivateAccount"]) - @(_user.IsActive ? "User will lose access to the system" : "Restore user access") + @(_user.IsActive ? UL["DeactivateDescription"] : UL["ActivateDescription"]) @@ -360,19 +365,19 @@ else Size="Size.Small" OnClick="ToggleStatus" Disabled="@IsToggleDisabled"> - @(_user.IsActive ? "Deactivate" : "Activate") + @(_user.IsActive ? UL["Deactivate"] : UL["Activate"]) - Delete Account - Permanently remove this user + @UL["DeleteAccount"] + @UL["DeleteAccountDescription"] - Delete + @L["Delete"] @@ -418,7 +423,7 @@ else } catch (Exception ex) { - Snackbar.Add($"Failed to load user: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -450,10 +455,10 @@ else { get { - if (_user is null) return "User not loaded"; - if (IsCurrentUser) return "Cannot modify your own account"; - if (_targetIsAdmin) return "Cannot deactivate administrators"; - return _user.IsActive ? "Deactivate user" : "Activate user"; + if (_user is null) return L["Loading"]; + if (IsCurrentUser) return UL["CannotModifyOwnAccount"]; + if (_targetIsAdmin) return UL["CannotDeactivateAdmin"]; + return _user.IsActive ? UL["Deactivate"] : UL["Activate"]; } } @@ -461,12 +466,11 @@ else { if (_user is null || string.IsNullOrWhiteSpace(_user.Id) || IsToggleDisabled) return; - var action = _user.IsActive ? "deactivate" : "activate"; var confirmed = await DialogService.ShowConfirmAsync( - $"{(_user.IsActive ? "Deactivate" : "Activate")} User", - $"Are you sure you want to {action} {_user.FirstName} {_user.LastName}?", - _user.IsActive ? "Deactivate" : "Activate", - "Cancel", + _user.IsActive ? UL["DeactivateAccount"] : UL["ActivateAccount"], + string.Format(UL["ConfirmToggleStatus"], _user.FirstName, _user.LastName), + _user.IsActive ? UL["Deactivate"] : UL["Activate"], + L["Cancel"], _user.IsActive ? Color.Warning : Color.Success); if (!confirmed) return; @@ -479,12 +483,12 @@ else ActivateUser = !_user.IsActive, UserId = _user.Id }); - Snackbar.Add($"User {(_user.IsActive ? "deactivated" : "activated")} successfully", Severity.Success); + Snackbar.Add(UL["UserUpdatedSuccessfully"], Severity.Success); await LoadUser(); } catch (Exception ex) { - Snackbar.Add($"Failed: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -496,19 +500,19 @@ else { if (_user is null || string.IsNullOrWhiteSpace(_user.Id)) return; - var confirmed = await DialogService.ShowDeleteConfirmAsync($"{_user.FirstName} {_user.LastName}"); + var confirmed = await DialogService.ShowDeleteConfirmAsync($"{_user.FirstName} {_user.LastName}", L); if (!confirmed) return; _busy = true; try { await IdentityClient.UsersDeleteAsync(Id); - Snackbar.Add("User deleted", Severity.Success); + Snackbar.Add(UL["UserDeletedSuccessfully"], Severity.Success); Navigation.NavigateTo("/users"); } catch (Exception ex) { - Snackbar.Add($"Failed: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -523,12 +527,12 @@ else try { await IdentityClient.ResetPasswordAsync(TenantContext, _resetModel); - Snackbar.Add("Password reset successfully", Severity.Success); + Snackbar.Add(UL["PasswordResetSuccessfully"], Severity.Success); _resetModel = new ResetPasswordCommand { Email = _user.Email ?? string.Empty }; } catch (Exception ex) { - Snackbar.Add($"Failed: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -543,12 +547,12 @@ else try { await IdentityClient.ConfirmEmailAsync(_confirmModel.UserId, _confirmModel.Code ?? string.Empty, TenantContext); - Snackbar.Add("Email confirmed successfully", Severity.Success); + Snackbar.Add(UL["EmailConfirmedSuccessfully"], Severity.Success); await LoadUser(); } catch (Exception ex) { - Snackbar.Add($"Failed: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor index 7bfa316653..dc61d6f238 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor @@ -1,21 +1,26 @@ @page "/users/{Id:guid}/roles" @using FSH.Playground.Blazor.ApiClient @using FSH.Framework.Blazor.UI.Components.Dialogs +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Users +@using Microsoft.Extensions.Localization @inherits ComponentBase @inject IIdentityClient IdentityClient @inject IUsersClient UsersClient @inject NavigationManager Navigation @inject ISnackbar Snackbar @inject IDialogService DialogService +@inject IStringLocalizer L +@inject IStringLocalizer UL - + - Back to Users + @L["Back"] @@ -25,7 +30,7 @@ - Loading user information... + @L["Loading"] } @@ -34,10 +39,10 @@ else if (_user is null) - User Not Found - The requested user could not be found. + @L["NotFound"] + @L["NotFound"] - Back to Users + @L["Back"] @@ -63,16 +68,16 @@ else @if (_user.IsActive) { - Active + @L["Active"] } else { - Inactive + @L["Inactive"] } @if (_user.EmailConfirmed) { - Verified + @L["Verified"] } @@ -84,9 +89,9 @@ else - Current Roles + @L["Roles"] - @_currentRoleIds.Count assigned + @_currentRoleIds.Count @L["Assigned"] @@ -111,7 +116,7 @@ else else { - This user has no roles assigned. Assign roles below to grant permissions. + @L["NoDataAvailable"] } @@ -121,7 +126,7 @@ else - Available Roles + @L["AvailableRoles"] @if (HasChanges) { @@ -129,7 +134,7 @@ else Color="Color.Default" OnClick="ResetChanges" Disabled="_saving"> - Reset + @L["Reset"] } } - Save Changes + @L["Save"] @@ -176,7 +181,7 @@ else - @(isAssigned ? "Will be added" : "Will be removed") + @(isAssigned ? L["WillBeAdded"] : L["WillBeRemoved"]) } @if (isAssigned) @@ -193,7 +198,7 @@ else else { - No roles available in the system. Contact an administrator. + @L["NoDataAvailable"] } @@ -266,7 +271,7 @@ else } catch (Exception ex) { - Snackbar.Add($"Failed to load data: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -300,10 +305,10 @@ else if (string.IsNullOrEmpty(roleId)) return; var confirmed = await DialogService.ShowConfirmAsync( - "Remove Role", - $"Remove the '{roleName}' role from this user?", - "Remove", - "Cancel", + UL["RemoveRole"], + string.Format(UL["ConfirmRemoveRole"], roleName), + L["Delete"], + L["Cancel"], Color.Warning); if (!confirmed) return; @@ -343,11 +348,11 @@ else // Update original state to match current _originalRoleIds = new HashSet(_currentRoleIds); - Snackbar.Add("Roles updated successfully.", Severity.Success); + Snackbar.Add(UL["RolesUpdatedSuccessfully"], Severity.Success); } catch (Exception ex) { - Snackbar.Add($"Failed to save roles: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor index 548467c81d..48547aa8d2 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor @@ -2,6 +2,9 @@ @using FSH.Playground.Blazor.ApiClient @using FSH.Framework.Blazor.UI.Components.Dialogs @using FSH.Framework.Blazor.UI.Components.Cards +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization.Users +@using Microsoft.Extensions.Localization @inherits ComponentBase @using System.Security.Claims @using FSH.Framework.Shared.Constants @@ -12,9 +15,11 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService @inject AuthenticationStateProvider AuthenticationStateProvider +@inject IStringLocalizer L +@inject IStringLocalizer UL - + @if (_selectedUsers.Any()) { @@ -23,23 +28,23 @@ Color="Color.Success" OnClick="BulkActivate" Disabled="_bulkBusy"> - Activate (@_selectedUsers.Count) + @L["Active"] (@_selectedUsers.Count) - Deactivate + @L["Inactive"] - Delete + @L["Delete"] - Clear + @L["Clear"] } @@ -47,7 +52,7 @@ Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="ShowCreate"> - New User + @UL["NewUser"] @@ -58,40 +63,40 @@ + Label="@UL["TotalUsers"]" + Badge="@L["Total"]" /> + Label="@UL["ActiveUsers"]" + Badge="@L["Active"]" /> + Label="@UL["InactiveUsers"]" + Badge="@L["Inactive"]" /> + Label="@L["Pending"]" + Badge="@L["Unverified"]" /> @* Filter Panel *@ - + - - All - Active - Inactive + + @L["All"] + @L["Active"] + @L["Inactive"] - - All - Confirmed - Unconfirmed + + @L["All"] + @L["Verified"] + @L["Unverified"] @@ -117,7 +122,7 @@ Color="Color.Default" Variant="Variant.Text" OnClick="ClearFilters"> - Clear Filters + @L["Clear"] - + @context.Item.Email @if (context.Item.EmailConfirmed) { - + } else { - + } - - + + @if (_userRolesCache.TryGetValue(context.Item.Id ?? "", out var roles) && roles.Any()) { @@ -208,33 +213,33 @@ else { - No roles + @L["NoDataAvailable"] } - + @if (context.Item.IsActive) { - Active + @L["Active"] } else { - Inactive + @L["Inactive"] } - + - + - + - + - No users found - Try adjusting your filters or create a new user. + @UL["NoUsersFound"] + @UL["TryAdjustingFilters"] @@ -343,7 +348,7 @@ } catch (Exception ex) { - Snackbar.Add($"Failed to load users: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -489,8 +494,8 @@ private string GetToggleTooltip(UserDto user) { - if (IsCurrentUser(user)) return "Cannot modify your own account"; - return user.IsActive ? "Deactivate user" : "Activate user"; + if (IsCurrentUser(user)) return UL["CannotDeleteSelf"]; + return user.IsActive ? UL["Deactivate"] : UL["Activate"]; } private async Task IsUserAdminAsync(string userId) @@ -539,23 +544,22 @@ if (IsCurrentUser(user)) { - Snackbar.Add("You cannot modify your own account.", Severity.Warning); + Snackbar.Add(UL["CannotDeleteSelf"], Severity.Warning); return; } var isAdmin = await IsUserAdminAsync(user.Id); if (isAdmin && user.IsActive) { - Snackbar.Add("Administrators cannot be deactivated.", Severity.Warning); + Snackbar.Add(UL["CannotDeactivateAdmin"], Severity.Warning); return; } - var action = user.IsActive ? "deactivate" : "activate"; var confirmed = await DialogService.ShowConfirmAsync( - $"{(user.IsActive ? "Deactivate" : "Activate")} User", - $"Are you sure you want to {action} {user.FirstName} {user.LastName}?", - user.IsActive ? "Deactivate" : "Activate", - "Cancel", + user.IsActive ? UL["Deactivate"] : UL["Activate"], + string.Format(UL["ConfirmToggleStatus"], user.FirstName, user.LastName), + user.IsActive ? UL["Deactivate"] : UL["Activate"], + L["Cancel"], user.IsActive ? Color.Warning : Color.Success); if (!confirmed) return; @@ -565,12 +569,12 @@ { var command = new ToggleUserStatusCommand { ActivateUser = !user.IsActive, UserId = user.Id }; await IdentityClient.UsersPatchAsync(Guid.Parse(user.Id), command); - Snackbar.Add($"User {(user.IsActive ? "deactivated" : "activated")} successfully.", Severity.Success); + Snackbar.Add(UL["UserUpdatedSuccessfully"], Severity.Success); await LoadData(); } catch (Exception ex) { - Snackbar.Add($"Failed to update user: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -582,19 +586,19 @@ { if (string.IsNullOrWhiteSpace(user.Id)) return; - var confirmed = await DialogService.ShowDeleteConfirmAsync($"{user.FirstName} {user.LastName}"); + var confirmed = await DialogService.ShowDeleteConfirmAsync($"{user.FirstName} {user.LastName}", L); if (!confirmed) return; _busyUserId = user.Id; try { await IdentityClient.UsersDeleteAsync(Guid.Parse(user.Id)); - Snackbar.Add("User deleted successfully.", Severity.Success); + Snackbar.Add(UL["UserDeletedSuccessfully"], Severity.Success); await LoadData(); } catch (Exception ex) { - Snackbar.Add($"Failed to delete user: {ex.Message}", Severity.Error); + Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); } finally { @@ -607,15 +611,15 @@ var users = _selectedUsers.Where(u => !u.IsActive && !IsCurrentUser(u)).ToList(); if (!users.Any()) { - Snackbar.Add("No inactive users selected.", Severity.Info); + Snackbar.Add(L["NoDataAvailable"], Severity.Info); return; } var confirmed = await DialogService.ShowConfirmAsync( - "Bulk Activate", - $"Activate {users.Count} user(s)?", - "Activate All", - "Cancel", + UL["Activate"], + string.Format(UL["UsersActivated"], users.Count), + UL["Activate"], + L["Cancel"], Color.Success); if (!confirmed) return; @@ -637,7 +641,7 @@ })); var success = results.Count(r => r); _bulkBusy = false; - Snackbar.Add($"Activated {success} of {users.Count} users.", Severity.Success); + Snackbar.Add(string.Format(UL["UsersActivated"], success), Severity.Success); ClearSelection(); await LoadData(); } @@ -647,15 +651,15 @@ var users = _selectedUsers.Where(u => u.IsActive && !IsCurrentUser(u)).ToList(); if (!users.Any()) { - Snackbar.Add("No active users selected (excluding yourself).", Severity.Info); + Snackbar.Add(L["NoDataAvailable"], Severity.Info); return; } var confirmed = await DialogService.ShowConfirmAsync( - "Bulk Deactivate", - $"Deactivate {users.Count} user(s)?", - "Deactivate All", - "Cancel", + UL["Deactivate"], + string.Format(UL["UsersDeactivated"], users.Count), + UL["Deactivate"], + L["Cancel"], Color.Warning); if (!confirmed) return; @@ -678,7 +682,7 @@ })); var success = results.Count(r => r); _bulkBusy = false; - Snackbar.Add($"Deactivated {success} of {users.Count} users.", Severity.Success); + Snackbar.Add(string.Format(UL["UsersDeactivated"], success), Severity.Success); ClearSelection(); await LoadData(); } @@ -688,15 +692,15 @@ var users = _selectedUsers.Where(u => !IsCurrentUser(u)).ToList(); if (!users.Any()) { - Snackbar.Add("No users selected (excluding yourself).", Severity.Info); + Snackbar.Add(L["NoDataAvailable"], Severity.Info); return; } var confirmed = await DialogService.ShowConfirmAsync( - "Bulk Delete", - $"Delete {users.Count} user(s)? This action cannot be undone.", - "Delete All", - "Cancel", + UL["BulkDelete"], + string.Format(UL["DeleteUsersConfirm"], users.Count), + L["Delete"], + L["Cancel"], Color.Error, Icons.Material.Outlined.DeleteForever, Color.Error); @@ -718,7 +722,7 @@ } } _bulkBusy = false; - Snackbar.Add($"Deleted {success} of {users.Count} users.", Severity.Success); + Snackbar.Add(string.Format(UL["UsersDeleted"], success, users.Count), Severity.Success); ClearSelection(); await LoadData(); } @@ -733,7 +737,7 @@ CloseButton = true }; - var dialog = await DialogService.ShowAsync("Create User", options); + var dialog = await DialogService.ShowAsync(UL["CreateUser"], options); var result = await dialog.Result; if (result is not null && !result.Canceled) diff --git a/src/Playground/Playground.Blazor/Components/Pages/Weather.razor b/src/Playground/Playground.Blazor/Components/Pages/Weather.razor index 22ebe557a2..9597578dd9 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Weather.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Weather.razor @@ -1,9 +1,14 @@ @page "/weather" +@using FSH.Framework.Shared.Localization +@using FSH.Playground.Blazor.Localization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer L +@inject IStringLocalizer PL -Weather +@PL["Weather"] - @if (forecasts == null) @@ -14,16 +19,16 @@ else { - Date - Temp. (C) - Temp. (F) - Summary + @L["Date"] + @PL["TemperatureC"] + @PL["TemperatureF"] + @PL["Summary"] - @context.Date - @context.TemperatureC - @context.TemperatureF - @context.Summary + @context.Date + @context.TemperatureC + @context.TemperatureF + @context.Summary diff --git a/src/Playground/Playground.Blazor/Components/Validation/LocalizedDataAnnotationsValidator.razor b/src/Playground/Playground.Blazor/Components/Validation/LocalizedDataAnnotationsValidator.razor new file mode 100644 index 0000000000..687bf29790 --- /dev/null +++ b/src/Playground/Playground.Blazor/Components/Validation/LocalizedDataAnnotationsValidator.razor @@ -0,0 +1,86 @@ +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.Extensions.Localization +@using Microsoft.Extensions.DependencyInjection +@using FSH.Framework.Shared.Localization +@using System.ComponentModel.DataAnnotations +@inherits ComponentBase +@implements IDisposable +@inject IServiceProvider ServiceProvider +@inject IStringLocalizer Localizer + +@code { + [CascadingParameter] + private EditContext? EditContext { get; set; } + + private ValidationMessageStore? _validationMessageStore; + + protected override void OnInitialized() + { + if (EditContext == null) + { + throw new InvalidOperationException($"{nameof(LocalizedDataAnnotationsValidator)} requires a cascading parameter of type {nameof(EditContext)}."); + } + + _validationMessageStore = new ValidationMessageStore(EditContext); + EditContext.OnValidationRequested += HandleValidationRequested; + EditContext.OnFieldChanged += HandleFieldChanged; + } + + private void HandleValidationRequested(object? sender, ValidationRequestedEventArgs args) + { + _validationMessageStore?.Clear(); + + if (EditContext?.Model is IValidatableObject validatableModel) + { + var validationContext = new ValidationContext(EditContext.Model, ServiceProvider, null); + var validationResults = validatableModel.Validate(validationContext); + + foreach (var validationResult in validationResults) + { + if (validationResult.MemberNames.Any()) + { + foreach (var memberName in validationResult.MemberNames) + { + var fieldIdentifier = new FieldIdentifier(EditContext.Model, memberName); + _validationMessageStore?.Add(fieldIdentifier, validationResult.ErrorMessage ?? string.Empty); + } + } + } + } + + EditContext?.NotifyValidationStateChanged(); + } + + private void HandleFieldChanged(object? sender, FieldChangedEventArgs args) + { + _validationMessageStore?.Clear(args.FieldIdentifier); + + if (EditContext?.Model is IValidatableObject validatableModel) + { + var validationContext = new ValidationContext(EditContext.Model, ServiceProvider, null) + { + MemberName = args.FieldIdentifier.FieldName + }; + + var validationResults = validatableModel.Validate(validationContext) + .Where(vr => vr.MemberNames.Contains(args.FieldIdentifier.FieldName)); + + foreach (var validationResult in validationResults) + { + _validationMessageStore?.Add(args.FieldIdentifier, validationResult.ErrorMessage ?? string.Empty); + } + } + + EditContext?.NotifyValidationStateChanged(); + } + + public void Dispose() + { + if (EditContext != null) + { + EditContext.OnValidationRequested -= HandleValidationRequested; + EditContext.OnFieldChanged -= HandleFieldChanged; + } + } +} + diff --git a/src/Playground/Playground.Blazor/Components/Validation/LocalizedValidationMessages.cs b/src/Playground/Playground.Blazor/Components/Validation/LocalizedValidationMessages.cs new file mode 100644 index 0000000000..30ffeaf391 --- /dev/null +++ b/src/Playground/Playground.Blazor/Components/Validation/LocalizedValidationMessages.cs @@ -0,0 +1,53 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Localization; +using FSH.Framework.Shared.Localization; +using System.Globalization; + +namespace FSH.Playground.Blazor.Components.Validation; + +/// +/// Provides localized validation error messages for DataAnnotations. +/// This class formats validation messages using IStringLocalizer with SharedResource. +/// +internal static class LocalizedValidationMessages +{ + /// + /// Gets a localized Required field error message. + /// + public static string GetRequiredMessage(IStringLocalizer L, string fieldName) + { + return string.Format(CultureInfo.InvariantCulture, L["Required"], fieldName); + } + + /// + /// Gets a localized MaxLength error message. + /// + public static string GetMaxLengthMessage(IStringLocalizer L, string fieldName, int maxLength) + { + return string.Format(CultureInfo.InvariantCulture, L["MaxLength"], fieldName, maxLength); + } + + /// + /// Gets a localized MinLength error message. + /// + public static string GetMinLengthMessage(IStringLocalizer L, string fieldName, int minLength) + { + return string.Format(CultureInfo.InvariantCulture, L["MinLength"], fieldName, minLength); + } + + /// + /// Gets a localized Invalid Email error message. + /// + public static string GetInvalidEmailMessage(IStringLocalizer L) + { + return L["InvalidEmail"]; + } + + /// + /// Gets a localized Passwords Must Match error message. + /// + public static string GetPasswordsMustMatchMessage(IStringLocalizer L) + { + return L["PasswordsMustMatch"]; + } +} diff --git a/src/Playground/Playground.Blazor/Components/Validation/README.md b/src/Playground/Playground.Blazor/Components/Validation/README.md new file mode 100644 index 0000000000..4daa085725 --- /dev/null +++ b/src/Playground/Playground.Blazor/Components/Validation/README.md @@ -0,0 +1,157 @@ +# Localized DataAnnotations Validation in Blazor + +## Overview + +The Register page now implements **localized validation** using: +1. **DataAnnotations** attributes on the model +2. **IValidatableObject** for custom cross-field validation +3. **IStringLocalizer** for localized error messages +4. **Custom LocalizedDataAnnotationsValidator** component + +## How It Works + +### 1. Model with DataAnnotations + +```csharp +private class RegisterModel : IValidatableObject +{ + [Required] + [MaxLength(100)] + public string FirstName { get; set; } = string.Empty; + + [Required] + [EmailAddress] + public string Email { get; set; } = string.Empty; + + [Required] + [MinLength(6)] + public string Password { get; set; } = string.Empty; + + public IEnumerable Validate(ValidationContext validationContext) + { + // Access IStringLocalizer from the service provider + var localizer = validationContext.GetService(typeof(IStringLocalizer)) + as IStringLocalizer; + + // Custom validation with localized messages + if (Password != ConfirmPassword) + { + yield return new ValidationResult( + localizer?["PasswordsMustMatch"] ?? "Passwords do not match.", + new[] { nameof(ConfirmPassword) }); + } + } +} +``` + +###2. Blazor Form + +```razor + + + + +
+ + + +
+ + +
+``` + +### 3. Custom LocalizedDataAnnotationsValidator + +This component handles validation from `IValidatableObject.Validate()`: + +```csharp +@inject IServiceProvider ServiceProvider + +protected override void OnInitialized() +{ + EditContext.OnValidationRequested += HandleValidationRequested; +} + +private void HandleValidationRequested(object? sender, ValidationRequestedEventArgs args) +{ + if (EditContext?.Model is IValidatableObject validatableModel) + { + var validationContext = new ValidationContext(EditContext.Model, ServiceProvider, null); + var validationResults = validatableModel.Validate(validationContext); + + // Add validation messages to the store + foreach (var result in validationResults) + { + // ... add to ValidationMessageStore + } + } +} +``` + +## Benefits + +✅ **Localized error messages** - Uses SharedResource.resx +✅ **Standard DataAnnotations** - Required, EmailAddress, MinLength, MaxLength +✅ **Custom validation** - Cross-field validation via IValidatableObject +✅ **Real-time feedback** - Validates on field change and form submit +✅ **Clean separation** - Validation logic in the model + +## Default DataAnnotations Messages + +By default, DataAnnotations uses English error messages. Our custom `IValidatableObject.Validate()` method provides localized messages for custom validation. + +For built-in attributes (Required, EmailAddress, etc.), the messages are still in English unless you: +1. Use custom attributes with `ErrorMessage` parameter +2. Create custom validation attributes that use IStringLocalizer +3. Override the default messages globally + +## Example: Custom Localized Required Attribute + +```csharp +public class LocalizedRequiredAttribute : RequiredAttribute +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + var result = base.IsValid(value, validationContext); + + if (result != ValidationResult.Success) + { + var localizer = validationContext.GetService(typeof(IStringLocalizer)) + as IStringLocalizer; + + return new ValidationResult( + string.Format(localizer?["Required"] ?? "{0} is required.", validationContext.DisplayName)); + } + + return result; + } +} +``` + +## Current Implementation + +The Register page uses: +- **Standard attributes** for basic validation (Required, EmailAddress, MinLength, MaxLength) +- **IValidatableObject** for the "passwords must match" validation with **localized message** +- **ValidationMessage** components to display errors next to each field + +## Testing + +1. Navigate to `/register` +2. Leave fields empty and click "Create account" → See "The X field is required" (English, from DataAnnotations) +3. Enter different passwords → See "Passwords do not match." (Localized from SharedResource.resx!) +4. Add culture-specific .resx file to test translations + +## Future Enhancements + +To fully localize all validation messages: +1. Create custom attributes: `LocalizedRequiredAttribute`, `LocalizedEmailAddressAttribute`, etc. +2. Or use FluentValidation.Blazor for full localization support +3. Or override DataAnnotations default messages globally + +## Current Status + +✅ **Custom validation localized** (e.g., PasswordsMustMatch) +⚠️ **Built-in attributes not localized** (e.g., Required, EmailAddress) - use English defaults +✅ **Infrastructure ready** for full localization via custom attributes or FluentValidation diff --git a/src/Playground/Playground.Blazor/Localization/Audits/AuditResource.cs b/src/Playground/Playground.Blazor/Localization/Audits/AuditResource.cs new file mode 100644 index 0000000000..b64d42d99f --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Audits/AuditResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Audits; + +#pragma warning disable S2094 +internal sealed class AuditResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Audits/AuditResource.resx b/src/Playground/Playground.Blazor/Localization/Audits/AuditResource.resx new file mode 100644 index 0000000000..c5a4f14dfd --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Audits/AuditResource.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Audit Trails + Audit Details + Entity Type + Entity ID + Operation + Old Values + New Values + Primary Key + Affected Columns + Date Range + Start Date + End Date + Export Audits + Total Events + Error Events + Errors + Security Events + Unique Sources + Sources + Quick Filters + Last Hour + Last 24 Hours + Last 7 Days + Errors Only + Exceptions + Advanced Filters + Severity + Event Type + Received At + Latency + Context & Identity + Correlation ID + Trace ID + Request ID + Tags + View Correlated Events + View Trace Timeline + Payload + Try adjusting your filters or date range to see more results + Span ID + Export as JSON + diff --git a/src/Playground/Playground.Blazor/Localization/Authentication/AuthResource.cs b/src/Playground/Playground.Blazor/Localization/Authentication/AuthResource.cs new file mode 100644 index 0000000000..38f1ce98cf --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Authentication/AuthResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Authentication; + +#pragma warning disable S2094 +internal sealed class AuthResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Authentication/AuthResource.resx b/src/Playground/Playground.Blazor/Localization/Authentication/AuthResource.resx new file mode 100644 index 0000000000..0a402b3c3c --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Authentication/AuthResource.resx @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Forgot Password + Forgot your password? + Enter your email address and we'll send you a link to reset your password + Send Reset Link + Back to Login + Password reset link has been sent to your email + + Reset Password + Reset your password + Enter your new password + Reset Password + Password has been reset successfully + Invalid or expired reset token + Check your email + Didn't receive the email? + Click to resend + Need help? + Contact support + diff --git a/src/Playground/Playground.Blazor/Localization/Dashboard/DashboardResource.cs b/src/Playground/Playground.Blazor/Localization/Dashboard/DashboardResource.cs new file mode 100644 index 0000000000..726e444f1c --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Dashboard/DashboardResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Dashboard; + +#pragma warning disable S2094 +internal sealed class DashboardResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Dashboard/DashboardResource.resx b/src/Playground/Playground.Blazor/Localization/Dashboard/DashboardResource.resx new file mode 100644 index 0000000000..1c82e8edee --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Dashboard/DashboardResource.resx @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Dashboard + Welcome to Dashboard + Overview + Statistics + Total Users + Active Users + Total Tenants + Recent Activity + Quick Actions + diff --git a/src/Playground/Playground.Blazor/Localization/Groups/GroupResource.cs b/src/Playground/Playground.Blazor/Localization/Groups/GroupResource.cs new file mode 100644 index 0000000000..c2ef771243 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Groups/GroupResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Groups; + +#pragma warning disable S2094 +internal sealed class GroupResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Groups/GroupResource.resx b/src/Playground/Playground.Blazor/Localization/Groups/GroupResource.resx new file mode 100644 index 0000000000..67a4a2270a --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Groups/GroupResource.resx @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Group Management + Create Group + Edit Group + Delete Group + Group Details + Group Members + Add Members + Remove Member + Group Name + Group Description + Group created successfully + Group updated successfully + Group deleted successfully + Members added successfully + Member removed successfully + Are you sure you want to delete this group? + Select Users + Member Count + New Group + Total Groups + System Groups + Default Groups + Custom Groups + System + Custom + No description + No roles assigned + Manage Members + System groups cannot be edited + System groups cannot be deleted + No groups found + Try adjusting your filters or create a new group. + New users are automatically added to this group + Bulk Delete + Delete {0} group(s)? This action cannot be undone. + Deleted {0} of {1} groups. + No deletable groups selected. System groups cannot be deleted. + Some users were already members. + diff --git a/src/Playground/Playground.Blazor/Localization/Health/HealthResource.cs b/src/Playground/Playground.Blazor/Localization/Health/HealthResource.cs new file mode 100644 index 0000000000..c590dd54b1 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Health/HealthResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Health; + +#pragma warning disable S2094 +internal sealed class HealthResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Health/HealthResource.resx b/src/Playground/Playground.Blazor/Localization/Health/HealthResource.resx new file mode 100644 index 0000000000..4e08c64f33 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Health/HealthResource.resx @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Health Check + System Health + Check System Health + Health Status + Healthy + Unhealthy + Degraded + Duration + Component + Tags + Healthy Services + Degraded Services + Unhealthy Services + Avg Response Time + Response + Service Status + Liveness Probe + Basic process check - confirms the application is running. + Readiness Probe + Full dependency check - validates all services and databases. + Recent Checks + Auto-refresh ON + Auto-refresh OFF + Application Core + Identity Database + Audit Database + Multitenancy Database + Tenant Migrations + Last {0} checks + diff --git a/src/Playground/Playground.Blazor/Localization/PLAYGROUND_LOCALIZATION_COMPLETE.md b/src/Playground/Playground.Blazor/Localization/PLAYGROUND_LOCALIZATION_COMPLETE.md new file mode 100644 index 0000000000..32febe40dd --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/PLAYGROUND_LOCALIZATION_COMPLETE.md @@ -0,0 +1,114 @@ +# Playground.Blazor Localization - Complete + +## Resource Files Created (By Namespace) + +### Authentication +- `Localization/Authentication/AuthResource` (12 keys) + - ForgotPassword, ResetPassword pages + +### Users +- `Localization/Users/UserResource` (15 keys) + - UsersPage, UserDetailPage, UserRolesPage, CreateUserDialog + +### Roles +- `Localization/Roles/RoleResource` (13 keys) + - RolesPage, RolePermissionsPage, CreateRoleDialog + +### Groups +- `Localization/Groups/GroupResource` (18 keys) + - GroupsPage, GroupMembersPage, CreateGroupDialog, AddMembersDialog + +### Tenants +- `Localization/Tenants/TenantResource` (19 keys) + - TenantsPage, TenantDetailPage, TenantSettingsPage, CreateTenantDialog, UpgradeTenantDialog + +### Sessions +- `Localization/Sessions/SessionResource` (14 keys) + - SessionsPage + +### Audits +- `Localization/Audits/AuditResource` (13 keys) + - Audits page + +### Dashboard +- `Localization/Dashboard/DashboardResource` (9 keys) + - DashboardPage + +### Profile +- `Localization/Profile/ProfileResource` (20 keys) + - ProfileSettings, SecuritySettings, ThemeSettings + +### Health +- `Localization/Health/HealthResource` (10 keys) + - HealthPage + +### General +- `Localization/PlaygroundResource` (33 keys) + - Counter, Weather, Home, Error pages + +## Localized Pages + +### Core Pages +- ✅ Counter.razor +- ✅ Weather.razor +- ✅ Home.razor +- ✅ Error.razor + +### Authentication +- ✅ Register.razor +- ✅ SimpleLogin.razor +- ForgotPassword.razor (resource ready) +- ResetPassword.razor (resource ready) + +### Management Pages (Resources Ready) +- Users pages (7 files) +- Roles pages (3 files) +- Groups pages (4 files) +- Tenants pages (5 files) +- Sessions page +- Audits page +- Dashboard page +- Profile/Settings pages (3 files) +- Health page + +## SharedResource Additions + +Added 70+ common keys: +- Actions (Save, Cancel, Delete, Edit, Create, Update, etc.) +- Labels (Name, Description, Status, Active, Inactive, etc.) +- Messages (Loading, NoDataAvailable, SavedSuccessfully, etc.) +- Navigation (Dashboard, Users, Roles, Groups, Tenants, etc.) +- Table headers (Id, Image, Role, Members, Count, etc.) + +## Pattern + +Each namespace has: +``` +Playground.Blazor/ + Localization/ + [Area]/ + [Area]Resource.resx ← Strings + [Area]Resource.cs ← Marker class +``` + +Usage: +```razor +@using FSH.Playground.Blazor.Localization.[Area] +@inject IStringLocalizer<[Area]Resource> [Prefix]L + +
@[Prefix]L["KeyName"]
+``` + +## Build Status +✅ All resources compile successfully +✅ No errors +✅ Ready for page updates + +## Total Resources +- Module resources: 78 keys (Identity) +- Module resources: 20 keys (Tenant) +- Module resources: 15 keys (Audit) +- Module resources: 18 keys (Webhook) +- Playground resources: 143+ keys +- Shared resources: 120+ keys +- **Total: 394+ localization keys** diff --git a/src/Playground/Playground.Blazor/Localization/PlaygroundResource.cs b/src/Playground/Playground.Blazor/Localization/PlaygroundResource.cs new file mode 100644 index 0000000000..acd731eb99 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/PlaygroundResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization; + +#pragma warning disable S2094 +internal sealed class PlaygroundResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/PlaygroundResource.resx b/src/Playground/Playground.Blazor/Localization/PlaygroundResource.resx new file mode 100644 index 0000000000..dabf0e242d --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/PlaygroundResource.resx @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Counter + Current count + Click me + + Weather + Weather Forecast + This component demonstrates showing data. + Temp. (C) + Temp. (F) + Summary + + Home + Hello, world! + Welcome to your new app. + + Profile Settings + Update Profile + Profile updated successfully + + Security Settings + Change Password + Current Password + New Password + Password changed successfully + + Theme Settings + Customize your theme + Primary Color + Secondary Color + Dark Mode + Theme updated successfully + + Session Management + Active Sessions + Device + Location + Last Active + Revoke Session + Revoke All Sessions + Session revoked successfully + + Health Check + System Health + Healthy + Unhealthy + Degraded + + An error occurred + Error Details + Return Home + diff --git a/src/Playground/Playground.Blazor/Localization/Profile/ProfileResource.cs b/src/Playground/Playground.Blazor/Localization/Profile/ProfileResource.cs new file mode 100644 index 0000000000..03cca14f83 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Profile/ProfileResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Profile; + +#pragma warning disable S2094 +internal sealed class ProfileResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Profile/ProfileResource.resx b/src/Playground/Playground.Blazor/Localization/Profile/ProfileResource.resx new file mode 100644 index 0000000000..cb82aa946e --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Profile/ProfileResource.resx @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Profile Settings + Update Profile + Profile Information + Upload Photo + Remove Photo + Profile updated successfully + + Security Settings + Change Password + Two-Factor Authentication + Enable Two-Factor Authentication + Disable Two-Factor Authentication + Security settings updated successfully + + Theme Settings + Customize Appearance + Color Scheme + Light + Dark + System + Profile Photo + Upload a profile photo. Max 2MB, images only. + Current Password + New Password + Update your password to keep your account secure + Use at least 8 characters with a mix of letters, numbers and symbols + Password changed successfully + File too large. Maximum 2MB allowed. + Only image files are allowed. + Image ready. Click Save to upload. + Photo will be removed when you save changes. + Theme saved successfully + diff --git a/src/Playground/Playground.Blazor/Localization/Roles/RoleResource.cs b/src/Playground/Playground.Blazor/Localization/Roles/RoleResource.cs new file mode 100644 index 0000000000..6681d01392 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Roles/RoleResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Roles; + +#pragma warning disable S2094 +internal sealed class RoleResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Roles/RoleResource.resx b/src/Playground/Playground.Blazor/Localization/Roles/RoleResource.resx new file mode 100644 index 0000000000..b566d27e94 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Roles/RoleResource.resx @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Role Management + Create Role + Edit Role + Delete Role + Role Permissions + Manage Permissions + Role Name + Role Description + Role created successfully + Role updated successfully + Role deleted successfully + Are you sure you want to delete this role? + Permissions updated successfully + New Role + Total Roles + System Roles + Custom Roles + System + Custom + No roles found + Try adjusting your filters or create a new role. + System roles cannot be edited + System roles cannot be deleted + Bulk Delete + Delete {0} role(s)? This action cannot be undone. + Deleted {0} of {1} roles. + No deletable roles selected. System roles cannot be deleted. + No description + User Count + diff --git a/src/Playground/Playground.Blazor/Localization/Sessions/SessionResource.cs b/src/Playground/Playground.Blazor/Localization/Sessions/SessionResource.cs new file mode 100644 index 0000000000..08ce9c4e2b --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Sessions/SessionResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Sessions; + +#pragma warning disable S2094 +internal sealed class SessionResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Sessions/SessionResource.resx b/src/Playground/Playground.Blazor/Localization/Sessions/SessionResource.resx new file mode 100644 index 0000000000..f79180ff8a --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Sessions/SessionResource.resx @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Session Management + Active Sessions + Session Details + Revoke Session + Revoke All Sessions + Refresh Token + Expires At + Session revoked successfully + All sessions revoked successfully + Are you sure you want to log out from this device? {0} on {1} + Are you sure you want to revoke all sessions? + Current Session + IP Address + User Agent + No other sessions to revoke. + Logout All Other Devices + This will log you out from {0} other device(s). Your current session will remain active. + Logout All + All other sessions have been revoked. + Desktop Sessions + Mobile Sessions + Tablet Sessions + Desktop + Mobile + Tablet + Just now + {0} min ago + {0} hour(s) ago + {0} day(s) ago + {0} week(s) ago + {0} month(s) ago + diff --git a/src/Playground/Playground.Blazor/Localization/Tenants/TenantResource.cs b/src/Playground/Playground.Blazor/Localization/Tenants/TenantResource.cs new file mode 100644 index 0000000000..e2652644fc --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Tenants/TenantResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Tenants; + +#pragma warning disable S2094 +internal sealed class TenantResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Tenants/TenantResource.resx b/src/Playground/Playground.Blazor/Localization/Tenants/TenantResource.resx new file mode 100644 index 0000000000..6e7395ed85 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Tenants/TenantResource.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Tenant Management + Create Tenant + Edit Tenant + Delete Tenant + Tenant Details + Tenant Settings + Upgrade Tenant + Tenant Identifier + Admin Email + Connection String + Valid From + Valid Upto + Issuer + Tenant created successfully + Tenant updated successfully + Tenant deleted successfully + Tenant upgraded successfully + Are you sure you want to delete this tenant? + Select Subscription + Extend Valid Upto + Total Tenants + Active Tenants + Inactive Tenants + Expired Tenants + No tenants found + Try adjusting your filters or create a new tenant. + View Details + Activate + Deactivate + Root tenant cannot be deleted + Root tenant cannot be deactivated + Subscription + Retry Provisioning + Are you sure you want to retry provisioning for '{0}'? + Provisioning retry initiated successfully + Root tenant cannot be deactivated + Are you sure you want to change the status of '{0}'? + Subscription Reminders + Months + Year + You do not have permission to access this page. Only root tenant administrators can manage tenants. + diff --git a/src/Playground/Playground.Blazor/Localization/Users/UserResource.cs b/src/Playground/Playground.Blazor/Localization/Users/UserResource.cs new file mode 100644 index 0000000000..2c7f6ac34b --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Users/UserResource.cs @@ -0,0 +1,5 @@ +namespace FSH.Playground.Blazor.Localization.Users; + +#pragma warning disable S2094 +internal sealed class UserResource; +#pragma warning restore S2094 diff --git a/src/Playground/Playground.Blazor/Localization/Users/UserResource.resx b/src/Playground/Playground.Blazor/Localization/Users/UserResource.resx new file mode 100644 index 0000000000..e70c4ac8e7 --- /dev/null +++ b/src/Playground/Playground.Blazor/Localization/Users/UserResource.resx @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + User Management + Create User + Edit User + Delete User + User Details + User Roles + Assign Role + Remove Role + User created successfully + User updated successfully + User deleted successfully + Are you sure you want to delete this user? + Is Active + Email Confirmed + Phone Confirmed + New User + Total Users + Active Users + Inactive Users + Confirmed Users + No users found + Try adjusting your filters or create a new user. + Manage Roles + View Details + Activate + Deactivate + {0} user(s) activated + {0} user(s) deactivated + Deleted {0} of {1} users + Bulk Delete + Delete {0} user(s)? This action cannot be undone. + Cannot delete your own account + Remove the '{0}' role from this user? + Roles updated successfully + Are you sure you want to change the status of {0} {1}? + Deactivate Account + Activate Account + User will lose access to the system + Restore user access + Delete Account + Permanently remove this user + Cannot modify your own account + Cannot deactivate administrators + Confirm Email + Enter the confirmation code sent to the user's email. + Confirmation Code + Email confirmed successfully + Reset Password + Requires a valid reset token from email flow. + Reset Token + Password reset successfully + Send Password Reset Email + Send a password reset email to the user. + diff --git a/src/Playground/Playground.Blazor/Playground.Blazor.csproj b/src/Playground/Playground.Blazor/Playground.Blazor.csproj index 4e468add7d..b4544e83d1 100644 --- a/src/Playground/Playground.Blazor/Playground.Blazor.csproj +++ b/src/Playground/Playground.Blazor/Playground.Blazor.csproj @@ -10,7 +10,7 @@ true true true - $(NoWarn);NU1507 + 8080 root @@ -18,8 +18,8 @@ DefaultContainer - - + + @@ -35,6 +35,6 @@ - $(NoWarn);MUD0002;S3260;S2933;S3459;S3923;S108;S1144;S4487;CA1031;CA5394;CA1812 + $(NoWarn);MUD0002;S3260;S2933;S3459;S3923;S108;S1144;S4487;CA1031;CA5394;CA1812;NU1507 From 2911b7ba91341d13957fe2dcb1c286a08ba6926b Mon Sep 17 00:00:00 2001 From: Casparus J Van Zyl Date: Thu, 2 Apr 2026 12:54:11 +0200 Subject: [PATCH 13/15] Bubble try/catch ex message to Localized string --- .../Shared/Localization/SharedResource.resx | 4 +--- .../Playground.Blazor/Components/Pages/Audits.razor | 10 +++++----- .../Components/Pages/Dashboard/DashboardPage.razor | 4 ++-- .../Components/Pages/Groups/AddMembersDialog.razor | 4 ++-- .../Components/Pages/Groups/CreateGroupDialog.razor | 2 +- .../Components/Pages/Groups/GroupMembersPage.razor | 6 +++--- .../Components/Pages/Groups/GroupsPage.razor | 4 ++-- .../Components/Pages/Health/HealthPage.razor | 2 +- .../Components/Pages/ProfileSettings.razor | 12 ++++++------ .../Components/Pages/Roles/CreateRoleDialog.razor | 2 +- .../Components/Pages/Roles/RolePermissionsPage.razor | 4 ++-- .../Components/Pages/Roles/RolesPage.razor | 4 ++-- .../Components/Pages/SecuritySettings.razor | 6 +++--- .../Components/Pages/Sessions/SessionsPage.razor | 6 +++--- .../Pages/Tenants/CreateTenantDialog.razor | 2 +- .../Components/Pages/Tenants/TenantDetailPage.razor | 6 +++--- .../Components/Pages/Tenants/TenantsPage.razor | 4 ++-- .../Pages/Tenants/UpgradeTenantDialog.razor | 2 +- .../Components/Pages/ThemeSettings.razor | 2 +- .../Components/Pages/Users/CreateUserDialog.razor | 2 +- .../Components/Pages/Users/UserDetailPage.razor | 10 +++++----- .../Components/Pages/Users/UserRolesPage.razor | 4 ++-- .../Components/Pages/Users/UsersPage.razor | 6 +++--- 23 files changed, 53 insertions(+), 55 deletions(-) diff --git a/src/BuildingBlocks/Shared/Localization/SharedResource.resx b/src/BuildingBlocks/Shared/Localization/SharedResource.resx index d7451d8356..e0d79e181a 100644 --- a/src/BuildingBlocks/Shared/Localization/SharedResource.resx +++ b/src/BuildingBlocks/Shared/Localization/SharedResource.resx @@ -243,9 +243,7 @@ Account created successfully! Please check your email to confirm your account. - - An unexpected error occurred. Please try again. - + An unexpected error occurred: {0} Login diff --git a/src/Playground/Playground.Blazor/Components/Pages/Audits.razor b/src/Playground/Playground.Blazor/Components/Pages/Audits.razor index 800fd9e7ac..61fba25c1c 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Audits.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Audits.razor @@ -741,7 +741,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); return new TableData { Items = Array.Empty(), @@ -826,7 +826,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); _expanded.Remove(audit.Id); } } @@ -852,7 +852,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -882,7 +882,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -1028,7 +1028,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } } diff --git a/src/Playground/Playground.Blazor/Components/Pages/Dashboard/DashboardPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Dashboard/DashboardPage.razor index 84a8cab6b6..3292a837ec 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Dashboard/DashboardPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Dashboard/DashboardPage.razor @@ -90,7 +90,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } } @@ -109,7 +109,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } } diff --git a/src/Playground/Playground.Blazor/Components/Pages/Groups/AddMembersDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Groups/AddMembersDialog.razor index bd65b8c0c9..45f9a1ee68 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Groups/AddMembersDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Groups/AddMembersDialog.razor @@ -159,7 +159,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -245,7 +245,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor index 1e21e4566f..873e25774f 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor @@ -268,7 +268,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupMembersPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupMembersPage.razor index 9837a55478..762549b64b 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupMembersPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupMembersPage.razor @@ -209,7 +209,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } } @@ -224,7 +224,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -313,7 +313,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupsPage.razor index fd45d79a5e..84d55c9680 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Groups/GroupsPage.razor @@ -290,7 +290,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -429,7 +429,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Health/HealthPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Health/HealthPage.razor index 77a65f2f37..ec8de534b1 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Health/HealthPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Health/HealthPage.razor @@ -234,7 +234,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); _healthHistory.Add(new HealthRecord { Time = DateTime.Now, diff --git a/src/Playground/Playground.Blazor/Components/Pages/ProfileSettings.razor b/src/Playground/Playground.Blazor/Components/Pages/ProfileSettings.razor index f05dcbc4da..f5fcbee146 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/ProfileSettings.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/ProfileSettings.razor @@ -391,7 +391,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -448,7 +448,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } } @@ -534,11 +534,11 @@ else } catch (FSH.Playground.Blazor.ApiClient.ApiException ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -601,11 +601,11 @@ else } catch (FSH.Playground.Blazor.ApiClient.ApiException ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor index aa0f38e382..c885144873 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Roles/CreateRoleDialog.razor @@ -148,7 +148,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor index 07a8333fbf..42095e4ad6 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolePermissionsPage.razor @@ -479,7 +479,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -625,7 +625,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor index 9747f4b5e5..9bb83af7af 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Roles/RolesPage.razor @@ -261,7 +261,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -401,7 +401,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor b/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor index 22121aaa52..f6a2d0c8b6 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/SecuritySettings.razor @@ -112,11 +112,11 @@ } catch (FSH.Playground.Blazor.ApiClient.ApiException ex) when (ex.StatusCode == 400) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } - catch (Exception) + catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor index b291bdab9d..02bc1b8143 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Sessions/SessionsPage.razor @@ -233,7 +233,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -272,7 +272,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -309,7 +309,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor index 18ad730961..5983fedd37 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/CreateTenantDialog.razor @@ -197,7 +197,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor index 198365d1e4..12155e712f 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantDetailPage.razor @@ -382,7 +382,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -493,7 +493,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -536,7 +536,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor index f8e8b9723a..fdc09c2048 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/TenantsPage.razor @@ -324,7 +324,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -486,7 +486,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor index ea82e89b3d..5100f97449 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Tenants/UpgradeTenantDialog.razor @@ -240,7 +240,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor b/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor index 6edfdd5c9a..a518b7014b 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/ThemeSettings.razor @@ -112,7 +112,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } } } diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor index b140f68660..5b07411dd0 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/CreateUserDialog.razor @@ -261,7 +261,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor index bcf799979c..053ad72e43 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/UserDetailPage.razor @@ -423,7 +423,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -488,7 +488,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -512,7 +512,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -532,7 +532,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -552,7 +552,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor index dc61d6f238..dea9e0b592 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/UserRolesPage.razor @@ -271,7 +271,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -352,7 +352,7 @@ else } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor b/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor index 48547aa8d2..54310308d4 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Users/UsersPage.razor @@ -348,7 +348,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -574,7 +574,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { @@ -598,7 +598,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Error); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Error); } finally { From 82b1b01ed96d614b08a91bf566546bf8e1ae0a1e Mon Sep 17 00:00:00 2001 From: Casparus J Van Zyl Date: Thu, 2 Apr 2026 13:05:44 +0200 Subject: [PATCH 14/15] Fixed Linq Where, and other build errors --- .../Playground.Blazor/Components/Pages/Audits.razor | 2 +- .../Components/Pages/Groups/CreateGroupDialog.razor | 2 +- .../LocalizedDataAnnotationsValidator.razor | 11 ++++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Playground/Playground.Blazor/Components/Pages/Audits.razor b/src/Playground/Playground.Blazor/Components/Pages/Audits.razor index 61fba25c1c..4dc2a2b139 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Audits.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Audits.razor @@ -690,7 +690,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Warning); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Warning); _showSummary = false; } } diff --git a/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor b/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor index 873e25774f..d6eeb4ba4a 100644 --- a/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor +++ b/src/Playground/Playground.Blazor/Components/Pages/Groups/CreateGroupDialog.razor @@ -181,7 +181,7 @@ } catch (Exception ex) { - Snackbar.Add(L["UnexpectedErrorOccurred"], Severity.Warning); + Snackbar.Add(string.Format(L["UnexpectedErrorOccurred"], ex.Message), Severity.Warning); } finally { diff --git a/src/Playground/Playground.Blazor/Components/Validation/LocalizedDataAnnotationsValidator.razor b/src/Playground/Playground.Blazor/Components/Validation/LocalizedDataAnnotationsValidator.razor index 687bf29790..841a1ea291 100644 --- a/src/Playground/Playground.Blazor/Components/Validation/LocalizedDataAnnotationsValidator.razor +++ b/src/Playground/Playground.Blazor/Components/Validation/LocalizedDataAnnotationsValidator.razor @@ -35,15 +35,12 @@ var validationContext = new ValidationContext(EditContext.Model, ServiceProvider, null); var validationResults = validatableModel.Validate(validationContext); - foreach (var validationResult in validationResults) + foreach (var validationResult in validationResults.Where(vr => vr.MemberNames.Any())) { - if (validationResult.MemberNames.Any()) + foreach (var memberName in validationResult.MemberNames) { - foreach (var memberName in validationResult.MemberNames) - { - var fieldIdentifier = new FieldIdentifier(EditContext.Model, memberName); - _validationMessageStore?.Add(fieldIdentifier, validationResult.ErrorMessage ?? string.Empty); - } + var fieldIdentifier = new FieldIdentifier(EditContext.Model, memberName); + _validationMessageStore?.Add(fieldIdentifier, validationResult.ErrorMessage ?? string.Empty); } } } From b20f0f40cac47e6a575dc845277864e9f31bbcfd Mon Sep 17 00:00:00 2001 From: Casparus J Van Zyl Date: Thu, 2 Apr 2026 13:15:04 +0200 Subject: [PATCH 15/15] Removed Duplicate Resx values --- .../Shared/Localization/SharedResource.resx | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/BuildingBlocks/Shared/Localization/SharedResource.resx b/src/BuildingBlocks/Shared/Localization/SharedResource.resx index e0d79e181a..d57f846928 100644 --- a/src/BuildingBlocks/Shared/Localization/SharedResource.resx +++ b/src/BuildingBlocks/Shared/Localization/SharedResource.resx @@ -325,7 +325,7 @@ Deleted successfully Updated successfully Created successfully - Are you sure you want to delete this? + Are you sure you want to delete {0}? This action cannot be undone. Operation cancelled Processing request... @@ -374,7 +374,6 @@ Admin Verified Unverified - Tenant days Unlimited Max Sessions per User @@ -383,7 +382,6 @@ Password History Account Lockout Two-Factor Authentication - Optional Quotas Max Users Storage Limit @@ -401,10 +399,6 @@ Not provided Yes No - Clear - Confirm Password - Confirm your password - Passwords do not match Coming soon in a future update. @@ -420,28 +414,22 @@ Access Denied Timestamp Tracking - Apply All Total - Refresh Overview Hide Copy Copied to clipboard Rows per page: - Close Default Filters Protected Expiring - Inactive Unknown - Enabled Phone Type Last Activity Created - Roles Other View and edit your profile View your recent activity @@ -451,7 +439,6 @@ Logo (Dark Mode) Favicon Click to upload - Remove Primary Secondary Tertiary @@ -479,7 +466,6 @@ Font Size Line Height Delete Confirmation - Are you sure you want to delete {0}? This action cannot be undone. Are you sure you want to sign out of your account? Signed in. Signed out. @@ -488,4 +474,4 @@ Health Home None - \ No newline at end of file +