Cache localized strings for performance (#1006)

This commit is contained in:
Leendert de Borst
2025-07-14 21:10:06 +02:00
parent 44979916f6
commit 32a2d13fcc
29 changed files with 1179 additions and 393 deletions

View File

@@ -14,17 +14,17 @@
@if (_showTwoFactorAuthStep)
{
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
@Localizer["TwoFactorAuthenticationTitle"]
@_twoFactorAuthenticationTitle
</h2>
<ServerValidationErrors @ref="_serverValidationErrors" />
<p class="text-gray-700 dark:text-gray-300 mb-6">@Localizer["TwoFactorAuthenticationDescription"]</p>
<p class="text-gray-700 dark:text-gray-300 mb-6">@_twoFactorAuthenticationDescription</p>
<div class="w-full">
<EditForm Model="_loginModel2Fa" FormName="login-with-2fa" OnValidSubmit="Handle2Fa" method="post" class="space-y-6">
<DataAnnotationsValidator/>
<div>
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["AuthenticatorCodeLabel"]</label>
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_authenticatorCodeLabel</label>
<InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode"
id="two-factor-code"
@oninput="OnTwoFactorCodeInput"
@@ -37,48 +37,48 @@
<InputCheckbox @bind-Value="_loginModel2Fa.RememberMachine" id="remember-machine" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"/>
</div>
<div class="ml-3 text-sm">
<label for="remember-machine" class="font-medium text-gray-900 dark:text-white">@Localizer["RememberMachineLabel"]</label>
<label for="remember-machine" class="font-medium text-gray-900 dark:text-white">@_rememberMachineLabel</label>
</div>
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@Localizer["LoginButton"]</button>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@_loginButton</button>
</EditForm>
</div>
<p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
@Localizer["DontHaveAuthenticatorText"]
<button @onclick="LoginWithRecoveryCode" class="text-primary-600 hover:underline dark:text-primary-500">@Localizer["LoginWithRecoveryCodeLink"]</button>.
@_dontHaveAuthenticatorText
<button @onclick="LoginWithRecoveryCode" class="text-primary-600 hover:underline dark:text-primary-500">@_loginWithRecoveryCodeLink</button>.
</p>
}
else if (_showLoginWithRecoveryCodeStep)
{
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
@Localizer["RecoveryCodeVerificationTitle"]
@_recoveryCodeVerificationTitle
</h2>
<ServerValidationErrors @ref="_serverValidationErrors" />
<p class="text-gray-700 dark:text-gray-300 mb-6">
@Localizer["RecoveryCodeDescription"]
@_recoveryCodeDescription
</p>
<div class="w-full">
<EditForm Model="_loginModelRecoveryCode" FormName="login-with-recovery-code" OnValidSubmit="HandleRecoveryCode" method="post" class="space-y-6">
<DataAnnotationsValidator/>
<div>
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["RecoveryCodeLabel"]</label>
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_recoveryCodeLabel</label>
<InputText @bind-Value="_loginModelRecoveryCode.RecoveryCode" id="recovery-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
<ValidationMessage For="() => _loginModelRecoveryCode.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@Localizer["LoginButton"]</button>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@_loginButton</button>
</EditForm>
</div>
<p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
@Localizer["RegainedAccessText"]
<button @onclick="LoginWithAuthenticator" class="text-primary-600 hover:underline dark:text-primary-500">@Localizer["LoginWithAuthenticatorLink"]</button> instead.
@_regainedAccessText
<button @onclick="LoginWithAuthenticator" class="text-primary-600 hover:underline dark:text-primary-500">@_loginWithAuthenticatorLink</button> instead.
</p>
}
else
{
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
@Localizer["PageTitle"]
@_pageTitle
</h2>
<FullScreenLoadingIndicator @ref="_loadingIndicator"/>
@@ -87,13 +87,13 @@ else
<ServerValidationErrors @ref="_serverValidationErrors"/>
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["UsernameOrEmailLabel"]</label>
<InputTextField id="email" @bind-Value="_loginModel.Username" type="text" placeholder="@Localizer["UsernamePlaceholder"]" autocapitalize="off" autocorrect="off"/>
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_usernameOrEmailLabel</label>
<InputTextField id="email" @bind-Value="_loginModel.Username" type="text" placeholder="@_usernamePlaceholder" autocapitalize="off" autocorrect="off"/>
<ValidationMessage For="() => _loginModel.Username"/>
</div>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["PasswordLabel"]</label>
<InputTextField id="password" @bind-Value="_loginModel.Password" type="password" placeholder="@Localizer["PasswordPlaceholder"]"/>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_passwordLabel</label>
<InputTextField id="password" @bind-Value="_loginModel.Password" type="password" placeholder="@_passwordPlaceholder"/>
<ValidationMessage For="() => _loginModel.Password"/>
</div>
@@ -102,16 +102,16 @@ else
<InputCheckbox @bind-Value="_loginModel.RememberMe" id="remember" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
</div>
<div class="ml-3 text-sm">
<label for="remember" class="font-medium text-gray-900 dark:text-white">@Localizer["RememberMeLabel"]</label>
<label for="remember" class="font-medium text-gray-900 dark:text-white">@_rememberMeLabel</label>
</div>
<a href="/user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">@Localizer["LostPasswordLink"]</a>
<a href="/user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">@_lostPasswordLink</a>
</div>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@Localizer["LoginToAccountButton"]</button>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@_loginToAccountButton</button>
@if (Config.PublicRegistrationEnabled)
{
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
@Localizer["NoAccountYetText"] <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">@Localizer["CreateNewVaultLink"]</a>
@_noAccountYetText <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">@_createNewVaultLink</a>
</div>
}
</EditForm>
@@ -133,6 +133,35 @@ else
private SrpSession _clientSession = new();
private byte[] _passwordHash = [];
// Cached localized strings
private string _twoFactorAuthenticationTitle = string.Empty;
private string _twoFactorAuthenticationDescription = string.Empty;
private string _authenticatorCodeLabel = string.Empty;
private string _rememberMachineLabel = string.Empty;
private string _loginButton = string.Empty;
private string _dontHaveAuthenticatorText = string.Empty;
private string _loginWithRecoveryCodeLink = string.Empty;
private string _recoveryCodeVerificationTitle = string.Empty;
private string _recoveryCodeDescription = string.Empty;
private string _recoveryCodeLabel = string.Empty;
private string _regainedAccessText = string.Empty;
private string _loginWithAuthenticatorLink = string.Empty;
private string _pageTitle = string.Empty;
private string _usernameOrEmailLabel = string.Empty;
private string _usernamePlaceholder = string.Empty;
private string _passwordLabel = string.Empty;
private string _passwordPlaceholder = string.Empty;
private string _rememberMeLabel = string.Empty;
private string _lostPasswordLink = string.Empty;
private string _loginToAccountButton = string.Empty;
private string _noAccountYetText = string.Empty;
private string _createNewVaultLink = string.Empty;
private string _loggingInMessage = string.Empty;
private string _loginErrorMessage = string.Empty;
private string _loginRequestErrorMessage = string.Empty;
private string _verifyingRecoveryCodeMessage = string.Empty;
private string _verifyingTwoFactorCodeMessage = string.Empty;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
@@ -144,6 +173,35 @@ else
// Already authenticated, redirect to home page.
NavigationManager.NavigateTo("/");
}
// Cache localized strings to prevent infinite rendering loops
_twoFactorAuthenticationTitle = Localizer["TwoFactorAuthenticationTitle"];
_twoFactorAuthenticationDescription = Localizer["TwoFactorAuthenticationDescription"];
_authenticatorCodeLabel = Localizer["AuthenticatorCodeLabel"];
_rememberMachineLabel = Localizer["RememberMachineLabel"];
_loginButton = Localizer["LoginButton"];
_dontHaveAuthenticatorText = Localizer["DontHaveAuthenticatorText"];
_loginWithRecoveryCodeLink = Localizer["LoginWithRecoveryCodeLink"];
_recoveryCodeVerificationTitle = Localizer["RecoveryCodeVerificationTitle"];
_recoveryCodeDescription = Localizer["RecoveryCodeDescription"];
_recoveryCodeLabel = Localizer["RecoveryCodeLabel"];
_regainedAccessText = Localizer["RegainedAccessText"];
_loginWithAuthenticatorLink = Localizer["LoginWithAuthenticatorLink"];
_pageTitle = Localizer["PageTitle"];
_usernameOrEmailLabel = Localizer["UsernameOrEmailLabel"];
_usernamePlaceholder = Localizer["UsernamePlaceholder"];
_passwordLabel = Localizer["PasswordLabel"];
_passwordPlaceholder = Localizer["PasswordPlaceholder"];
_rememberMeLabel = Localizer["RememberMeLabel"];
_lostPasswordLink = Localizer["LostPasswordLink"];
_loginToAccountButton = Localizer["LoginToAccountButton"];
_noAccountYetText = Localizer["NoAccountYetText"];
_createNewVaultLink = Localizer["CreateNewVaultLink"];
_loggingInMessage = Localizer["LoggingInMessage"];
_loginErrorMessage = Localizer["LoginErrorMessage"];
_loginRequestErrorMessage = Localizer["LoginRequestErrorMessage"];
_verifyingRecoveryCodeMessage = Localizer["VerifyingRecoveryCodeMessage"];
_verifyingTwoFactorCodeMessage = Localizer["VerifyingTwoFactorCodeMessage"];
}
/// <inheritdoc />
@@ -178,7 +236,7 @@ else
/// </summary>
private async Task HandleLogin()
{
_loadingIndicator.Show(Localizer["LoggingInMessage"]);
_loadingIndicator.Show(_loggingInMessage);
_serverValidationErrors.Clear();
try
@@ -199,7 +257,7 @@ else
catch
{
// If in release mode show a generic error.
_serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
_serverValidationErrors.AddError(_loginErrorMessage);
}
#endif
finally
@@ -233,7 +291,7 @@ else
{
return
[
Localizer["LoginRequestErrorMessage"],
_loginRequestErrorMessage,
];
}
@@ -264,7 +322,7 @@ else
{
return
[
Localizer["LoginRequestErrorMessage"],
_loginRequestErrorMessage,
];
}
@@ -284,7 +342,7 @@ else
/// </summary>
private async Task HandleRecoveryCode()
{
_loadingIndicator.Show(Localizer["VerifyingRecoveryCodeMessage"]);
_loadingIndicator.Show(_verifyingRecoveryCodeMessage);
_serverValidationErrors.Clear();
try
@@ -308,7 +366,7 @@ else
var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
if (validateLoginResponse == null)
{
_serverValidationErrors.AddError(Localizer["LoginRequestErrorMessage"]);
_serverValidationErrors.AddError(_loginRequestErrorMessage);
return;
}
@@ -328,7 +386,7 @@ else
catch
{
// If in release mode show a generic error.
_serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
_serverValidationErrors.AddError(_loginErrorMessage);
}
#endif
finally
@@ -342,7 +400,7 @@ else
/// </summary>
private async Task Handle2Fa()
{
_loadingIndicator.Show(Localizer["VerifyingTwoFactorCodeMessage"]);
_loadingIndicator.Show(_verifyingTwoFactorCodeMessage);
_serverValidationErrors.Clear();
try
@@ -366,7 +424,7 @@ else
var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
if (validateLoginResponse == null)
{
_serverValidationErrors.AddError(Localizer["LoginRequestErrorMessage"]);
_serverValidationErrors.AddError(_loginRequestErrorMessage);
return;
}
@@ -386,7 +444,7 @@ else
catch
{
// If in release mode show a generic error.
_serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
_serverValidationErrors.AddError(_loginErrorMessage);
}
#endif
finally

View File

@@ -7,7 +7,7 @@
@using Microsoft.Extensions.Localization
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
@Localizer["PageTitle"]
@_pageTitle
</h2>
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
@@ -16,18 +16,18 @@
<EditForm Model="_registerModel" OnValidSubmit="HandleRegister" class="mt-8 space-y-6">
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["UsernameOrEmailLabel"]</label>
<InputTextField id="email" @bind-Value="_registerModel.Username" type="text" placeholder="@Localizer["UsernamePlaceholder"]" />
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_usernameOrEmailLabel</label>
<InputTextField id="email" @bind-Value="_registerModel.Username" type="text" placeholder="@_usernamePlaceholder" />
<ValidationMessage For="() => _registerModel.Username"/>
</div>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["PasswordLabel"]</label>
<InputTextField id="password" @bind-Value="_registerModel.Password" type="password" placeholder="@Localizer["PasswordPlaceholder"]" />
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_passwordLabel</label>
<InputTextField id="password" @bind-Value="_registerModel.Password" type="password" placeholder="@_passwordPlaceholder" />
<ValidationMessage For="() => _registerModel.Password"/>
</div>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmPasswordLabel"]</label>
<InputTextField id="password2" @bind-Value="_registerModel.PasswordConfirm" type="password" placeholder="@Localizer["PasswordPlaceholder"]" />
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_confirmPasswordLabel</label>
<InputTextField id="password2" @bind-Value="_registerModel.PasswordConfirm" type="password" placeholder="@_passwordPlaceholder" />
<ValidationMessage For="() => _registerModel.PasswordConfirm"/>
</div>
@@ -36,14 +36,14 @@
<InputCheckbox id="terms" @bind-Value="_registerModel.AcceptTerms" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
</div>
<div class="ml-3 text-sm">
<label for="terms" class="font-medium text-gray-900 dark:text-white">@Localizer["AcceptTermsLabel"] <a href="https://github.com/lanedirt/AliasVault/blob/main/LICENSE.md" target="_blank" class="text-primary-700 hover:underline dark:text-primary-500">@Localizer["TermsAndConditionsLink"]</a></label>
<label for="terms" class="font-medium text-gray-900 dark:text-white">@_acceptTermsLabel <a href="https://github.com/lanedirt/AliasVault/blob/main/LICENSE.md" target="_blank" class="text-primary-700 hover:underline dark:text-primary-500">@_termsAndConditionsLink</a></label>
<ValidationMessage For="() => _registerModel.AcceptTerms"/>
</div>
</div>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@Localizer["CreateAccountButton"]</button>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@_createAccountButton</button>
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
@Localizer["AlreadyRegisteredText"] <a href="/user/login" class="text-primary-700 hover:underline dark:text-primary-500">@Localizer["LoginHereLink"]</a>
@_alreadyRegisteredText <a href="/user/login" class="text-primary-700 hover:underline dark:text-primary-500">@_loginHereLink</a>
</div>
</EditForm>
@@ -54,9 +54,44 @@
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Auth.Register", "AliasVault.Client");
// Cached localized strings
private string _pageTitle = string.Empty;
private string _usernameOrEmailLabel = string.Empty;
private string _usernamePlaceholder = string.Empty;
private string _passwordLabel = string.Empty;
private string _passwordPlaceholder = string.Empty;
private string _confirmPasswordLabel = string.Empty;
private string _acceptTermsLabel = string.Empty;
private string _termsAndConditionsLink = string.Empty;
private string _createAccountButton = string.Empty;
private string _alreadyRegisteredText = string.Empty;
private string _loginHereLink = string.Empty;
private string _creatingAccountMessage = string.Empty;
private string _registrationErrorMessage = string.Empty;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// Cache localized strings to prevent infinite rendering loops
_pageTitle = Localizer["PageTitle"];
_usernameOrEmailLabel = Localizer["UsernameOrEmailLabel"];
_usernamePlaceholder = Localizer["UsernamePlaceholder"];
_passwordLabel = Localizer["PasswordLabel"];
_passwordPlaceholder = Localizer["PasswordPlaceholder"];
_confirmPasswordLabel = Localizer["ConfirmPasswordLabel"];
_acceptTermsLabel = Localizer["AcceptTermsLabel"];
_termsAndConditionsLink = Localizer["TermsAndConditionsLink"];
_createAccountButton = Localizer["CreateAccountButton"];
_alreadyRegisteredText = Localizer["AlreadyRegisteredText"];
_loginHereLink = Localizer["LoginHereLink"];
_creatingAccountMessage = Localizer["CreatingAccountMessage"];
_registrationErrorMessage = Localizer["RegistrationErrorMessage"];
}
private async Task HandleRegister()
{
_loadingIndicator.Show(Localizer["CreatingAccountMessage"]);
_loadingIndicator.Show(_creatingAccountMessage);
_serverValidationErrors.Clear();
var (success, errorMessage) = await UserRegistrationService.RegisterUserAsync(_registerModel.Username, _registerModel.Password);
@@ -67,7 +102,7 @@
}
else
{
_serverValidationErrors.AddError(errorMessage ?? Localizer["RegistrationErrorMessage"]);
_serverValidationErrors.AddError(errorMessage ?? _registrationErrorMessage);
StateHasChanged();
}

View File

@@ -17,7 +17,7 @@
</div>
<div class="ml-3 bg-blue-100 dark:bg-blue-900 rounded-lg p-3">
<p class="text-sm text-gray-900 dark:text-white">
@Localizer["WelcomeMessage"]
@_welcomeMessage
</p>
</div>
</div>
@@ -25,26 +25,26 @@
<div class="p-4 mb-6 bg-gray-100 dark:bg-gray-900 rounded-lg text-gray-900 dark:text-gray-100">
<p class="text-sm font-semibold">
@Localizer["ImportantNote"]
@_importantNote
</p>
<ul class="text-sm mt-3 list-disc list-inside">
<li>@Localizer["SecurityPoint1"]</li>
<li>@Localizer["SecurityPoint2"]</li>
<li>@Localizer["SecurityPoint3"]</li>
<li>@_securityPoint1</li>
<li>@_securityPoint2</li>
<li>@_securityPoint3</li>
</ul>
</div>
<div class="space-y-4">
<div>
<div class="">
<EditFormRow Id="password" Label="@Localizer["MasterPasswordLabel"]" @bind-Value="Password" Type="password" Placeholder="@Localizer["MasterPasswordPlaceholder"]" OnFocus="@OnPasswordInputFocus"/>
<EditFormRow Id="password" Label="@_masterPasswordLabel" @bind-Value="Password" Type="password" Placeholder="@_masterPasswordPlaceholder" OnFocus="@OnPasswordInputFocus"/>
</div>
<div class="mt-4">
<EditFormRow Id="confirmPassword" Label="@Localizer["ConfirmMasterPasswordLabel"]" @bind-Value="ConfirmPassword" Type="password" Placeholder="@Localizer["ConfirmMasterPasswordPlaceholder"]" OnFocus="@OnPasswordInputFocus" />
<EditFormRow Id="confirmPassword" Label="@_confirmMasterPasswordLabel" @bind-Value="ConfirmPassword" Type="password" Placeholder="@_confirmMasterPasswordPlaceholder" OnFocus="@OnPasswordInputFocus" />
</div>
@if (_isValidating)
{
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">@Localizer["ValidatingPasswordMessage"]</div>
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">@_validatingPasswordMessage</div>
}
else if (_isValid)
{
@@ -54,7 +54,7 @@
}
else
{
<div class="mt-2 text-sm text-green-600 dark:text-green-400">@Localizer["PasswordValidAndStrongMessage"]</div>
<div class="mt-2 text-sm text-green-600 dark:text-green-400">@_passwordValidAndStrongMessage</div>
}
}
else if (!string.IsNullOrEmpty(_errorMessage))
@@ -69,6 +69,21 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Auth.Setup.PasswordStep", "AliasVault.Client");
// Cached localized strings for performance
private string _welcomeMessage = string.Empty;
private string _importantNote = string.Empty;
private string _securityPoint1 = string.Empty;
private string _securityPoint2 = string.Empty;
private string _securityPoint3 = string.Empty;
private string _masterPasswordLabel = string.Empty;
private string _masterPasswordPlaceholder = string.Empty;
private string _confirmMasterPasswordLabel = string.Empty;
private string _confirmMasterPasswordPlaceholder = string.Empty;
private string _validatingPasswordMessage = string.Empty;
private string _passwordValidAndStrongMessage = string.Empty;
private string _passwordTooShortError = string.Empty;
private string _passwordsMismatchError = string.Empty;
/// <summary>
/// The event callback for when the password changes.
/// </summary>
@@ -122,6 +137,21 @@
/// <inheritdoc />
protected override void OnInitialized()
{
// Cache localized strings for performance
_welcomeMessage = Localizer["WelcomeMessage"];
_importantNote = Localizer["ImportantNote"];
_securityPoint1 = Localizer["SecurityPoint1"];
_securityPoint2 = Localizer["SecurityPoint2"];
_securityPoint3 = Localizer["SecurityPoint3"];
_masterPasswordLabel = Localizer["MasterPasswordLabel"];
_masterPasswordPlaceholder = Localizer["MasterPasswordPlaceholder"];
_confirmMasterPasswordLabel = Localizer["ConfirmMasterPasswordLabel"];
_confirmMasterPasswordPlaceholder = Localizer["ConfirmMasterPasswordPlaceholder"];
_validatingPasswordMessage = Localizer["ValidatingPasswordMessage"];
_passwordValidAndStrongMessage = Localizer["PasswordValidAndStrongMessage"];
_passwordTooShortError = Localizer["PasswordTooShortError"];
_passwordsMismatchError = Localizer["PasswordsMismatchError"];
_loadingTimer = new Timer(300);
_loadingTimer.Elapsed += (sender, e) => FinishLoading();
_loadingTimer.AutoReset = false;
@@ -177,7 +207,7 @@
{
_isValidating = false;
_isValid = false;
_errorMessage = Localizer["PasswordTooShortError"];
_errorMessage = _passwordTooShortError;
await OnPasswordChange.InvokeAsync(string.Empty);
StateHasChanged();
return;
@@ -197,7 +227,7 @@
{
_isValidating = false;
_isValid = false;
_errorMessage = Localizer["PasswordsMismatchError"];
_errorMessage = _passwordsMismatchError;
await OnPasswordChange.InvokeAsync(string.Empty);
StateHasChanged();
return;

View File

@@ -13,18 +13,18 @@
<div class="@(_isLoading ? "invisible opacity-0" : "opacity-100") transition-opacity duration-300 w-full">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-6">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
@Localizer["PleaseReadAndAgree"]
@_pleaseReadAndAgree
</p>
<div class="bg-gray-100 dark:bg-gray-700 rounded-lg p-4 mb-8 h-80 overflow-y-auto">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">@Localizer["TermsAndConditionsTitle"]</h3>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">@_termsAndConditionsTitle</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-line">
@Localizer["TermsContent"]
@_termsContent
</p>
</div>
<div class="flex items-center">
<input type="checkbox" id="agreeTerms" @bind="AgreedToTerms" @bind:after="OnAgreedToTerms" class="mr-2">
<label for="agreeTerms" class="text-sm font-bold text-gray-600 dark:text-gray-400">
@Localizer["AgreementCheckboxLabel"]
@_agreementCheckboxLabel
</label>
</div>
</div>
@@ -33,6 +33,12 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Auth.Setup.TermsAndConditionsStep", "AliasVault.Client");
// Cached localized strings for performance
private string _pleaseReadAndAgree = string.Empty;
private string _termsAndConditionsTitle = string.Empty;
private string _termsContent = string.Empty;
private string _agreementCheckboxLabel = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the user has agreed to the terms and conditions.
/// </summary>
@@ -57,6 +63,12 @@
/// <inheritdoc />
protected override void OnInitialized()
{
// Cache localized strings for performance
_pleaseReadAndAgree = Localizer["PleaseReadAndAgree"];
_termsAndConditionsTitle = Localizer["TermsAndConditionsTitle"];
_termsContent = Localizer["TermsContent"];
_agreementCheckboxLabel = Localizer["AgreementCheckboxLabel"];
_loadingTimer = new Timer(300);
_loadingTimer.Elapsed += (sender, e) => FinishLoading();
_loadingTimer.AutoReset = false;

View File

@@ -14,17 +14,17 @@
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-6 mb-6">
<div class="flex items-start mb-4">
<div class="flex-shrink-0">
<img class="h-10 w-10 rounded-full" src="/img/avatar.webp" alt="@Localizer["AssistantAvatarAlt"]">
<img class="h-10 w-10 rounded-full" src="/img/avatar.webp" alt="@_assistantAvatarAlt">
</div>
<div class="ml-3 bg-blue-100 dark:bg-blue-900 rounded-lg p-3">
<p class="text-sm text-gray-900 dark:text-white">
@Localizer["GreatNowLetsSetupUsername"]
@_greatNowLetsSetupUsername
</p>
<p class="text-sm text-gray-900 dark:text-white mt-3">
@Localizer["EnterUsernameInstructions"]
@_enterUsernameInstructions
</p>
<p class="text-sm text-gray-900 dark:text-white mt-3 font-semibold">
@Localizer["RememberUsernameNote"]
@_rememberUsernameNote
</p>
</div>
</div>
@@ -32,14 +32,14 @@
<div class="space-y-4">
<div>
<EditFormRow Id="username" Label="@Localizer["UsernameLabel"]" @bind-Value="Username" Placeholder="@Localizer["UsernamePlaceholder"]" OnFocus="@OnUsernameInputFocus" />
<EditFormRow Id="username" Label="@_usernameLabel" @bind-Value="Username" Placeholder="@_usernamePlaceholder" OnFocus="@OnUsernameInputFocus" />
@if (_isValidating)
{
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">@Localizer["ValidatingUsernameMessage"]</div>
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">@_validatingUsernameMessage</div>
}
else if (_isValid)
{
<div class="mt-2 text-sm text-green-600 dark:text-green-400">@Localizer["UsernameAvailableMessage"]</div>
<div class="mt-2 text-sm text-green-600 dark:text-green-400">@_usernameAvailableMessage</div>
}
else if (!string.IsNullOrEmpty(_errorMessage))
{
@@ -54,6 +54,18 @@
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Auth.Setup.UsernameStep", "AliasVault.Client");
private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
// Cached localized strings for performance
private string _assistantAvatarAlt = string.Empty;
private string _greatNowLetsSetupUsername = string.Empty;
private string _enterUsernameInstructions = string.Empty;
private string _rememberUsernameNote = string.Empty;
private string _usernameLabel = string.Empty;
private string _usernamePlaceholder = string.Empty;
private string _validatingUsernameMessage = string.Empty;
private string _usernameAvailableMessage = string.Empty;
private string _usernameRequiredError = string.Empty;
private string _serverCommunicationError = string.Empty;
/// <summary>
/// The username that is previously entered by the user. When a user navigates with back/continue
/// and entered a username already, the existing username might be provided by the parent component.
@@ -103,6 +115,18 @@
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
// Cache localized strings for performance
_assistantAvatarAlt = Localizer["AssistantAvatarAlt"];
_greatNowLetsSetupUsername = Localizer["GreatNowLetsSetupUsername"];
_enterUsernameInstructions = Localizer["EnterUsernameInstructions"];
_rememberUsernameNote = Localizer["RememberUsernameNote"];
_usernameLabel = Localizer["UsernameLabel"];
_usernamePlaceholder = Localizer["UsernamePlaceholder"];
_validatingUsernameMessage = Localizer["ValidatingUsernameMessage"];
_usernameAvailableMessage = Localizer["UsernameAvailableMessage"];
_usernameRequiredError = Localizer["UsernameRequiredError"];
_serverCommunicationError = Localizer["ServerCommunicationError"];
// Set the default username if provided.
_username = DefaultUsername;
@@ -166,7 +190,7 @@
{
_isValidating = false;
_isValid = false;
_errorMessage = Localizer["UsernameRequiredError"];
_errorMessage = _usernameRequiredError;
await OnUsernameChange.InvokeAsync(string.Empty);
StateHasChanged();
return;
@@ -192,7 +216,7 @@
}
catch
{
_errorMessage = Localizer["ServerCommunicationError"];
_errorMessage = _serverCommunicationError;
_isValid = false;
await OnUsernameChange.InvokeAsync(string.Empty);
}

View File

@@ -19,7 +19,7 @@
else if (IsWebAuthnLoading) {
<BoldLoadingIndicator />
<p class="mt-6 text-center font-normal text-gray-500 dark:text-gray-400">
@Localizer["LoggingInWithWebAuthn"]
@_loggingInWithWebAuthn
</p>
}
else
@@ -33,15 +33,15 @@ else
{
<div class="mb-6">
<p class="text-base font-normal text-gray-500 dark:text-gray-400 mb-4">
@Localizer["QuickUnlockDescription"]
@_quickUnlockDescription
</p>
<div class="flex space-x-4">
<button type="button" @onclick="UnlockWithWebAuthn" class="flex-grow inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"></path></svg>
@Localizer["UnlockWithWebAuthn"]
@_unlockWithWebAuthn
</button>
<button type="button" @onclick="ShowPasswordLogin" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-gray-900 rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
@Localizer["UnlockWithPassword"]
@_unlockWithPassword
</button>
</div>
</div>
@@ -49,26 +49,26 @@ else
else
{
<p class="text-base font-normal text-gray-500 dark:text-gray-400 mb-4">
@Localizer["EnterMasterPasswordDescription"]
@_enterMasterPasswordDescription
</p>
<EditForm Model="_unlockModel" OnValidSubmit="UnlockSubmit" class="mt-8 space-y-6">
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["YourPasswordLabel"]</label>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_yourPasswordLabel</label>
<InputTextField id="password" @bind-Value="_unlockModel.Password" type="password" placeholder="••••••••"/>
<ValidationMessage For="() => _unlockModel.Password"/>
</div>
<button type="submit" class="w-full inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z"></path></svg>
@Localizer["UnlockButton"]
@_unlockButton
</button>
</EditForm>
}
<div class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-6">
@Localizer["SwitchAccountsText"] <a href="/user/logout" class="text-primary-700 hover:underline dark:text-primary-500">@Localizer["LogOutLink"]</a>
@_switchAccountsText <a href="/user/logout" class="text-primary-700 hover:underline dark:text-primary-500">@_logOutLink</a>
</div>
}
@@ -89,11 +89,47 @@ else
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Auth.Unlock", "AliasVault.Client");
private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
// Cached localized strings
private string _loggingInWithWebAuthn = string.Empty;
private string _quickUnlockDescription = string.Empty;
private string _unlockWithWebAuthn = string.Empty;
private string _unlockWithPassword = string.Empty;
private string _enterMasterPasswordDescription = string.Empty;
private string _yourPasswordLabel = string.Empty;
private string _unlockButton = string.Empty;
private string _switchAccountsText = string.Empty;
private string _logOutLink = string.Empty;
private string _unlockingVaultMessage = string.Empty;
private string _unlockRequestError = string.Empty;
private string _incorrectPasswordError = string.Empty;
private string _genericUnlockError = string.Empty;
private string _sessionTimedOutError = string.Empty;
private string _connectionFailedError = string.Empty;
private string _webAuthnNotSupportedError = string.Empty;
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Cache localized strings to prevent infinite rendering loops
_loggingInWithWebAuthn = Localizer["LoggingInWithWebAuthn"];
_quickUnlockDescription = Localizer["QuickUnlockDescription"];
_unlockWithWebAuthn = Localizer["UnlockWithWebAuthn"];
_unlockWithPassword = Localizer["UnlockWithPassword"];
_enterMasterPasswordDescription = Localizer["EnterMasterPasswordDescription"];
_yourPasswordLabel = Localizer["YourPasswordLabel"];
_unlockButton = Localizer["UnlockButton"];
_switchAccountsText = Localizer["SwitchAccountsText"];
_logOutLink = Localizer["LogOutLink"];
_unlockingVaultMessage = Localizer["UnlockingVaultMessage"];
_unlockRequestError = Localizer["UnlockRequestError"];
_incorrectPasswordError = Localizer["IncorrectPasswordError"];
_genericUnlockError = Localizer["GenericUnlockError"];
_sessionTimedOutError = Localizer["SessionTimedOutError"];
_connectionFailedError = Localizer["ConnectionFailedError"];
_webAuthnNotSupportedError = Localizer["WebAuthnNotSupportedError"];
// Trigger status API call to check if the user is still authenticated.
// If user is not authenticated a redirect to the login page will be triggered automatically.
await StatusCheck();
@@ -117,7 +153,7 @@ else
/// </summary>
private async Task UnlockSubmit()
{
_loadingIndicator.Show(Localizer["UnlockingVaultMessage"]);
_loadingIndicator.Show(_unlockingVaultMessage);
_serverValidationErrors.Clear();
try
@@ -142,7 +178,7 @@ else
var loginResponse = JsonSerializer.Deserialize<LoginInitiateResponse>(responseContent);
if (loginResponse == null)
{
_serverValidationErrors.AddError(Localizer["UnlockRequestError"]);
_serverValidationErrors.AddError(_unlockRequestError);
return;
}
@@ -154,7 +190,7 @@ else
if (!validPassword)
{
_serverValidationErrors.AddError(Localizer["IncorrectPasswordError"]);
_serverValidationErrors.AddError(_incorrectPasswordError);
return;
}
@@ -183,7 +219,7 @@ else
catch
{
// If in release mode show a generic error.
_serverValidationErrors.AddError(Localizer["GenericUnlockError"]);
_serverValidationErrors.AddError(_genericUnlockError);
}
#endif
finally
@@ -202,7 +238,7 @@ else
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == false) {
// Not authenticated (anymore), redirect to login page.
GlobalNotificationService.AddErrorMessage(Localizer["SessionTimedOutError"]);
GlobalNotificationService.AddErrorMessage(_sessionTimedOutError);
NavigationManager.NavigateTo("/user/login");
return;
}
@@ -214,7 +250,7 @@ else
// Clear all tokens and redirect to login page.
await AuthService.RemoveTokensAsync();
GlobalNotificationService.ClearMessages();
GlobalNotificationService.AddErrorMessage(Localizer["SessionTimedOutError"]);
GlobalNotificationService.AddErrorMessage(_sessionTimedOutError);
NavigationManager.NavigateTo("/user/login");
return;
}
@@ -228,7 +264,7 @@ else
await AuthService.RemoveTokensAsync();
await AuthStateProvider.GetAuthenticationStateAsync();
GlobalNotificationService.ClearMessages();
GlobalNotificationService.AddErrorMessage(Localizer["SessionTimedOutError"]);
GlobalNotificationService.AddErrorMessage(_sessionTimedOutError);
NavigationManager.NavigateTo("/user/login");
return;
}
@@ -241,7 +277,7 @@ else
}
catch (Exception ex)
{
_serverValidationErrors.AddError(Localizer["ConnectionFailedError"]);
_serverValidationErrors.AddError(_connectionFailedError);
Logger.LogError(ex, "An error occurred while checking the user status.");
StateHasChanged();
}
@@ -284,7 +320,7 @@ else
}
catch (NotSupportedException)
{
GlobalNotificationService.AddErrorMessage(Localizer["WebAuthnNotSupportedError"], true);
GlobalNotificationService.AddErrorMessage(_webAuthnNotSupportedError, true);
}
catch (Exception ex)
{

View File

@@ -37,12 +37,12 @@
<!-- 2-column layout for email details -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm text-gray-500 dark:text-gray-400">
<div class="space-y-1">
<p>@Localizer["FromLabel"] @(Email?.FromLocal)@@@(Email?.FromDomain)</p>
<p>@Localizer["ToLabel"] @(Email?.ToLocal)@@@(Email?.ToDomain)</p>
<p>@_fromLabel @(Email?.FromLocal)@@@(Email?.FromDomain)</p>
<p>@_toLabel @(Email?.ToLocal)@@@(Email?.ToDomain)</p>
</div>
<div class="space-y-1">
<p>@Localizer["DateLabel"] @Email?.DateSystem</p>
<p>@Localizer["ActionsLabel"] <button @onclick="ShowDeleteConfirmation" class="text-red-500 hover:text-red-700 text-sm">@Localizer["DeleteButton"]</button></p>
<p>@_dateLabel @Email?.DateSystem</p>
<p>@_actionsLabel <button @onclick="ShowDeleteConfirmation" class="text-red-500 hover:text-red-700 text-sm">@_deleteButton</button></p>
</div>
</div>
</div>
@@ -59,7 +59,7 @@
@if (Email?.Attachments?.Any() == true)
{
<div class="border-t border-gray-200 dark:border-gray-600 pt-4">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">@Localizer["AttachmentsLabel"]</h3>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">@_attachmentsLabel</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
@foreach (var attachment in Email.Attachments)
{
@@ -78,7 +78,7 @@
}
</div>
<div class="mt-6 flex justify-end space-x-4">
<button id="close-email-modal" @onclick="Close" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">@Localizer["CloseButton"]</button>
<button id="close-email-modal" @onclick="Close" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">@_closeButton</button>
</div>
</div>
</div>
@@ -87,6 +87,23 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Email.EmailModal", "AliasVault.Client");
// Cached localized strings
private string _fromLabel = "";
private string _toLabel = "";
private string _dateLabel = "";
private string _actionsLabel = "";
private string _deleteButton = "";
private string _attachmentsLabel = "";
private string _closeButton = "";
private string _deleteEmailTitle = "";
private string _deleteEmailConfirmation = "";
private string _emailDeletedSuccess = "";
private string _emailDeleteFailed = "";
private string _genericError = "";
private string _noEmailBody = "";
private string _attachmentDownloadFailed = "";
private string _attachmentDownloadError = "";
/// <summary>
/// The email to show in the modal.
@@ -128,8 +145,8 @@
}
var result = await ConfirmModalService.ShowConfirmation(
Localizer["DeleteEmailTitle"],
Localizer["DeleteEmailConfirmation"]
_deleteEmailTitle,
_deleteEmailConfirmation
);
if (result)
@@ -189,18 +206,18 @@
if (response.IsSuccessStatusCode)
{
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
GlobalNotificationService.AddSuccessMessage(_emailDeletedSuccess, true);
await Close();
}
else
{
var errorMessage = await response.Content.ReadAsStringAsync();
GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {errorMessage}", true);
GlobalNotificationService.AddErrorMessage($"{_emailDeleteFailed}: {errorMessage}", true);
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["GenericError"]}: {ex.Message}", true);
GlobalNotificationService.AddErrorMessage($"{_genericError}: {ex.Message}", true);
}
}
@@ -220,22 +237,39 @@
if (response.IsSuccessStatusCode)
{
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
GlobalNotificationService.AddSuccessMessage(_emailDeletedSuccess, true);
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["EmailDeleteFailed"], true);
GlobalNotificationService.AddErrorMessage(_emailDeleteFailed, true);
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {ex.Message}", true);
GlobalNotificationService.AddErrorMessage($"{_emailDeleteFailed}: {ex.Message}", true);
}
}
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_fromLabel = Localizer["FromLabel"];
_toLabel = Localizer["ToLabel"];
_dateLabel = Localizer["DateLabel"];
_actionsLabel = Localizer["ActionsLabel"];
_deleteButton = Localizer["DeleteButton"];
_attachmentsLabel = Localizer["AttachmentsLabel"];
_closeButton = Localizer["CloseButton"];
_deleteEmailTitle = Localizer["DeleteEmailTitle"];
_deleteEmailConfirmation = Localizer["DeleteEmailConfirmation"];
_emailDeletedSuccess = Localizer["EmailDeletedSuccess"];
_emailDeleteFailed = Localizer["EmailDeleteFailed"];
_genericError = Localizer["GenericError"];
_noEmailBody = Localizer["NoEmailBody"];
_attachmentDownloadFailed = Localizer["AttachmentDownloadFailed"];
_attachmentDownloadError = Localizer["AttachmentDownloadError"];
await base.OnInitializedAsync();
// Determine email body
@@ -255,7 +289,7 @@
else
{
// No HTML and no plain text available
EmailBody = Localizer["NoEmailBody"];
EmailBody = _noEmailBody;
}
}
}
@@ -282,7 +316,7 @@
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
GlobalNotificationService.AddErrorMessage(_attachmentDownloadFailed, true);
}
}
else
@@ -305,13 +339,13 @@
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
GlobalNotificationService.AddErrorMessage(_attachmentDownloadFailed, true);
}
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["AttachmentDownloadError"]}: {ex.Message}", true);
GlobalNotificationService.AddErrorMessage($"{_attachmentDownloadError}: {ex.Message}", true);
}
}
}

View File

@@ -37,14 +37,14 @@
<!-- 2-column layout for email details -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm text-gray-600 dark:text-gray-300">
<div class="space-y-1">
<p><span class="font-medium">@Localizer["FromLabel"]</span> @(Email.FromLocal)@@@(Email.FromDomain)</p>
<p><span class="font-medium">@Localizer["ToLabel"]</span> @(Email.ToLocal)@@@(Email.ToDomain)</p>
<p><span class="font-medium">@_fromLabel</span> @(Email.FromLocal)@@@(Email.FromDomain)</p>
<p><span class="font-medium">@_toLabel</span> @(Email.ToLocal)@@@(Email.ToDomain)</p>
</div>
<div class="space-y-1">
<p><span class="font-medium">@Localizer["DateLabel"]</span> @Email.DateSystem</p>
<p><span class="font-medium">@_dateLabel</span> @Email.DateSystem</p>
@if (!string.IsNullOrEmpty(CredentialName) && CredentialId != Guid.Empty)
{
<p><span class="font-medium">@Localizer["CredentialLabel"]</span>
<p><span class="font-medium">@_credentialLabel</span>
<button @onclick="@(() => OnCredentialClick.InvokeAsync(CredentialId))"
class="text-blue-600 hover:underline dark:text-blue-400 cursor-pointer">
@CredentialName
@@ -53,7 +53,7 @@
}
else
{
<p><span class="font-medium">@Localizer["CredentialLabel"]</span> <span class="text-gray-400 dark:text-gray-500">@Localizer["NoneValue"]</span></p>
<p><span class="font-medium">@_credentialLabel</span> <span class="text-gray-400 dark:text-gray-500">@_noneValue</span></p>
}
</div>
</div>
@@ -73,7 +73,7 @@
{
<!-- Attachments Section - Fixed height -->
<div class="border-t border-gray-200 dark:border-gray-600 p-4 bg-gray-50 dark:bg-gray-800">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">@Localizer["AttachmentsLabel"]</h3>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">@_attachmentsLabel</h3>
<div class="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto">
@foreach (var attachment in Email.Attachments)
{
@@ -99,7 +99,7 @@
<svg class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<p class="mt-2 text-sm">@Localizer["SelectEmailMessage"]</p>
<p class="mt-2 text-sm">@_selectEmailMessage</p>
</div>
</div>
}
@@ -107,6 +107,23 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Email.EmailPreview", "AliasVault.Client");
// Cached localized strings
private string _fromLabel = "";
private string _toLabel = "";
private string _dateLabel = "";
private string _credentialLabel = "";
private string _noneValue = "";
private string _attachmentsLabel = "";
private string _selectEmailMessage = "";
private string _deleteEmailTitle = "";
private string _deleteEmailConfirmation = "";
private string _emailDeletedSuccess = "";
private string _emailDeleteFailed = "";
private string _genericError = "";
private string _noEmailBody = "";
private string _attachmentDownloadFailed = "";
private string _attachmentDownloadError = "";
/// <summary>
/// The email to show in the preview.
/// </summary>
@@ -159,8 +176,8 @@
}
var result = await ConfirmModalService.ShowConfirmation(
Localizer["DeleteEmailTitle"],
Localizer["DeleteEmailConfirmation"]
_deleteEmailTitle,
_deleteEmailConfirmation
);
if (result)
@@ -211,17 +228,17 @@
if (response.IsSuccessStatusCode)
{
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
GlobalNotificationService.AddSuccessMessage(_emailDeletedSuccess, true);
}
else
{
var errorMessage = await response.Content.ReadAsStringAsync();
GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {errorMessage}", true);
GlobalNotificationService.AddErrorMessage($"{_emailDeleteFailed}: {errorMessage}", true);
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["GenericError"]}: {ex.Message}", true);
GlobalNotificationService.AddErrorMessage($"{_genericError}: {ex.Message}", true);
}
}
@@ -241,19 +258,41 @@
if (response.IsSuccessStatusCode)
{
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
GlobalNotificationService.AddSuccessMessage(_emailDeletedSuccess, true);
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["EmailDeleteFailed"], true);
GlobalNotificationService.AddErrorMessage(_emailDeleteFailed, true);
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {ex.Message}", true);
GlobalNotificationService.AddErrorMessage($"{_emailDeleteFailed}: {ex.Message}", true);
}
}
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_fromLabel = Localizer["FromLabel"];
_toLabel = Localizer["ToLabel"];
_dateLabel = Localizer["DateLabel"];
_credentialLabel = Localizer["CredentialLabel"];
_noneValue = Localizer["NoneValue"];
_attachmentsLabel = Localizer["AttachmentsLabel"];
_selectEmailMessage = Localizer["SelectEmailMessage"];
_deleteEmailTitle = Localizer["DeleteEmailTitle"];
_deleteEmailConfirmation = Localizer["DeleteEmailConfirmation"];
_emailDeletedSuccess = Localizer["EmailDeletedSuccess"];
_emailDeleteFailed = Localizer["EmailDeleteFailed"];
_genericError = Localizer["GenericError"];
_noEmailBody = Localizer["NoEmailBody"];
_attachmentDownloadFailed = Localizer["AttachmentDownloadFailed"];
_attachmentDownloadError = Localizer["AttachmentDownloadError"];
await base.OnInitializedAsync();
}
/// <inheritdoc />
protected override async Task OnParametersSetAsync()
{
@@ -275,7 +314,7 @@
else
{
// No HTML and no plain text available
EmailBody = Localizer["NoEmailBody"];
EmailBody = _noEmailBody;
}
}
}
@@ -302,7 +341,7 @@
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
GlobalNotificationService.AddErrorMessage(_attachmentDownloadFailed, true);
}
}
else
@@ -325,13 +364,13 @@
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
GlobalNotificationService.AddErrorMessage(_attachmentDownloadFailed, true);
}
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["AttachmentDownloadError"]}: {ex.Message}", true);
GlobalNotificationService.AddErrorMessage($"{_attachmentDownloadError}: {ex.Message}", true);
}
}
}

View File

@@ -21,7 +21,7 @@
}
@if (IsNewEmail)
{
<div class="w-2 h-2 ml-1 bg-yellow-500 rounded-full animate-pulse flex-shrink-0" title="@Localizer["NewEmailTooltip"]"></div>
<div class="w-2 h-2 ml-1 bg-yellow-500 rounded-full animate-pulse flex-shrink-0" title="@_newEmailTooltip"></div>
}
</div>
<!-- Subject (smaller, below from) -->
@@ -44,6 +44,10 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Email.EmailRow", "AliasVault.Client");
// Cached localized strings
private string _newEmailTooltip = "";
/// <summary>
/// The email model.
/// </summary>
@@ -68,4 +72,12 @@
/// </summary>
[Parameter]
public bool IsNewEmail { get; set; }
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_newEmailTooltip = Localizer["NewEmailTooltip"];
await base.OnInitializedAsync();
}
}

View File

@@ -26,12 +26,12 @@
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="flex justify-between">
<div>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["EmailSectionTitle"]</h3>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@_emailSectionTitle</h3>
</div>
<div class="flex justify-end items-center space-x-2">
@if (DbService.Settings.AutoEmailRefresh)
{
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="@Localizer["AutoRefreshEnabledTooltip"]"></div>
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="@_autoRefreshEnabledTooltip"></div>
}
<button id="recent-email-refresh" @onclick="ManualRefresh" type="button" class="text-blue-700 border border-blue-700 hover:bg-blue-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2 text-center inline-flex items-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:focus:ring-blue-800 dark:hover:bg-blue-500">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
@@ -51,10 +51,10 @@
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
@Localizer["SubjectColumn"]
@_subjectColumn
</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
@Localizer["DateColumn"]
@_dateColumn
</th>
</tr>
</thead>
@@ -87,7 +87,7 @@
}
else if (MailboxEmails.Count == 0)
{
<div class="text-gray-500 dark:text-gray-400">@Localizer["NoEmailsReceivedMessage"]</div>
<div class="text-gray-500 dark:text-gray-400">@_noEmailsReceivedMessage</div>
}
else
{
@@ -99,10 +99,10 @@
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
@Localizer["SubjectColumn"]
@_subjectColumn
</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
@Localizer["DateColumn"]
@_dateColumn
</th>
</tr>
</thead>
@@ -150,6 +150,15 @@
private bool IsLoading => LoadingService.IsLoading("recentemails");
private bool IsSpamOk { get; set; } = false;
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Email.RecentEmails", "AliasVault.Client");
// Cached localized strings
private string _emailSectionTitle = "";
private string _autoRefreshEnabledTooltip = "";
private string _subjectColumn = "";
private string _dateColumn = "";
private string _noEmailsReceivedMessage = "";
private string _emailAddressInUseError = "";
private string _emailLoadError = "";
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
private CancellationTokenSource? _pollingCts;
@@ -229,6 +238,15 @@
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_emailSectionTitle = Localizer["EmailSectionTitle"];
_autoRefreshEnabledTooltip = Localizer["AutoRefreshEnabledTooltip"];
_subjectColumn = Localizer["SubjectColumn"];
_dateColumn = Localizer["DateColumn"];
_noEmailsReceivedMessage = Localizer["NoEmailsReceivedMessage"];
_emailAddressInUseError = Localizer["EmailAddressInUseError"];
_emailLoadError = Localizer["EmailLoadError"];
await base.OnInitializedAsync();
if (EmailAddress is null)
@@ -430,10 +448,10 @@
switch (errorResponse.Code)
{
case "CLAIM_DOES_NOT_MATCH_USER":
Error = Localizer["EmailAddressInUseError"];
Error = _emailAddressInUseError;
break;
case "CLAIM_DOES_NOT_EXIST":
Error = Localizer["EmailLoadError"];
Error = _emailLoadError;
break;
default:
throw new ArgumentException(errorResponse.Message);

View File

@@ -14,13 +14,16 @@
@if (Copied)
{
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-green-500 dark:text-green-400">
@Localizer["CopiedMessage"]
@_copiedMessage
</span>
}
</div>
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Forms.CopyPasteFormRow", "AliasVault.Client");
// Cached localized strings
private string _copiedMessage = "";
/// <summary>
/// Id for the input field. Defaults to a random GUID if not provided.
@@ -45,6 +48,9 @@
/// <inheritdoc />
protected override void OnInitialized()
{
// Cache localized strings
_copiedMessage = Localizer["CopiedMessage"];
ClipboardCopyService.OnCopy += HandleCopy;
}

View File

@@ -20,13 +20,13 @@
@if (IsCustomDomain)
{
<button type="button" class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" @onclick="TogglePopup">
@Localizer["UseDomainChooserButton"]
@_useDomainChooserButton
</button>
}
else
{
<button type="button" class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" @onclick="ToggleCustomDomain">
@Localizer["EnterCustomDomainButton"]
@_enterCustomDomainButton
</button>
}
</div>
@@ -36,13 +36,13 @@
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 z-30 overflow-y-auto h-full w-full" @onclick="ClosePopup">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800" @onclick:stopPropagation>
<div class="mt-3 text-center">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">@Localizer["SelectEmailDomainTitle"]</h3>
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">@_selectEmailDomainTitle</h3>
<div class="mt-2 px-7 py-3">
@if (ShowPrivateDomains)
{
<div class="mb-4">
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300">@Localizer["PrivateEmailTitle"]</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">@Localizer["PrivateEmailDescription"]</p>
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300">@_privateEmailTitle</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">@_privateEmailDescription</p>
@foreach (var domain in PrivateDomains)
{
<button class="mt-2 px-4 py-2 bg-primary-300 text-gray-700 rounded hover:bg-primary-400 focus:outline-none focus:ring-2 focus:ring-gray-400 mr-2" @onclick="() => SelectDomain(domain)">
@@ -52,8 +52,8 @@
</div>
}
<div class="@(ShowPrivateDomains ? "border-t border-gray-200 dark:border-gray-600 pt-4" : "")">
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300">@Localizer["PublicEmailTitle"]</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">@Localizer["PublicEmailDescription"]</p>
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300">@_publicEmailTitle</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">@_publicEmailDescription</p>
@foreach (var domain in PublicDomains)
{
<button class="mt-2 px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400 mr-2" @onclick="() => SelectDomain(domain)">
@@ -81,6 +81,15 @@
public string Label { get; set; } = "Email Address";
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Forms.EditEmailFormRow", "AliasVault.Client");
// Cached localized strings
private string _useDomainChooserButton = "";
private string _enterCustomDomainButton = "";
private string _selectEmailDomainTitle = "";
private string _privateEmailTitle = "";
private string _privateEmailDescription = "";
private string _publicEmailTitle = "";
private string _publicEmailDescription = "";
/// <summary>
/// The value of the input field. This should be the full email address.
@@ -107,6 +116,15 @@
/// <inheritdoc />
protected override void OnInitialized()
{
// Cache localized strings
_useDomainChooserButton = Localizer["UseDomainChooserButton"];
_enterCustomDomainButton = Localizer["EnterCustomDomainButton"];
_selectEmailDomainTitle = Localizer["SelectEmailDomainTitle"];
_privateEmailTitle = Localizer["PrivateEmailTitle"];
_privateEmailDescription = Localizer["PrivateEmailDescription"];
_publicEmailTitle = Localizer["PublicEmailTitle"];
_publicEmailDescription = Localizer["PublicEmailDescription"];
base.OnInitialized();
IsCustomDomain = !PublicDomains.Contains(SelectedDomain) && !PrivateDomains.Contains(SelectedDomain);

View File

@@ -3,12 +3,12 @@
@using Microsoft.Extensions.Localization
<div class="mb-4">
<label for="password-generator-settings-modal" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["PasswordGeneratorSettingsLabel"]</label>
<label for="password-generator-settings-modal" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_passwordGeneratorSettingsLabel</label>
<button type="button" id="password-generator-settings-modal" class="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-primary-700 dark:hover:bg-primary-600" @onclick="OpenSettings">
@Localizer["ConfigureButton"]
@_configureButton
</button>
<span class="block text-sm font-normal text-gray-500 truncate dark:text-gray-400 mt-2">
@Localizer["PasswordGeneratorSettingsDescription"]
@_passwordGeneratorSettingsDescription
</span>
</div>
@@ -23,6 +23,11 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Settings.DefaultPasswordSettings", "AliasVault.Client");
// Cached localized strings
private string _passwordGeneratorSettingsLabel = "";
private string _configureButton = "";
private string _passwordGeneratorSettingsDescription = "";
private PasswordSettings PasswordSettings { get; set; } = new();
private bool IsSettingsVisible { get; set; }
@@ -30,6 +35,11 @@
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_passwordGeneratorSettingsLabel = Localizer["PasswordGeneratorSettingsLabel"];
_configureButton = Localizer["ConfigureButton"];
_passwordGeneratorSettingsDescription = Localizer["PasswordGeneratorSettingsDescription"];
await base.OnInitializedAsync();
PasswordSettings = DbService.Settings.PasswordSettings;
}

View File

@@ -10,10 +10,10 @@
<ModalWrapper OnEnter="HandleEnterKey">
<div id="passwordSettingsModal" class="relative top-20 mx-auto p-5 shadow-lg rounded-md bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-400">
<div class="m-2">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">@Localizer["Title"]</h3>
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">@_title</h3>
<div class="mt-4 space-y-4">
<div>
<label for="password-length" class="block text-sm font-medium text-gray-700 dark:text-gray-300">@string.Format(Localizer["PasswordLengthLabel"], _workingSettings.Length)</label>
<label for="password-length" class="block text-sm font-medium text-gray-700 dark:text-gray-300">@string.Format(_passwordLengthLabel, _workingSettings.Length)</label>
<input type="range" id="password-length" min="8" max="64"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
@bind="_workingSettings.Length" @oninput="HandleLengthInput">
@@ -22,35 +22,35 @@
<div class="flex items-center">
<input id="use-lowercase" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
@bind="_workingSettings.UseLowercase" @bind:after="OnPasswordSettingsChanged">
<label for="use-lowercase" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@Localizer["IncludeLowercaseLabel"]</label>
<label for="use-lowercase" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@_includeLowercaseLabel</label>
</div>
<div class="flex items-center">
<input id="use-uppercase" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
@bind="_workingSettings.UseUppercase" @bind:after="OnPasswordSettingsChanged">
<label for="use-uppercase" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@Localizer["IncludeUppercaseLabel"]</label>
<label for="use-uppercase" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@_includeUppercaseLabel</label>
</div>
<div class="flex items-center">
<input id="use-numbers" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
@bind="_workingSettings.UseNumbers" @bind:after="OnPasswordSettingsChanged">
<label for="use-numbers" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@Localizer["IncludeNumbersLabel"]</label>
<label for="use-numbers" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@_includeNumbersLabel</label>
</div>
<div class="flex items-center">
<input id="use-special-chars" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
@bind="_workingSettings.UseSpecialChars" @bind:after="OnPasswordSettingsChanged">
<label for="use-special-chars" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@Localizer["IncludeSpecialCharsLabel"]</label>
<label for="use-special-chars" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@_includeSpecialCharsLabel</label>
</div>
<div class="flex items-center">
<input id="use-non-ambiguous" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
@bind="_workingSettings.UseNonAmbiguousChars" @bind:after="OnPasswordSettingsChanged">
<label for="use-non-ambiguous" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@Localizer["AvoidAmbiguousCharsLabel"]</label>
<label for="use-non-ambiguous" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@_avoidAmbiguousCharsLabel</label>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">@Localizer["PreviewLabel"]</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">@_previewLabel</label>
<div class="mt-1 flex">
<CopyPasteFormRow Id="preview-password" Value="@_previewPassword" />
<button type="button" class="ml-2 px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-md dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="RefreshPreview">
@@ -63,16 +63,16 @@
<div class="flex @(IsTemporary ? "justify-between" : "justify-end") pt-4 gap-2">
<button type="button" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600" @onclick="OnClose">
@Localizer["CancelButton"]
@_cancelButton
</button>
@if (IsTemporary)
{
<button type="button" class="px-4 py-2 bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-white rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500" @onclick="OnSaveTemporary">
@Localizer["UseJustOnceButton"]
@_useJustOnceButton
</button>
}
<button type="button" id="save-button" class="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-primary-700 dark:hover:bg-primary-600" @onclick="OnSaveGlobal">
@Localizer["SaveGloballyButton"]
@_saveGloballyButton
</button>
</div>
</div>
@@ -83,6 +83,20 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Settings.PasswordSettingsPopup", "AliasVault.Client");
// Cached localized strings
private string _title = "";
private string _passwordLengthLabel = "";
private string _includeLowercaseLabel = "";
private string _includeUppercaseLabel = "";
private string _includeNumbersLabel = "";
private string _includeSpecialCharsLabel = "";
private string _avoidAmbiguousCharsLabel = "";
private string _previewLabel = "";
private string _cancelButton = "";
private string _useJustOnceButton = "";
private string _saveGloballyButton = "";
private string _settingsUpdatedMessage = "";
/// <summary>
/// The PasswordSettings to mutate.
@@ -122,6 +136,20 @@
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_title = Localizer["Title"];
_passwordLengthLabel = Localizer["PasswordLengthLabel"];
_includeLowercaseLabel = Localizer["IncludeLowercaseLabel"];
_includeUppercaseLabel = Localizer["IncludeUppercaseLabel"];
_includeNumbersLabel = Localizer["IncludeNumbersLabel"];
_includeSpecialCharsLabel = Localizer["IncludeSpecialCharsLabel"];
_avoidAmbiguousCharsLabel = Localizer["AvoidAmbiguousCharsLabel"];
_previewLabel = Localizer["PreviewLabel"];
_cancelButton = Localizer["CancelButton"];
_useJustOnceButton = Localizer["UseJustOnceButton"];
_saveGloballyButton = Localizer["SaveGloballyButton"];
_settingsUpdatedMessage = Localizer["SettingsUpdatedMessage"];
// Clone the settings to avoid reference issues
_workingSettings = new PasswordSettings
{
@@ -182,7 +210,7 @@
var settingsJson = System.Text.Json.JsonSerializer.Serialize(_workingSettings);
await DbService.Settings.SetSettingAsync("PasswordGenerationSettings", settingsJson);
GlobalLoadingService.Hide();
GlobalNotificationService.AddSuccessMessage(Localizer["SettingsUpdatedMessage"], true);
GlobalNotificationService.AddSuccessMessage(_settingsUpdatedMessage, true);
// Notify parent with both settings and the generated password.
await OnSaveSettings.InvokeAsync((_workingSettings, _previewPassword));

View File

@@ -9,13 +9,13 @@
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="flex justify-between">
<div>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["TwoFactorAuthenticationTitle"]</h3>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@_twoFactorAuthenticationTitle</h3>
</div>
@if (TotpCodeList.Any(t => !t.IsDeleted) && !IsAddFormVisible)
{
<div>
<button id="add-totp-code" @onclick="ShowAddForm" type="button" class="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-2 text-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-600 dark:focus:ring-blue-800">
@Localizer["AddTotpCodeButton"]
@_addTotpCodeButton
</button>
</div>
}
@@ -24,7 +24,7 @@
@if ((TotpCodeList.Count == 0 || TotpCodeList.All(t => t.IsDeleted)) && !IsAddFormVisible)
{
<div class="flex flex-col justify-center">
<p class="text-gray-500 dark:text-gray-400"><a @onclick="ShowAddForm" id="add-totp-code" href="javascript:void(0)" class="text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400">@Localizer["AddTotpCodeDescription"]</a></p>
<p class="text-gray-500 dark:text-gray-400"><a @onclick="ShowAddForm" id="add-totp-code" href="javascript:void(0)" class="text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400">@_addTotpCodeDescription</a></p>
</div>
}
else
@@ -35,28 +35,28 @@
<EditForm Model="@NewTotpCode" OnValidSubmit="AddTotpCode">
<DataAnnotationsValidator />
<div class="flex justify-between items-center mb-4">
<h4 class="text-lg font-medium text-gray-900 dark:text-white">@Localizer["AddTotpCodeModalTitle"]</h4>
<h4 class="text-lg font-medium text-gray-900 dark:text-white">@_addTotpCodeModalTitle</h4>
<button @onclick="HideAddForm" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
<span class="sr-only">@Localizer["CloseFormButton"]</span>
<span class="sr-only">@_closeFormButton</span>
</button>
</div>
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">@Localizer["TotpInstructions"]</p>
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">@_totpInstructions</p>
<div class="mb-4">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["NameOptionalLabel"]</label>
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_nameOptionalLabel</label>
<InputText id="name" @bind-Value="NewTotpCode.Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" />
<ValidationMessage For="@(() => NewTotpCode.Name)" class="text-red-600 dark:text-red-400 text-sm mt-1" />
</div>
<div class="mb-4">
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["SecretKeyLabel"]</label>
<InputText id="secretKey" @bind-Value="NewTotpCode.SecretKey" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="@Localizer["SecretKeyPlaceholder"]" />
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_secretKeyLabel</label>
<InputText id="secretKey" @bind-Value="NewTotpCode.SecretKey" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="@_secretKeyPlaceholder" />
<ValidationMessage For="@(() => NewTotpCode.SecretKey)" class="text-red-600 dark:text-red-400 text-sm mt-1" />
</div>
<div class="flex justify-end">
<button id="save-totp-code" type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
@Localizer["SaveButton"]
@_saveButton
</button>
</div>
</EditForm>
@@ -73,7 +73,7 @@
</div>
<div class="flex items-center gap-2">
<div class="flex flex-col items-end">
<div class="text-sm text-gray-500 dark:text-gray-400">@Localizer["SaveToViewCodeMessage"]</div>
<div class="text-sm text-gray-500 dark:text-gray-400">@_saveToViewCodeMessage</div>
</div>
<button type="button" @onclick="() => DeleteTotpCode(totpCode)" class="delete-totp-code text-red-600 hover:text-red-800 dark:text-red-500 dark:hover:text-red-400">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
@@ -90,6 +90,21 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Components.TotpCodes.TotpCodes", "AliasVault.Client");
// Cached localized strings
private string _twoFactorAuthenticationTitle = "";
private string _addTotpCodeButton = "";
private string _addTotpCodeDescription = "";
private string _addTotpCodeModalTitle = "";
private string _closeFormButton = "";
private string _totpInstructions = "";
private string _nameOptionalLabel = "";
private string _secretKeyLabel = "";
private string _secretKeyPlaceholder = "";
private string _saveButton = "";
private string _saveToViewCodeMessage = "";
private string _deleteTotpCodeTitle = "";
private string _deleteTotpCodeConfirmation = "";
/// <summary>
/// The list of TOTP codes.
@@ -110,6 +125,21 @@
/// <inheritdoc/>
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_twoFactorAuthenticationTitle = Localizer["TwoFactorAuthenticationTitle"];
_addTotpCodeButton = Localizer["AddTotpCodeButton"];
_addTotpCodeDescription = Localizer["AddTotpCodeDescription"];
_addTotpCodeModalTitle = Localizer["AddTotpCodeModalTitle"];
_closeFormButton = Localizer["CloseFormButton"];
_totpInstructions = Localizer["TotpInstructions"];
_nameOptionalLabel = Localizer["NameOptionalLabel"];
_secretKeyLabel = Localizer["SecretKeyLabel"];
_secretKeyPlaceholder = Localizer["SecretKeyPlaceholder"];
_saveButton = Localizer["SaveButton"];
_saveToViewCodeMessage = Localizer["SaveToViewCodeMessage"];
_deleteTotpCodeTitle = Localizer["DeleteTotpCodeTitle"];
_deleteTotpCodeConfirmation = Localizer["DeleteTotpCodeConfirmation"];
await base.OnInitializedAsync();
}
@@ -177,7 +207,7 @@
private async Task DeleteTotpCode(TotpCode totpCode)
{
// Show confirmation modal.
var result = await ConfirmModalService.ShowConfirmation(Localizer["DeleteTotpCodeTitle"], Localizer["DeleteTotpCodeConfirmation"]);
var result = await ConfirmModalService.ShowConfirmation(_deleteTotpCodeTitle, _deleteTotpCodeConfirmation);
if (!result)
{
return;

View File

@@ -9,7 +9,7 @@
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="flex justify-between">
<div>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["TwoFactorAuthenticationTitle"]</h3>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@_twoFactorAuthenticationTitle</h3>
</div>
</div>
@@ -20,7 +20,7 @@
else if (TotpCodeList.Count == 0)
{
<div class="flex flex-col justify-center">
<p class="text-gray-500 dark:text-gray-400">@Localizer["NoTotpCodesMessage"]</p>
<p class="text-gray-500 dark:text-gray-400">@_noTotpCodesMessage</p>
</div>
}
else
@@ -41,7 +41,7 @@
<div class="text-xs">
@if (IsCopied(totpCode.Id.ToString()))
{
<span class="text-green-600 dark:text-green-400">@Localizer["CopiedMessage"]</span>
<span class="text-green-600 dark:text-green-400">@_copiedMessage</span>
}
else
{
@@ -62,6 +62,11 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Components.TotpCodes.TotpViewer", "AliasVault.Client");
// Cached localized strings
private string _twoFactorAuthenticationTitle = "";
private string _noTotpCodesMessage = "";
private string _copiedMessage = "";
/// <summary>
/// The list of TOTP codes to display.
@@ -86,6 +91,11 @@
/// <inheritdoc/>
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_twoFactorAuthenticationTitle = Localizer["TwoFactorAuthenticationTitle"];
_noTotpCodesMessage = Localizer["NoTotpCodesMessage"];
_copiedMessage = Localizer["CopiedMessage"];
await base.OnInitializedAsync();
// Generate initial codes

View File

@@ -7,7 +7,7 @@
@implements IAsyncDisposable
<button @ref="buttonRef" @onclick="TogglePopup" id="quickIdentityButton" class="px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-primary-500 to-primary-600 hover:from-primary-600 hover:to-primary-700 focus:outline-none dark:from-primary-400 dark:to-primary-500 dark:hover:from-primary-500 dark:hover:to-primary-600 rounded-md shadow-sm transition duration-150 ease-in-out transform hover:scale-105 active:scale-95 focus:shadow-outline">
@Localizer["NewAliasButtonShort"] <span class="hidden md:inline">@Localizer["NewAliasButtonText"].Value.Substring(1).Trim()</span>
@_newAliasButtonShort <span class="hidden md:inline">@_newAliasButtonText.Substring(1).Trim()</span>
</button>
@if (IsPopupVisible)
@@ -15,25 +15,25 @@
<ClickOutsideHandler OnClose="ClosePopup" ContentId="quickIdentityPopup,quickIdentityButton">
<div id="quickIdentityPopup" class="absolute z-50 mt-2 p-4 bg-white rounded-lg shadow-xl border border-gray-300 dark:bg-gray-800 dark:border-gray-400"
style="@PopupStyle">
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">@Localizer["CreateNewAliasTitle"]</h3>
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">@_createNewAliasTitle</h3>
<EditForm Model="Model" OnValidSubmit="CreateIdentity">
<DataAnnotationsValidator />
<div class="mb-4">
<EditFormRow Id="serviceName" Label="@Localizer["ServiceNameLabel"]" Placeholder="@Localizer["ServiceNamePlaceholder"]" @bind-Value="Model.ServiceName"></EditFormRow>
<EditFormRow Id="serviceName" Label="@_serviceNameLabel" Placeholder="@_serviceNamePlaceholder" @bind-Value="Model.ServiceName"></EditFormRow>
<ValidationMessage For="() => Model.ServiceName"/>
</div>
<div class="mb-4">
<EditFormRow Id="serviceUrl" Label="@Localizer["ServiceUrlLabel"]" OnFocus="OnFocusUrlInput" @bind-Value="Model.ServiceUrl"></EditFormRow>
<EditFormRow Id="serviceUrl" Label="@_serviceUrlLabel" OnFocus="OnFocusUrlInput" @bind-Value="Model.ServiceUrl"></EditFormRow>
<ValidationMessage For="() => Model.ServiceUrl"/>
</div>
<div class="flex justify-between items-center">
<button id="quickIdentitySubmit" type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
@Localizer["CreateButton"]
@_createButton
</button>
</div>
<div class="pt-2">
<a href="#" @onclick="OpenAdvancedMode" @onclick:preventDefault class="text-sm text-blue-500 hover:text-blue-700">
@Localizer["AdvancedModeLink"]
@_advancedModeLink
</a>
</div>
</EditForm>
@@ -43,6 +43,19 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Widgets.CreateNewIdentityWidget", "AliasVault.Client");
// Cached localized strings
private string _newAliasButtonShort = "";
private string _newAliasButtonText = "";
private string _createNewAliasTitle = "";
private string _serviceNameLabel = "";
private string _serviceNamePlaceholder = "";
private string _serviceUrlLabel = "";
private string _createButton = "";
private string _advancedModeLink = "";
private string _creatingNewAliasMessage = "";
private string _createCredentialErrorMessage = "";
private string _credentialCreatedSuccessMessage = "";
private bool IsPopupVisible = false;
private bool IsCreating = false;
@@ -61,6 +74,24 @@
}
}
protected override async Task OnInitializedAsync()
{
// Cache localized strings
_newAliasButtonShort = Localizer["NewAliasButtonShort"];
_newAliasButtonText = Localizer["NewAliasButtonText"];
_createNewAliasTitle = Localizer["CreateNewAliasTitle"];
_serviceNameLabel = Localizer["ServiceNameLabel"];
_serviceNamePlaceholder = Localizer["ServiceNamePlaceholder"];
_serviceUrlLabel = Localizer["ServiceUrlLabel"];
_createButton = Localizer["CreateButton"];
_advancedModeLink = Localizer["AdvancedModeLink"];
_creatingNewAliasMessage = Localizer["CreatingNewAliasMessage"];
_createCredentialErrorMessage = Localizer["CreateCredentialErrorMessage"];
_credentialCreatedSuccessMessage = Localizer["CredentialCreatedSuccessMessage"];
await base.OnInitializedAsync();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -150,7 +181,7 @@
}
IsCreating = true;
GlobalLoadingSpinner.Show(Localizer["CreatingNewAliasMessage"]);
GlobalLoadingSpinner.Show(_creatingNewAliasMessage);
StateHasChanged();
var credential = new Credential();
@@ -173,12 +204,12 @@
// Error saving.
IsCreating = false;
GlobalLoadingSpinner.Hide();
GlobalNotificationService.AddErrorMessage(Localizer["CreateCredentialErrorMessage"], true);
GlobalNotificationService.AddErrorMessage(_createCredentialErrorMessage, true);
return;
}
// No error, add success message.
GlobalNotificationService.AddSuccessMessage(Localizer["CredentialCreatedSuccessMessage"]);
GlobalNotificationService.AddSuccessMessage(_credentialCreatedSuccessMessage);
NavigationManager.NavigateTo("/credentials/" + id);

View File

@@ -6,78 +6,90 @@
@implements IAsyncDisposable
@using Microsoft.Extensions.Localization
<ClickOutsideHandler OnClose="OnClose" ContentId="searchWidgetContainer">
<div class="relative" id="searchWidgetContainer">
<input
id="searchWidget"
type="text"
placeholder="@Localizer["SearchVaultPlaceholder"]"
autocomplete="off"
class="w-full px-4 py-2 text-gray-700 bg-white border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:ring-primary-500"
@bind-value="SearchTerm"
@oninput="SearchTermChanged"
@onfocus="OnFocus"
@onkeydown="HandleKeyDown"/>
<div class="relative" id="searchWidgetContainer">
<input
id="searchWidget"
type="text"
placeholder="@_searchPlaceholder"
autocomplete="off"
class="w-full px-4 py-2 text-gray-700 bg-white border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:ring-primary-500"
@bind-value="SearchTerm"
@oninput="SearchTermChanged"
@onfocus="OnFocus"
@onkeydown="HandleKeyDown"/>
@if (ShowHelpText)
{
<div class="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 p-2 text-sm text-gray-600 dark:text-gray-400">
@if (string.IsNullOrEmpty(SearchTerm))
{
<p>@Localizer["SearchHelpText"]</p>
}
else if (SearchTerm.Length == 1)
{
<p>@Localizer["SearchTooShortMessage"]</p>
}
else
{
<p>@string.Format(Localizer["SearchingForMessage"], SearchTerm)</p>
}
</div>
}
@if (ShowResults && SearchTerm.Length >= 2)
{
@if (SearchResults.Any())
@if (ShowHelpText || ShowResults)
{
<ClickOutsideHandler OnClose="OnClose" ContentId="searchWidgetContainer">
@if (ShowHelpText)
{
<div class="absolute z-10 w-screen left-0 sm:left-auto sm:w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
@for (int i = 0; i < SearchResults.Count; i++)
<div class="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 p-2 text-sm text-gray-600 dark:text-gray-400">
@if (string.IsNullOrEmpty(SearchTerm))
{
var result = SearchResults[i];
<div
class="search-result @(i == SelectedIndex ? "bg-gray-100 dark:bg-gray-700" : "") px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center"
@onclick="() => SelectResult(result)">
<DisplayFavicon FaviconBytes="@result.Service.Logo" Width="24" />
<div class="ml-2">
<div>@result.Service.Name</div>
@if (!string.IsNullOrEmpty(result.Alias.Email))
{
<span class="text-gray-500">(@result.Alias.Email)</span>
}
else if (!string.IsNullOrEmpty(result.Username))
{
<span class="text-gray-500">(@result.Username)</span>
}
</div>
</div>
<p>@_searchHelpText</p>
}
else if (SearchTerm.Length == 1)
{
<p>@_searchTooShortMessage</p>
}
else
{
<p>@string.Format(_searchingForMessage, SearchTerm)</p>
}
</div>
}
else
@if (ShowResults && SearchTerm.Length >= 2)
{
<div class="absolute z-10 w-screen left-0 sm:left-auto sm:w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
<div class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
@Localizer["NoResultsFoundMessage"]
@if (SearchResults.Any())
{
<div class="absolute z-10 w-screen left-0 sm:left-auto sm:w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
@for (int i = 0; i < SearchResults.Count; i++)
{
var result = SearchResults[i];
<div
class="search-result @(i == SelectedIndex ? "bg-gray-100 dark:bg-gray-700" : "") px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center"
@onclick="() => SelectResult(result)">
<DisplayFavicon FaviconBytes="@result.Service.Logo" Width="24" />
<div class="ml-2">
<div>@result.Service.Name</div>
@if (!string.IsNullOrEmpty(result.Alias.Email))
{
<span class="text-gray-500">(@result.Alias.Email)</span>
}
else if (!string.IsNullOrEmpty(result.Username))
{
<span class="text-gray-500">(@result.Username)</span>
}
</div>
</div>
}
</div>
</div>
}
else
{
<div class="absolute z-10 w-screen left-0 sm:left-auto sm:w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
<div class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
@_noResultsFoundMessage
</div>
</div>
}
}
}
</div>
</ClickOutsideHandler>
</ClickOutsideHandler>
}
</div>
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Widgets.SearchWidget", "AliasVault.Client");
private IStringLocalizer? _localizer;
private IStringLocalizer Localizer => _localizer ??= LocalizerFactory.Create("Components.Main.Widgets.SearchWidget", "AliasVault.Client");
// Cached localized strings
private string _searchPlaceholder = string.Empty;
private string _searchHelpText = string.Empty;
private string _searchTooShortMessage = string.Empty;
private string _searchingForMessage = string.Empty;
private string _noResultsFoundMessage = string.Empty;
private string SearchTerm { get; set; } = string.Empty;
private List<Credential> SearchResults { get; set; } = new();
private bool ShowResults { get; set; }
@@ -99,6 +111,13 @@
if (firstRender)
{
// Initialize localized strings once
_searchPlaceholder = Localizer["SearchVaultPlaceholder"];
_searchHelpText = Localizer["SearchHelpText"];
_searchTooShortMessage = Localizer["SearchTooShortMessage"];
_searchingForMessage = Localizer["SearchingForMessage"];
_noResultsFoundMessage = Localizer["NoResultsFoundMessage"];
await KeyboardShortcutService.RegisterShortcutAsync("gs", FocusSearchField);
await KeyboardShortcutService.RegisterShortcutAsync("gf", FocusSearchField);
NavigationManager.LocationChanged += ResetSearchField;

View File

@@ -8,7 +8,7 @@
<div class="container mx-auto px-4 py-4">
<div class="flex flex-col lg:flex-row justify-between items-center">
<p class="text-sm text-center text-gray-500 mb-4 lg:mb-0">
© 2024 <span>@AppInfo.ApplicationName v@(AppInfo.GetFullVersion())</span>. @Localizer["CopyrightText"]
© 2024 <span>@AppInfo.ApplicationName v@(AppInfo.GetFullVersion())</span>. @_copyrightText
</p>
<div class="hidden lg:block text-center text-gray-400 text-sm">@_randomQuote</div>
<ul class="flex flex-wrap items-center justify-center">
@@ -29,14 +29,9 @@
private IStringLocalizer Localizer => LocalizerFactory.Create("Layout.Footer", "AliasVault.Client");
private string[] Quotes =>
[
Localizer["TipCreateShortcut"],
Localizer["TipFindShortcut"],
Localizer["TipHomeShortcut"],
Localizer["TipLockShortcut"],
];
// Cached localized strings
private string _copyrightText = string.Empty;
private string[] _quotes = [];
private string _randomQuote = string.Empty;
/// <inheritdoc />
@@ -50,7 +45,16 @@
{
if (firstRender)
{
_randomQuote = Quotes[Random.Shared.Next(Quotes.Length)];
// Cache localized strings to prevent infinite rendering loops
_copyrightText = Localizer["CopyrightText"];
_quotes = [
Localizer["TipCreateShortcut"],
Localizer["TipFindShortcut"],
Localizer["TipHomeShortcut"],
Localizer["TipLockShortcut"],
];
_randomQuote = _quotes[Random.Shared.Next(_quotes.Length)];
NavigationManager.LocationChanged += RefreshQuote;
}
}
@@ -60,7 +64,7 @@
/// </summary>
private void RefreshQuote(object? sender, LocationChangedEventArgs e)
{
_randomQuote = Quotes[Random.Shared.Next(Quotes.Length)];
_randomQuote = _quotes[Random.Shared.Next(_quotes.Length)];
StateHasChanged();
}
}

View File

@@ -1,5 +1,7 @@
@inherits AliasVault.Client.Main.Pages.MainBase
@using Microsoft.Extensions.Localization
@using AliasVault.Client.Services
@inject LanguageService LanguageService
@implements IDisposable
<header>
@@ -10,17 +12,17 @@
<img src="/img/icon-nopadding.png" class="mr-3 h-8 w-10" alt="AliasVault Logo">
<span class="self-center hidden sm:flex text-2xl font-semibold content-start align-top whitespace-nowrap dark:text-white">
AliasVault
<span class="text-primary-500 text-[10px] ml-1 font-normal hidden sm:inline-block">@Localizer["BetaLabel"]</span>
<span class="text-primary-500 text-[10px] ml-1 font-normal hidden sm:inline-block">@_betaLabel</span>
</span>
</a>
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1">
<ul class="flex flex-col mt-4 space-x-6 text-sm font-medium lg:flex-row xl:space-x-8 lg:mt-0">
<NavLink href="/credentials" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["CredentialsNav"]
@_credentialsNav
</NavLink>
<NavLink href="/emails" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["EmailsNav"]
@_emailsNav
</NavLink>
</ul>
</div>
@@ -36,7 +38,7 @@
<DbLockButton />
<DbStatusIndicator />
<button @onclick="ToggleMobileMenu" type="button" id="toggleMobileMenuButton" class="items-center p-2 text-gray-500 rounded-lg md:ml-2 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" aria-expanded="false" data-dropdown-toggle="mobileMenuDropdown">
<span class="sr-only">@Localizer["OpenMenuLabel"]</span>
<span class="sr-only">@_openMenuLabel</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
</button>
</div>
@@ -45,12 +47,12 @@
<ul class="lg:hidden py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="mobileMenuDropdownButton">
<li>
<NavLink href="/credentials" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.Prefix">
@Localizer["CredentialsNav"]
@_credentialsNav
</NavLink>
</li>
<li>
<NavLink href="/emails" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["EmailsNav"]
@_emailsNav
</NavLink>
</li>
</ul>
@@ -60,37 +62,37 @@
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="mobileMenuDropdownButton">
<li>
<NavLink href="/settings/general" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["GeneralSettingsNav"]
@_generalSettingsNav
</NavLink>
</li>
<li>
<NavLink href="/settings/security" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["SecuritySettingsNav"]
@_securitySettingsNav
</NavLink>
</li>
<li>
<NavLink href="/settings/import-export" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["ImportExportNav"]
@_importExportNav
</NavLink>
</li>
<li class="border-t border-b border-gray-100 dark:border-gray-600">
<NavLink href="/settings/apps" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["ExtensionsAppsNav"]
@_extensionsAppsNav
<span class="ml-2 inline-flex items-center justify-center px-2 py-0.5 text-xs font-medium rounded bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200">
@Localizer["NewLabel"]
@_newLabel
</span>
</NavLink>
</li>
<li>
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="w-full text-start py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">
@Localizer["ToggleDarkMode"]
@_toggleDarkMode
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5 align-middle inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5 align-middle inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
</button>
</li>
<li>
<NavLink href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
@Localizer["LogOut"]
@_logOut
</NavLink>
</li>
</ul>
@@ -101,11 +103,25 @@
</header>
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Layout.TopMenu", "AliasVault.Client");
private IStringLocalizer? _localizer;
private IStringLocalizer Localizer => _localizer ??= LocalizerFactory.Create("Layout.TopMenu", "AliasVault.Client");
private bool IsUserMenuOpen { get; set; } = false;
private bool IsMobileMenuOpen { get; set; } = false;
private string Username { get; set; } = string.Empty;
// Cached localized strings
private string _betaLabel = string.Empty;
private string _credentialsNav = string.Empty;
private string _emailsNav = string.Empty;
private string _openMenuLabel = string.Empty;
private string _generalSettingsNav = string.Empty;
private string _securitySettingsNav = string.Empty;
private string _importExportNav = string.Empty;
private string _extensionsAppsNav = string.Empty;
private string _newLabel = string.Empty;
private string _toggleDarkMode = string.Empty;
private string _logOut = string.Empty;
/// <summary>
/// Close the menu.
/// </summary>
@@ -132,6 +148,7 @@
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
LanguageService.LanguageChanged -= OnLanguageChanged;
}
/// <inheritdoc />
@@ -140,6 +157,40 @@
await base.OnInitializedAsync();
Username = await GetUsernameAsync();
NavigationManager.LocationChanged += LocationChanged;
LanguageService.LanguageChanged += OnLanguageChanged;
// Cache localized strings to prevent infinite rendering loops
RefreshLocalizedStrings();
}
/// <summary>
/// Refreshes all cached localized strings.
/// </summary>
private void RefreshLocalizedStrings()
{
_betaLabel = Localizer["BetaLabel"];
_credentialsNav = Localizer["CredentialsNav"];
_emailsNav = Localizer["EmailsNav"];
_openMenuLabel = Localizer["OpenMenuLabel"];
_generalSettingsNav = Localizer["GeneralSettingsNav"];
_securitySettingsNav = Localizer["SecuritySettingsNav"];
_importExportNav = Localizer["ImportExportNav"];
_extensionsAppsNav = Localizer["ExtensionsAppsNav"];
_newLabel = Localizer["NewLabel"];
_toggleDarkMode = Localizer["ToggleDarkMode"];
_logOut = Localizer["LogOut"];
}
/// <summary>
/// Handles language change events.
/// </summary>
/// <param name="newLanguage">The new language code.</param>
private void OnLanguageChanged(string newLanguage)
{
// Re-create the localizer to pick up the new culture
_localizer = null;
RefreshLocalizedStrings();
StateHasChanged();
}
/// <inheritdoc />

View File

@@ -8,8 +8,8 @@
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@Localizer["PageTitle"]"
Description="@Localizer["PageDescription"]">
Title="@_pageTitle"
Description="@_pageDescription">
<CustomActions>
<div class="relative">
<button @onclick="ToggleSettingsDropdown" id="settingsButton" class="p-2 text-gray-500 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700">
@@ -24,17 +24,17 @@
<div id="settingsDropdown" class="absolute right-0 z-10 mt-2 min-w-[220px] origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-700">
<div class="p-4">
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ViewModeLabel"]</label>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_viewModeLabel</label>
<select @bind="ViewMode" @bind:after="CloseSettingsPopup" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="grid">@Localizer["GridViewOption"]</option>
<option value="table">@Localizer["TableViewOption"]</option>
<option value="grid">@_gridViewOption</option>
<option value="table">@_tableViewOption</option>
</select>
</div>
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["SortOrderLabel"]</label>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_sortOrderLabel</label>
<select @bind="SortOrder" @bind:after="CloseSettingsPopup" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="asc">@Localizer["OldestFirstOption"]</option>
<option value="desc">@Localizer["NewestFirstOption"]</option>
<option value="asc">@_oldestFirstOption</option>
<option value="desc">@_newestFirstOption</option>
</select>
</div>
</div>
@@ -65,23 +65,23 @@ else
{
<div class="credential-card col-span-full p-4 space-y-2 bg-amber-50 border border-primary-500 rounded-lg shadow-sm dark:border-primary-700 dark:bg-gray-800">
<div class="px-4 py-6 text-gray-700 dark:text-gray-200 rounded text-center flex flex-col items-center">
<p class="mb-2 text-lg font-semibold text-primary-700 dark:text-primary-400">@Localizer["NoCredentialsTitle"]</p>
<p class="mb-2 text-lg font-semibold text-primary-700 dark:text-primary-400">@_noCredentialsTitle</p>
<div class="max-w-md mx-auto">
<div class="mb-6">
<p class="text-sm mb-2">@Localizer["CreateFirstCredentialText"] <span class="hidden md:inline">@Localizer["NewAliasButtonText"]</span><span class="md:hidden">@Localizer["NewAliasButtonTextMobile"]</span> @Localizer["ButtonLocationText"]</p>
<p class="text-sm mb-2">@_createFirstCredentialText <span class="hidden md:inline">@_newAliasButtonText</span><span class="md:hidden">@_newAliasButtonTextMobile</span> @_buttonLocationText</p>
</div>
<div class="flex items-center my-6">
<div class="flex-1 h-px bg-gray-300 dark:bg-gray-600"></div>
<span class="px-4 text-sm text-gray-500 dark:text-gray-400">@Localizer["OrText"]</span>
<span class="px-4 text-sm text-gray-500 dark:text-gray-400">@_orText</span>
<div class="flex-1 h-px bg-gray-300 dark:bg-gray-600"></div>
</div>
<div>
<p class="text-sm mb-2">@Localizer["ImportCredentialsText"]</p>
<p class="text-sm mb-2">@_importCredentialsText</p>
<a href="/settings/import-export" class="inline-block text-sm px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors dark:bg-primary-700 dark:hover:bg-primary-600">
@Localizer["ImportButtonText"]
@_importButtonText
</a>
</div>
</div>
@@ -99,6 +99,25 @@ else
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Credentials.Home", "AliasVault.Client");
// Cached localized strings
private string _pageTitle = string.Empty;
private string _pageDescription = string.Empty;
private string _viewModeLabel = string.Empty;
private string _gridViewOption = string.Empty;
private string _tableViewOption = string.Empty;
private string _sortOrderLabel = string.Empty;
private string _oldestFirstOption = string.Empty;
private string _newestFirstOption = string.Empty;
private string _noCredentialsTitle = string.Empty;
private string _createFirstCredentialText = string.Empty;
private string _newAliasButtonText = string.Empty;
private string _newAliasButtonTextMobile = string.Empty;
private string _buttonLocationText = string.Empty;
private string _orText = string.Empty;
private string _importCredentialsText = string.Empty;
private string _importButtonText = string.Empty;
private string _failedToLoadCredentialsMessage = string.Empty;
/// <summary>
/// Gets or sets whether the credentials are being loaded.
/// </summary>
@@ -159,6 +178,25 @@ else
if (firstRender)
{
// Cache localized strings to prevent infinite rendering loops
_pageTitle = Localizer["PageTitle"];
_pageDescription = Localizer["PageDescription"];
_viewModeLabel = Localizer["ViewModeLabel"];
_gridViewOption = Localizer["GridViewOption"];
_tableViewOption = Localizer["TableViewOption"];
_sortOrderLabel = Localizer["SortOrderLabel"];
_oldestFirstOption = Localizer["OldestFirstOption"];
_newestFirstOption = Localizer["NewestFirstOption"];
_noCredentialsTitle = Localizer["NoCredentialsTitle"];
_createFirstCredentialText = Localizer["CreateFirstCredentialText"];
_newAliasButtonText = Localizer["NewAliasButtonText"];
_newAliasButtonTextMobile = Localizer["NewAliasButtonTextMobile"];
_buttonLocationText = Localizer["ButtonLocationText"];
_orText = Localizer["OrText"];
_importCredentialsText = Localizer["ImportCredentialsText"];
_importButtonText = Localizer["ImportButtonText"];
_failedToLoadCredentialsMessage = Localizer["FailedToLoadCredentialsMessage"];
await LoadCredentialsAsync();
}
}
@@ -176,7 +214,7 @@ else
if (credentialListEntries is null)
{
// Error loading aliases.
GlobalNotificationService.AddErrorMessage(Localizer["FailedToLoadCredentialsMessage"], true);
GlobalNotificationService.AddErrorMessage(_failedToLoadCredentialsMessage, true);
return;
}

View File

@@ -16,7 +16,7 @@
@using Microsoft.Extensions.Localization
@implements IAsyncDisposable
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<LayoutPageTitle>@_pageTitle</LayoutPageTitle>
@if (EmailModalVisible)
{
@@ -25,14 +25,14 @@
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@Localizer["PageTitle"]"
Description="@Localizer["PageDescription"]">
Title="@_pageTitle"
Description="@_pageDescription">
<CustomActions>
@if (DbService.Settings.AutoEmailRefresh)
{
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="@Localizer["AutoRefreshEnabledTooltip"]"></div>
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="@_autoRefreshEnabledTooltip"></div>
}
<RefreshButton OnClick="RefreshData" ButtonText="@Localizer["RefreshButton"]" />
<RefreshButton OnClick="RefreshData" ButtonText="@_refreshButton" />
</CustomActions>
</PageHeader>
@@ -76,7 +76,7 @@ else if (NoEmailClaims)
{
<div class="p-4 mx-4 mt-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div class="px-4 py-2 text-gray-400 rounded">
<p class="text-gray-500 dark:text-gray-400">@Localizer["NoEmailClaimsMessage"]</p>
<p class="text-gray-500 dark:text-gray-400">@_noEmailClaimsMessage</p>
</div>
</div>
}
@@ -92,7 +92,7 @@ else
@if (EmailList.Count == 0)
{
<li class="p-4 text-center text-gray-500 dark:text-gray-300">
@Localizer["NoEmailsReceivedMessage"]
@_noEmailsReceivedMessage
</li>
}
else
@@ -150,12 +150,12 @@ else
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
@Localizer["LoadingText"]
@_loadingText
</span>
}
else
{
<span>@string.Format(Localizer["LoadMoreButtonText"], TotalRecords - EmailList.Count)</span>
<span>@string.Format(_loadMoreButtonText, TotalRecords - EmailList.Count)</span>
}
</button>
</li>
@@ -182,6 +182,20 @@ else
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Emails.Home", "AliasVault.Client");
// Cached localized strings
private string _pageTitle = string.Empty;
private string _pageDescription = string.Empty;
private string _autoRefreshEnabledTooltip = string.Empty;
private string _refreshButton = string.Empty;
private string _noEmailClaimsMessage = string.Empty;
private string _noEmailsReceivedMessage = string.Empty;
private string _loadingText = string.Empty;
private string _loadMoreButtonText = string.Empty;
private string _claimDoesNotExistError = string.Empty;
private string _loadEmailsFailedMessage = string.Empty;
private string _loadMoreEmailsFailedMessage = string.Empty;
private List<MailListViewModel> EmailList { get; set; } = [];
private bool IsLoading => LoadingService.IsLoading("emails");
private int CurrentPage { get; set; } = 1;
@@ -361,7 +375,7 @@ else
switch (errorResponse.Code)
{
case "CLAIM_DOES_NOT_EXIST":
GlobalNotificationService.AddErrorMessage(Localizer["ClaimDoesNotExistError"], true);
GlobalNotificationService.AddErrorMessage(_claimDoesNotExistError, true);
break;
default:
throw new ArgumentException(errorResponse.Message);
@@ -447,6 +461,19 @@ else
{
await base.OnInitializedAsync();
// Cache localized strings to prevent infinite rendering loops
_pageTitle = Localizer["PageTitle"];
_pageDescription = Localizer["PageDescription"];
_autoRefreshEnabledTooltip = Localizer["AutoRefreshEnabledTooltip"];
_refreshButton = Localizer["RefreshButton"];
_noEmailClaimsMessage = Localizer["NoEmailClaimsMessage"];
_noEmailsReceivedMessage = Localizer["NoEmailsReceivedMessage"];
_loadingText = Localizer["LoadingText"];
_loadMoreButtonText = Localizer["LoadMoreButtonText"];
_claimDoesNotExistError = Localizer["ClaimDoesNotExistError"];
_loadEmailsFailedMessage = Localizer["LoadEmailsFailedMessage"];
_loadMoreEmailsFailedMessage = Localizer["LoadMoreEmailsFailedMessage"];
// Create a single object reference for JS interop
_dotNetRef = DotNetObjectReference.Create(this);
await JsInteropService.RegisterVisibilityCallback(_dotNetRef);
@@ -634,7 +661,7 @@ else
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage(string.Format(Localizer["LoadEmailsFailedMessage"], ex.Message), true);
GlobalNotificationService.AddErrorMessage(string.Format(_loadEmailsFailedMessage, ex.Message), true);
Logger.LogError(ex, "An error occurred while loading email for preview");
}
}
@@ -730,7 +757,7 @@ else
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage(string.Format(Localizer["LoadMoreEmailsFailedMessage"], ex.Message), true);
GlobalNotificationService.AddErrorMessage(string.Format(_loadMoreEmailsFailedMessage, ex.Message), true);
Logger.LogError(ex, "An error occurred while loading more emails");
}
finally

View File

@@ -8,18 +8,18 @@
@using AliasVault.Client.Main.Pages.Settings.ImportExport.Components
@using AliasVault.ImportExport
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<LayoutPageTitle>@_pageTitle</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@Localizer["PageTitle"]"
Description="@Localizer["PageDescription"]">
Title="@_pageTitle"
Description="@_pageDescription">
</PageHeader>
<div class="p-4 mx-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["ImportSectionTitle"]</h3>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@_importSectionTitle</h3>
<div class="mb-4 text-sm text-gray-500 dark:text-gray-400">
@((MarkupString)Localizer["ImportSectionDescription"].Value)
@((MarkupString)_importSectionDescription)
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<ImportService1Password />
@@ -38,16 +38,16 @@
</div>
<div class="p-4 mx-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["ExportSectionTitle"]</h3>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@_exportSectionTitle</h3>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
@Localizer["ExportSectionDescription"]
@_exportSectionDescription
</p>
<div>
<Button OnClick="@(() => ShowExportConfirmation(ExportType.Csv))">@Localizer["ExportCsvButton"]</Button>
<Button OnClick="@(() => ShowExportConfirmation(ExportType.Csv))">@_exportCsvButton</Button>
</div>
<div class="mt-6">
<Button OnClick="@(() => ShowExportConfirmation(ExportType.Sqlite))">@Localizer["ExportSqliteButton"]</Button>
<Button OnClick="@(() => ShowExportConfirmation(ExportType.Sqlite))">@_exportSqliteButton</Button>
</div>
</div>
</div>
@@ -56,6 +56,18 @@
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.ImportExport.ImportExport", "AliasVault.Client");
private ExportType _currentExportType;
// Cached localized strings for performance
private string _pageTitle = string.Empty;
private string _pageDescription = string.Empty;
private string _importSectionTitle = string.Empty;
private string _importSectionDescription = string.Empty;
private string _exportSectionTitle = string.Empty;
private string _exportSectionDescription = string.Empty;
private string _exportCsvButton = string.Empty;
private string _exportSqliteButton = string.Empty;
private string _exportWarningMessage = string.Empty;
private string _exportConfirmTitle = string.Empty;
private enum ExportType
{
Csv,
@@ -66,15 +78,28 @@
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["PageTitle"] });
// Cache localized strings for performance
_pageTitle = Localizer["PageTitle"];
_pageDescription = Localizer["PageDescription"];
_importSectionTitle = Localizer["ImportSectionTitle"];
_importSectionDescription = Localizer["ImportSectionDescription"];
_exportSectionTitle = Localizer["ExportSectionTitle"];
_exportSectionDescription = Localizer["ExportSectionDescription"];
_exportCsvButton = Localizer["ExportCsvButton"];
_exportSqliteButton = Localizer["ExportSqliteButton"];
_exportWarningMessage = Localizer["ExportWarningMessage"];
_exportConfirmTitle = Localizer["ExportConfirmTitle"];
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _pageTitle });
}
private async Task ShowExportConfirmation(ExportType exportType)
{
_currentExportType = exportType;
var confirmMessage = Localizer["ExportWarningMessage"];
var confirmMessage = _exportWarningMessage;
var result = await ConfirmModalService.ShowConfirmation(Localizer["ExportConfirmTitle"], confirmMessage, SharedLocalizer["Confirm"], SharedLocalizer["Cancel"]);
var result = await ConfirmModalService.ShowConfirmation(_exportConfirmTitle, confirmMessage, SharedLocalizer["Confirm"], SharedLocalizer["Cancel"]);
if (!result)
{
return;

View File

@@ -9,13 +9,13 @@
@inherits MainBase
@inject HttpClient Http
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<LayoutPageTitle>@_pageTitle</LayoutPageTitle>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<H1>@Localizer["PageTitle"]</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@Localizer["PageDescription"]</p>
<H1>@_pageTitle</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@_pageDescription</p>
</div>
</div>
@@ -31,21 +31,21 @@ else
<ValidationSummary />
<div>
<label for="currentPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["CurrentPasswordLabel"]</label>
<label for="currentPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_currentPasswordLabel</label>
<InputText type="password" id="currentPassword" @bind-Value="PasswordChangeFormModel.CurrentPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
required />
</div>
<div>
<label for="newPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["NewPasswordLabel"]</label>
<label for="newPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_newPasswordLabel</label>
<InputText type="password" id="newPassword" @bind-Value="PasswordChangeFormModel.NewPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
required />
</div>
<div>
<label for="newPasswordConfirm" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmNewPasswordLabel"]</label>
<label for="newPasswordConfirm" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_confirmNewPasswordLabel</label>
<InputText type="password" id="newPasswordConfirm" @bind-Value="PasswordChangeFormModel.NewPasswordConfirm"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
required />
@@ -53,7 +53,7 @@ else
<button type="submit"
class="w-full bg-primary-500 text-white py-2 px-4 rounded-md hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition duration-150 ease-in-out">
@Localizer["ChangePasswordButton"]
@_changePasswordButton
</button>
</EditForm>
</div>
@@ -70,8 +70,24 @@ else
/// </summary>
private PasswordChangeFormModel PasswordChangeFormModel { get; set; } = new();
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Pages.Settings.Security.ChangePassword", "AliasVault.Client");
private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
private IStringLocalizer? _localizer;
private IStringLocalizer Localizer => _localizer ??= LocalizerFactory.Create("Components.Main.Pages.Settings.Security.ChangePassword", "AliasVault.Client");
private IStringLocalizer? _apiErrorLocalizer;
private IStringLocalizer ApiErrorLocalizer => _apiErrorLocalizer ??= LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
// Cached localized strings for performance
private string _pageTitle = string.Empty;
private string _pageDescription = string.Empty;
private string _breadcrumbSecuritySettings = string.Empty;
private string _breadcrumbChangePassword = string.Empty;
private string _currentPasswordLabel = string.Empty;
private string _newPasswordLabel = string.Empty;
private string _confirmNewPasswordLabel = string.Empty;
private string _changePasswordButton = string.Empty;
private string _failedToInitiatePasswordChange = string.Empty;
private string _changingPasswordMessage = string.Empty;
private string _failedToChangePassword = string.Empty;
private string _passwordChangedSuccessfully = string.Empty;
/// <summary>
/// Gets or sets the current user's password salt.
@@ -101,8 +117,22 @@ else
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbSecuritySettings"], Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbChangePassword"] });
// Cache localized strings for performance
_pageTitle = Localizer["PageTitle"];
_pageDescription = Localizer["PageDescription"];
_breadcrumbSecuritySettings = Localizer["BreadcrumbSecuritySettings"];
_breadcrumbChangePassword = Localizer["BreadcrumbChangePassword"];
_currentPasswordLabel = Localizer["CurrentPasswordLabel"];
_newPasswordLabel = Localizer["NewPasswordLabel"];
_confirmNewPasswordLabel = Localizer["ConfirmNewPasswordLabel"];
_changePasswordButton = Localizer["ChangePasswordButton"];
_failedToInitiatePasswordChange = Localizer["FailedToInitiatePasswordChange"];
_changingPasswordMessage = Localizer["ChangingPasswordMessage"];
_failedToChangePassword = Localizer["FailedToChangePassword"];
_passwordChangedSuccessfully = Localizer["PasswordChangedSuccessfully"];
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbSecuritySettings, Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbChangePassword });
}
/// <inheritdoc />
@@ -131,7 +161,7 @@ else
if (response == null)
{
GlobalNotificationService.AddErrorMessage(Localizer["FailedToInitiatePasswordChange"], true);
GlobalNotificationService.AddErrorMessage(_failedToInitiatePasswordChange, true);
IsLoading = false;
StateHasChanged();
return;
@@ -148,7 +178,7 @@ else
/// </summary>
private async Task InitiatePasswordChange()
{
GlobalLoadingSpinner.Show(Localizer["ChangingPasswordMessage"]);
GlobalLoadingSpinner.Show(_changingPasswordMessage);
GlobalNotificationService.ClearMessages();
StateHasChanged();
@@ -238,7 +268,7 @@ else
}
catch
{
GlobalNotificationService.AddErrorMessage(Localizer["FailedToChangePassword"], true);
GlobalNotificationService.AddErrorMessage(_failedToChangePassword, true);
// Set currentPasswordHash back to original, so we're back to the original state.
await AuthService.StoreEncryptionKeyAsync(backupPasswordHash);
@@ -249,7 +279,7 @@ else
}
// Set success message.
GlobalNotificationService.AddSuccessMessage(Localizer["PasswordChangedSuccessfully"], true);
GlobalNotificationService.AddSuccessMessage(_passwordChangedSuccessfully, true);
// Get the new password ephemeral and salt from the server, which is required if the usre
// wants to change the password again.

View File

@@ -10,12 +10,12 @@
@using AliasVault.Client.Resources
@inject HttpClient Http
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<LayoutPageTitle>@_pageTitle</LayoutPageTitle>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<H1>@Localizer["PageTitle"]</H1>
<H1>@_pageTitle</H1>
</div>
</div>
@@ -23,25 +23,25 @@
@if (!_showPasswordConfirm)
{
<div class="mb-6">
<MessageWarning Message="@Localizer["PermanentActionWarning"]" />
<MessageWarning Message="@_permanentActionWarning" />
<div class="mt-4 mb-6 text-gray-600 dark:text-gray-400">
<p class="mb-2">@Localizer["PleaseNote"]</p>
<p class="mb-2">@_pleaseNote</p>
<ul class="list-disc list-inside space-y-2">
<li>@Localizer["VaultsDeletedNote"]</li>
<li>@Localizer["EmailAliasesOrphanedNote"]</li>
<li>@Localizer["AccountCannotBeRecoveredNote"]</li>
<li>@_vaultsDeletedNote</li>
<li>@_emailAliasesOrphanedNote</li>
<li>@_accountCannotBeRecoveredNote</li>
</ul>
</div>
<EditForm Model="@_usernameModel" OnSubmit="@ConfirmUsername">
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmUsernameLabel"]</label>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_confirmUsernameLabel</label>
<InputText id="username" @bind-Value="_usernameModel.Username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
</div>
<div class="flex space-x-3">
<Button Type="submit" Color="danger">@Localizer["ContinueWithAccountDeletion"]</Button>
<Button Type="submit" Color="danger">@_continueWithAccountDeletion</Button>
<Button Type="button" Color="secondary" OnClick="Cancel">Cancel</Button>
</div>
</EditForm>
@@ -50,12 +50,12 @@
else
{
<div class="mb-6">
<MessageWarning Message="@Localizer["FinalWarning"]" />
<MessageWarning Message="@_finalWarning" />
<div class="mt-4 mb-6 text-gray-600 dark:text-gray-400">
<p class="mb-2">@Localizer["PleaseNote"]</p>
<p class="mb-2">@_pleaseNote</p>
<ul class="list-disc list-inside space-y-2">
<li>@Localizer["DeletionIrreversibleNote"]</li>
<li>@_deletionIrreversibleNote</li>
</ul>
</div>
@@ -64,12 +64,12 @@
<ValidationSummary />
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["EnterPasswordLabel"]</label>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@_enterPasswordLabel</label>
<InputText id="password" type="password" @bind-Value="_passwordModel.Password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
</div>
<div class="flex space-x-3">
<Button Type="submit" Color="danger">@Localizer["DeleteMyAccount"]</Button>
<Button Type="submit" Color="danger">@_deleteMyAccount</Button>
<Button Type="button" Color="secondary" OnClick="Cancel">Cancel</Button>
</div>
</EditForm>
@@ -88,8 +88,30 @@
/// </summary>
private readonly DeleteAccountPasswordModel _passwordModel = new();
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Pages.Settings.Security.DeleteAccount", "AliasVault.Client");
private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
private IStringLocalizer? _localizer;
private IStringLocalizer Localizer => _localizer ??= LocalizerFactory.Create("Components.Main.Pages.Settings.Security.DeleteAccount", "AliasVault.Client");
private IStringLocalizer? _apiErrorLocalizer;
private IStringLocalizer ApiErrorLocalizer => _apiErrorLocalizer ??= LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
// Cached localized strings for performance
private string _pageTitle = string.Empty;
private string _breadcrumbSecuritySettings = string.Empty;
private string _breadcrumbDeleteAccount = string.Empty;
private string _permanentActionWarning = string.Empty;
private string _pleaseNote = string.Empty;
private string _vaultsDeletedNote = string.Empty;
private string _emailAliasesOrphanedNote = string.Empty;
private string _accountCannotBeRecoveredNote = string.Empty;
private string _confirmUsernameLabel = string.Empty;
private string _continueWithAccountDeletion = string.Empty;
private string _finalWarning = string.Empty;
private string _deletionIrreversibleNote = string.Empty;
private string _enterPasswordLabel = string.Empty;
private string _deleteMyAccount = string.Empty;
private string _usernameRequired = string.Empty;
private string _usernameDoesNotMatch = string.Empty;
private string _deletingAccountMessage = string.Empty;
private string _errorProcessingRequest = string.Empty;
/// <summary>
/// Whether to show the password confirmation step.
@@ -111,8 +133,28 @@
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbSecuritySettings"], Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbDeleteAccount"] });
// Cache localized strings for performance
_pageTitle = Localizer["PageTitle"];
_breadcrumbSecuritySettings = Localizer["BreadcrumbSecuritySettings"];
_breadcrumbDeleteAccount = Localizer["BreadcrumbDeleteAccount"];
_permanentActionWarning = Localizer["PermanentActionWarning"];
_pleaseNote = Localizer["PleaseNote"];
_vaultsDeletedNote = Localizer["VaultsDeletedNote"];
_emailAliasesOrphanedNote = Localizer["EmailAliasesOrphanedNote"];
_accountCannotBeRecoveredNote = Localizer["AccountCannotBeRecoveredNote"];
_confirmUsernameLabel = Localizer["ConfirmUsernameLabel"];
_continueWithAccountDeletion = Localizer["ContinueWithAccountDeletion"];
_finalWarning = Localizer["FinalWarning"];
_deletionIrreversibleNote = Localizer["DeletionIrreversibleNote"];
_enterPasswordLabel = Localizer["EnterPasswordLabel"];
_deleteMyAccount = Localizer["DeleteMyAccount"];
_usernameRequired = Localizer["UsernameRequired"];
_usernameDoesNotMatch = Localizer["UsernameDoesNotMatch"];
_deletingAccountMessage = Localizer["DeletingAccountMessage"];
_errorProcessingRequest = Localizer["ErrorProcessingRequest"];
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbSecuritySettings, Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbDeleteAccount });
}
/// <summary>
@@ -124,7 +166,7 @@
if (string.IsNullOrEmpty(_usernameModel.Username))
{
GlobalNotificationService.AddErrorMessage(Localizer["UsernameRequired"], true);
GlobalNotificationService.AddErrorMessage(_usernameRequired, true);
return;
}
@@ -132,7 +174,7 @@
var usernameMatches = string.Equals(_usernameModel.Username.Trim(), username.Trim(), StringComparison.OrdinalIgnoreCase);
if (!usernameMatches)
{
GlobalNotificationService.AddErrorMessage(Localizer["UsernameDoesNotMatch"], true);
GlobalNotificationService.AddErrorMessage(_usernameDoesNotMatch, true);
return;
}
@@ -145,7 +187,7 @@
/// </summary>
private async Task DeleteAccountConfirmed()
{
GlobalLoadingSpinner.Show(Localizer["DeletingAccountMessage"]);
GlobalLoadingSpinner.Show(_deletingAccountMessage);
GlobalNotificationService.ClearMessages();
try
@@ -167,7 +209,7 @@
var loginResponse = JsonSerializer.Deserialize<LoginInitiateResponse>(responseContent);
if (loginResponse == null)
{
GlobalNotificationService.AddErrorMessage(Localizer["ErrorProcessingRequest"], true);
GlobalNotificationService.AddErrorMessage(_errorProcessingRequest, true);
return;
}

View File

@@ -1,8 +1,9 @@
@page "/settings/security/disable-2fa"
@inherits MainBase
@inject HttpClient Http
@using Microsoft.Extensions.Localization
<LayoutPageTitle>Disable two-factor authentication</LayoutPageTitle>
<LayoutPageTitle>@_pageTitle</LayoutPageTitle>
@if (IsLoading)
{
@@ -13,17 +14,17 @@ else
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<H1>Disable two-factor authentication</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Disabling two-factor authentication means you will be able to login with only your password.</p>
<H1>@_pageTitle</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@_pageDescription</p>
</div>
</div>
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<AlertMessageError Message="Please note: after disabling two-factor authentication any configured authenticator app(s) will stop working. When you want to re-enable two-factor authentication you will have to configure the authenticator app(s) again." />
<div class="mb-3 text-sm text-gray-600 dark:text-gray-400">Two factor authentication is currently enabled. Disable it in order to be able to access your vault with your password only.</div>
<AlertMessageError Message="@_alertMessage" />
<div class="mb-3 text-sm text-gray-600 dark:text-gray-400">@_descriptionText</div>
<button @onclick="DisableTwoFactor"
class="bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition duration-150 ease-in-out">
Confirm Disable Two-Factor Authentication
@_confirmDisableButton
</button>
</div>
}
@@ -31,13 +32,40 @@ else
@code {
private bool IsLoading { get; set; } = true;
private IStringLocalizer? _localizer;
private IStringLocalizer Localizer => _localizer ??= LocalizerFactory.Create("Components.Main.Pages.Settings.Security.Disable2Fa", "AliasVault.Client");
// Cached localized strings for performance
private string _pageTitle = string.Empty;
private string _pageDescription = string.Empty;
private string _breadcrumbSecuritySettings = string.Empty;
private string _breadcrumbDisable2Fa = string.Empty;
private string _alertMessage = string.Empty;
private string _descriptionText = string.Empty;
private string _confirmDisableButton = string.Empty;
private string _twoFactorNotEnabled = string.Empty;
private string _twoFactorDisabledSuccess = string.Empty;
private string _failedToDisable2Fa = string.Empty;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Security settings", Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Disable two-factor authentication" });
// Cache localized strings for performance
_pageTitle = Localizer["PageTitle"];
_pageDescription = Localizer["PageDescription"];
_breadcrumbSecuritySettings = Localizer["BreadcrumbSecuritySettings"];
_breadcrumbDisable2Fa = Localizer["BreadcrumbDisable2Fa"];
_alertMessage = Localizer["AlertMessage"];
_descriptionText = Localizer["DescriptionText"];
_confirmDisableButton = Localizer["ConfirmDisableButton"];
_twoFactorNotEnabled = Localizer["TwoFactorNotEnabled"];
_twoFactorDisabledSuccess = Localizer["TwoFactorDisabledSuccess"];
_failedToDisable2Fa = Localizer["FailedToDisable2Fa"];
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbSecuritySettings, Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbDisable2Fa });
}
/// <inheritdoc />
@@ -51,7 +79,7 @@ else
var response = await Http.GetFromJsonAsync<TwoFactorEnabledResult>("v1/TwoFactorAuth/status");
if (response is not null && !response.TwoFactorEnabled)
{
GlobalNotificationService.AddErrorMessage("Two-factor authentication is not enabled.");
GlobalNotificationService.AddErrorMessage(_twoFactorNotEnabled);
NavigationManager.NavigateTo("/settings/security");
return;
}
@@ -66,13 +94,13 @@ else
var response = await Http.PostAsync("v1/TwoFactorAuth/disable", null);
if (response.IsSuccessStatusCode)
{
GlobalNotificationService.AddSuccessMessage("Two-factor authentication is now successfully disabled.");
GlobalNotificationService.AddSuccessMessage(_twoFactorDisabledSuccess);
NavigationManager.NavigateTo("/settings/security");
return;
}
// Handle error
GlobalNotificationService.AddErrorMessage("Failed to disable two-factor authentication.", true);
GlobalNotificationService.AddErrorMessage(_failedToDisable2Fa, true);
StateHasChanged();
}

View File

@@ -4,13 +4,13 @@
@inherits MainBase
@inject HttpClient Http
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<LayoutPageTitle>@_pageTitle</LayoutPageTitle>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<H1>@Localizer["PageTitle"]</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@Localizer["PageDescription"]</p>
<H1>@_pageTitle</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@_pageDescription</p>
</div>
</div>
@@ -31,7 +31,7 @@ else
</div>
<p class="text-sm text-gray-600 text-center">
@Localizer["QrCodeInstructions"]
@_qrCodeInstructions
</p>
<div class="text-lg font-mono text-center bg-gray-100 p-2 rounded" id="authenticator-secret">@Secret</div>
@@ -39,11 +39,11 @@ else
<div>
<InputText id="verificationCode" @bind-Value="VerifyModel.Code"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="@Localizer["VerificationCodePlaceholder"]"/>
placeholder="@_verificationCodePlaceholder"/>
</div>
<button type="submit"
class="w-full bg-primary-500 text-white py-2 px-4 rounded-md hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition duration-150 ease-in-out">
@Localizer["VerifyAndEnableButton"]
@_verifyAndEnableButton
</button>
</EditForm>
</div>
@@ -57,15 +57,38 @@ else
private List<string>? RecoveryCodes { get; set; }
private readonly VerificationModel VerifyModel = new();
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Pages.Settings.Security.Enable2Fa", "AliasVault.Client");
private IStringLocalizer? _localizer;
private IStringLocalizer Localizer => _localizer ??= LocalizerFactory.Create("Components.Main.Pages.Settings.Security.Enable2Fa", "AliasVault.Client");
// Cached localized strings
private string _pageTitle = string.Empty;
private string _pageDescription = string.Empty;
private string _breadcrumbSecuritySettings = string.Empty;
private string _breadcrumbEnable2Fa = string.Empty;
private string _qrCodeInstructions = string.Empty;
private string _verificationCodePlaceholder = string.Empty;
private string _verifyAndEnableButton = string.Empty;
private string _twoFactorEnabledSuccess = string.Empty;
private string _failedToEnable2Fa = string.Empty;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbSecuritySettings"], Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbEnable2Fa"] });
// Initialize localized strings once
_pageTitle = Localizer["PageTitle"];
_pageDescription = Localizer["PageDescription"];
_breadcrumbSecuritySettings = Localizer["BreadcrumbSecuritySettings"];
_breadcrumbEnable2Fa = Localizer["BreadcrumbEnable2Fa"];
_qrCodeInstructions = Localizer["QrCodeInstructions"];
_verificationCodePlaceholder = Localizer["VerificationCodePlaceholder"];
_verifyAndEnableButton = Localizer["VerifyAndEnableButton"];
_twoFactorEnabledSuccess = Localizer["TwoFactorEnabledSuccess"];
_failedToEnable2Fa = Localizer["FailedToEnable2Fa"];
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbSecuritySettings, Url = "/settings/security" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbEnable2Fa });
}
/// <inheritdoc />
@@ -110,7 +133,7 @@ else
if (result != null)
{
GlobalNotificationService.AddSuccessMessage(Localizer["TwoFactorEnabledSuccess"], true);
GlobalNotificationService.AddSuccessMessage(_twoFactorEnabledSuccess, true);
// Show recovery codes.
RecoveryCodes = result.RecoveryCodes;
@@ -120,7 +143,7 @@ else
}
}
GlobalNotificationService.AddErrorMessage(Localizer["FailedToEnable2Fa"], true);
GlobalNotificationService.AddErrorMessage(_failedToEnable2Fa, true);
StateHasChanged();
}

View File

@@ -3,14 +3,14 @@
@inherits MainBase
@using Microsoft.Extensions.Localization
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<LayoutPageTitle>@_pageTitle</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@Localizer["PageTitle"]"
Description="@Localizer["PageDescription"]">
Title="@_pageTitle"
Description="@_pageDescription">
<CustomActions>
<RefreshButton OnClick="LoadData" ButtonText="@Localizer["RefreshButton"]" />
<RefreshButton OnClick="LoadData" ButtonText="@_refreshButton" />
</CustomActions>
</PageHeader>
@@ -23,6 +23,12 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.Security.Security", "AliasVault.Client");
// Cached localized strings for performance
private string _pageTitle = string.Empty;
private string _pageDescription = string.Empty;
private string _refreshButton = string.Empty;
private string _breadcrumbTitle = string.Empty;
private TwoFactorAuthenticationSection? TwoFactorSection;
private QuickVaultUnlockSection? QuickVaultUnlockSection;
private ActiveSessionsSection? SessionsSection;
@@ -33,7 +39,13 @@
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbTitle"] });
// Cache localized strings for performance
_pageTitle = Localizer["PageTitle"];
_pageDescription = Localizer["PageDescription"];
_refreshButton = Localizer["RefreshButton"];
_breadcrumbTitle = Localizer["BreadcrumbTitle"];
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = _breadcrumbTitle });
}
/// <inheritdoc />

View File

@@ -26,7 +26,7 @@
case TutorialStep.Welcome:
<div class="space-y-4">
<p class="text-gray-600 dark:text-gray-400">
@Localizer["WelcomeMessage"]
@_welcomeMessage
</p>
</div>
break;
@@ -34,13 +34,13 @@
case TutorialStep.HowAliasVaultWorks:
<div class="space-y-4">
<p class="text-gray-600 dark:text-gray-400">
@Localizer["HowItWorksIntro"]
@_howItWorksIntro
</p>
<ol class="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400">
<li>@Localizer["HowItWorksStep1"]</li>
<li>@Localizer["HowItWorksStep2"]</li>
<li>@Localizer["HowItWorksStep3"]</li>
<li>@Localizer["HowItWorksStep4"]</li>
<li>@_howItWorksStep1</li>
<li>@_howItWorksStep2</li>
<li>@_howItWorksStep3</li>
<li>@_howItWorksStep4</li>
</ol>
</div>
break;
@@ -49,18 +49,18 @@
<div class="space-y-4">
<div class="space-y-3">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 class="font-semibold text-gray-900 dark:text-white">@Localizer["MasterPasswordTipTitle"]</h4>
<h4 class="font-semibold text-gray-900 dark:text-white">@_masterPasswordTipTitle</h4>
<p class="text-gray-600 dark:text-gray-400">
@Localizer["MasterPasswordTipContent"]
@_masterPasswordTipContent
</p>
</div>
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 class="font-semibold text-gray-900 dark:text-white">@Localizer["TwoFactorTipTitle"]</h4>
<p class="text-gray-600 dark:text-gray-400">@Localizer["TwoFactorTipContent"]</p>
<h4 class="font-semibold text-gray-900 dark:text-white">@_twoFactorTipTitle</h4>
<p class="text-gray-600 dark:text-gray-400">@_twoFactorTipContent</p>
</div>
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 class="font-semibold text-gray-900 dark:text-white">@Localizer["ExtensionsAppsTipTitle"]</h4>
<p class="text-gray-600 dark:text-gray-400 mb-4">@Localizer["ExtensionsAppsTipContent"]</p>
<h4 class="font-semibold text-gray-900 dark:text-white">@_extensionsAppsTipTitle</h4>
<p class="text-gray-600 dark:text-gray-400 mb-4">@_extensionsAppsTipContent</p>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
@foreach (var extension in AliasVault.Shared.Core.BrowserExtensions.Constants.Extensions.Where(x => x.Key != BrowserType.Unknown).Select(x => x.Value))
{
@@ -80,7 +80,7 @@
<div class="flex flex-col items-center p-3 rounded-lg bg-gray-200 dark:bg-gray-600">
<img src="@extension.IconPath" alt="@extension.Name" class="w-8 h-8 mb-2 opacity-50">
<span class="text-xs text-gray-500 dark:text-gray-400">
@Localizer["ComingSoonLabel"]
@_comingSoonLabel
</span>
</div>
}
@@ -94,7 +94,7 @@
class="flex flex-col items-center p-3 rounded-lg bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors">
<img src="@app.IconPath" alt="@app.Name" class="w-8 h-8 mb-2">
<span class="text-xs text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
@Localizer["DownloadForPrefix"] @app.Name
@_downloadForPrefix @app.Name
</span>
</a>
}
@@ -103,7 +103,7 @@
<div class="flex flex-col items-center p-3 rounded-lg bg-gray-200 dark:bg-gray-600">
<img src="@app.IconPath" alt="@app.Name" class="w-8 h-8 mb-2 opacity-50">
<span class="text-xs text-gray-500 dark:text-gray-400">
@app.Name @Localizer["SoonSuffix"]
@app.Name @_soonSuffix
</span>
</div>
}
@@ -116,14 +116,14 @@
case TutorialStep.CreateFirstIdentity:
<div class="space-y-4">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white">@Localizer["ReadyToStartTitle"]</h3>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white">@_readyToStartTitle</h3>
<p class="text-gray-600 dark:text-gray-400">
@Localizer["ReadyToStartMessage"]
@_readyToStartMessage
</p>
<div class="mt-4">
<button @onclick="CreateFirstIdentity"
class="w-full bg-primary-600 hover:bg-primary-700 text-white font-semibold py-3 px-4 rounded-lg transition duration-300">
@Localizer["CreateFirstIdentityButton"]
@_createFirstIdentityButton
</button>
</div>
</div>
@@ -136,14 +136,14 @@
{
<button @onclick="GoNext"
class="w-full py-3 px-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition duration-300">
@Localizer["ContinueButton"]
@_continueButton
</button>
}
else
{
<button @onclick="FinishTutorial"
class="w-full py-3 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition duration-300">
@Localizer["GetStartedButton"]
@_getStartedButton
</button>
}
</div>
@@ -154,6 +154,34 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Welcome", "AliasVault.Client");
// Cached localized strings for performance
private string _welcomeStepTitle = string.Empty;
private string _howAliasVaultWorksStepTitle = string.Empty;
private string _tipsStepTitle = string.Empty;
private string _getStartedStepTitle = string.Empty;
private string _tutorialStepTitle = string.Empty;
private string _welcomeMessage = string.Empty;
private string _howItWorksIntro = string.Empty;
private string _howItWorksStep1 = string.Empty;
private string _howItWorksStep2 = string.Empty;
private string _howItWorksStep3 = string.Empty;
private string _howItWorksStep4 = string.Empty;
private string _masterPasswordTipTitle = string.Empty;
private string _masterPasswordTipContent = string.Empty;
private string _twoFactorTipTitle = string.Empty;
private string _twoFactorTipContent = string.Empty;
private string _extensionsAppsTipTitle = string.Empty;
private string _extensionsAppsTipContent = string.Empty;
private string _comingSoonLabel = string.Empty;
private string _downloadForPrefix = string.Empty;
private string _soonSuffix = string.Empty;
private string _readyToStartTitle = string.Empty;
private string _readyToStartMessage = string.Empty;
private string _createFirstIdentityButton = string.Empty;
private string _continueButton = string.Empty;
private string _getStartedButton = string.Empty;
private string _finishingTutorialMessage = string.Empty;
private TutorialStep _currentStep = TutorialStep.Welcome;
private enum TutorialStep
@@ -168,11 +196,11 @@
{
return step switch
{
TutorialStep.Welcome => Localizer["WelcomeStepTitle"],
TutorialStep.HowAliasVaultWorks => Localizer["HowAliasVaultWorksStepTitle"],
TutorialStep.Tips => Localizer["TipsStepTitle"],
TutorialStep.CreateFirstIdentity => Localizer["GetStartedStepTitle"],
_ => Localizer["TutorialStepTitle"]
TutorialStep.Welcome => _welcomeStepTitle,
TutorialStep.HowAliasVaultWorks => _howAliasVaultWorksStepTitle,
TutorialStep.Tips => _tipsStepTitle,
TutorialStep.CreateFirstIdentity => _getStartedStepTitle,
_ => _tutorialStepTitle
};
}
@@ -181,6 +209,34 @@
{
await base.OnInitializedAsync();
// Cache localized strings for performance
_welcomeStepTitle = Localizer["WelcomeStepTitle"];
_howAliasVaultWorksStepTitle = Localizer["HowAliasVaultWorksStepTitle"];
_tipsStepTitle = Localizer["TipsStepTitle"];
_getStartedStepTitle = Localizer["GetStartedStepTitle"];
_tutorialStepTitle = Localizer["TutorialStepTitle"];
_welcomeMessage = Localizer["WelcomeMessage"];
_howItWorksIntro = Localizer["HowItWorksIntro"];
_howItWorksStep1 = Localizer["HowItWorksStep1"];
_howItWorksStep2 = Localizer["HowItWorksStep2"];
_howItWorksStep3 = Localizer["HowItWorksStep3"];
_howItWorksStep4 = Localizer["HowItWorksStep4"];
_masterPasswordTipTitle = Localizer["MasterPasswordTipTitle"];
_masterPasswordTipContent = Localizer["MasterPasswordTipContent"];
_twoFactorTipTitle = Localizer["TwoFactorTipTitle"];
_twoFactorTipContent = Localizer["TwoFactorTipContent"];
_extensionsAppsTipTitle = Localizer["ExtensionsAppsTipTitle"];
_extensionsAppsTipContent = Localizer["ExtensionsAppsTipContent"];
_comingSoonLabel = Localizer["ComingSoonLabel"];
_downloadForPrefix = Localizer["DownloadForPrefix"];
_soonSuffix = Localizer["SoonSuffix"];
_readyToStartTitle = Localizer["ReadyToStartTitle"];
_readyToStartMessage = Localizer["ReadyToStartMessage"];
_createFirstIdentityButton = Localizer["CreateFirstIdentityButton"];
_continueButton = Localizer["ContinueButton"];
_getStartedButton = Localizer["GetStartedButton"];
_finishingTutorialMessage = Localizer["FinishingTutorialMessage"];
// If tutorial is already done, redirect to the home page.
if (DbService.Settings.TutorialDone)
{
@@ -208,7 +264,7 @@
private async Task FinishTutorial()
{
GlobalLoadingSpinner.Show(Localizer["FinishingTutorialMessage"]);
GlobalLoadingSpinner.Show(_finishingTutorialMessage);
await DbService.Settings.SetTutorialDoneAsync(true);
NavigationManager.NavigateTo("credentials");
GlobalLoadingSpinner.Hide();